* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace TheliaLibrary\Service; use Imagine\Gd\Imagine; use Imagine\Gmagick\Imagine as GmagickImagine; use Imagine\Image\Box; use Imagine\Image\ImageInterface; use Imagine\Image\Palette\RGB; use Imagine\Image\Point; use Imagine\Imagick\Imagine as ImagickImagine; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Exception\HttpException; use Thelia\Model\ConfigQuery; use Thelia\Model\Lang; use TheliaLibrary\Model\LibraryImage; use TheliaLibrary\Model\LibraryImageQuery; use TheliaLibrary\TheliaLibrary; class ImageService { public const MAX_ALLOWED_SIZE_FACTOR = 2; public function __construct(private RequestStack $requestStack) { } public function getUrlForImage( $identifier, $format, $region = 'full', $size = 'max', $rotation = 0, $quality = 'default' ) { return "/image-library/$identifier/$region/$size/$rotation/$quality.$format"; } public function geFormattedImage( $identifier, $region, $size, $rotation, $quality, $format ) { if (!\in_array(strtolower($format), ['jpg', 'jpeg', 'png', 'gif', 'jp2', 'webp'])) { throw new HttpException(400, 'Bad format value'); } $formattedImagePath = THELIA_WEB_DIR.'image-library'.DS.$identifier.DS.$region.DS.$size.DS.$rotation; if (!is_dir($formattedImagePath)) { if (!@mkdir($formattedImagePath, 0755, true)) { throw new \RuntimeException(sprintf('Failed to create %s file in cache directory', $formattedImagePath)); } } $formattedImagePath .= DS.$quality.'.'.$format; if (file_exists($formattedImagePath)) { return $formattedImagePath; } $image = $this->openImage($identifier); $image = $this->applyRegion($image, $region); $image = $this->applySize($image, $size); $image = $this->applyRotation($image, $rotation, $format); $image = $this->applyQuality($image, $quality); $image->save($formattedImagePath); return $formattedImagePath; } public function applyRegion(ImageInterface $image, $region) { if ($region === 'full') { return $image; } $height = $image->getSize()->getHeight(); $width = $image->getSize()->getWidth(); if ($region === 'square') { $squareSize = min($width, $height); return $image->crop( new Point(($width - $squareSize) / 2, ($height - $squareSize) / 2), new Box($squareSize, $squareSize) ); } // If region start with pct: values are percent of size $regionMode = strpos($region, 'pct:') === false ? 'value' : 'percentage'; $values = explode(',', str_replace('pct:', '', $region)); if (\count($values) !== 4) { throw new HttpException(400, 'Bad region value'); } $xPositionValue = $regionMode === 'value' ? $values[0] : $width * $values[0] / 100; $yPositionValue = $regionMode === 'value' ? $values[1] : $height * $values[1] / 100; $widthValue = $regionMode === 'value' ? $values[2] : $width * $values[2] / 100; $heightValue = $regionMode === 'value' ? $values[3] : $height * $values[3] / 100; // If width or height + start position go outside of image crop the width and/or height if (($xPositionValue + $widthValue) > $width) { $widthValue = $width - $xPositionValue; } if (($yPositionValue + $heightValue) > $height) { $heightValue = $height - $yPositionValue; } if ($height <= 0 || $width <= 0) { throw new HttpException(400, 'Size out of bound'); } return $image->crop( new Point($xPositionValue, $yPositionValue), new Box($widthValue, $heightValue) ); } public function applySize(ImageInterface $image, $size) { $upscaleMode = false !== strpos($size, '^'); $keepAspectRatio = false !== strpos($size, '!'); $borders = false !== strpos($size, '*'); $size = str_replace('^', '', $size); $size = str_replace('!', '', $size); $size = str_replace('*', '', $size); if ($size === 'max') { if (!$upscaleMode) { return $image; } return $image->resize($this->getMaxSize($image)); } $width = $image->getSize()->getWidth(); $height = $image->getSize()->getHeight(); if (false !== strpos($size, 'pct:')) { $values = explode(':', $size); if (!isset($values[1])) { throw new HttpException(400, 'Bad size values'); } $percent = $values[1]; if (!$upscaleMode && $percent > 100) { throw new HttpException(400, 'Size out of bound'); } return $image->resize( new Box( $percent * $width / 100, $percent * $height / 100, ) ); } $values = explode(',', $size); if (\count($values) !== 2) { throw new HttpException(400, 'Bad size values'); } $newWidth = $values[0] != '' ? $values[0] : $values[1]; $newHeight = $values[1] != '' ? $values[1] : $values[0]; if ($keepAspectRatio) { $originalRatio = $width / $height; $askedRatio = $newWidth / $newHeight; // Keep the ratio and take the larger image possible but not greater than asked width and height $newWidth = $askedRatio <= $originalRatio ? $newWidth : $newHeight * $originalRatio; $newHeight = $askedRatio >= $originalRatio ? $newHeight : $newWidth / $originalRatio; } if (!$upscaleMode && ($newWidth > $width || $newHeight > $height)) { throw new HttpException(400, 'Size out of bound'); } $resizedImage = $image->resize( new Box( $newWidth, $newHeight, ) ); if ($borders) { $palette = new RGB(); $canvas = new Box($values[0], $values[1]); $canvasInstance = $this->getImagineInstance() ->create($canvas, $palette->color('fff', 0)); $borderWidth = (int) (($values[0] - $resizedImage->getSize()->getWidth()) / 2); $borderHeight = (int) (($values[1] - $resizedImage->getSize()->getHeight()) / 2); return $canvasInstance ->paste($resizedImage, new Point($borderWidth, $borderHeight)); } return $resizedImage; } public function applyRotation(ImageInterface $image, $rotation, $format) { if (false !== strpos($rotation, '!')) { $image = $image->flipHorizontally(); } $rotation = str_replace('!', '', $rotation); if (!is_numeric($rotation)) { throw new HttpException(400, 'Bad rotation values'); } $rotationValue = (float) $rotation; if (0 > $rotationValue || $rotationValue > 360) { throw new HttpException(400, 'Rotation out of bound'); } $color = new RGB(); $alpha = \in_array( strtolower($format), [ 'png', 'gif', 'webp', 'tiff', 'jp2', ] ) ? 0 : 100; return $image->rotate($rotationValue, $color->color('fff', $alpha)); } public function applyQuality(ImageInterface $image, $quality) { if (!\in_array($quality, ['color', 'gray', 'default', 'bitonal'])) { throw new HttpException(400, 'Bad quality values'); } if ($quality === 'gray') { $image->effects()->grayscale(); } return $image; } public function getImageFileName( LibraryImage $image = null ) { if (null == $image) { return null; } $locale = $this->requestStack?->getCurrentRequest()?->getSession()?->getLang()->getLocale(); if (null !== $locale) { $image->setLocale($locale); } $fileName = $image->getFileName(); if (null === $fileName) { $fileName = $image->setLocale(Lang::getDefaultLanguage()->getLocale())->getFileName() ?? null; $image->setLocale($locale); } return $fileName; } public function openImage($identifier) { $imageModel = LibraryImageQuery::create() ->filterById($identifier) ->findOne(); if (null === $imageModel) { throw new HttpException(404, 'Image not found'); } $fileName = $this->getImageFileName($imageModel); return $this->getImagineInstance()->open(TheliaLibrary::getImageDirectory().$fileName); } public function getImagineInstance() { $driver = ConfigQuery::read('imagine_graphic_driver', 'gd'); switch ($driver) { case 'imagick': $image = new ImagickImagine(); break; case 'gmagick': $image = new GmagickImagine(); break; case 'gd': default: $image = new Imagine(); } return $image; } public function getMaxSize(ImageInterface $image) { return new Box($image->getSize()->getWidth() * 2, $image->getSize()->getHeight() * 2); } }