55 changed files with 7534 additions and 5873 deletions
-
318calculate/templates/format/backgrounds_format.py
-
20calculate/templates/format/contents_format.py
-
14calculate/templates/format/patch_format.py
-
357calculate/templates/template_engine.py
-
57calculate/templates/template_processor.py
-
18calculate/utils/files.py
-
143calculate/utils/images.py
-
2calculate/utils/package.py
-
42calculate/vars/install/os/__init__.py
-
2calculate/vars/install/os/func.py
-
9calculate/vars/main/__init__.py
-
5calculate/vars/main/cl/__init__.py
-
12calculate/vars/main/os/x11/__init__.py
-
15calculate/vars/update/cl/__init__.py
-
46conftest.py
-
3pytest.ini
-
331tests/templates/format/test_backgrounds.py
-
14tests/templates/format/test_contents.py
-
10tests/templates/format/test_patch.py
-
BINtests/templates/format/testfiles/backgrounds/picture_0.png
-
BINtests/templates/format/testfiles/backgrounds/picture_1.jpg
-
BINtests/templates/format/testfiles/backgrounds/picture_2.png
-
BINtests/templates/format/testfiles/backgrounds/tmp.backup/result_10-1024x768.png
-
BINtests/templates/format/testfiles/backgrounds/tmp.backup/result_10-1650x1050.png
-
1tests/templates/format/testfiles/backgrounds/tmp.backup/result_10.md5sum
-
BINtests/templates/format/testfiles/backgrounds/tmp.backup/result_11-1024x768.png
-
BINtests/templates/format/testfiles/backgrounds/tmp.backup/result_11-1650x1050.png
-
1tests/templates/format/testfiles/backgrounds/tmp.backup/result_11.md5sum
-
1tests/templates/format/testfiles/backgrounds/tmp.backup/result_12.md5sum
-
BINtests/templates/format/testfiles/backgrounds/tmp.backup/result_13-1024x768.png
-
BINtests/templates/format/testfiles/backgrounds/tmp.backup/result_13-1650x1050.png
-
3405tests/templates/test_directory_processor.py
-
6017tests/templates/test_template_executor.py
-
2410tests/templates/test_template_wrapper.py
-
BINtests/templates/testfiles/test_dir_processor_root/etc.backup/dir_80/image-300x100.jpeg
-
BINtests/templates/testfiles/test_dir_processor_root/etc.backup/dir_80/image-320x180.jpeg
-
1tests/templates/testfiles/test_dir_processor_root/etc.backup/dir_80/image.md5sum
-
BINtests/templates/testfiles/test_dir_processor_root/etc.backup/dir_82/image-300x100.jpeg
-
BINtests/templates/testfiles/test_dir_processor_root/etc.backup/dir_82/image-320x180.jpeg
-
1tests/templates/testfiles/test_dir_processor_root/etc.backup/dir_82/image.md5sum
-
BINtests/templates/testfiles/test_dir_processor_root/etc.backup/picture.jpg
-
2tests/templates/testfiles/test_dir_processor_root/templates_66/install/.calculate_directory
-
1tests/templates/testfiles/test_dir_processor_root/templates_66/install/dir_79/.calculate_directory
-
3tests/templates/testfiles/test_dir_processor_root/templates_66/install/dir_79/image
-
2tests/templates/testfiles/test_dir_processor_root/templates_67/install/.calculate_directory
-
1tests/templates/testfiles/test_dir_processor_root/templates_67/install/dir_80/.calculate_directory
-
4tests/templates/testfiles/test_dir_processor_root/templates_67/install/dir_80/image
-
2tests/templates/testfiles/test_dir_processor_root/templates_68/install/.calculate_directory
-
1tests/templates/testfiles/test_dir_processor_root/templates_68/install/dir_81/.calculate_directory
-
3tests/templates/testfiles/test_dir_processor_root/templates_68/install/dir_81/image
-
2tests/templates/testfiles/test_dir_processor_root/templates_69/install/.calculate_directory
-
1tests/templates/testfiles/test_dir_processor_root/templates_69/install/dir_82/.calculate_directory
-
4tests/templates/testfiles/test_dir_processor_root/templates_69/install/dir_82/image
-
122tests/utils/test_images.py
-
4tests/variables/test_datavars.py
@ -0,0 +1,318 @@ |
|||
# vim: fileencoding=utf-8 |
|||
# |
|||
from ..template_engine import ParametersContainer, Variables |
|||
from ...variables.datavars import NamespaceNode, VariableNotFoundError |
|||
from ...variables.loader import Datavars |
|||
from ...utils.images import ImageMagick |
|||
from .base_format import Format, FormatError |
|||
|
|||
from typing import Union, List, Tuple, NoReturn |
|||
import hashlib |
|||
import re |
|||
import os |
|||
|
|||
|
|||
class BackgroundsFormat(Format): |
|||
FORMAT = 'backgrounds' |
|||
EXECUTABLE = True |
|||
FORMAT_PARAMETERS = {'convert', 'stretch'} |
|||
|
|||
def __init__(self, template_text: str, |
|||
template_path: str, |
|||
parameters: ParametersContainer, |
|||
datavars: Union[Datavars, NamespaceNode, Variables], |
|||
chroot_path: str = "/"): |
|||
self._lines: List[str] = [line for line |
|||
in template_text.strip().split('\n') if line] |
|||
self._datavars: Union[Datavars, NamespaceNode, Variables] = datavars |
|||
|
|||
if parameters.source: |
|||
self._source = parameters.source |
|||
else: |
|||
raise FormatError("source parameter is not set for template with" |
|||
"with 'backgrounds' format.") |
|||
self._mirror: bool = parameters.mirror |
|||
|
|||
self._stretch: bool = parameters.stretch |
|||
self._convert: Union[bool, str] = parameters.convert |
|||
self._empty_name: bool = (parameters.name == '') |
|||
|
|||
# Измененные файлы. |
|||
self.changed_files: dict = dict() |
|||
|
|||
# Список предупреждений. |
|||
self._warnings: list = [] |
|||
|
|||
# Флаг для тестов. |
|||
self._fake_chroot: bool = False |
|||
try: |
|||
self._fake_chroot = datavars.main.fake_chroot |
|||
except Exception: |
|||
pass |
|||
|
|||
def execute_format(self, target_path: str, chroot_path: str = '/') -> dict: |
|||
'''Метод для запуска работы формата.''' |
|||
if not self._check_source(self._source, target_path, self._mirror): |
|||
return self.changed_files |
|||
|
|||
print(f"FAKE_CHROOT: {self._fake_chroot}") |
|||
self._magician = ImageMagick( |
|||
chroot=chroot_path if not self._fake_chroot else "/") |
|||
source_resolution = self._magician.get_image_resolution(self._source) |
|||
|
|||
resolutions_from_var = False |
|||
resolutions = self._get_resolutions_from_lines(self._lines, |
|||
source_resolution) |
|||
if not resolutions: |
|||
resolutions = self._get_resolutions_from_var(self._datavars) |
|||
resolutions_from_var = True |
|||
|
|||
action_md5sum = self._get_action_md5sum(self._source, resolutions) |
|||
if not self._check_target_directory(target_path, self._mirror, |
|||
resolutions, action_md5sum): |
|||
return {} |
|||
|
|||
images_format = self._get_format_value(self._convert, |
|||
self._datavars) |
|||
if self._convert and self._convert == "GFXBOOT": |
|||
converter = self._magician.convert_resize_gfxboot |
|||
else: |
|||
converter = self._magician.convert_resize_crop_center |
|||
|
|||
output_paths = self._make_output_paths(self._lines, images_format, |
|||
target_path, resolutions, |
|||
resolutions_from_var) |
|||
|
|||
# Выполняем преобразование изображений в требуемый размер и формат. |
|||
for resolution, output_path in output_paths.items(): |
|||
if not self._check_stretch(source_resolution, |
|||
resolution, |
|||
self._stretch): |
|||
continue |
|||
width, height = resolution |
|||
converter(self._source, output_path, height, width, |
|||
image_format=images_format) |
|||
if output_path in self.changed_files: |
|||
self.changed_files[output_path] = 'M' |
|||
else: |
|||
self.changed_files[output_path] = 'N' |
|||
|
|||
self._create_md5sum_file(target_path, action_md5sum) |
|||
return self.changed_files |
|||
|
|||
def _get_format_value(self, convert: Union[bool, str], |
|||
datavars: Union[Datavars, NamespaceNode, Variables] |
|||
) -> str: |
|||
"""Метод для получения значения формата.""" |
|||
if convert: |
|||
if convert == "JPG" or convert == "GFXBOOT": |
|||
images_format = "JPEG" |
|||
else: |
|||
images_format = convert |
|||
else: |
|||
images_format = self._magician.get_image_format(self._source) |
|||
return images_format |
|||
|
|||
def _make_output_paths(self, lines: List[str], |
|||
images_format: str, |
|||
target_path: str, |
|||
resolutions: List[Tuple[int, int]], |
|||
resolutions_from_var: bool |
|||
) -> List[str]: |
|||
"""Метод для получения списка путей, по которым будут создаваться |
|||
изображения.""" |
|||
if not self._empty_name: |
|||
path, name = os.path.split(target_path) |
|||
else: |
|||
path = os.path.dirname(target_path) |
|||
name = '' |
|||
paths = {} |
|||
|
|||
if not resolutions_from_var and len(resolutions) == 1: |
|||
if self._empty_name: |
|||
raise FormatError("'name' parameter is empty in 'backgrounds'" |
|||
" format template with single file output.") |
|||
paths = {next(iter(resolutions)): target_path} |
|||
return paths |
|||
|
|||
for width, height in resolutions: |
|||
paths[(width, height)] = (f"{path}/{name}{width}x{height}" |
|||
f".{images_format.lower()}") |
|||
return paths |
|||
|
|||
def _get_resolutions_from_lines(self, lines: List[str], |
|||
source_resolution: Tuple[int, int] |
|||
) -> List[Tuple[int, int]]: |
|||
"""Метод для получения списка кортежей с разрешениями выходных |
|||
изображений из текста шаблона.""" |
|||
resolutions = [] |
|||
if lines: |
|||
for line in lines: |
|||
if (line.strip() == "original" |
|||
and source_resolution not in resolutions): |
|||
resolutions.append(source_resolution) |
|||
else: |
|||
try: |
|||
width, height = line.lower().strip().split("x") |
|||
resolution = (int(width), int(height)) |
|||
except Exception: |
|||
raise FormatError("can not parse line from template" |
|||
" with 'backgrounds' format:" |
|||
f" '{line}'") |
|||
if resolution not in resolutions: |
|||
resolutions.append(resolution) |
|||
return resolutions |
|||
|
|||
def _get_resolutions_from_var(self, datavars: Union[Datavars, |
|||
NamespaceNode, |
|||
Variables] |
|||
) -> List[Tuple[int, int]]: |
|||
"""Метод для получения списка кортежей с разрешениями выходных |
|||
изображений из переменной.""" |
|||
try: |
|||
resolutions = [] |
|||
for resolution in self._datavars.main.cl_resolutions: |
|||
resolution = tuple(resolution.strip().split("x")) |
|||
resolutions.append(resolution) |
|||
return resolutions |
|||
except VariableNotFoundError: |
|||
raise FormatError("resolutions values was not found.") |
|||
except Exception: |
|||
raise FormatError("can not use resolution values from variable:" |
|||
" 'main.cl_resolutions'") |
|||
|
|||
def _check_target_directory(self, target_path: str, mirror: bool, |
|||
resolutions: List[Tuple[int, int]], |
|||
action_md5sum: str |
|||
) -> Union[bool, str]: |
|||
"""Метод для проверки содержимого целевой директории, удаления |
|||
изображений и md5-сумм, подлежащих удалению, а также сравнения |
|||
имеющейся в директории md5-суммы с суммой, полученной для действия |
|||
текущего шаблона.""" |
|||
if not self._empty_name: |
|||
path, name = os.path.split(target_path) |
|||
else: |
|||
path = os.path.dirname(target_path) |
|||
name = '' |
|||
|
|||
name_pattern = re.compile(rf"^{name}\d+x\d+") |
|||
md5sum = None |
|||
images = [] |
|||
|
|||
# Проверяем содержимое целевой директории. |
|||
for node in os.scandir(path): |
|||
if node.is_file(): |
|||
if (node.name == |
|||
f"{name.strip('-_.')}{'.' if name else ''}md5sum"): |
|||
md5sum = node.name |
|||
elif (node.name == name |
|||
or (name_pattern.search(node.name) is not None)): |
|||
images.append(node.name) |
|||
|
|||
if not images and md5sum is None: |
|||
# Если нет файла суммы и нет изображений -- продолжаем выполнение |
|||
# шаблона. |
|||
return True |
|||
elif not images and md5sum is not None: |
|||
# Если есть файл суммы, но нет изображений -- удаляем файл суммы. |
|||
md5sum_path = os.path.join(path, md5sum) |
|||
os.unlink(md5sum_path) |
|||
self.changed_files[md5sum_path] = 'D' |
|||
return True |
|||
elif images and md5sum is None: |
|||
# Если есть файлы изображений, но нет суммы -- удаляем файлы |
|||
# изображений. |
|||
for image in images: |
|||
image_path = os.path.join(path, image) |
|||
os.unlink(image_path) |
|||
self.changed_files[image_path] = 'D' |
|||
return True |
|||
else: |
|||
# Сравниваем суммы из md5sum и для текущего действия если они |
|||
# сходятся, то делать ничего не надо, если нет -- удаляем |
|||
# имеющиеся изображения и md5-сумму. |
|||
with open(os.path.join(path, md5sum), "r") as md5sum_file: |
|||
current_md5sum = md5sum_file.read().strip() |
|||
if current_md5sum != action_md5sum: |
|||
for image in images: |
|||
image_path = os.path.join(path, image) |
|||
os.unlink(image_path) |
|||
self.changed_files[image_path] = 'D' |
|||
md5sum_path = os.path.join(path, md5sum) |
|||
os.unlink(md5sum_path) |
|||
self.changed_files[md5sum_path] = 'D' |
|||
return True |
|||
else: |
|||
return False |
|||
|
|||
def _check_source(self, source: str, target_path: str, mirror: bool |
|||
) -> bool: |
|||
"""Метод для проверки исходного изображения.""" |
|||
if not self._empty_name: |
|||
path, name = os.path.split(target_path) |
|||
else: |
|||
path = os.path.dirname(target_path) |
|||
name = '' |
|||
|
|||
name_pattern = re.compile(rf"^{name}\d+x\d+") |
|||
|
|||
if not os.path.exists(source): |
|||
if mirror: |
|||
for node in os.scandir(path): |
|||
if (node.name == name |
|||
or name_pattern.search(node.name) is not None |
|||
or node.name == (f"{node.name.strip('_-.')}" |
|||
f"{ '.' if name else '' }md5sum")): |
|||
os.unlink(node.path) |
|||
self.changed_files[node.path] = "D" |
|||
return False |
|||
else: |
|||
raise FormatError("image from 'source' parameter does not" |
|||
f" exist: {source}.") |
|||
return True |
|||
|
|||
def _get_action_md5sum(self, source: str, |
|||
resolutions: List[Tuple[int, int]]) -> bool: |
|||
"""Метод для получения md5-суммы текущего действия шаблона, |
|||
рассчитываемой из последовательности байтов изображения и списка |
|||
разрешений, в которые данный файл должен быть конвертирован.""" |
|||
print("RESOLUTIONS:", resolutions) |
|||
with open(source, "rb") as source_file: |
|||
md5_object = hashlib.md5(source_file.read()) |
|||
for width, height in resolutions: |
|||
resolution = f"{width}x{height}" |
|||
md5_object.update(resolution.encode()) |
|||
return md5_object.hexdigest() |
|||
|
|||
def _create_md5sum_file(self, target_path: str, action_md5sum: str |
|||
) -> NoReturn: |
|||
"""Метод для создания файла с md5-суммой действия, выполненного |
|||
данным шаблоном.""" |
|||
if not self._empty_name: |
|||
path, name = os.path.split(target_path) |
|||
else: |
|||
path = os.path.dirname(target_path) |
|||
name = '' |
|||
|
|||
md5sum_path = (f"{path}/{name.strip('_-.')}" |
|||
f"{ '.' if name else '' }md5sum") |
|||
with open(md5sum_path, "w") as md5sum_file: |
|||
md5sum_file.write(action_md5sum) |
|||
if md5sum_path in self.changed_files: |
|||
self.changed_files[md5sum_path] = 'M' |
|||
else: |
|||
self.changed_files[md5sum_path] = 'N' |
|||
|
|||
def _check_stretch(self, source_resolution: Tuple[int, int], |
|||
resolution: Tuple[int, int], |
|||
stretch: bool) -> bool: |
|||
"""Метод определяющий необходимость растягивания исходного изображения |
|||
и делающий вывод о том, возможно ли создание изображения заданного |
|||
разрешения исходя из значения параметра stretch.""" |
|||
return (stretch |
|||
or (source_resolution[0] >= resolution[0] |
|||
and source_resolution[1] >= resolution[1])) |
|||
|
|||
@property |
|||
def warnings(self): |
|||
return self._warnings |