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