vendor/contao/core-bundle/src/Image/PictureFactory.php line 332

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of Contao.
  5. *
  6. * (c) Leo Feyer
  7. *
  8. * @license LGPL-3.0-or-later
  9. */
  10. namespace Contao\CoreBundle\Image;
  11. use Contao\CoreBundle\Framework\ContaoFramework;
  12. use Contao\Image\ImageInterface;
  13. use Contao\Image\Picture;
  14. use Contao\Image\PictureConfiguration;
  15. use Contao\Image\PictureConfigurationItem;
  16. use Contao\Image\PictureGeneratorInterface;
  17. use Contao\Image\PictureInterface;
  18. use Contao\Image\ResizeConfiguration;
  19. use Contao\Image\ResizeOptions;
  20. use Contao\ImageSizeItemModel;
  21. use Contao\ImageSizeModel;
  22. use Contao\StringUtil;
  23. class PictureFactory implements PictureFactoryInterface
  24. {
  25. private const ASPECT_RATIO_THRESHOLD = 0.05;
  26. private const FORMATS_ORDER = [
  27. 'jxl' => 1,
  28. 'avif' => 2,
  29. 'heic' => 3,
  30. 'webp' => 4,
  31. 'png' => 5,
  32. 'jpg' => 6,
  33. 'jpeg' => 7,
  34. 'gif' => 8,
  35. ];
  36. private array $imageSizeItemsCache = [];
  37. private PictureGeneratorInterface $pictureGenerator;
  38. private ImageFactoryInterface $imageFactory;
  39. private ContaoFramework $framework;
  40. private bool $bypassCache;
  41. private array $imagineOptions;
  42. private string $defaultDensities = '';
  43. private array $predefinedSizes = [];
  44. /**
  45. * @internal
  46. */
  47. public function __construct(PictureGeneratorInterface $pictureGenerator, ImageFactoryInterface $imageFactory, ContaoFramework $framework, bool $bypassCache, array $imagineOptions)
  48. {
  49. $this->pictureGenerator = $pictureGenerator;
  50. $this->imageFactory = $imageFactory;
  51. $this->framework = $framework;
  52. $this->bypassCache = $bypassCache;
  53. $this->imagineOptions = $imagineOptions;
  54. }
  55. public function setDefaultDensities($densities): self
  56. {
  57. $this->defaultDensities = (string) $densities;
  58. return $this;
  59. }
  60. /**
  61. * Sets the predefined image sizes.
  62. */
  63. public function setPredefinedSizes(array $predefinedSizes): void
  64. {
  65. $this->predefinedSizes = $predefinedSizes;
  66. }
  67. public function create($path, $size = null, ?ResizeOptions $options = null): PictureInterface
  68. {
  69. $attributes = [];
  70. if ($path instanceof ImageInterface) {
  71. $image = $path;
  72. } else {
  73. $image = $this->imageFactory->create($path);
  74. }
  75. // Support arrays in a serialized form
  76. $size = StringUtil::deserialize($size);
  77. if (
  78. \is_array($size)
  79. && isset($size[2])
  80. && \is_string($size[2])
  81. && !isset($this->predefinedSizes[$size[2]])
  82. && 1 === substr_count($size[2], '_')
  83. ) {
  84. $image->setImportantPart($this->imageFactory->getImportantPartFromLegacyMode($image, $size[2]));
  85. $size[2] = ResizeConfiguration::MODE_CROP;
  86. }
  87. if ($size instanceof PictureConfiguration) {
  88. $config = $size;
  89. } else {
  90. [$config, $attributes, $configOptions] = $this->createConfig($size);
  91. }
  92. // Always prefer options passed to this function
  93. $options ??= $configOptions ?? new ResizeOptions();
  94. if (!$options->getImagineOptions()) {
  95. $options->setImagineOptions($this->imagineOptions);
  96. }
  97. $options->setBypassCache($options->getBypassCache() || $this->bypassCache);
  98. $picture = $this->pictureGenerator->generate($image, $config, $options);
  99. $attributes['hasSingleAspectRatio'] = $this->hasSingleAspectRatio($picture);
  100. return $this->addImageAttributes($picture, $attributes);
  101. }
  102. /**
  103. * Creates a picture configuration.
  104. *
  105. * @param int|array|null $size
  106. *
  107. * @phpstan-return array{0:PictureConfiguration, 1:array<string, string>, 2:ResizeOptions}
  108. */
  109. private function createConfig($size): array
  110. {
  111. if (!\is_array($size)) {
  112. $size = [0, 0, $size];
  113. }
  114. $options = new ResizeOptions();
  115. $config = new PictureConfiguration();
  116. $attributes = [];
  117. if (isset($size[2])) {
  118. // Database record
  119. if (is_numeric($size[2])) {
  120. $imageSizeModel = $this->framework->getAdapter(ImageSizeModel::class);
  121. $imageSizes = $imageSizeModel->findByPk($size[2]);
  122. $config->setSize($this->createConfigItem(null !== $imageSizes ? $imageSizes->row() : null));
  123. if (null !== $imageSizes) {
  124. $options->setSkipIfDimensionsMatch((bool) $imageSizes->skipIfDimensionsMatch);
  125. $formats = [];
  126. if ('' !== $imageSizes->formats) {
  127. $formatsString = implode(';', StringUtil::deserialize($imageSizes->formats, true));
  128. foreach (explode(';', $formatsString) as $format) {
  129. [$source, $targets] = explode(':', $format, 2);
  130. $targets = explode(',', $targets);
  131. if (!isset($formats[$source])) {
  132. $formats[$source] = $targets;
  133. continue;
  134. }
  135. $formats[$source] = array_unique(array_merge($formats[$source], $targets));
  136. usort(
  137. $formats[$source],
  138. static fn ($a, $b) => (self::FORMATS_ORDER[$a] ?? $a) <=> (self::FORMATS_ORDER[$b] ?? $b)
  139. );
  140. }
  141. }
  142. $config->setFormats($formats);
  143. }
  144. if ($imageSizes) {
  145. if ($imageSizes->cssClass) {
  146. $attributes['class'] = $imageSizes->cssClass;
  147. }
  148. if ($imageSizes->lazyLoading) {
  149. $attributes['loading'] = 'lazy';
  150. }
  151. }
  152. if (!\array_key_exists($size[2], $this->imageSizeItemsCache)) {
  153. $adapter = $this->framework->getAdapter(ImageSizeItemModel::class);
  154. $this->imageSizeItemsCache[$size[2]] = $adapter->findVisibleByPid($size[2], ['order' => 'sorting ASC']);
  155. }
  156. /** @var array<ImageSizeItemModel> $imageSizeItems */
  157. $imageSizeItems = $this->imageSizeItemsCache[$size[2]];
  158. if (null !== $imageSizeItems) {
  159. $configItems = [];
  160. foreach ($imageSizeItems as $imageSizeItem) {
  161. $configItems[] = $this->createConfigItem($imageSizeItem->row());
  162. }
  163. $config->setSizeItems($configItems);
  164. }
  165. return [$config, $attributes, $options];
  166. }
  167. // Predefined size
  168. if (isset($this->predefinedSizes[$size[2]])) {
  169. $imageSizes = $this->predefinedSizes[$size[2]];
  170. $config->setSize($this->createConfigItem($imageSizes));
  171. $config->setFormats($imageSizes['formats'] ?? []);
  172. $options->setSkipIfDimensionsMatch($imageSizes['skipIfDimensionsMatch'] ?? false);
  173. if (!empty($imageSizes['cssClass'])) {
  174. $attributes['class'] = $imageSizes['cssClass'];
  175. }
  176. if (!empty($imageSizes['lazyLoading'])) {
  177. $attributes['loading'] = 'lazy';
  178. }
  179. if (\count($imageSizes['items']) > 0) {
  180. $configItems = [];
  181. foreach ($imageSizes['items'] as $imageSizeItem) {
  182. $configItems[] = $this->createConfigItem($imageSizeItem);
  183. }
  184. $config->setSizeItems($configItems);
  185. }
  186. return [$config, $attributes, $options];
  187. }
  188. }
  189. $resizeConfig = new ResizeConfiguration();
  190. if (!empty($size[0])) {
  191. $resizeConfig->setWidth((int) $size[0]);
  192. }
  193. if (!empty($size[1])) {
  194. $resizeConfig->setHeight((int) $size[1]);
  195. }
  196. if (!empty($size[2])) {
  197. $resizeConfig->setMode($size[2]);
  198. }
  199. if ($resizeConfig->isEmpty()) {
  200. $options->setSkipIfDimensionsMatch(true);
  201. }
  202. $configItem = new PictureConfigurationItem();
  203. $configItem->setResizeConfig($resizeConfig);
  204. if ($this->defaultDensities) {
  205. $configItem->setDensities($this->defaultDensities);
  206. }
  207. $config->setSize($configItem);
  208. return [$config, $attributes, $options];
  209. }
  210. /**
  211. * Creates a picture configuration item.
  212. */
  213. private function createConfigItem(?array $imageSize = null): PictureConfigurationItem
  214. {
  215. $configItem = new PictureConfigurationItem();
  216. $resizeConfig = new ResizeConfiguration();
  217. if (null !== $imageSize) {
  218. if (!empty($imageSize['width'])) {
  219. $resizeConfig->setWidth((int) $imageSize['width']);
  220. }
  221. if (!empty($imageSize['height'])) {
  222. $resizeConfig->setHeight((int) $imageSize['height']);
  223. }
  224. if (!empty($imageSize['zoom'])) {
  225. $resizeConfig->setZoomLevel((int) $imageSize['zoom']);
  226. }
  227. if (!empty($imageSize['resizeMode'])) {
  228. $resizeConfig->setMode((string) $imageSize['resizeMode']);
  229. }
  230. $configItem->setResizeConfig($resizeConfig);
  231. if (!empty($imageSize['sizes'])) {
  232. $configItem->setSizes((string) $imageSize['sizes']);
  233. }
  234. if (!empty($imageSize['densities'])) {
  235. $configItem->setDensities((string) $imageSize['densities']);
  236. }
  237. if (!empty($imageSize['media'])) {
  238. $configItem->setMedia((string) $imageSize['media']);
  239. }
  240. }
  241. return $configItem;
  242. }
  243. private function addImageAttributes(PictureInterface $picture, array $attributes): PictureInterface
  244. {
  245. if (empty($attributes)) {
  246. return $picture;
  247. }
  248. $img = $picture->getImg();
  249. foreach ($attributes as $attribute => $value) {
  250. $img[$attribute] = $value;
  251. }
  252. return new Picture($img, $picture->getSources());
  253. }
  254. /**
  255. * Returns true if the aspect ratios of all sources of the picture are
  256. * nearly the same and differ less than the ASPECT_RATIO_THRESHOLD.
  257. */
  258. private function hasSingleAspectRatio(PictureInterface $picture): bool
  259. {
  260. if (0 === \count($picture->getSources())) {
  261. return true;
  262. }
  263. $img = $picture->getImg();
  264. if (empty($img['width']) || empty($img['height'])) {
  265. return false;
  266. }
  267. foreach ($picture->getSources() as $source) {
  268. if (empty($source['width']) || empty($source['height'])) {
  269. return false;
  270. }
  271. $diffA = abs($img['width'] / $img['height'] / ($source['width'] / $source['height']) - 1);
  272. $diffB = abs($img['height'] / $img['width'] / ($source['height'] / $source['width']) - 1);
  273. if ($diffA > self::ASPECT_RATIO_THRESHOLD && $diffB > self::ASPECT_RATIO_THRESHOLD) {
  274. return false;
  275. }
  276. }
  277. return true;
  278. }
  279. }