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