vendor/pimcore/pimcore/models/Asset/Image.php line 30

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;
  15. use Pimcore\Event\FrontendEvents;
  16. use Pimcore\File;
  17. use Pimcore\Model;
  18. use Pimcore\Tool;
  19. use Pimcore\Tool\Console;
  20. use Pimcore\Tool\Storage;
  21. use Symfony\Component\EventDispatcher\GenericEvent;
  22. use Symfony\Component\Process\Process;
  23. /**
  24.  * @method \Pimcore\Model\Asset\Dao getDao()
  25.  */
  26. class Image extends Model\Asset
  27. {
  28.     use Model\Asset\MetaData\EmbeddedMetaDataTrait;
  29.     /**
  30.      * {@inheritdoc}
  31.      */
  32.     protected $type 'image';
  33.     private bool $clearThumbnailsOnSave false;
  34.     /**
  35.      * {@inheritdoc}
  36.      */
  37.     protected function update($params = [])
  38.     {
  39.         if ($this->getDataChanged()) {
  40.             foreach (['imageWidth''imageHeight''imageDimensionsCalculated'] as $key) {
  41.                 $this->removeCustomSetting($key);
  42.             }
  43.         }
  44.         if ($params['isUpdate']) {
  45.             $this->clearThumbnails($this->clearThumbnailsOnSave);
  46.             $this->clearThumbnailsOnSave false// reset to default
  47.         }
  48.         parent::update($params);
  49.     }
  50.     /**
  51.      * @internal
  52.      */
  53.     public function detectFocalPoint(): bool
  54.     {
  55.         if ($this->getCustomSetting('focalPointX') && $this->getCustomSetting('focalPointY')) {
  56.             return false;
  57.         }
  58.         if ($faceCordintates $this->getCustomSetting('faceCoordinates')) {
  59.             $xPoints = [];
  60.             $yPoints = [];
  61.             foreach ($faceCordintates as $fc) {
  62.                 // focal point calculation
  63.                 $xPoints[] = ($fc['x'] + $fc['x'] + $fc['width']) / 2;
  64.                 $yPoints[] = ($fc['y'] + $fc['y'] + $fc['height']) / 2;
  65.             }
  66.             $focalPointX array_sum($xPoints) / count($xPoints);
  67.             $focalPointY array_sum($yPoints) / count($yPoints);
  68.             $this->setCustomSetting('focalPointX'$focalPointX);
  69.             $this->setCustomSetting('focalPointY'$focalPointY);
  70.             return true;
  71.         }
  72.         return false;
  73.     }
  74.     /**
  75.      * @internal
  76.      */
  77.     public function detectFaces(): bool
  78.     {
  79.         if ($this->getCustomSetting('faceCoordinates')) {
  80.             return false;
  81.         }
  82.         $config \Pimcore::getContainer()->getParameter('pimcore.config')['assets']['image']['focal_point_detection'];
  83.         if (!$config['enabled']) {
  84.             return false;
  85.         }
  86.         $facedetectBin \Pimcore\Tool\Console::getExecutable('facedetect');
  87.         if ($facedetectBin) {
  88.             $faceCoordinates = [];
  89.             $thumbnail $this->getThumbnail(Image\Thumbnail\Config::getPreviewConfig());
  90.             $reference $thumbnail->getPathReference();
  91.             if (in_array($reference['type'], ['asset''thumbnail'])) {
  92.                 $image $thumbnail->getLocalFile();
  93.                 if (null === $image) {
  94.                     return false;
  95.                 }
  96.                 $imageWidth $thumbnail->getWidth();
  97.                 $imageHeight $thumbnail->getHeight();
  98.                 $command = [$facedetectBin$image];
  99.                 Console::addLowProcessPriority($command);
  100.                 $process = new Process($command);
  101.                 $process->run();
  102.                 $result $process->getOutput();
  103.                 if (strpos($result"\n")) {
  104.                     $faces explode("\n"trim($result));
  105.                     foreach ($faces as $coordinates) {
  106.                         list($x$y$width$height) = explode(' '$coordinates);
  107.                         // percentages
  108.                         $Px = (int) $x $imageWidth 100;
  109.                         $Py = (int) $y $imageHeight 100;
  110.                         $Pw = (int) $width $imageWidth 100;
  111.                         $Ph = (int) $height $imageHeight 100;
  112.                         $faceCoordinates[] = [
  113.                             'x' => $Px,
  114.                             'y' => $Py,
  115.                             'width' => $Pw,
  116.                             'height' => $Ph,
  117.                         ];
  118.                     }
  119.                     $this->setCustomSetting('faceCoordinates'$faceCoordinates);
  120.                     return true;
  121.                 }
  122.             }
  123.         }
  124.         return false;
  125.     }
  126.     private function isLowQualityPreviewEnabled(): bool
  127.     {
  128.         return \Pimcore::getContainer()->getParameter('pimcore.config')['assets']['image']['low_quality_image_preview']['enabled'];
  129.     }
  130.     /**
  131.      * @internal
  132.      *
  133.      * @param null|string $generator
  134.      *
  135.      * @return bool|string
  136.      *
  137.      * @throws \Exception
  138.      */
  139.     public function generateLowQualityPreview($generator null)
  140.     {
  141.         if (!$this->isLowQualityPreviewEnabled()) {
  142.             return false;
  143.         }
  144.         // fallback
  145.         if (class_exists('Imagick')) {
  146.             // Imagick fallback
  147.             $path $this->getThumbnail(Image\Thumbnail\Config::getPreviewConfig())->getLocalFile();
  148.             if (null === $path) {
  149.                 return false;
  150.             }
  151.             $imagick = new \Imagick($path);
  152.             $imagick->setImageFormat('jpg');
  153.             $imagick->setOption('jpeg:extent''1kb');
  154.             $width $imagick->getImageWidth();
  155.             $height $imagick->getImageHeight();
  156.             // we can't use getImageBlob() here, because of a bug in combination with jpeg:extent
  157.             // http://www.imagemagick.org/discourse-server/viewtopic.php?f=3&t=24366
  158.             $tmpFile File::getLocalTempFilePath('jpg');
  159.             $imagick->writeImage($tmpFile);
  160.             $imageBase64 base64_encode(file_get_contents($tmpFile));
  161.             $imagick->destroy();
  162.             $svg = <<<EOT
  163. <?xml version="1.0" encoding="utf-8"?>
  164. <svg version="1.1"  xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="$width" height="$height" viewBox="0 0 $width $height" preserveAspectRatio="xMidYMid slice">
  165.     <filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
  166.     <feGaussianBlur stdDeviation="20 20" edgeMode="duplicate" />
  167.     <feComponentTransfer>
  168.       <feFuncA type="discrete" tableValues="1 1" />
  169.     </feComponentTransfer>
  170.   </filter>
  171.     <image filter="url(#blur)" x="0" y="0" height="100%" width="100%" xlink:href="data:image/jpg;base64,$imageBase64" />
  172. </svg>
  173. EOT;
  174.             $storagePath $this->getLowQualityPreviewStoragePath();
  175.             Storage::get('thumbnail')->write($storagePath$svg);
  176.             return $storagePath;
  177.         }
  178.         return false;
  179.     }
  180.     /**
  181.      * @return string
  182.      */
  183.     public function getLowQualityPreviewPath()
  184.     {
  185.         $storagePath $this->getLowQualityPreviewStoragePath();
  186.         $path $storagePath;
  187.         if (Tool::isFrontend()) {
  188.             $path urlencode_ignore_slash($storagePath);
  189.             $prefix \Pimcore::getContainer()->getParameter('pimcore.config')['assets']['frontend_prefixes']['thumbnail'];
  190.             $path $prefix $path;
  191.         }
  192.         $event = new GenericEvent($this, [
  193.             'storagePath' => $storagePath,
  194.             'frontendPath' => $path,
  195.         ]);
  196.         \Pimcore::getEventDispatcher()->dispatch($eventFrontendEvents::ASSET_IMAGE_THUMBNAIL);
  197.         $path $event->getArgument('frontendPath');
  198.         return $path;
  199.     }
  200.     /**
  201.      * @return string
  202.      */
  203.     private function getLowQualityPreviewStoragePath()
  204.     {
  205.         return sprintf(
  206.             '%s/%s/image-thumb__%s__-low-quality-preview.svg',
  207.             rtrim($this->getRealPath(), '/'),
  208.             $this->getId(),
  209.             $this->getId()
  210.         );
  211.     }
  212.     /**
  213.      * @return string|null
  214.      */
  215.     public function getLowQualityPreviewDataUri(): ?string
  216.     {
  217.         if (!$this->isLowQualityPreviewEnabled()) {
  218.             return null;
  219.         }
  220.         try {
  221.             $dataUri 'data:image/svg+xml;base64,' base64_encode(Storage::get('thumbnail')->read($this->getLowQualityPreviewStoragePath()));
  222.         } catch (\Exception $e) {
  223.             $dataUri null;
  224.         }
  225.         return $dataUri;
  226.     }
  227.     /**
  228.      * Legacy method for backwards compatibility. Use getThumbnail($config)->getConfig() instead.
  229.      *
  230.      * @internal
  231.      *
  232.      * @param string|array|Image\Thumbnail\Config|null $config
  233.      *
  234.      * @return Image\Thumbnail\Config|null
  235.      */
  236.     public function getThumbnailConfig($config)
  237.     {
  238.         $thumbnail $this->getThumbnail($config);
  239.         return $thumbnail->getConfig();
  240.     }
  241.     /**
  242.      * Returns a path to a given thumbnail or an thumbnail configuration.
  243.      *
  244.      * @param null|string|array|Image\Thumbnail\Config $config
  245.      * @param bool $deferred
  246.      *
  247.      * @return Image\Thumbnail
  248.      */
  249.     public function getThumbnail($config null$deferred true)
  250.     {
  251.         return new Image\Thumbnail($this$config$deferred);
  252.     }
  253.     /**
  254.      * @internal
  255.      *
  256.      * @throws \Exception
  257.      *
  258.      * @return null|\Pimcore\Image\Adapter
  259.      */
  260.     public static function getImageTransformInstance()
  261.     {
  262.         try {
  263.             $image \Pimcore\Image::getInstance();
  264.         } catch (\Exception $e) {
  265.             $image null;
  266.         }
  267.         if (!$image instanceof \Pimcore\Image\Adapter) {
  268.             throw new \Exception("Couldn't get instance of image tranform processor.");
  269.         }
  270.         return $image;
  271.     }
  272.     /**
  273.      * @return string
  274.      */
  275.     public function getFormat()
  276.     {
  277.         if ($this->getWidth() > $this->getHeight()) {
  278.             return 'landscape';
  279.         } elseif ($this->getWidth() == $this->getHeight()) {
  280.             return 'square';
  281.         } elseif ($this->getHeight() > $this->getWidth()) {
  282.             return 'portrait';
  283.         }
  284.         return 'unknown';
  285.     }
  286.     /**
  287.      * @param string|null $path
  288.      * @param bool $force
  289.      *
  290.      * @return array|null
  291.      *
  292.      * @throws \Exception
  293.      */
  294.     public function getDimensions($path null$force false)
  295.     {
  296.         if (!$force) {
  297.             $width $this->getCustomSetting('imageWidth');
  298.             $height $this->getCustomSetting('imageHeight');
  299.             if ($width && $height) {
  300.                 return [
  301.                     'width' => $width,
  302.                     'height' => $height,
  303.                 ];
  304.             }
  305.         }
  306.         if (!$path) {
  307.             $path $this->getLocalFile();
  308.         }
  309.         if (!$path) {
  310.             return null;
  311.         }
  312.         $dimensions null;
  313.         //try to get the dimensions with getimagesize because it is much faster than e.g. the Imagick-Adapter
  314.         if (is_readable($path)) {
  315.             $imageSize getimagesize($path);
  316.             if ($imageSize && $imageSize[0] && $imageSize[1]) {
  317.                 $dimensions = [
  318.                     'width' => $imageSize[0],
  319.                     'height' => $imageSize[1],
  320.                 ];
  321.             }
  322.         }
  323.         if (!$dimensions) {
  324.             $image self::getImageTransformInstance();
  325.             $status $image->load($path, ['preserveColor' => true'asset' => $this]);
  326.             if ($status === false) {
  327.                 return null;
  328.             }
  329.             $dimensions = [
  330.                 'width' => $image->getWidth(),
  331.                 'height' => $image->getHeight(),
  332.             ];
  333.         }
  334.         // EXIF orientation
  335.         if (function_exists('exif_read_data')) {
  336.             $exif = @exif_read_data($path);
  337.             if (is_array($exif)) {
  338.                 if (array_key_exists('Orientation'$exif)) {
  339.                     $orientation = (int)$exif['Orientation'];
  340.                     if (in_array($orientation, [5678])) {
  341.                         // flip height & width
  342.                         $dimensions = [
  343.                             'width' => $dimensions['height'],
  344.                             'height' => $dimensions['width'],
  345.                         ];
  346.                     }
  347.                 }
  348.             }
  349.         }
  350.         if (($width $dimensions['width']) && ($height $dimensions['height'])) {
  351.             // persist dimensions to database
  352.             $this->setCustomSetting('imageDimensionsCalculated'true);
  353.             $this->setCustomSetting('imageWidth'$width);
  354.             $this->setCustomSetting('imageHeight'$height);
  355.             $this->getDao()->updateCustomSettings();
  356.             $this->clearDependentCache();
  357.         }
  358.         return $dimensions;
  359.     }
  360.     /**
  361.      * @return int
  362.      */
  363.     public function getWidth()
  364.     {
  365.         $dimensions $this->getDimensions();
  366.         if ($dimensions) {
  367.             return $dimensions['width'];
  368.         }
  369.         return 0;
  370.     }
  371.     /**
  372.      * @return int
  373.      */
  374.     public function getHeight()
  375.     {
  376.         $dimensions $this->getDimensions();
  377.         if ($dimensions) {
  378.             return $dimensions['height'];
  379.         }
  380.         return 0;
  381.     }
  382.     /**
  383.      * {@inheritdoc}
  384.      */
  385.     public function setCustomSetting($key$value)
  386.     {
  387.         if (in_array($key, ['focalPointX''focalPointY'])) {
  388.             // if the focal point changes we need to clean all thumbnails on save
  389.             if ($this->getCustomSetting($key) != $value) {
  390.                 $this->clearThumbnailsOnSave true;
  391.             }
  392.         }
  393.         return parent::setCustomSetting($key$value);
  394.     }
  395.     /**
  396.      * @return bool
  397.      */
  398.     public function isVectorGraphic()
  399.     {
  400.         // we use a simple file-extension check, for performance reasons
  401.         if (preg_match("@\.(svgz?|eps|pdf|ps|ai|indd)$@"$this->getFilename())) {
  402.             return true;
  403.         }
  404.         return false;
  405.     }
  406.     /**
  407.      * Checks if this file represents an animated image (png or gif)
  408.      *
  409.      * @return bool
  410.      */
  411.     public function isAnimated()
  412.     {
  413.         $isAnimated false;
  414.         switch ($this->getMimeType()) {
  415.             case 'image/gif':
  416.                 $isAnimated $this->isAnimatedGif();
  417.                 break;
  418.             case 'image/png':
  419.                 $isAnimated $this->isAnimatedPng();
  420.                 break;
  421.             default:
  422.                 break;
  423.         }
  424.         return $isAnimated;
  425.     }
  426.     /**
  427.      * Checks if this object represents an animated gif file
  428.      *
  429.      * @return bool
  430.      */
  431.     private function isAnimatedGif()
  432.     {
  433.         $isAnimated false;
  434.         if ($this->getMimeType() == 'image/gif') {
  435.             $fileContent $this->getData();
  436.             /**
  437.              * An animated gif contains multiple "frames", with each frame having a header made up of:
  438.              *  - a static 4-byte sequence (\x00\x21\xF9\x04)
  439.              *  - 4 variable bytes
  440.              *  - a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?)
  441.              *
  442.              * @see http://it.php.net/manual/en/function.imagecreatefromgif.php#104473
  443.              */
  444.             $numberOfFrames preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s'$fileContent$matches);
  445.             $isAnimated $numberOfFrames 1;
  446.         }
  447.         return $isAnimated;
  448.     }
  449.     /**
  450.      * Checks if this object represents an animated png file
  451.      *
  452.      * @return bool
  453.      */
  454.     private function isAnimatedPng()
  455.     {
  456.         $isAnimated false;
  457.         if ($this->getMimeType() == 'image/png') {
  458.             $fileContent $this->getData();
  459.             /**
  460.              * Valid APNGs have an "acTL" chunk somewhere before their first "IDAT" chunk.
  461.              *
  462.              * @see http://foone.org/apng/
  463.              */
  464.             $posIDAT strpos($fileContent'IDAT');
  465.             if ($posIDAT !== false) {
  466.                 $isAnimated str_contains(substr($fileContent0$posIDAT), 'acTL');
  467.             }
  468.         }
  469.         return $isAnimated;
  470.     }
  471. }