Files

326 lines
9.7 KiB
PHP
Executable File

<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* 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);
}
}