vendor/pimcore/pimcore/models/Asset/Image/Thumbnail.php line 57

Open in your IDE?
  1. <?php
  2. /**
  3. * Pimcore
  4. *
  5. * This source file is available under two different licenses:
  6. * - GNU General Public License version 3 (GPLv3)
  7. * - Pimcore Commercial License (PCL)
  8. * Full copyright and license information is available in
  9. * LICENSE.md which is distributed with this source code.
  10. *
  11. * @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12. * @license http://www.pimcore.org/license GPLv3 and PCL
  13. */
  14. namespace Pimcore\Model\Asset\Image;
  15. use Pimcore\Event\AssetEvents;
  16. use Pimcore\Event\FrontendEvents;
  17. use Pimcore\Logger;
  18. use Pimcore\Model\Asset;
  19. use Pimcore\Model\Asset\Image;
  20. use Pimcore\Model\Asset\Thumbnail\ImageThumbnailTrait;
  21. use Pimcore\Model\Exception\NotFoundException;
  22. use Pimcore\Tool;
  23. use Symfony\Component\EventDispatcher\GenericEvent;
  24. final class Thumbnail
  25. {
  26. use ImageThumbnailTrait;
  27. /**
  28. * @internal
  29. *
  30. * @var bool[]
  31. */
  32. protected static $hasListenersCache = [];
  33. /**
  34. * @param Image $asset
  35. * @param string|array|Thumbnail\Config|null $config
  36. * @param bool $deferred
  37. */
  38. public function __construct($asset, $config = null, $deferred = true)
  39. {
  40. $this->asset = $asset;
  41. $this->deferred = $deferred;
  42. $this->config = $this->createConfig($config);
  43. }
  44. /**
  45. * @param bool $deferredAllowed
  46. * @param bool $cacheBuster
  47. *
  48. * @return string
  49. */
  50. public function getPath($deferredAllowed = true, $cacheBuster = false)
  51. {
  52. $pathReference = null;
  53. if ($this->getConfig()) {
  54. if ($this->useOriginalFile($this->asset->getFilename()) && $this->getConfig()->isSvgTargetFormatPossible()) {
  55. // we still generate the raster image, to get the final size of the thumbnail
  56. // we use getRealFullPath() here, to avoid double encoding (getFullPath() returns already encoded path)
  57. $pathReference = [
  58. 'src' => $this->asset->getRealFullPath(),
  59. 'type' => 'asset',
  60. ];
  61. }
  62. }
  63. if (!$pathReference) {
  64. $pathReference = $this->getPathReference($deferredAllowed);
  65. }
  66. $path = $this->convertToWebPath($pathReference);
  67. if ($cacheBuster) {
  68. $path = $this->addCacheBuster($path, ['cacheBuster' => true], $this->getAsset());
  69. }
  70. if ($this->hasListeners(FrontendEvents::ASSET_IMAGE_THUMBNAIL)) {
  71. $event = new GenericEvent($this, [
  72. 'pathReference' => $pathReference,
  73. 'frontendPath' => $path,
  74. ]);
  75. \Pimcore::getEventDispatcher()->dispatch($event, FrontendEvents::ASSET_IMAGE_THUMBNAIL);
  76. $path = $event->getArgument('frontendPath');
  77. }
  78. return $path;
  79. }
  80. public function getFileSize(): ?int
  81. {
  82. $pathReference = $this->getPathReference(false);
  83. if ($pathReference['type'] === 'asset') {
  84. return $this->asset->getFileSize();
  85. } elseif (isset($pathReference['storagePath'])) {
  86. return Tool\Storage::get('thumbnail')->fileSize($pathReference['storagePath']);
  87. }
  88. return null;
  89. }
  90. /**
  91. * @return null|resource
  92. */
  93. public function getStream()
  94. {
  95. $pathReference = $this->getPathReference(false);
  96. if ($pathReference['type'] === 'asset') {
  97. return $this->asset->getStream();
  98. } elseif (isset($pathReference['storagePath'])) {
  99. return Tool\Storage::get('thumbnail')->readStream($pathReference['storagePath']);
  100. }
  101. return null;
  102. }
  103. /**
  104. * @param string $eventName
  105. *
  106. * @return bool
  107. */
  108. protected function hasListeners(string $eventName): bool
  109. {
  110. if (!isset(self::$hasListenersCache[$eventName])) {
  111. self::$hasListenersCache[$eventName] = \Pimcore::getEventDispatcher()->hasListeners($eventName);
  112. }
  113. return self::$hasListenersCache[$eventName];
  114. }
  115. /**
  116. * @param string $filename
  117. *
  118. * @return bool
  119. */
  120. protected function useOriginalFile($filename)
  121. {
  122. if ($this->getConfig()) {
  123. if (!$this->getConfig()->isRasterizeSVG() && preg_match("@\.svgz?$@", $filename)) {
  124. return true;
  125. }
  126. }
  127. return false;
  128. }
  129. /**
  130. * @internal
  131. *
  132. * @param bool $deferredAllowed
  133. */
  134. public function generate($deferredAllowed = true)
  135. {
  136. $deferred = false;
  137. $generated = false;
  138. if ($this->asset && empty($this->pathReference)) {
  139. // if no correct thumbnail config is given use the original image as thumbnail
  140. if (!$this->config) {
  141. $this->pathReference = [
  142. 'type' => 'asset',
  143. 'src' => $this->asset->getRealFullPath(),
  144. ];
  145. } else {
  146. try {
  147. $deferred = $deferredAllowed && $this->deferred;
  148. $this->pathReference = Thumbnail\Processor::process($this->asset, $this->config, null, $deferred, $generated);
  149. } catch (\Exception $e) {
  150. Logger::error("Couldn't create thumbnail of image " . $this->asset->getRealFullPath());
  151. Logger::error($e);
  152. }
  153. }
  154. }
  155. if (empty($this->pathReference)) {
  156. $this->pathReference = [
  157. 'type' => 'error',
  158. 'src' => '/bundles/pimcoreadmin/img/filetype-not-supported.svg',
  159. ];
  160. }
  161. if ($this->hasListeners(AssetEvents::IMAGE_THUMBNAIL)) {
  162. $event = new GenericEvent($this, [
  163. 'deferred' => $deferred,
  164. 'generated' => $generated,
  165. ]);
  166. \Pimcore::getEventDispatcher()->dispatch($event, AssetEvents::IMAGE_THUMBNAIL);
  167. }
  168. }
  169. /**
  170. * @return string Public path to thumbnail image.
  171. */
  172. public function __toString()
  173. {
  174. return $this->getPath(true);
  175. }
  176. /**
  177. * @param string $path
  178. * @param array $options
  179. * @param Asset $asset
  180. *
  181. * @return string
  182. */
  183. private function addCacheBuster(string $path, array $options, Asset $asset): string
  184. {
  185. if (isset($options['cacheBuster']) && $options['cacheBuster']) {
  186. $path = '/cache-buster-' . $asset->getVersionCount() . $path;
  187. }
  188. return $path;
  189. }
  190. private function getSourceTagHtml(Image\Thumbnail\Config $thumbConfig, string $mediaQuery, Image $image, array $options): string
  191. {
  192. $srcSetValues = [];
  193. $sourceTagAttributes = [];
  194. $thumb = null;
  195. foreach ([1, 2] as $highRes) {
  196. $thumbConfigRes = clone $thumbConfig;
  197. $thumbConfigRes->selectMedia($mediaQuery);
  198. $thumbConfigRes->setHighResolution($highRes);
  199. $thumb = $image->getThumbnail($thumbConfigRes, true);
  200. $descriptor = $highRes . 'x';
  201. $srcSetValues[] = $this->addCacheBuster($thumb . ' ' . $descriptor, $options, $image);
  202. if ($this->useOriginalFile($this->asset->getFilename()) && $this->getConfig()->isSvgTargetFormatPossible()) {
  203. break;
  204. }
  205. }
  206. $sourceTagAttributes['srcset'] = implode(', ', $srcSetValues);
  207. if ($mediaQuery) {
  208. $sourceTagAttributes['media'] = $mediaQuery;
  209. $thumb->reset();
  210. }
  211. if (isset($options['previewDataUri'])) {
  212. $sourceTagAttributes['data-srcset'] = $sourceTagAttributes['srcset'];
  213. unset($sourceTagAttributes['srcset']);
  214. }
  215. $sourceTagAttributes['type'] = $thumb->getMimeType();
  216. $sourceCallback = $options['sourceCallback'] ?? null;
  217. if ($sourceCallback) {
  218. $sourceTagAttributes = $sourceCallback($sourceTagAttributes);
  219. }
  220. return '<source ' . array_to_html_attribute_string($sourceTagAttributes) . ' />';
  221. }
  222. /**
  223. * Get generated HTML for displaying the thumbnail image in a HTML document.
  224. *
  225. * @param array $options Custom configuration
  226. *
  227. * @return string
  228. */
  229. public function getHtml($options = [])
  230. {
  231. /** @var Image $image */
  232. $image = $this->getAsset();
  233. $thumbConfig = $this->getConfig();
  234. $pictureTagAttributes = $options['pictureAttributes'] ?? []; // this is used for the html5 <picture> element
  235. if ((isset($options['lowQualityPlaceholder']) && $options['lowQualityPlaceholder']) && !Tool::isFrontendRequestByAdmin()) {
  236. $previewDataUri = $image->getLowQualityPreviewDataUri();
  237. if (!$previewDataUri) {
  238. // use a 1x1 transparent GIF as a fallback if no LQIP exists
  239. $previewDataUri = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
  240. }
  241. // this gets used in getImagTag() later
  242. $options['previewDataUri'] = $previewDataUri;
  243. }
  244. $isAutoFormat = $thumbConfig instanceof Image\Thumbnail\Config ? strtolower($thumbConfig->getFormat()) === 'source' : false;
  245. if ($isAutoFormat) {
  246. // ensure the default image is not WebP
  247. $this->pathReference = [];
  248. }
  249. $pictureCallback = $options['pictureCallback'] ?? null;
  250. if ($pictureCallback) {
  251. $pictureTagAttributes = $pictureCallback($pictureTagAttributes);
  252. }
  253. $html = '<picture ' . array_to_html_attribute_string($pictureTagAttributes) . '>' . "\n";
  254. if ($thumbConfig instanceof Image\Thumbnail\Config) {
  255. $mediaConfigs = $thumbConfig->getMedias();
  256. // currently only max-width is supported, the key of the media is WIDTHw (eg. 400w) according to the srcset specification
  257. ksort($mediaConfigs, SORT_NUMERIC);
  258. array_push($mediaConfigs, $thumbConfig->getItems()); //add the default config at the end - picturePolyfill v4
  259. foreach ($mediaConfigs as $mediaQuery => $config) {
  260. $sourceHtml = $this->getSourceTagHtml($thumbConfig, $mediaQuery, $image, $options);
  261. if (!empty($sourceHtml)) {
  262. if ($isAutoFormat && self::supportsFormat('webp')) {
  263. $thumbConfigWebP = clone $thumbConfig;
  264. $thumbConfigWebP->setFormat('webp');
  265. $sourceWebP = $this->getSourceTagHtml($thumbConfigWebP, $mediaQuery, $image, $options);
  266. if (!empty($sourceWebP)) {
  267. $html .= "\t" . $sourceWebP . "\n";
  268. }
  269. }
  270. $html .= "\t" . $sourceHtml . "\n";
  271. }
  272. }
  273. }
  274. if (!($options['disableImgTag'] ?? null)) {
  275. $html .= "\t" . $this->getImageTag($options) . "\n";
  276. }
  277. $html .= '</picture>' . "\n";
  278. if (isset($options['useDataSrc']) && $options['useDataSrc']) {
  279. $html = preg_replace('/ src(set)?=/i', ' data-src$1=', $html);
  280. }
  281. return $html;
  282. }
  283. /**
  284. * @param array $options
  285. * @param array $removeAttributes
  286. *
  287. * @return string
  288. */
  289. public function getImageTag(array $options = [], array $removeAttributes = []): string
  290. {
  291. /** @var Image $image */
  292. $image = $this->getAsset();
  293. $attributes = $options['imgAttributes'] ?? [];
  294. $callback = $options['imgCallback'] ?? null;
  295. if (isset($options['previewDataUri'])) {
  296. $attributes['src'] = $options['previewDataUri'];
  297. } else {
  298. $path = $this->getPath(true);
  299. $attributes['src'] = $this->addCacheBuster($path, $options, $image);
  300. }
  301. if (!isset($options['disableWidthHeightAttributes'])) {
  302. if ($this->getWidth()) {
  303. $attributes['width'] = $this->getWidth();
  304. }
  305. if ($this->getHeight()) {
  306. $attributes['height'] = $this->getHeight();
  307. }
  308. }
  309. $altText = $attributes['alt'] ?? '';
  310. $titleText = $attributes['title'] ?? '';
  311. if (empty($titleText) && (!isset($options['disableAutoTitle']) || !$options['disableAutoTitle'])) {
  312. if ($image->getMetadata('title')) {
  313. $titleText = $image->getMetadata('title');
  314. }
  315. }
  316. if (empty($altText) && (!isset($options['disableAutoAlt']) || !$options['disableAutoAlt'])) {
  317. if ($image->getMetadata('alt')) {
  318. $altText = $image->getMetadata('alt');
  319. } elseif (isset($options['defaultalt'])) {
  320. $altText = $options['defaultalt'];
  321. } else {
  322. $altText = $titleText;
  323. }
  324. }
  325. // get copyright from asset
  326. if ($image->getMetadata('copyright') && (!isset($options['disableAutoCopyright']) || !$options['disableAutoCopyright'])) {
  327. if (!empty($altText)) {
  328. $altText .= ' | ';
  329. }
  330. if (!empty($titleText)) {
  331. $titleText .= ' | ';
  332. }
  333. $altText .= ('© ' . $image->getMetadata('copyright'));
  334. $titleText .= ('© ' . $image->getMetadata('copyright'));
  335. }
  336. $attributes['alt'] = $altText;
  337. if (!empty($titleText)) {
  338. $attributes['title'] = $titleText;
  339. }
  340. if (!isset($attributes['loading'])) {
  341. $attributes['loading'] = 'lazy';
  342. }
  343. foreach ($removeAttributes as $attribute) {
  344. unset($attributes[$attribute]);
  345. }
  346. if ($callback) {
  347. $attributes = $callback($attributes);
  348. }
  349. $htmlImgTag = '';
  350. if (!empty($attributes)) {
  351. $htmlImgTag = '<img ' . array_to_html_attribute_string($attributes) . ' />';
  352. }
  353. return $htmlImgTag;
  354. }
  355. /**
  356. * @param string $name
  357. * @param int $highRes
  358. *
  359. * @return Thumbnail
  360. *
  361. * @throws \Exception
  362. */
  363. public function getMedia($name, $highRes = 1)
  364. {
  365. $thumbConfig = $this->getConfig();
  366. $mediaConfigs = $thumbConfig->getMedias();
  367. if (isset($mediaConfigs[$name])) {
  368. $thumbConfigRes = clone $thumbConfig;
  369. $thumbConfigRes->selectMedia($name);
  370. $thumbConfigRes->setHighResolution($highRes);
  371. $thumbConfigRes->setMedias([]);
  372. /** @var Image $asset */
  373. $asset = $this->getAsset();
  374. $thumb = $asset->getThumbnail($thumbConfigRes);
  375. return $thumb;
  376. } else {
  377. throw new \Exception("Media query '" . $name . "' doesn't exist in thumbnail configuration: " . $thumbConfig->getName());
  378. }
  379. }
  380. /**
  381. * Get a thumbnail image configuration.
  382. *
  383. * @param string|array|Thumbnail\Config $selector Name, array or object describing a thumbnail configuration.
  384. *
  385. * @return Thumbnail\Config
  386. *
  387. * @throws NotFoundException
  388. */
  389. private function createConfig($selector)
  390. {
  391. $thumbnailConfig = Thumbnail\Config::getByAutoDetect($selector);
  392. if (!empty($selector) && $thumbnailConfig === null) {
  393. throw new NotFoundException('Thumbnail definition "' . (is_string($selector) ? $selector : '') . '" does not exist');
  394. }
  395. return $thumbnailConfig;
  396. }
  397. }