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

  1. # vim: fileencoding=utf-8
  2. #
  3. from ..template_engine import ParametersContainer, Variables
  4. from ...variables.datavars import NamespaceNode, VariableNotFoundError
  5. from ...variables.loader import Datavars
  6. from ...utils.images import ImageMagick
  7. from .base_format import Format, FormatError
  8. from typing import Union, List, Tuple, NoReturn
  9. import hashlib
  10. import re
  11. import os
  12. class BackgroundsFormat(Format):
  13. FORMAT = 'backgrounds'
  14. EXECUTABLE = True
  15. FORMAT_PARAMETERS = {'convert', 'stretch'}
  16. def __init__(self, template_text: str,
  17. template_path: str,
  18. parameters: ParametersContainer,
  19. datavars: Union[Datavars, NamespaceNode, Variables],
  20. chroot_path: str = "/"):
  21. self._lines: List[str] = [line for line
  22. in template_text.strip().split('\n') if line]
  23. self._datavars: Union[Datavars, NamespaceNode, Variables] = datavars
  24. if parameters.source:
  25. self._source = parameters.source
  26. else:
  27. raise FormatError("source parameter is not set for template with"
  28. "with 'backgrounds' format.")
  29. self._mirror: bool = parameters.mirror
  30. self._stretch: bool = parameters.stretch
  31. self._convert: Union[bool, str] = parameters.convert
  32. self._empty_name: bool = (parameters.name == '')
  33. # Измененные файлы.
  34. self.changed_files: dict = dict()
  35. # Список предупреждений.
  36. self._warnings: list = []
  37. # Флаг для тестов.
  38. self._fake_chroot: bool = False
  39. try:
  40. self._fake_chroot = datavars.main.fake_chroot
  41. except Exception:
  42. pass
  43. def execute_format(self, target_path: str, chroot_path: str = '/') -> dict:
  44. '''Метод для запуска работы формата.'''
  45. if not self._check_source(self._source, target_path, self._mirror):
  46. return self.changed_files
  47. print(f"FAKE_CHROOT: {self._fake_chroot}")
  48. self._magician = ImageMagick(
  49. chroot=chroot_path if not self._fake_chroot else "/")
  50. source_resolution = self._magician.get_image_resolution(self._source)
  51. resolutions_from_var = False
  52. resolutions = self._get_resolutions_from_lines(self._lines,
  53. source_resolution)
  54. if not resolutions:
  55. resolutions = self._get_resolutions_from_var(self._datavars)
  56. resolutions_from_var = True
  57. action_md5sum = self._get_action_md5sum(self._source, resolutions)
  58. if not self._check_target_directory(target_path, self._mirror,
  59. resolutions, action_md5sum):
  60. return {}
  61. images_format = self._get_format_value(self._convert,
  62. self._datavars)
  63. if self._convert and self._convert == "GFXBOOT":
  64. converter = self._magician.convert_resize_gfxboot
  65. else:
  66. converter = self._magician.convert_resize_crop_center
  67. output_paths = self._make_output_paths(self._lines, images_format,
  68. target_path, resolutions,
  69. resolutions_from_var)
  70. # Выполняем преобразование изображений в требуемый размер и формат.
  71. for resolution, output_path in output_paths.items():
  72. if not self._check_stretch(source_resolution,
  73. resolution,
  74. self._stretch):
  75. continue
  76. width, height = resolution
  77. converter(self._source, output_path, height, width,
  78. image_format=images_format)
  79. if output_path in self.changed_files:
  80. self.changed_files[output_path] = 'M'
  81. else:
  82. self.changed_files[output_path] = 'N'
  83. self._create_md5sum_file(target_path, action_md5sum)
  84. return self.changed_files
  85. def _get_format_value(self, convert: Union[bool, str],
  86. datavars: Union[Datavars, NamespaceNode, Variables]
  87. ) -> str:
  88. """Метод для получения значения формата."""
  89. if convert:
  90. if convert == "JPG" or convert == "GFXBOOT":
  91. images_format = "JPEG"
  92. else:
  93. images_format = convert
  94. else:
  95. images_format = self._magician.get_image_format(self._source)
  96. return images_format
  97. def _make_output_paths(self, lines: List[str],
  98. images_format: str,
  99. target_path: str,
  100. resolutions: List[Tuple[int, int]],
  101. resolutions_from_var: bool
  102. ) -> List[str]:
  103. """Метод для получения списка путей, по которым будут создаваться
  104. изображения."""
  105. if not self._empty_name:
  106. path, name = os.path.split(target_path)
  107. else:
  108. path = os.path.dirname(target_path)
  109. name = ''
  110. paths = {}
  111. if not resolutions_from_var and len(resolutions) == 1:
  112. if self._empty_name:
  113. raise FormatError("'name' parameter is empty in 'backgrounds'"
  114. " format template with single file output.")
  115. paths = {next(iter(resolutions)): target_path}
  116. return paths
  117. for width, height in resolutions:
  118. paths[(width, height)] = (f"{path}/{name}{width}x{height}"
  119. f".{images_format.lower()}")
  120. return paths
  121. def _get_resolutions_from_lines(self, lines: List[str],
  122. source_resolution: Tuple[int, int]
  123. ) -> List[Tuple[int, int]]:
  124. """Метод для получения списка кортежей с разрешениями выходных
  125. изображений из текста шаблона."""
  126. resolutions = []
  127. if lines:
  128. for line in lines:
  129. if (line.strip() == "original"
  130. and source_resolution not in resolutions):
  131. resolutions.append(source_resolution)
  132. else:
  133. try:
  134. width, height = line.lower().strip().split("x")
  135. resolution = (int(width), int(height))
  136. except Exception:
  137. raise FormatError("can not parse line from template"
  138. " with 'backgrounds' format:"
  139. f" '{line}'")
  140. if resolution not in resolutions:
  141. resolutions.append(resolution)
  142. return resolutions
  143. def _get_resolutions_from_var(self, datavars: Union[Datavars,
  144. NamespaceNode,
  145. Variables]
  146. ) -> List[Tuple[int, int]]:
  147. """Метод для получения списка кортежей с разрешениями выходных
  148. изображений из переменной."""
  149. try:
  150. resolutions = []
  151. for resolution in self._datavars.main.cl_resolutions:
  152. resolution = tuple(resolution.strip().split("x"))
  153. resolutions.append(resolution)
  154. return resolutions
  155. except VariableNotFoundError:
  156. raise FormatError("resolutions values was not found.")
  157. except Exception:
  158. raise FormatError("can not use resolution values from variable:"
  159. " 'main.cl_resolutions'")
  160. def _check_target_directory(self, target_path: str, mirror: bool,
  161. resolutions: List[Tuple[int, int]],
  162. action_md5sum: str
  163. ) -> Union[bool, str]:
  164. """Метод для проверки содержимого целевой директории, удаления
  165. изображений и md5-сумм, подлежащих удалению, а также сравнения
  166. имеющейся в директории md5-суммы с суммой, полученной для действия
  167. текущего шаблона."""
  168. if not self._empty_name:
  169. path, name = os.path.split(target_path)
  170. else:
  171. path = os.path.dirname(target_path)
  172. name = ''
  173. name_pattern = re.compile(rf"^{name}\d+x\d+")
  174. md5sum = None
  175. images = []
  176. # Проверяем содержимое целевой директории.
  177. for node in os.scandir(path):
  178. if node.is_file():
  179. if (node.name ==
  180. f"{name.strip('-_.')}{'.' if name else ''}md5sum"):
  181. md5sum = node.name
  182. elif (node.name == name
  183. or (name_pattern.search(node.name) is not None)):
  184. images.append(node.name)
  185. if not images and md5sum is None:
  186. # Если нет файла суммы и нет изображений -- продолжаем выполнение
  187. # шаблона.
  188. return True
  189. elif not images and md5sum is not None:
  190. # Если есть файл суммы, но нет изображений -- удаляем файл суммы.
  191. md5sum_path = os.path.join(path, md5sum)
  192. os.unlink(md5sum_path)
  193. self.changed_files[md5sum_path] = 'D'
  194. return True
  195. elif images and md5sum is None:
  196. # Если есть файлы изображений, но нет суммы -- удаляем файлы
  197. # изображений.
  198. for image in images:
  199. image_path = os.path.join(path, image)
  200. os.unlink(image_path)
  201. self.changed_files[image_path] = 'D'
  202. return True
  203. else:
  204. # Сравниваем суммы из md5sum и для текущего действия если они
  205. # сходятся, то делать ничего не надо, если нет -- удаляем
  206. # имеющиеся изображения и md5-сумму.
  207. with open(os.path.join(path, md5sum), "r") as md5sum_file:
  208. current_md5sum = md5sum_file.read().strip()
  209. if current_md5sum != action_md5sum:
  210. for image in images:
  211. image_path = os.path.join(path, image)
  212. os.unlink(image_path)
  213. self.changed_files[image_path] = 'D'
  214. md5sum_path = os.path.join(path, md5sum)
  215. os.unlink(md5sum_path)
  216. self.changed_files[md5sum_path] = 'D'
  217. return True
  218. else:
  219. return False
  220. def _check_source(self, source: str, target_path: str, mirror: bool
  221. ) -> bool:
  222. """Метод для проверки исходного изображения."""
  223. if not self._empty_name:
  224. path, name = os.path.split(target_path)
  225. else:
  226. path = os.path.dirname(target_path)
  227. name = ''
  228. name_pattern = re.compile(rf"^{name}\d+x\d+")
  229. if not os.path.exists(source):
  230. if mirror:
  231. for node in os.scandir(path):
  232. if (node.name == name
  233. or name_pattern.search(node.name) is not None
  234. or node.name == (f"{node.name.strip('_-.')}"
  235. f"{ '.' if name else '' }md5sum")):
  236. os.unlink(node.path)
  237. self.changed_files[node.path] = "D"
  238. return False
  239. else:
  240. raise FormatError("image from 'source' parameter does not"
  241. f" exist: {source}.")
  242. return True
  243. def _get_action_md5sum(self, source: str,
  244. resolutions: List[Tuple[int, int]]) -> bool:
  245. """Метод для получения md5-суммы текущего действия шаблона,
  246. рассчитываемой из последовательности байтов изображения и списка
  247. разрешений, в которые данный файл должен быть конвертирован."""
  248. print("RESOLUTIONS:", resolutions)
  249. with open(source, "rb") as source_file:
  250. md5_object = hashlib.md5(source_file.read())
  251. for width, height in resolutions:
  252. resolution = f"{width}x{height}"
  253. md5_object.update(resolution.encode())
  254. return md5_object.hexdigest()
  255. def _create_md5sum_file(self, target_path: str, action_md5sum: str
  256. ) -> NoReturn:
  257. """Метод для создания файла с md5-суммой действия, выполненного
  258. данным шаблоном."""
  259. if not self._empty_name:
  260. path, name = os.path.split(target_path)
  261. else:
  262. path = os.path.dirname(target_path)
  263. name = ''
  264. md5sum_path = (f"{path}/{name.strip('_-.')}"
  265. f"{ '.' if name else '' }md5sum")
  266. with open(md5sum_path, "w") as md5sum_file:
  267. md5sum_file.write(action_md5sum)
  268. if md5sum_path in self.changed_files:
  269. self.changed_files[md5sum_path] = 'M'
  270. else:
  271. self.changed_files[md5sum_path] = 'N'
  272. def _check_stretch(self, source_resolution: Tuple[int, int],
  273. resolution: Tuple[int, int],
  274. stretch: bool) -> bool:
  275. """Метод определяющий необходимость растягивания исходного изображения
  276. и делающий вывод о том, возможно ли создание изображения заданного
  277. разрешения исходя из значения параметра stretch."""
  278. return (stretch
  279. or (source_resolution[0] >= resolution[0]
  280. and source_resolution[1] >= resolution[1]))
  281. @property
  282. def warnings(self):
  283. return self._warnings