326 lines
9.7 KiB
PHP
Executable File
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);
|
|
}
|
|
}
|