src/ResizeImageFunction.php
<?php
namespace Imponeer\Smarty\Extensions\Image;
use Imponeer\Contracts\Smarty\Extension\SmartyFunctionInterface;
use Imponeer\Smarty\Extensions\Image\Exceptions\AttributeEmptyException;
use Imponeer\Smarty\Extensions\Image\Exceptions\AttributeMustBeNumericException;
use Imponeer\Smarty\Extensions\Image\Exceptions\AttributeMustBeStringException;
use Imponeer\Smarty\Extensions\Image\Exceptions\BadFitValueException;
use Imponeer\Smarty\Extensions\Image\Exceptions\AtLeastWidthOrHeightMustBeUsedException;
use Imponeer\Smarty\Extensions\Image\Exceptions\RequiredArgumentException;
use Intervention\Image\Exception\NotReadableException;
use Intervention\Image\Image as SingleImage;
use Intervention\Image\ImageManagerStatic as Image;
use JsonException;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\InvalidArgumentException;
use Smarty_Internal_Template;
use SmartyCompilerException;
/**
* Describes {resize_image} smarty function
*
* @package Imponeer\Smarty\Extensions\Image
*/
class ResizeImageFunction implements SmartyFunctionInterface
{
/**
* @var CacheItemPoolInterface
*/
private $cache;
/**
* ResizeImageFunction constructor.
*
* @param CacheItemPoolInterface $cache
*/
public function __construct(CacheItemPoolInterface $cache)
{
$this->cache = $cache;
}
/**
* @inheritDoc
*/
public function getName(): string
{
return 'resized_image';
}
/**
* @inheritDoc
*
* @throws AttributeEmptyException
* @throws AttributeMustBeNumericException
* @throws AttributeMustBeStringException
* @throws BadFitValueException
* @throws InvalidArgumentException
* @throws NotReadableException
* @throws AtLeastWidthOrHeightMustBeUsedException
* @throws RequiredArgumentException
*/
public function execute($params, Smarty_Internal_Template $template)
{
$otherParams = $this->getOtherParams($params);
$this->validateParams($params, $otherParams, $template);
$this->fixParams($params);
$this->fixOtherParams($otherParams);
$encodedStr = serialize($params);
/** @noinspection SpellCheckingInspection */
$cacheKey = 'imponeer-spri-' . md5($encodedStr) . '-' . strlen($encodedStr);
$cachedItem = $this->cache->getItem($cacheKey);
if (!$cachedItem->isHit()) {
$cachedItem->set(
$this->renderOutput(
$params['return'],
$this->doResize($params['fit'], $params['file'], $params['width'], $params['height']),
$otherParams
)
);
$this->cache->save($cachedItem);
}
return $cachedItem->get();
}
/**
* Applies some fixes to params values
*
* @param array $params Params to fix
*
* @return void
*/
protected function fixParams(array &$params): void
{
$params['width'] = isset($params['width']) ? (int)$params['width'] : null;
$params['height'] = isset($params['height']) ? (int)$params['height'] : null;
$params['fit'] = isset($params['fit']) ? strtolower($params['fit']) : 'outside';
$params['return'] = isset($params['return']) ? strtolower($params['return']) : 'image';
if (
(strpos($params['file'], 'data:') !== 0) &&
!filter_var($params['file'], FILTER_VALIDATE_URL) &&
!file_exists($params['file'])
) {
$params['file'] = ($params['basedir'] ?? $_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR . $params['file'];
}
}
/**
* Applies some fixes to other-params values
*
* @param array $params Other-params to fix
*
* @return void
*/
protected function fixOtherParams(array &$params): void
{
if (isset($params['link'])) {
$params['href'] = $params['link'];
unset($params['link']);
}
}
/**
* Renders output string
*
* @param string $return Return format
* @param SingleImage $image Image for the output
* @param array $otherParams Other params
*
* @return string
*/
protected function renderOutput(string $return, SingleImage $image, array $otherParams): string
{
if ($return === 'image') {
return $this->renderImageTag($image, $otherParams);
}
if ($return === 'url') {
return (string)$image->encode('data-url');
}
return '???';
}
/**
* Renders HTML IMG tag for the image
*
* @param SingleImage $image Image to be returned for output
* @param array $otherParams Some params
*
* @return string
*/
protected function renderImageTag(SingleImage $image, array $otherParams): string
{
$ret = '';
if (isset($otherParams['href'])) {
$ret .= $this->buildHTMLTag('a', ['href' => $otherParams['href']]);
}
$allAttributes = $otherParams + [
'alt' => '',
'src' => (string)$image->encode('data-url')
];
if (isset($allAttributes['link'])) {
unset($allAttributes['link']);
}
if (isset($allAttributes['href'])) {
unset($allAttributes['href']);
}
if (isset($allAttributes['basedir'])) {
unset($allAttributes['basedir']);
}
$ret .= $this->buildHTMLTag('img', $allAttributes, true);
if (isset($otherParams['href'])) {
$ret .= '</a>';
}
return $ret;
}
/**
* Builds HTML tag
*
* @param string $name Tag name
* @param array $attributes Dictionary of tag attributes
* @param bool $quickCloseTag Is tag should be quick close tag?
*
* @return string
*/
private function buildHTMLTag(string $name, array $attributes, bool $quickCloseTag = false): string
{
$ret = '<' . $name;
foreach ($attributes as $attrName => $attrValue) {
$ret .= ' ' . $attrName . '="' . htmlentities($attrValue) . '"';
}
if ($quickCloseTag) {
return $ret . '/>';
}
return $ret . '>';
}
/**
* Reads image and do resize
*
* @param string $method Resize method to use
* @param string $file File to resize
* @param int|null $width New image width
* @param int|null $height New image height
*
* @return SingleImage
*/
protected function doResize(string $method, string $file, ?int $width, ?int $height): SingleImage
{
$image = Image::make($file);
switch ($method) {
case 'fill':
return $image->resize($width, $height);
case 'inside':
if ($width && $height) {
if ($image->width() > $image->height()) {
$width = null;
} else {
$height = null;
}
}
return $image->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
});
case 'outside':
return $image->fit($width, $height);
}
return $image;
}
/**
* Validates params
*
* @param array $params Current function arguments (aka params) to be validated
* @param array $otherParams Params with params that doesnt have specific role
* @param Smarty_Internal_Template $template Current smarty instance
*
* @throws RequiredArgumentException
* @throws AttributeEmptyException
* @throws AttributeMustBeStringException
* @throws AttributeMustBeNumericException
* @throws BadFitValueException
* @throws AtLeastWidthOrHeightMustBeUsedException
*/
protected function validateParams(array $params, array $otherParams, Smarty_Internal_Template $template): void
{
if (!isset($params['file'])) {
throw new RequiredArgumentException('file');
}
if (empty($params['file'])) {
throw new AttributeEmptyException('file');
}
if (!is_string($params['file'])) {
throw new AttributeMustBeStringException('file');
}
if (isset($params['width']) && !is_numeric($params['width'])) {
throw new AttributeMustBeNumericException('width');
}
if (isset($params['height']) && !is_numeric($params['height'])) {
throw new AttributeMustBeNumericException('height');
}
if (isset($params['fit']) && !in_array(strtolower($params['fit']), ['inside', 'outside', 'fill'], true)) {
throw new BadFitValueException();
}
if (isset($params['return']) && $params['return'] === 'image') {
$this->validateImageOtherParams($otherParams, $template);
}
if (!isset($params['width']) && !isset($params['height'])) {
throw new AtLeastWidthOrHeightMustBeUsedException();
}
}
/**
* Validates other params for image return type
*
* @param array $otherParams Other params array
* @param Smarty_Internal_Template $template Current smarty instance
*
* @throws AttributeMustBeStringException
*/
protected function validateImageOtherParams(array $otherParams, Smarty_Internal_Template $template): void
{
foreach ($otherParams as $key => $value) {
if (!isset($value) || is_string($value)) {
continue;
}
throw new AttributeMustBeStringException($key);
}
}
/**
* Gets array from params that doesn't have any specific role
*
* @param array $params Smarty function supplied arguments
*
* @return array
*/
private function getOtherParams(array $params): array
{
$ret = [];
foreach ($params as $key => $value) {
$k = strtolower(trim($key));
if (in_array($k, ['fit', 'width', 'height', 'return', 'file', 'src'])) {
continue;
}
$ret[$k] = (string)$value;
}
return $ret;
}
}