You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

319 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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