# 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