|
|
# vim: fileencoding=utf-8
|
|
|
#
|
|
|
from ..template_engine import ParametersContainer, Variables
|
|
|
from ...variables.datavars import NamespaceNode, VariableNotFoundError
|
|
|
from ...utils.images import ImageMagick, ImageMagickError
|
|
|
from ...variables.loader import Datavars
|
|
|
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 = ParametersContainer(),
|
|
|
datavars: Union[Datavars,
|
|
|
NamespaceNode,
|
|
|
Variables] = NamespaceNode("<root>"),
|
|
|
**kwargs):
|
|
|
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
|
|
|
|
|
|
try:
|
|
|
self._magician = ImageMagick(chroot=chroot_path
|
|
|
if not self._fake_chroot else "/")
|
|
|
source_resolution = self._magician.get_image_resolution(
|
|
|
self._source)
|
|
|
except ImageMagickError as error:
|
|
|
raise FormatError(f"ImageMagickError: {error}")
|
|
|
|
|
|
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
|
|
|
try:
|
|
|
converter(self._source, output_path, height, width,
|
|
|
image_format=images_format)
|
|
|
except ImageMagickError as error:
|
|
|
raise FormatError(f"ImageMagickError: {error}")
|
|
|
|
|
|
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:
|
|
|
try:
|
|
|
images_format = self._magician.get_image_format(self._source)
|
|
|
except ImageMagickError as error:
|
|
|
raise FormatError(f"ImageMagickError: {error}")
|
|
|
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-суммы текущего действия шаблона,
|
|
|
рассчитываемой из последовательности байтов изображения и списка
|
|
|
разрешений, в которые данный файл должен быть конвертирован."""
|
|
|
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
|