2691 lines
121 KiB
PHP
2691 lines
121 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Frontend\Imaging;
|
|
|
|
use TYPO3\CMS\Core\Charset\CharsetConverter;
|
|
use TYPO3\CMS\Core\Context\Context;
|
|
use TYPO3\CMS\Core\Context\FileProcessingAspect;
|
|
use TYPO3\CMS\Core\Core\Environment;
|
|
use TYPO3\CMS\Core\Imaging\GraphicalFunctions;
|
|
use TYPO3\CMS\Core\Imaging\ImageResource;
|
|
use TYPO3\CMS\Core\Resource\File;
|
|
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
|
use TYPO3\CMS\Core\Resource\Service\ConfigurationService;
|
|
use TYPO3\CMS\Core\Type\File\ImageInfo;
|
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
|
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
|
use TYPO3\CMS\Core\Utility\File\BasicFileUtility;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
|
use TYPO3\CMS\Core\Utility\PathUtility;
|
|
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
|
|
|
/**
|
|
* GIFBUILDER
|
|
*
|
|
* Generating image files from TypoScript
|
|
* Used by imgResource in TypoScript.
|
|
*
|
|
* This class allows for advanced rendering of images with various layers of images, text and graphical primitives.
|
|
* The concept is known from TypoScript as "GIFBUILDER" where you can define a "numerical array" (TypoScript term as well)
|
|
* of "GIFBUILDER OBJECTS" (like "TEXT", "IMAGE", etc.) and they will be rendered onto an image one by one.
|
|
* The name "GIFBUILDER" comes from the time when GIF was the only file format supported.
|
|
* .png, .jpg, .webp and .avif files are just as well to create today (configured with TYPO3_CONF_VARS[GFX])
|
|
*
|
|
* Here is an example of how to use this class:
|
|
*
|
|
* $imageCreator = GeneralUtility::makeInstance(GifBuilder::class);
|
|
* $imageCreator->start($fileArray, $this->data);
|
|
* $theImage = $imageCreator->gifBuild();
|
|
* return GeneralUtility::makeInstance(GraphicalFunctions::class)->getImageDimensions($theImage);
|
|
*/
|
|
class GifBuilder
|
|
{
|
|
/**
|
|
* Contains all text strings used on this image
|
|
*
|
|
* @var list<string>
|
|
*/
|
|
protected array $combinedTextStrings = [];
|
|
|
|
/**
|
|
* Contains all filenames (basename without extension) used on this image
|
|
*
|
|
* @var list<string>
|
|
*/
|
|
protected array $combinedFileNames = [];
|
|
|
|
/**
|
|
* This is the array from which data->field: [key] is fetched. So this is the current record!
|
|
*/
|
|
protected array $data = [];
|
|
protected array $objBB = [];
|
|
|
|
/**
|
|
* @var array<string, array>
|
|
*/
|
|
protected array $charRangeMap = [];
|
|
|
|
/**
|
|
* @var array{0?: int<0, max>, 1?: int<0, max>}
|
|
*/
|
|
protected array $XY = [];
|
|
protected ?ContentObjectRenderer $cObj = null;
|
|
|
|
/**
|
|
* @var list<int>
|
|
*/
|
|
protected array $workArea = [];
|
|
|
|
/**
|
|
* @var list<int>
|
|
*/
|
|
protected array $defaultWorkArea = [];
|
|
|
|
/**
|
|
* Preserve the alpha transparency layer of read PNG images
|
|
*/
|
|
protected bool $saveAlphaLayer = false;
|
|
|
|
/**
|
|
* Array mapping HTML color names to RGB values.
|
|
*
|
|
* @var array<non-empty-string, array{0: int<0, 255>, 1: int<0, 255>, 2: int<0, 255>}>
|
|
*/
|
|
protected array $colMap = [
|
|
'aqua' => [0, 255, 255],
|
|
'black' => [0, 0, 0],
|
|
'blue' => [0, 0, 255],
|
|
'fuchsia' => [255, 0, 255],
|
|
'gray' => [128, 128, 128],
|
|
'green' => [0, 128, 0],
|
|
'lime' => [0, 255, 0],
|
|
'maroon' => [128, 0, 0],
|
|
'navy' => [0, 0, 128],
|
|
'olive' => [128, 128, 0],
|
|
'purple' => [128, 0, 128],
|
|
'red' => [255, 0, 0],
|
|
'silver' => [192, 192, 192],
|
|
'teal' => [0, 128, 128],
|
|
'yellow' => [255, 255, 0],
|
|
'white' => [255, 255, 255],
|
|
];
|
|
|
|
/**
|
|
* This holds the operational setup.
|
|
* Basically this is a TypoScript array with properties.
|
|
*
|
|
* @internal
|
|
*/
|
|
public array $setup = [];
|
|
|
|
/**
|
|
* @var int<0, max>
|
|
*/
|
|
protected int $w = 0;
|
|
/**
|
|
* @var int<0, max>
|
|
*/
|
|
protected int $h = 0;
|
|
|
|
/**
|
|
* @var list<int>
|
|
*/
|
|
protected array $offset;
|
|
|
|
/**
|
|
* File formats supported by gdlib. This variable gets filled in the constructor
|
|
*
|
|
* @var list<non-empty-string>
|
|
*/
|
|
protected array $gdlibExtensions = [];
|
|
|
|
protected CharsetConverter $csConvObj;
|
|
protected GraphicalFunctions $imageService;
|
|
|
|
/**
|
|
* Enable ImageMagick effects, disabled by default as IM5+ effects slow down the image generation
|
|
*/
|
|
protected bool $processorEffectsEnabled = false;
|
|
|
|
/**
|
|
* @var int<10, 100>
|
|
*/
|
|
protected int $jpegQuality = 85;
|
|
/**
|
|
* @var int<10, 101>
|
|
*/
|
|
protected int $webpQuality = 85;
|
|
|
|
/**
|
|
* @var int<-1, 100>
|
|
*/
|
|
protected int $avifQuality = 85;
|
|
|
|
public function __construct()
|
|
{
|
|
$gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
|
|
if ($gfxConf['processor_effects'] ?? false) {
|
|
$this->processorEffectsEnabled = true;
|
|
}
|
|
$this->jpegQuality = MathUtility::forceIntegerInRange($gfxConf['jpg_quality'], 10, 100, $this->jpegQuality);
|
|
$this->avifQuality = MathUtility::forceIntegerInRange($gfxConf['avif_quality'] ?? 0, -1, 100, $this->avifQuality);
|
|
if (isset($gfxConf['webp_quality'])) {
|
|
// see IMG_WEBP_LOSSLESS // https://www.php.net/manual/en/image.constants.php
|
|
if ($gfxConf['webp_quality'] === 'lossless') {
|
|
$this->webpQuality = 101;
|
|
} else {
|
|
$this->webpQuality = MathUtility::forceIntegerInRange($gfxConf['webp_quality'], 10, 101, $this->webpQuality);
|
|
}
|
|
}
|
|
if (function_exists('imagecreatefromjpeg') && function_exists('imagejpeg')) {
|
|
$this->gdlibExtensions[] = 'jpg';
|
|
$this->gdlibExtensions[] = 'jpeg';
|
|
}
|
|
if (function_exists('imagecreatefrompng') && function_exists('imagepng')) {
|
|
$this->gdlibExtensions[] = 'png';
|
|
}
|
|
if (function_exists('imagecreatefromwebp') && function_exists('imagewebp')) {
|
|
$this->gdlibExtensions[] = 'webp';
|
|
}
|
|
if (function_exists('imagecreatefromavif') && function_exists('imageavif')) {
|
|
$this->gdlibExtensions[] = 'avif';
|
|
}
|
|
if (function_exists('imagecreatefromgif') && function_exists('imagegif')) {
|
|
$this->gdlibExtensions[] = 'gif';
|
|
}
|
|
$this->imageService = GeneralUtility::makeInstance(GraphicalFunctions::class);
|
|
$this->csConvObj = GeneralUtility::makeInstance(CharsetConverter::class);
|
|
}
|
|
|
|
/**
|
|
* Initialization of the GIFBUILDER objects, in particular TEXT and IMAGE. This includes finding the bounding box, setting dimensions and offset values before the actual rendering is started.
|
|
* Modifies the ->setup, ->objBB internal arrays
|
|
*
|
|
* @param array $conf TypoScript properties for the GIFBUILDER session. Stored internally in the variable ->setup
|
|
* @param array $data The current data record from \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer. Stored internally in the variable ->data
|
|
* @see ContentObjectRenderer::getImgResource()
|
|
*/
|
|
public function start(array $conf, array $data): void
|
|
{
|
|
if (!class_exists(\GdImage::class)) {
|
|
return;
|
|
}
|
|
$this->setup = $conf;
|
|
$this->data = $data;
|
|
$this->cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
|
$this->cObj->start($this->data);
|
|
// Initializing Char Range Map
|
|
$this->charRangeMap = [];
|
|
foreach ($conf['charRangeMap.'] ?? [] as $cRMcfgkey => $cRMcfg) {
|
|
if (is_array($cRMcfg)) {
|
|
$cRMkey = $conf['charRangeMap.'][substr($cRMcfgkey, 0, -1)];
|
|
$this->charRangeMap[$cRMkey] = [];
|
|
$this->charRangeMap[$cRMkey]['charMapConfig'] = $cRMcfg['charMapConfig.'] ?? [];
|
|
$this->charRangeMap[$cRMkey]['cfgKey'] = substr($cRMcfgkey, 0, -1);
|
|
$this->charRangeMap[$cRMkey]['multiplicator'] = (float)$cRMcfg['fontSizeMultiplicator'];
|
|
$this->charRangeMap[$cRMkey]['pixelSpace'] = (int)$cRMcfg['pixelSpaceFontSizeRef'];
|
|
}
|
|
}
|
|
// Getting sorted list of TypoScript keys from setup.
|
|
$sKeyArray = ArrayUtility::filterAndSortByNumericKeys($this->setup);
|
|
// Setting the background color, passing it through stdWrap
|
|
$this->setup['backColor'] = $this->cObj->stdWrapValue('backColor', $this->setup, 'white');
|
|
$this->setup['transparentColor_array'] = explode('|', trim((string)$this->cObj->stdWrapValue('transparentColor', $this->setup)));
|
|
$this->setup['transparentBackground'] = $this->cObj->stdWrapValue('transparentBackground', $this->setup);
|
|
// Set default dimensions
|
|
$this->setup['XY'] = $this->cObj->stdWrapValue('XY', $this->setup);
|
|
if (!$this->setup['XY']) {
|
|
$this->setup['XY'] = '120,50';
|
|
}
|
|
// Checking TEXT and IMAGE objects for files. If any errors the objects are cleared.
|
|
// The Bounding Box for the objects is stored in an array
|
|
foreach ($sKeyArray as $index => $theKey) {
|
|
if (!($theValue = $this->setup[$theKey] ?? false)) {
|
|
continue;
|
|
}
|
|
if ((int)$theKey && ($conf = $this->setup[$theKey . '.'] ?? [])) {
|
|
// Swipes through TEXT and IMAGE-objects
|
|
switch ($theValue) {
|
|
case 'TEXT':
|
|
if ($this->setup[$theKey . '.'] = $this->checkTextObj($conf)) {
|
|
// Adjust font width if max size is set:
|
|
$maxWidth = $this->cObj->stdWrapValue('maxWidth', $this->setup[$theKey . '.'] ?? []);
|
|
if ($maxWidth) {
|
|
$this->setup[$theKey . '.']['fontSize'] = $this->fontResize($this->setup[$theKey . '.']);
|
|
}
|
|
// Calculate bounding box:
|
|
$txtInfo = $this->calcBBox($this->setup[$theKey . '.']);
|
|
$this->setup[$theKey . '.']['BBOX'] = $txtInfo;
|
|
$this->objBB[$theKey] = $txtInfo;
|
|
}
|
|
break;
|
|
case 'IMAGE':
|
|
$imageResource = $this->getResource($conf['file'] ?? '', $conf['file.'] ?? []);
|
|
if ($imageResource !== null) {
|
|
$this->combinedFileNames[] = preg_replace('/\\.[[:alnum:]]+$/', '', PathUtility::basename($imageResource->getFullPath()));
|
|
if ($imageResource->getProcessedFile() instanceof ProcessedFile) {
|
|
// Use processed file, if a FAL file has been processed by GIFBUILDER (e.g. scaled/cropped)
|
|
$this->setup[$theKey . '.']['file'] = $imageResource->getProcessedFile()->getForLocalProcessing(false);
|
|
} elseif ($imageResource->getOriginalFile() instanceof File) {
|
|
// Use FAL file with getForLocalProcessing to circumvent problems with umlauts, if it is a FAL file (origFile not set)
|
|
$this->setup[$theKey . '.']['file'] = $imageResource->getOriginalFile()->getForLocalProcessing(false);
|
|
} else {
|
|
// Use normal path from fileInfo if it is a non-FAL file (even non-FAL files have originalFile set, but only non-FAL files have origFile set)
|
|
$this->setup[$theKey . '.']['file'] = $imageResource->getFullPath();
|
|
}
|
|
|
|
// only pass necessary parts of ImageResource further down, to not incorporate facts as
|
|
// CropScaleMask runs in this request, that may not occur in subsequent calls and change
|
|
// the md5 of the generated file name
|
|
$this->setup[$theKey . '.']['BBOX'] = $imageResource->getLegacyImageResourceInformation();
|
|
$this->objBB[$theKey] = $imageResource->getLegacyImageResourceInformation();
|
|
if ($conf['mask'] ?? false) {
|
|
$maskResource = $this->getResource($conf['mask'], $conf['mask.'] ?? []);
|
|
if ($maskResource !== null) {
|
|
// the same selection criteria as regarding fileInfo above apply here
|
|
if ($maskResource->getProcessedFile() instanceof ProcessedFile) {
|
|
$this->setup[$theKey . '.']['mask'] = $maskResource->getProcessedFile()->getForLocalProcessing(false);
|
|
} elseif ($maskResource->getOriginalFile() instanceof File) {
|
|
$this->setup[$theKey . '.']['mask'] = $maskResource->getOriginalFile()->getForLocalProcessing(false);
|
|
} else {
|
|
$this->setup[$theKey . '.']['mask'] = $maskResource->getFullPath();
|
|
}
|
|
} else {
|
|
$this->setup[$theKey . '.']['mask'] = '';
|
|
}
|
|
}
|
|
} else {
|
|
unset($this->setup[$theKey . '.']);
|
|
}
|
|
break;
|
|
}
|
|
// Checks if disabled is set
|
|
if (($conf['if.'] ?? false) && !$this->cObj->checkIf($conf['if.'])) {
|
|
unset($sKeyArray[$index]);
|
|
unset($this->setup[$theKey]);
|
|
unset($this->setup[$theKey . '.']);
|
|
unset($this->objBB[$theKey]);
|
|
}
|
|
}
|
|
}
|
|
// Calculate offsets on elements
|
|
$this->setup['XY'] = $this->calcOffset($this->setup['XY']);
|
|
$this->setup['offset'] = (string)$this->cObj->stdWrapValue('offset', $this->setup);
|
|
$this->setup['offset'] = $this->calcOffset($this->setup['offset']);
|
|
$this->setup['workArea'] = (string)$this->cObj->stdWrapValue('workArea', $this->setup);
|
|
$this->setup['workArea'] = $this->calcOffset($this->setup['workArea']);
|
|
foreach ($sKeyArray as $theKey) {
|
|
if (!($theValue = $this->setup[$theKey] ?? false)) {
|
|
continue;
|
|
}
|
|
if ((int)$theKey && ($this->setup[$theKey . '.'] ?? false)) {
|
|
switch ($theValue) {
|
|
case 'TEXT':
|
|
|
|
case 'IMAGE':
|
|
if (isset($this->setup[$theKey . '.']['offset.'])) {
|
|
$this->setup[$theKey . '.']['offset'] = $this->cObj->stdWrapValue('offset', $this->setup[$theKey . '.']);
|
|
unset($this->setup[$theKey . '.']['offset.']);
|
|
}
|
|
if ($this->setup[$theKey . '.']['offset'] ?? false) {
|
|
$this->setup[$theKey . '.']['offset'] = $this->calcOffset($this->setup[$theKey . '.']['offset']);
|
|
}
|
|
break;
|
|
case 'BOX':
|
|
|
|
case 'ELLIPSE':
|
|
if (isset($this->setup[$theKey . '.']['dimensions.'])) {
|
|
$this->setup[$theKey . '.']['dimensions'] = $this->cObj->stdWrapValue('dimensions', $this->setup[$theKey . '.']);
|
|
unset($this->setup[$theKey . '.']['dimensions.']);
|
|
}
|
|
if ($this->setup[$theKey . '.']['dimensions'] ?? false) {
|
|
$this->setup[$theKey . '.']['dimensions'] = $this->calcOffset($this->setup[$theKey . '.']['dimensions']);
|
|
}
|
|
break;
|
|
case 'WORKAREA':
|
|
if (isset($this->setup[$theKey . '.']['set.'])) {
|
|
$this->setup[$theKey . '.']['set'] = $this->cObj->stdWrapValue('set', $this->setup[$theKey . '.']);
|
|
unset($this->setup[$theKey . '.']['set.']);
|
|
}
|
|
if ($this->setup[$theKey . '.']['set'] ?? false) {
|
|
$this->setup[$theKey . '.']['set'] = $this->calcOffset($this->setup[$theKey . '.']['set']);
|
|
}
|
|
break;
|
|
case 'CROP':
|
|
if (isset($this->setup[$theKey . '.']['crop.'])) {
|
|
$this->setup[$theKey . '.']['crop'] = $this->cObj->stdWrapValue('crop', $this->setup[$theKey . '.']);
|
|
unset($this->setup[$theKey . '.']['crop.']);
|
|
}
|
|
if ($this->setup[$theKey . '.']['crop'] ?? false) {
|
|
$this->setup[$theKey . '.']['crop'] = $this->calcOffset($this->setup[$theKey . '.']['crop']);
|
|
}
|
|
break;
|
|
case 'SCALE':
|
|
if (isset($this->setup[$theKey . '.']['width.'])) {
|
|
$this->setup[$theKey . '.']['width'] = $this->cObj->stdWrapValue('width', $this->setup[$theKey . '.']);
|
|
unset($this->setup[$theKey . '.']['width.']);
|
|
}
|
|
if ($this->setup[$theKey . '.']['width'] ?? false) {
|
|
$this->setup[$theKey . '.']['width'] = $this->calcOffset($this->setup[$theKey . '.']['width']);
|
|
}
|
|
if (isset($this->setup[$theKey . '.']['height.'])) {
|
|
$this->setup[$theKey . '.']['height'] = $this->cObj->stdWrapValue('height', $this->setup[$theKey . '.']);
|
|
unset($this->setup[$theKey . '.']['height.']);
|
|
}
|
|
if ($this->setup[$theKey . '.']['height'] ?? false) {
|
|
$this->setup[$theKey . '.']['height'] = $this->calcOffset($this->setup[$theKey . '.']['height']);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Get trivial data
|
|
$XY = GeneralUtility::intExplode(',', $this->setup['XY']);
|
|
$maxWidth = (int)$this->cObj->stdWrapValue('maxWidth', $this->setup);
|
|
$maxHeight = (int)$this->cObj->stdWrapValue('maxHeight', $this->setup);
|
|
$XY[0] = MathUtility::forceIntegerInRange($XY[0], 1, $maxWidth ?: 2000);
|
|
$XY[1] = MathUtility::forceIntegerInRange($XY[1], 1, $maxHeight ?: 2000);
|
|
$this->XY = $XY;
|
|
$this->w = $XY[0];
|
|
$this->h = $XY[1];
|
|
$this->offset = GeneralUtility::intExplode(',', $this->setup['offset']);
|
|
// this sets the workArea
|
|
$this->setWorkArea($this->setup['workArea']);
|
|
// this sets the default to the current
|
|
$this->defaultWorkArea = $this->workArea;
|
|
}
|
|
|
|
/**
|
|
* Initiates the image file generation if ->setup is TRUE and if the file did not
|
|
* exist already. Gets filename from fileName() and if file exists in typo3temp/assets/images/
|
|
* dir it will- of course - not be rendered again. Otherwise rendering means calling ->make(),
|
|
* then ->output(), then destroys the image and returns the ImageResource DTO.
|
|
*
|
|
* @return ImageResource|null Returns the ImageResource DTO with file information from ContentObjectRenderer::getImgResource() - or NULL
|
|
* @see make()
|
|
* @see fileName()
|
|
*/
|
|
public function gifBuild(): ?ImageResource
|
|
{
|
|
if (!$this->setup || !class_exists(\GdImage::class)) {
|
|
return null;
|
|
}
|
|
|
|
$fullFileName = Environment::getPublicPath() . '/typo3temp/assets/images/' . $this->fileName();
|
|
if (!file_exists($fullFileName)) {
|
|
// Create temporary directory if not done
|
|
GeneralUtility::mkdir_deep(dirname($fullFileName));
|
|
// Create file
|
|
$gdImage = $this->make();
|
|
$this->output($gdImage, $fullFileName);
|
|
}
|
|
|
|
$imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $fullFileName);
|
|
if ($imageInfo->getWidth() > 0) {
|
|
return ImageResource::createFromImageInfo($imageInfo);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Writing the image pointer, to file based on the extension of the input filename.
|
|
* Uses $this->setup['quality'] for jpg images to reduce size/quality if needed.
|
|
*
|
|
* @param string $file The absolute filename to write to
|
|
* @see gifBuild()
|
|
*/
|
|
protected function output(\GdImage $gdImage, string $file): void
|
|
{
|
|
if ($file === '') {
|
|
return;
|
|
}
|
|
|
|
$reg = [];
|
|
preg_match('/([^\\.]*)$/', $file, $reg);
|
|
$ext = strtolower($reg[0]);
|
|
switch ($ext) {
|
|
case 'gif':
|
|
case 'png':
|
|
$this->ImageWrite($gdImage, $file);
|
|
break;
|
|
case 'jpg':
|
|
case 'jpeg':
|
|
// Use the default
|
|
$quality = isset($this->setup['quality']) ? MathUtility::forceIntegerInRange((int)$this->setup['quality'], 10, 100) : 0;
|
|
$this->ImageWrite($gdImage, $file, $quality);
|
|
break;
|
|
case 'webp':
|
|
// Quality can also be set to IMG_WEBP_LOSSLESS = 101
|
|
$quality = isset($this->setup['quality']) ? MathUtility::forceIntegerInRange((int)$this->setup['quality'], 10, 101) : 0;
|
|
$this->ImageWrite($gdImage, $file, $quality);
|
|
break;
|
|
case 'avif':
|
|
$quality = isset($this->setup['quality']) ? MathUtility::forceIntegerInRange((int)$this->setup['quality'], -1, 100) : 0;
|
|
$speed = isset($this->setup['speed']) ? MathUtility::forceIntegerInRange((int)$this->setup['speed'], -1, 10) : -1;
|
|
$this->ImageWrite($gdImage, $file, $quality, $speed);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The actual rendering of the image file.
|
|
*
|
|
* Creates a GDlib resource, works on that and returns it.
|
|
*
|
|
* Basically sets the dimensions, the background color, the traverses the array of GIFBUILDER objects
|
|
* and finally setting the transparent color if defined.
|
|
*
|
|
* Called by gifBuild()
|
|
*
|
|
* @see gifBuild()
|
|
*/
|
|
protected function make(): \GdImage
|
|
{
|
|
// Get trivial data
|
|
$XY = $this->XY;
|
|
// Reset internal properties
|
|
$this->saveAlphaLayer = false;
|
|
// Gif-start
|
|
$im = imagecreatetruecolor($XY[0], $XY[1]);
|
|
if (!$im instanceof \GdImage) {
|
|
throw new \RuntimeException('imagecreatetruecolor returned false', 1598350445);
|
|
}
|
|
$this->w = $XY[0];
|
|
$this->h = $XY[1];
|
|
// Transparent layer as background if set and requirements are met
|
|
if (($this->setup['backColor'] ?? '') === 'transparent' && (empty($this->setup['format']) || $this->setup['format'] === 'png')) {
|
|
// Set transparency properties
|
|
imagesavealpha($im, true);
|
|
// Fill with a transparent background
|
|
$transparentColor = imagecolorallocatealpha($im, 0, 0, 0, 127);
|
|
imagefill($im, 0, 0, $transparentColor);
|
|
// Set internal properties to keep the transparency over the rendering process
|
|
$this->saveAlphaLayer = true;
|
|
// Force PNG in case no format is set
|
|
$this->setup['format'] = 'png';
|
|
$BGcols = [];
|
|
} else {
|
|
// Fill the background with the given color
|
|
$BGcols = $this->convertColor($this->setup['backColor']);
|
|
$Bcolor = imagecolorallocate($im, $BGcols[0], $BGcols[1], $BGcols[2]);
|
|
imagefilledrectangle($im, 0, 0, $XY[0], $XY[1], $Bcolor);
|
|
}
|
|
// Traverse the GIFBUILDER objects and render each one:
|
|
$sKeyArray = ArrayUtility::filterAndSortByNumericKeys($this->setup);
|
|
foreach ($sKeyArray as $theKey) {
|
|
$theValue = $this->setup[$theKey];
|
|
if ((int)$theKey && ($conf = $this->setup[$theKey . '.'] ?? [])) {
|
|
// apply stdWrap to all properties, except for TEXT objects
|
|
// all properties of the TEXT sub-object have already been stdWrap-ped
|
|
// before in ->checkTextObj()
|
|
if ($theValue !== 'TEXT') {
|
|
$isStdWrapped = [];
|
|
foreach ($conf as $key => $value) {
|
|
$parameter = rtrim($key, '.');
|
|
if (!($isStdWrapped[$parameter] ?? false) && isset($conf[$parameter . '.'])) {
|
|
$conf[$parameter] = $this->cObj->stdWrapValue($parameter, $conf);
|
|
$isStdWrapped[$parameter] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch ($theValue) {
|
|
case 'IMAGE':
|
|
if ($conf['mask'] ?? false) {
|
|
$this->maskImageOntoImage($im, $conf, $this->workArea);
|
|
} else {
|
|
$this->copyImageOntoImage($im, $conf, $this->workArea);
|
|
}
|
|
break;
|
|
case 'TEXT':
|
|
if (!($conf['hide'] ?? false)) {
|
|
if (is_array($conf['shadow.'] ?? null)) {
|
|
$isStdWrapped = [];
|
|
foreach ($conf['shadow.'] as $key => $value) {
|
|
$parameter = rtrim($key, '.');
|
|
if (!($isStdWrapped[$parameter] ?? false) && isset($conf[$parameter . '.'])) {
|
|
$conf['shadow.'][$parameter] = $this->cObj->stdWrapValue($parameter, $conf);
|
|
$isStdWrapped[$parameter] = 1;
|
|
}
|
|
}
|
|
$this->makeShadow($im, $conf['shadow.'], $this->workArea, $conf);
|
|
}
|
|
if (is_array($conf['emboss.'] ?? null)) {
|
|
$isStdWrapped = [];
|
|
foreach ($conf['emboss.'] as $key => $value) {
|
|
$parameter = rtrim($key, '.');
|
|
if (!($isStdWrapped[$parameter] ?? false) && isset($conf[$parameter . '.'])) {
|
|
$conf['emboss.'][$parameter] = $this->cObj->stdWrapValue($parameter, $conf);
|
|
$isStdWrapped[$parameter] = 1;
|
|
}
|
|
}
|
|
$this->makeEmboss($im, $conf['emboss.'], $this->workArea, $conf);
|
|
}
|
|
if (is_array($conf['outline.'] ?? null)) {
|
|
$isStdWrapped = [];
|
|
foreach ($conf['outline.'] as $key => $value) {
|
|
$parameter = rtrim($key, '.');
|
|
if (!($isStdWrapped[$parameter] ?? false) && isset($conf[$parameter . '.'])) {
|
|
$conf['outline.'][$parameter] = $this->cObj->stdWrapValue($parameter, $conf);
|
|
$isStdWrapped[$parameter] = 1;
|
|
}
|
|
}
|
|
$this->makeOutline($im, $conf['outline.'], $this->workArea, $conf);
|
|
}
|
|
$this->makeText($im, $conf, $this->workArea);
|
|
}
|
|
break;
|
|
case 'OUTLINE':
|
|
if ($this->setup[$conf['textObjNum']] === 'TEXT' && ($txtConf = $this->checkTextObj($this->setup[$conf['textObjNum'] . '.']))) {
|
|
$this->makeOutline($im, $conf, $this->workArea, $txtConf);
|
|
}
|
|
break;
|
|
case 'EMBOSS':
|
|
if ($this->setup[$conf['textObjNum']] === 'TEXT' && ($txtConf = $this->checkTextObj($this->setup[$conf['textObjNum'] . '.']))) {
|
|
$this->makeEmboss($im, $conf, $this->workArea, $txtConf);
|
|
}
|
|
break;
|
|
case 'SHADOW':
|
|
if ($this->setup[$conf['textObjNum']] === 'TEXT' && ($txtConf = $this->checkTextObj($this->setup[$conf['textObjNum'] . '.']))) {
|
|
$this->makeShadow($im, $conf, $this->workArea, $txtConf);
|
|
}
|
|
break;
|
|
case 'BOX':
|
|
$this->makeBox($im, $conf, $this->workArea);
|
|
break;
|
|
case 'EFFECT':
|
|
$this->makeEffect($im, $conf);
|
|
break;
|
|
case 'ADJUST':
|
|
$this->adjust($im, $conf);
|
|
break;
|
|
case 'CROP':
|
|
$this->crop($im, $conf);
|
|
break;
|
|
case 'SCALE':
|
|
$this->scale($im, $conf);
|
|
break;
|
|
case 'WORKAREA':
|
|
if ($conf['set']) {
|
|
// this sets the workArea
|
|
$this->setWorkArea($conf['set']);
|
|
}
|
|
if (isset($conf['clear'])) {
|
|
// This sets the current to the default;
|
|
$this->workArea = $this->defaultWorkArea;
|
|
}
|
|
break;
|
|
case 'ELLIPSE':
|
|
$this->makeEllipse($im, $conf, $this->workArea);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Preserve alpha transparency
|
|
if (!$this->saveAlphaLayer) {
|
|
if ($this->setup['transparentBackground']) {
|
|
// Auto transparent background is set
|
|
$Bcolor = imagecolorclosest($im, $BGcols[0], $BGcols[1], $BGcols[2]);
|
|
imagecolortransparent($im, $Bcolor);
|
|
} elseif (is_array($this->setup['transparentColor_array'])) {
|
|
// Multiple transparent colors are set. This is done via the trick that all transparent colors get
|
|
// converted to one color and then this one gets set as transparent as png/gif can just have one
|
|
// transparent color.
|
|
$Tcolor = $this->unifyColors($im, $this->setup['transparentColor_array'], (bool)($this->setup['transparentColor.']['closest'] ?? false));
|
|
if ($Tcolor >= 0) {
|
|
imagecolortransparent($im, $Tcolor);
|
|
}
|
|
}
|
|
}
|
|
return $im;
|
|
}
|
|
|
|
/**
|
|
* Implements the "IMAGE" GIFBUILDER object, when the "mask" property is TRUE.
|
|
* It reads the two images defined by $conf['file'] and $conf['mask'] and copies the $conf['file'] onto the input image pointer image using the $conf['mask'] as a grayscale mask
|
|
* The operation involves ImageMagick for combining.
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @param array $workArea The current working area coordinates.
|
|
* @see make()
|
|
*/
|
|
protected function maskImageOntoImage(\GdImage &$im, array $conf, array $workArea): void
|
|
{
|
|
if ($conf['file'] && $conf['mask']) {
|
|
$imgInf = pathinfo($conf['file']);
|
|
$imgExt = strtolower($imgInf['extension']);
|
|
if (!in_array($imgExt, $this->gdlibExtensions, true)) {
|
|
$BBimage = $this->imageService->convert($conf['file'], 'png');
|
|
} else {
|
|
$BBimage = $this->imageService->getImageDimensions($conf['file'], true);
|
|
}
|
|
$maskInf = pathinfo($conf['mask']);
|
|
$maskExt = strtolower($maskInf['extension']);
|
|
if (!in_array($maskExt, $this->gdlibExtensions, true)) {
|
|
$BBmask = $this->imageService->convert($conf['mask'], 'png');
|
|
} else {
|
|
$BBmask = $this->imageService->getImageDimensions($conf['mask'], true);
|
|
}
|
|
if ($BBimage && $BBmask) {
|
|
$w = imagesx($im);
|
|
$h = imagesy($im);
|
|
$tmpStr = $this->imageService->randomName();
|
|
$theImage = $tmpStr . '_img.png';
|
|
$theDest = $tmpStr . '_dest.png';
|
|
$theMask = $tmpStr . '_mask.png';
|
|
// Prepare overlay image
|
|
$cpImg = $this->imageCreateFromFile($BBimage->getRealPath());
|
|
$destImg = imagecreatetruecolor($w, $h);
|
|
// Preserve alpha transparency
|
|
if ($this->saveAlphaLayer) {
|
|
imagesavealpha($destImg, true);
|
|
$Bcolor = imagecolorallocatealpha($destImg, 0, 0, 0, 127);
|
|
imagefill($destImg, 0, 0, $Bcolor);
|
|
} else {
|
|
$Bcolor = imagecolorallocate($destImg, 0, 0, 0);
|
|
imagefilledrectangle($destImg, 0, 0, $w, $h, $Bcolor);
|
|
}
|
|
$this->copyGifOntoGif($destImg, $cpImg, $conf, $workArea);
|
|
$this->ImageWrite($destImg, $theImage);
|
|
// Prepare mask image
|
|
$cpImg = $this->imageCreateFromFile($BBmask->getRealPath());
|
|
$destImg = imagecreatetruecolor($w, $h);
|
|
if ($this->saveAlphaLayer) {
|
|
imagesavealpha($destImg, true);
|
|
$Bcolor = imagecolorallocatealpha($destImg, 0, 0, 0, 127);
|
|
imagefill($destImg, 0, 0, $Bcolor);
|
|
} else {
|
|
$Bcolor = imagecolorallocate($destImg, 0, 0, 0);
|
|
imagefilledrectangle($destImg, 0, 0, $w, $h, $Bcolor);
|
|
}
|
|
$this->copyGifOntoGif($destImg, $cpImg, $conf, $workArea);
|
|
$this->ImageWrite($destImg, $theMask);
|
|
// Mask the images
|
|
$this->ImageWrite($im, $theDest);
|
|
// Let combineExec handle maskNegation
|
|
$this->imageService->combineExec($theDest, $theImage, $theMask, $theDest);
|
|
// The main image is loaded again...
|
|
$backIm = $this->imageCreateFromFile($theDest);
|
|
// ... and if nothing went wrong we load it onto the old one.
|
|
if ($backIm) {
|
|
if (!$this->saveAlphaLayer) {
|
|
imagecolortransparent($backIm, -1);
|
|
}
|
|
$im = $backIm;
|
|
}
|
|
// Unlink files from process
|
|
unlink($theDest);
|
|
unlink($theImage);
|
|
unlink($theMask);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "IMAGE" GIFBUILDER object, when the "mask" property is FALSE (using only $conf['file'])
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @param array $workArea The current working area coordinates.
|
|
* @see make()
|
|
* @see maskImageOntoImage()
|
|
*/
|
|
protected function copyImageOntoImage(\GdImage &$im, array $conf, array $workArea): void
|
|
{
|
|
if ($conf['file']) {
|
|
if (!in_array($conf['BBOX'][2], $this->gdlibExtensions, true)) {
|
|
$conf['BBOX'] = $this->imageService->convert($conf['BBOX']->getRealPath(), 'png');
|
|
$conf['file'] = $conf['BBOX'] ? $conf['BBOX']->getRealPath() : null;
|
|
}
|
|
$cpImg = $this->imageCreateFromFile($conf['file']);
|
|
$this->copyGifOntoGif($im, $cpImg, $conf, $workArea);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "TEXT" GIFBUILDER object
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @param array $workArea The current working area coordinates.
|
|
* @see make()
|
|
* @internal
|
|
*/
|
|
public function makeText(\GdImage &$im, array $conf, array $workArea): void
|
|
{
|
|
// Spacing
|
|
[$spacing, $wordSpacing] = $this->calcWordSpacing($conf);
|
|
// Position
|
|
$txtPos = $this->txtPosition($conf, $workArea, $conf['BBOX']);
|
|
$theText = $conf['text'] ?? '';
|
|
// Font Color:
|
|
$cols = $this->convertColor($conf['fontColor']);
|
|
// NiceText is calculated
|
|
if (!($conf['niceText'] ?? false)) {
|
|
$Fcolor = imagecolorallocate($im, $cols[0], $cols[1], $cols[2]);
|
|
// antiAliasing is setup:
|
|
$Fcolor = $conf['antiAlias'] ? $Fcolor : -$Fcolor;
|
|
for ($a = 0; $a < $conf['iterations']; $a++) {
|
|
// If any kind of spacing applies, we use this function:
|
|
if ($spacing || $wordSpacing) {
|
|
$this->SpacedImageTTFText($im, $conf['fontSize'], $conf['angle'] ?? 0, $txtPos[0], $txtPos[1], $Fcolor, GeneralUtility::getFileAbsFileName($conf['fontFile']), $theText, $spacing, $wordSpacing, $conf['splitRendering.']);
|
|
} else {
|
|
$this->renderTTFText($im, $conf['fontSize'], $conf['angle'] ?? 0, $txtPos[0], $txtPos[1], $Fcolor, $conf['fontFile'], $theText, $conf['splitRendering.'] ?? [], $conf);
|
|
}
|
|
}
|
|
} else {
|
|
// NICETEXT::
|
|
// options anti_aliased and iterations is NOT available when doing this!!
|
|
$w = imagesx($im);
|
|
$h = imagesy($im);
|
|
$tmpStr = $this->imageService->randomName();
|
|
$fileMenu = $tmpStr . '_menuNT.png';
|
|
$fileColor = $tmpStr . '_colorNT.png';
|
|
$fileMask = $tmpStr . '_maskNT.png';
|
|
// Scalefactor
|
|
$sF = MathUtility::forceIntegerInRange(($conf['niceText.']['scaleFactor'] ?? 2), 2, 5);
|
|
$newW = (int)ceil($sF * imagesx($im));
|
|
$newH = (int)ceil($sF * imagesy($im));
|
|
// Make mask
|
|
$maskImg = imagecreatetruecolor($newW, $newH);
|
|
$Bcolor = imagecolorallocate($maskImg, 255, 255, 255);
|
|
imagefilledrectangle($maskImg, 0, 0, $newW, $newH, $Bcolor);
|
|
$Fcolor = imagecolorallocate($maskImg, 0, 0, 0);
|
|
// If any kind of spacing applies, we use this function:
|
|
if ($spacing || $wordSpacing) {
|
|
$this->SpacedImageTTFText($maskImg, $conf['fontSize'], $conf['angle'] ?? 0, $txtPos[0], $txtPos[1], $Fcolor, GeneralUtility::getFileAbsFileName($conf['fontFile']), $theText, $spacing, $wordSpacing, $conf['splitRendering.'], $sF);
|
|
} else {
|
|
$this->renderTTFText($maskImg, $conf['fontSize'], $conf['angle'] ?? 0, $txtPos[0], $txtPos[1], $Fcolor, $conf['fontFile'], $theText, $conf['splitRendering.'] ?? [], $conf, $sF);
|
|
}
|
|
$this->ImageWrite($maskImg, $fileMask);
|
|
// Downscales the mask
|
|
if (!$this->processorEffectsEnabled) {
|
|
$command = trim($this->imageService->scalecmd . ' ' . $w . 'x' . $h . '! -negate');
|
|
} else {
|
|
$command = trim(($conf['niceText.']['before'] ?? '') . ' ' . $this->imageService->scalecmd . ' ' . $w . 'x' . $h . '! ' . ($conf['niceText.']['after'] ?? '') . ' -negate');
|
|
if (isset($conf['niceText.']['sharpen'])) {
|
|
$command .= $this->imageService->v5_sharpen($conf['niceText.']['sharpen']);
|
|
}
|
|
}
|
|
$this->imageService->imageMagickExec($fileMask, $fileMask, $command);
|
|
// Make the color-file
|
|
$colorImg = imagecreatetruecolor($w, $h);
|
|
$Ccolor = imagecolorallocate($colorImg, $cols[0], $cols[1], $cols[2]);
|
|
imagefilledrectangle($colorImg, 0, 0, $w, $h, $Ccolor);
|
|
$this->ImageWrite($colorImg, $fileColor);
|
|
// The mask is applied
|
|
// The main pictures is saved temporarily
|
|
$this->ImageWrite($im, $fileMenu);
|
|
$this->imageService->combineExec($fileMenu, $fileColor, $fileMask, $fileMenu);
|
|
// The main image is loaded again...
|
|
$backIm = $this->imageCreateFromFile($fileMenu);
|
|
// ... and if nothing went wrong we load it onto the old one.
|
|
if ($backIm) {
|
|
if (!$this->saveAlphaLayer) {
|
|
imagecolortransparent($backIm, -1);
|
|
}
|
|
$im = $backIm;
|
|
}
|
|
// Deleting temporary files;
|
|
unlink($fileMenu);
|
|
unlink($fileColor);
|
|
unlink($fileMask);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "OUTLINE" GIFBUILDER object / property for the TEXT object
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @param array $workArea The current working area coordinates.
|
|
* @param array $txtConf TypoScript array with configuration for the associated TEXT GIFBUILDER object.
|
|
* @see make()
|
|
* @see makeText()
|
|
*/
|
|
protected function makeOutline(\GdImage &$im, array $conf, array $workArea, array $txtConf): void
|
|
{
|
|
$thickness = (int)$conf['thickness'];
|
|
if ($thickness) {
|
|
$txtConf['fontColor'] = $conf['color'];
|
|
$outLineDist = MathUtility::forceIntegerInRange($thickness, 1, 2);
|
|
for ($b = 1; $b <= $outLineDist; $b++) {
|
|
if ($b == 1) {
|
|
$it = 8;
|
|
} else {
|
|
$it = 16;
|
|
}
|
|
$outL = $this->circleOffset($b, $it);
|
|
for ($a = 0; $a < $it; $a++) {
|
|
$this->makeText($im, $txtConf, $this->applyOffset($workArea, $outL[$a]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "EMBOSS" GIFBUILDER object / property for the TEXT object
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @param array $workArea The current working area coordinates.
|
|
* @param array $txtConf TypoScript array with configuration for the associated TEXT GIFBUILDER object.
|
|
* @see make()
|
|
* @see makeShadow()
|
|
*/
|
|
protected function makeEmboss(\GdImage &$im, array $conf, array $workArea, array $txtConf): void
|
|
{
|
|
$conf['color'] = $conf['highColor'];
|
|
$this->makeShadow($im, $conf, $workArea, $txtConf);
|
|
$newOffset = GeneralUtility::intExplode(',', (string)($conf['offset'] ?? ''));
|
|
$newOffset[0] *= -1;
|
|
$newOffset[1] *= -1;
|
|
$conf['offset'] = implode(',', $newOffset);
|
|
$conf['color'] = $conf['lowColor'];
|
|
$this->makeShadow($im, $conf, $workArea, $txtConf);
|
|
}
|
|
|
|
/**
|
|
* Implements the "SHADOW" GIFBUILDER object / property for the TEXT object
|
|
* The operation involves ImageMagick for combining.
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @param array $workArea The current working area coordinates.
|
|
* @param array $txtConf TypoScript array with configuration for the associated TEXT GIFBUILDER object.
|
|
* @see make()
|
|
* @see makeText()
|
|
* @see makeEmboss()
|
|
* @internal
|
|
*/
|
|
public function makeShadow(\GdImage &$im, array $conf, array $workArea, array $txtConf): void
|
|
{
|
|
$workArea = $this->applyOffset($workArea, GeneralUtility::intExplode(',', (string)($conf['offset'])));
|
|
$blurRate = MathUtility::forceIntegerInRange((int)$conf['blur'], 0, 99);
|
|
// No effects if ImageMagick ver. 5+
|
|
if (!$blurRate || !$this->processorEffectsEnabled) {
|
|
$txtConf['fontColor'] = $conf['color'];
|
|
$this->makeText($im, $txtConf, $workArea);
|
|
} else {
|
|
$w = imagesx($im);
|
|
$h = imagesy($im);
|
|
// Area around the blur used for cropping something
|
|
$blurBorder = 3;
|
|
$tmpStr = $this->imageService->randomName();
|
|
$fileMenu = $tmpStr . '_menu.png';
|
|
$fileColor = $tmpStr . '_color.png';
|
|
$fileMask = $tmpStr . '_mask.png';
|
|
// BlurColor Image laves
|
|
$blurColImg = imagecreatetruecolor($w, $h);
|
|
$bcols = $this->convertColor($conf['color']);
|
|
$Bcolor = imagecolorallocate($blurColImg, $bcols[0], $bcols[1], $bcols[2]);
|
|
imagefilledrectangle($blurColImg, 0, 0, $w, $h, $Bcolor);
|
|
$this->ImageWrite($blurColImg, $fileColor);
|
|
// The mask is made: BlurTextImage
|
|
$blurTextImg = imagecreatetruecolor($w + $blurBorder * 2, $h + $blurBorder * 2);
|
|
// Black background
|
|
$Bcolor = imagecolorallocate($blurTextImg, 0, 0, 0);
|
|
imagefilledrectangle($blurTextImg, 0, 0, $w + $blurBorder * 2, $h + $blurBorder * 2, $Bcolor);
|
|
$txtConf['fontColor'] = 'white';
|
|
$blurBordArr = [$blurBorder, $blurBorder];
|
|
$this->makeText($blurTextImg, $txtConf, $this->applyOffset($workArea, $blurBordArr));
|
|
// Dump to temporary file
|
|
$this->ImageWrite($blurTextImg, $fileMask);
|
|
$command = $this->imageService->v5_blur($blurRate + 1);
|
|
$this->imageService->imageMagickExec($fileMask, $fileMask, $command . ($GLOBALS['TYPO3_CONF_VARS']['GFX']['processor'] === 'ImageMagick' ? ' -alpha off' : ' +matte'));
|
|
// The mask is loaded again
|
|
$blurTextImg_tmp = $this->imageCreateFromFile($fileMask);
|
|
// If nothing went wrong we continue with the blurred mask
|
|
if ($blurTextImg_tmp) {
|
|
// Cropping the border from the mask
|
|
$blurTextImg = imagecreatetruecolor($w, $h);
|
|
$blurTextImg = $this->imagecopyresized($blurTextImg, $blurTextImg_tmp, 0, 0, $blurBorder, $blurBorder, $w, $h, $w, $h);
|
|
// Adjust the mask
|
|
$intensity = 40;
|
|
if ($conf['intensity'] ?? false) {
|
|
$intensity = MathUtility::forceIntegerInRange($conf['intensity'], 0, 100);
|
|
}
|
|
$intensity = (int)ceil(255 - $intensity / 100 * 255);
|
|
$this->inputLevels($blurTextImg, 0, $intensity);
|
|
$opacity = MathUtility::forceIntegerInRange((int)$conf['opacity'], 0, 100);
|
|
if ($opacity && $opacity < 100) {
|
|
$high = (int)ceil(255 * $opacity / 100);
|
|
// Reducing levels as the opacity demands
|
|
$this->outputLevels($blurTextImg, 0, $high);
|
|
}
|
|
// Dump the mask again
|
|
$this->ImageWrite($blurTextImg, $fileMask);
|
|
// The pictures are combined
|
|
// The main pictures is saved temporarily
|
|
$this->ImageWrite($im, $fileMenu);
|
|
$this->imageService->combineExec($fileMenu, $fileColor, $fileMask, $fileMenu);
|
|
// The main image is loaded again...
|
|
$backIm = $this->imageCreateFromFile($fileMenu);
|
|
// ... and if nothing went wrong we load it onto the old one.
|
|
if ($backIm) {
|
|
if (!$this->saveAlphaLayer) {
|
|
imagecolortransparent($backIm, -1);
|
|
}
|
|
$im = $backIm;
|
|
}
|
|
}
|
|
// Deleting temporary files;
|
|
unlink($fileMenu);
|
|
unlink($fileColor);
|
|
unlink($fileMask);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "BOX" GIFBUILDER object
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @param array $workArea The current working area coordinates.
|
|
* @see make()
|
|
* @internal
|
|
*/
|
|
public function makeBox(\GdImage &$im, array $conf, array $workArea): void
|
|
{
|
|
$cords = GeneralUtility::intExplode(',', $conf['dimensions'] . ',,,');
|
|
$conf['offset'] = $cords[0] . ',' . $cords[1];
|
|
$cords = $this->objPosition($conf, $workArea, [$cords[2], $cords[3]]);
|
|
$cols = $this->convertColor($conf['color'] ?? '');
|
|
$opacity = 0;
|
|
if (isset($conf['opacity'])) {
|
|
// conversion:
|
|
// PHP 0 = opaque, 127 = transparent
|
|
// TYPO3 100 = opaque, 0 = transparent
|
|
$opacity = MathUtility::forceIntegerInRange((int)$conf['opacity'], 1, 100, 1);
|
|
$opacity = (int)abs($opacity - 100);
|
|
$opacity = (int)round(127 * $opacity / 100);
|
|
}
|
|
$tmpColor = imagecolorallocatealpha($im, $cols[0], $cols[1], $cols[2], $opacity);
|
|
imagefilledrectangle($im, $cords[0], $cords[1], $cords[0] + $cords[2] - 1, $cords[1] + $cords[3] - 1, $tmpColor);
|
|
}
|
|
|
|
/**
|
|
* Implements the "Ellipse" GIFBUILDER object
|
|
* Example Typoscript:
|
|
* file = GIFBUILDER
|
|
* file {
|
|
* XY = 200,200
|
|
* format = jpg
|
|
* quality = 100
|
|
* 10 = ELLIPSE
|
|
* 10.dimensions = 100,100,50,50
|
|
* 10.color = red
|
|
*
|
|
* $workArea = X,Y
|
|
* $conf['dimensions'] = offset x, offset y, width of ellipse, height of ellipse
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @param array $workArea The current working area coordinates.
|
|
* @see make()
|
|
*/
|
|
public function makeEllipse(\GdImage &$im, array $conf, array $workArea): void
|
|
{
|
|
$ellipseConfiguration = GeneralUtility::intExplode(',', $conf['dimensions'] . ',,,');
|
|
// Ellipse offset inside workArea (x/y)
|
|
$conf['offset'] = $ellipseConfiguration[0] . ',' . $ellipseConfiguration[1];
|
|
// @see objPosition
|
|
$imageCoordinates = $this->objPosition($conf, $workArea, [$ellipseConfiguration[2], $ellipseConfiguration[3]]);
|
|
$color = $this->convertColor($conf['color'] ?? '');
|
|
$fillingColor = imagecolorallocate($im, $color[0], $color[1], $color[2]);
|
|
imagefilledellipse($im, $imageCoordinates[0], $imageCoordinates[1], $imageCoordinates[2], $imageCoordinates[3], $fillingColor);
|
|
}
|
|
|
|
/**
|
|
* Implements the "EFFECT" GIFBUILDER object
|
|
* The operation involves ImageMagick for applying effects
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @see make()
|
|
* @see applyImageMagickToPHPGif()
|
|
*/
|
|
protected function makeEffect(\GdImage &$im, array $conf): void
|
|
{
|
|
$commands = $this->IMparams($conf['value']);
|
|
if ($commands) {
|
|
$this->applyImageMagickToPHPGif($im, $commands);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "ADJUST" GIFBUILDER object
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @see make()
|
|
* @see autoLevels()
|
|
* @see outputLevels()
|
|
* @see inputLevels()
|
|
*/
|
|
protected function adjust(\GdImage &$im, array $conf): void
|
|
{
|
|
$setup = $conf['value'];
|
|
if (!trim($setup)) {
|
|
return;
|
|
}
|
|
$effects = explode('|', $setup);
|
|
foreach ($effects as $val) {
|
|
$pairs = explode('=', $val, 2);
|
|
$value = trim($pairs[1]);
|
|
$effect = strtolower(trim($pairs[0]));
|
|
switch ($effect) {
|
|
case 'inputlevels':
|
|
// low,high
|
|
$params = GeneralUtility::intExplode(',', $value);
|
|
$this->inputLevels($im, $params[0], $params[1]);
|
|
break;
|
|
case 'outputlevels':
|
|
$params = GeneralUtility::intExplode(',', $value);
|
|
$this->outputLevels($im, $params[0], $params[1]);
|
|
break;
|
|
case 'autolevels':
|
|
$this->autolevels($im);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "CROP" GIFBUILDER object
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @see make()
|
|
*/
|
|
protected function crop(\GdImage &$im, array $conf): void
|
|
{
|
|
// Clears workArea to total image
|
|
$this->setWorkArea('');
|
|
$cords = GeneralUtility::intExplode(',', $conf['crop'] . ',,,');
|
|
$conf['offset'] = $cords[0] . ',' . $cords[1];
|
|
$cords = $this->objPosition($conf, $this->workArea, [$cords[2], $cords[3]]);
|
|
$newIm = imagecreatetruecolor($cords[2], $cords[3]);
|
|
$cols = $this->convertColor(!empty($conf['backColor']) ? $conf['backColor'] : $this->setup['backColor']);
|
|
$Bcolor = imagecolorallocate($newIm, $cols[0], $cols[1], $cols[2]);
|
|
imagefilledrectangle($newIm, 0, 0, $cords[2], $cords[3], $Bcolor);
|
|
$newConf = [];
|
|
$workArea = [0, 0, $cords[2], $cords[3]];
|
|
if ($cords[0] < 0) {
|
|
$workArea[0] = abs($cords[0]);
|
|
} else {
|
|
$newConf['offset'] = -$cords[0];
|
|
}
|
|
if ($cords[1] < 0) {
|
|
$workArea[1] = abs($cords[1]);
|
|
} else {
|
|
$newConf['offset'] .= ',' . -$cords[1];
|
|
}
|
|
$this->copyGifOntoGif($newIm, $im, $newConf, $workArea);
|
|
$im = $newIm;
|
|
$this->w = imagesx($im);
|
|
$this->h = imagesy($im);
|
|
// Clears workArea to total image
|
|
$this->setWorkArea('');
|
|
}
|
|
|
|
/**
|
|
* Implements the "SCALE" GIFBUILDER object
|
|
*
|
|
* @param \GdImage $im GDlib image pointer
|
|
* @param array $conf TypoScript array with configuration for the GIFBUILDER object.
|
|
* @see make()
|
|
*/
|
|
protected function scale(\GdImage &$im, array $conf): void
|
|
{
|
|
// @todo: not covered with tests
|
|
if (isset($conf['width']) || isset($conf['height']) || isset($conf['params'])) {
|
|
$tmpStr = $this->imageService->randomName();
|
|
$theFile = $tmpStr . '.png';
|
|
$this->ImageWrite($im, $theFile);
|
|
$theNewFile = $this->imageService->resize($theFile, 'png', $conf['width'] ?? '', $conf['height'] ?? '', $conf['params'] ?? '');
|
|
if ($theNewFile->isFile()) {
|
|
$tmpImg = $this->imageCreateFromFile($theNewFile->getRealPath());
|
|
if ($tmpImg) {
|
|
$im = $tmpImg;
|
|
$this->w = imagesx($im);
|
|
$this->h = imagesy($im);
|
|
// Clears workArea to total image
|
|
$this->setWorkArea('');
|
|
}
|
|
unlink($theFile);
|
|
if ($theNewFile->getRealPath() !== $theFile) {
|
|
unlink($theNewFile->getRealPath());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements the "WORKAREA" GIFBUILDER object when setting it
|
|
* Setting internal working area boundaries (->workArea)
|
|
*
|
|
* @param string $workArea Working area dimensions, comma separated
|
|
* @internal
|
|
* @see make()
|
|
*/
|
|
protected function setWorkArea(string $workArea): void
|
|
{
|
|
$this->workArea = GeneralUtility::intExplode(',', $workArea);
|
|
$this->workArea = $this->applyOffset($this->workArea, $this->offset);
|
|
if (!($this->workArea[2] ?? false)) {
|
|
$this->workArea[2] = $this->w;
|
|
}
|
|
if (!($this->workArea[3] ?? false)) {
|
|
$this->workArea[3] = $this->h;
|
|
}
|
|
}
|
|
|
|
/*********************************************
|
|
*
|
|
* Various helper functions
|
|
*
|
|
********************************************/
|
|
/**
|
|
* Initializing/Cleaning of TypoScript properties for TEXT GIFBUILDER objects
|
|
*
|
|
* 'cleans' TEXT-object; Checks fontfile and other vital setup
|
|
* Finds the title if its a 'variable' (instantiates a cObj and loads it with the ->data record)
|
|
* Performs caseshift if any.
|
|
*
|
|
* @param array $conf GIFBUILDER object TypoScript properties
|
|
* @return array|null Modified $conf array IF the "text" property is not blank
|
|
*/
|
|
protected function checkTextObj(array $conf): ?array
|
|
{
|
|
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
|
$cObj->start($this->data);
|
|
$isStdWrapped = [];
|
|
foreach ($conf as $key => $value) {
|
|
$parameter = rtrim($key, '.');
|
|
if (!($isStdWrapped[$parameter] ?? false) && isset($conf[$parameter . '.'])) {
|
|
$conf[$parameter] = $cObj->stdWrapValue($parameter, $conf);
|
|
$isStdWrapped[$parameter] = 1;
|
|
}
|
|
}
|
|
|
|
if (!($conf['fontFile'] ?? false)) {
|
|
$conf['fontFile'] = 'EXT:core/Resources/Private/Font/nimbus.ttf';
|
|
}
|
|
if (!($conf['iterations'] ?? false)) {
|
|
$conf['iterations'] = 1;
|
|
}
|
|
if (!($conf['fontSize'] ?? false)) {
|
|
$conf['fontSize'] = 12;
|
|
}
|
|
// If any kind of spacing applies, we cannot use angles!!
|
|
if (($conf['spacing'] ?? false) || ($conf['wordSpacing'] ?? false)) {
|
|
$conf['angle'] = 0;
|
|
}
|
|
if (!isset($conf['antiAlias'])) {
|
|
$conf['antiAlias'] = 1;
|
|
}
|
|
$conf['fontColor'] = trim($conf['fontColor'] ?? '');
|
|
// Strip HTML
|
|
if (!($conf['doNotStripHTML'] ?? false)) {
|
|
$conf['text'] = strip_tags($conf['text'] ?? '');
|
|
}
|
|
$this->combinedTextStrings[] = strip_tags($conf['text'] ?? '');
|
|
// Max length = 100 if automatic line breaks are not defined:
|
|
if (!isset($conf['breakWidth']) || !$conf['breakWidth']) {
|
|
$tlen = (int)($conf['textMaxLength'] ?? 0) ?: 100;
|
|
$conf['text'] = mb_substr($conf['text'], 0, $tlen, 'utf-8');
|
|
}
|
|
if ((string)$conf['text'] != '') {
|
|
// Char range map thingie:
|
|
$fontBaseName = PathUtility::basename($conf['fontFile']);
|
|
if (is_array($this->charRangeMap[$fontBaseName] ?? null)) {
|
|
// Initialize splitRendering array:
|
|
if (!is_array($conf['splitRendering.'])) {
|
|
$conf['splitRendering.'] = [];
|
|
}
|
|
$cfgK = $this->charRangeMap[$fontBaseName]['cfgKey'];
|
|
// Do not impose settings if a splitRendering object already exists:
|
|
if (!isset($conf['splitRendering.'][$cfgK])) {
|
|
// Set configuration:
|
|
$conf['splitRendering.'][$cfgK] = 'charRange';
|
|
$conf['splitRendering.'][$cfgK . '.'] = $this->charRangeMap[$fontBaseName]['charMapConfig'];
|
|
// Multiplicator of fontsize:
|
|
if ($this->charRangeMap[$fontBaseName]['multiplicator']) {
|
|
$conf['splitRendering.'][$cfgK . '.']['fontSize'] = round($conf['fontSize'] * $this->charRangeMap[$fontBaseName]['multiplicator']);
|
|
}
|
|
// Multiplicator of pixelSpace:
|
|
if ($this->charRangeMap[$fontBaseName]['pixelSpace']) {
|
|
$travKeys = ['xSpaceBefore', 'xSpaceAfter', 'ySpaceBefore', 'ySpaceAfter'];
|
|
foreach ($travKeys as $pxKey) {
|
|
if (isset($conf['splitRendering.'][$cfgK . '.'][$pxKey])) {
|
|
$conf['splitRendering.'][$cfgK . '.'][$pxKey] = round($conf['splitRendering.'][$cfgK . '.'][$pxKey] * ($conf['fontSize'] / $this->charRangeMap[$fontBaseName]['pixelSpace']));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $conf;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Calculation of offset using "splitCalc" and insertion of dimensions from other GIFBUILDER objects.
|
|
*
|
|
* Example:
|
|
* Input: 2+2, 2*3, 123, [10.w]
|
|
* Output: 4,6,123,45 (provided that the width of object in position 10 was 45 pixels wide)
|
|
*
|
|
* @param string $string The string to resolve/calculate the result of. The string is divided by a comma first and each resulting part is calculated into an integer.
|
|
* @return string The resolved string with each part (separated by comma) returned separated by comma
|
|
* @internal
|
|
*/
|
|
public function calcOffset(string $string): string
|
|
{
|
|
$value = [];
|
|
$numbers = GeneralUtility::trimExplode(',', $this->calculateFunctions($string));
|
|
foreach ($numbers as $key => $val) {
|
|
if ((string)$val == (string)(int)$val) {
|
|
$value[$key] = (int)$val;
|
|
} else {
|
|
$value[$key] = $this->calculateValue($val);
|
|
}
|
|
}
|
|
$string = implode(',', $value);
|
|
return $string;
|
|
}
|
|
|
|
/**
|
|
* Returns an "imgResource" creating an instance of the ContentObjectRenderer class and calling ContentObjectRenderer::getImgResource
|
|
*
|
|
* @param string|File $file Filename value OR the string "GIFBUILDER", see documentation in TSref for the "datatype" called "imgResource" - can also be a FAL file
|
|
* @param array $fileArray TypoScript properties passed to the function. Either GIFBUILDER properties or imgResource properties, depending on the value of $file (whether that is "GIFBUILDER" or a file reference)
|
|
* @return ImageResource|null Returns the ImageResource DTO with file information from ContentObjectRenderer::getImgResource() - or NULL
|
|
* @see ContentObjectRenderer::getImgResource()
|
|
*/
|
|
protected function getResource(string|File $file, array $fileArray): ?ImageResource
|
|
{
|
|
$context = GeneralUtility::makeInstance(Context::class);
|
|
$deferProcessing = !$context->hasAspect('fileProcessing') || $context->getPropertyFromAspect('fileProcessing', 'deferProcessing');
|
|
$context->setAspect('fileProcessing', new FileProcessingAspect(false));
|
|
try {
|
|
if (!in_array($fileArray['ext'] ?? '', $this->imageService->getImageFileExt(), true)) {
|
|
$fileArray['ext'] = 'png';
|
|
}
|
|
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
|
$cObj->start($this->data);
|
|
return $cObj->getImgResource($file, $fileArray);
|
|
} finally {
|
|
$context->setAspect('fileProcessing', new FileProcessingAspect($deferProcessing));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculates the GIFBUILDER output filename based on a serialized, hashed
|
|
* value of this->setup and prefixes the original filename.
|
|
* The filename gets an additional prefix (max 100 characters),
|
|
* something like "GB_MD5HASH_myfilename_is_very_long_and_such.jpg".
|
|
*/
|
|
protected function fileName(): string
|
|
{
|
|
$basicFileFunctions = GeneralUtility::makeInstance(BasicFileUtility::class);
|
|
$filePrefix = implode('_', array_merge($this->combinedTextStrings, $this->combinedFileNames));
|
|
$filePrefix = $basicFileFunctions->cleanFileName(ltrim($filePrefix, '.'));
|
|
|
|
// shorten prefix to avoid overly long file names
|
|
$filePrefix = substr($filePrefix, 0, 100);
|
|
|
|
$configurationService = GeneralUtility::makeInstance(ConfigurationService::class);
|
|
|
|
// we use ConfigurationService::serialize here to use as much of $this->setup as possible,
|
|
// but preventing inclusion of objects that could cause problems with json_encode
|
|
$hashInputForFileName = [
|
|
$configurationService->serialize($this->setup),
|
|
$filePrefix,
|
|
$this->XY,
|
|
$this->w,
|
|
$this->h,
|
|
$this->offset,
|
|
$this->workArea,
|
|
$this->combinedTextStrings,
|
|
$this->combinedFileNames,
|
|
$this->data,
|
|
];
|
|
return $filePrefix . '_' . md5((string)json_encode($hashInputForFileName)) . '.' . $this->extension();
|
|
}
|
|
|
|
/**
|
|
* Returns the file extension used in the filename
|
|
*/
|
|
protected function extension(): string
|
|
{
|
|
return match (strtolower($this->setup['format'] ?? '')) {
|
|
'jpg', 'jpeg' => 'jpg',
|
|
'gif' => 'gif',
|
|
'webp' => 'webp',
|
|
'avif' => 'avif',
|
|
default => 'png',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculates the value concerning the dimensions of objects.
|
|
*
|
|
* @param string $string The string to be calculated (e.g. "[20.h]+13")
|
|
* @return int The calculated value (e.g. "23")
|
|
* @see calcOffset()
|
|
*/
|
|
protected function calculateValue(string $string): int
|
|
{
|
|
$calculatedValue = 0;
|
|
$parts = GeneralUtility::splitCalc($string, '+-*/%');
|
|
foreach ($parts as $part) {
|
|
$theVal = $part[1];
|
|
$sign = $part[0];
|
|
if (((string)(int)$theVal) == ((string)$theVal)) {
|
|
$theVal = (int)$theVal;
|
|
} elseif ('[' . substr($theVal, 1, -1) . ']' == $theVal) {
|
|
$objParts = explode('.', substr($theVal, 1, -1));
|
|
$theVal = 0;
|
|
if (isset($this->objBB[$objParts[0]], $objParts[1])) {
|
|
if ($objParts[1] === 'w' && isset($this->objBB[$objParts[0]][0])) {
|
|
$theVal = $this->objBB[$objParts[0]][0];
|
|
} elseif ($objParts[1] === 'h' && isset($this->objBB[$objParts[0]][1])) {
|
|
$theVal = $this->objBB[$objParts[0]][1];
|
|
} elseif ($objParts[1] === 'lineHeight' && isset($this->objBB[$objParts[0]][2]['lineHeight'])) {
|
|
$theVal = $this->objBB[$objParts[0]][2]['lineHeight'];
|
|
}
|
|
$theVal = (int)$theVal;
|
|
}
|
|
} elseif ((float)$theVal) {
|
|
$theVal = (float)$theVal;
|
|
} else {
|
|
$theVal = 0;
|
|
}
|
|
if ($sign === '-') {
|
|
$calculatedValue -= $theVal;
|
|
} elseif ($sign === '+') {
|
|
$calculatedValue += $theVal;
|
|
} elseif ($sign === '/' && $theVal) {
|
|
$calculatedValue /= $theVal;
|
|
} elseif ($sign === '*') {
|
|
$calculatedValue *= $theVal;
|
|
} elseif ($sign === '%' && $theVal) {
|
|
$calculatedValue %= $theVal;
|
|
}
|
|
}
|
|
return (int)round($calculatedValue);
|
|
}
|
|
|
|
/**
|
|
* Calculates special functions:
|
|
* + max([10.h], [20.h]) -> gets the maximum of the given values
|
|
*
|
|
* @param string $string The raw string with functions to be calculated
|
|
* @return string The calculated values
|
|
*/
|
|
protected function calculateFunctions(string $string): string
|
|
{
|
|
if (preg_match_all('#max\\(([^)]+)\\)#', $string, $matches)) {
|
|
foreach ($matches[1] as $index => $maxExpression) {
|
|
$string = str_replace($matches[0][$index], (string)$this->calculateMaximum($maxExpression), $string);
|
|
}
|
|
}
|
|
return $string;
|
|
}
|
|
|
|
/**
|
|
* Calculates the maximum of a set of values defined like "[10.h],[20.h],1000"
|
|
*
|
|
* @param string $value The string to be used to calculate the maximum (e.g. "[10.h],[20.h],1000")
|
|
* @return int The maximum value of the given comma separated and calculated values
|
|
*/
|
|
protected function calculateMaximum(string $value): int
|
|
{
|
|
$parts = GeneralUtility::trimExplode(',', $this->calcOffset($value), true);
|
|
return $parts !== [] ? (int)max($parts) : 0;
|
|
}
|
|
|
|
/**
|
|
* Create an array with object position/boundaries based on input TypoScript configuration (such as the "align" property is used), the work area definition and $BB array
|
|
*
|
|
* @param array $conf TypoScript configuration for a GIFBUILDER object
|
|
* @param array $workArea Workarea definition
|
|
* @param array $BB BB (Bounding box) array. Not just used for TEXT objects but also for others
|
|
* @return array [0]=x, [1]=y, [2]=w, [3]=h
|
|
* @internal
|
|
* @see copyGifOntoGif()
|
|
* @see makeBox()
|
|
* @see crop()
|
|
*/
|
|
protected function objPosition(array $conf, array $workArea, array $BB): array
|
|
{
|
|
// offset, align, valign, workarea
|
|
$result = [];
|
|
$result[2] = $BB[0];
|
|
$result[3] = $BB[1];
|
|
$w = $workArea[2];
|
|
$h = $workArea[3];
|
|
$align = explode(',', $conf['align'] ?? ',');
|
|
$align[0] = strtolower(substr(trim($align[0]), 0, 1));
|
|
$align[1] = strtolower(substr(trim($align[1]), 0, 1));
|
|
switch ($align[0]) {
|
|
case 'r':
|
|
$result[0] = $w - $result[2];
|
|
break;
|
|
case 'c':
|
|
$result[0] = round(($w - $result[2]) / 2);
|
|
break;
|
|
default:
|
|
$result[0] = 0;
|
|
}
|
|
switch ($align[1]) {
|
|
case 'b':
|
|
// y pos
|
|
$result[1] = $h - $result[3];
|
|
break;
|
|
case 'c':
|
|
$result[1] = round(($h - $result[3]) / 2);
|
|
break;
|
|
default:
|
|
$result[1] = 0;
|
|
}
|
|
$result = $this->applyOffset($result, GeneralUtility::intExplode(',', (string)($conf['offset'] ?? '')));
|
|
$result = $this->applyOffset($result, $workArea);
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Applies offset value to coordinated in $cords.
|
|
* Basically the value of key 0/1 of $OFFSET is added to keys 0/1 of $cords
|
|
*
|
|
* @param array $cords Integer coordinates in key 0/1
|
|
* @param array $offset Offset values in key 0/1
|
|
* @return array Modified $cords array
|
|
*/
|
|
protected function applyOffset(array $cords, array $offset): array
|
|
{
|
|
$cords[0] = (int)$cords[0] + (int)$offset[0];
|
|
$cords[1] = (int)($cords[1] ?? 0) + (int)($offset[1] ?? 0);
|
|
return $cords;
|
|
}
|
|
|
|
/**
|
|
* Copies two GDlib image pointers onto each other, using TypoScript configuration from $conf and the input $workArea definition.
|
|
*
|
|
* @param \GdImage $im GDlib image pointer, destination (bottom image)
|
|
* @param \GdImage $cpImg GDlib image pointer, source (top image)
|
|
* @param array $conf TypoScript array with the properties for the IMAGE GIFBUILDER object. Only used for the "tile" property value.
|
|
* @param array $workArea Work area
|
|
*/
|
|
protected function copyGifOntoGif(\GdImage &$im, \GdImage &$cpImg, array $conf, array $workArea): void
|
|
{
|
|
$cpW = imagesx($cpImg);
|
|
$cpH = imagesy($cpImg);
|
|
$tile = GeneralUtility::intExplode(',', (string)($conf['tile'] ?? ''));
|
|
$tile[0] = MathUtility::forceIntegerInRange($tile[0], 1, 20);
|
|
$tile[1] = MathUtility::forceIntegerInRange($tile[1] ?? 0, 1, 20);
|
|
$cpOff = $this->objPosition($conf, $workArea, [$cpW * $tile[0], $cpH * $tile[1]]);
|
|
for ($xt = 0; $xt < $tile[0]; $xt++) {
|
|
$Xstart = $cpOff[0] + $cpW * $xt;
|
|
// If this image is inside of the workArea, then go on
|
|
if ($Xstart + $cpW > $workArea[0]) {
|
|
// X:
|
|
if ($Xstart < $workArea[0]) {
|
|
$cpImgCutX = $workArea[0] - $Xstart;
|
|
$Xstart = $workArea[0];
|
|
} else {
|
|
$cpImgCutX = 0;
|
|
}
|
|
$w = $cpW - $cpImgCutX;
|
|
if ($Xstart > $workArea[0] + $workArea[2] - $w) {
|
|
$w = $workArea[0] + $workArea[2] - $Xstart;
|
|
}
|
|
// If this image is inside of the workArea, then go on
|
|
if ($Xstart < $workArea[0] + $workArea[2]) {
|
|
// Y:
|
|
for ($yt = 0; $yt < $tile[1]; $yt++) {
|
|
$Ystart = $cpOff[1] + $cpH * $yt;
|
|
// If this image is inside of the workArea, then go on
|
|
if ($Ystart + $cpH > $workArea[1]) {
|
|
if ($Ystart < $workArea[1]) {
|
|
$cpImgCutY = $workArea[1] - $Ystart;
|
|
$Ystart = $workArea[1];
|
|
} else {
|
|
$cpImgCutY = 0;
|
|
}
|
|
$h = $cpH - $cpImgCutY;
|
|
if ($Ystart > $workArea[1] + $workArea[3] - $h) {
|
|
$h = $workArea[1] + $workArea[3] - $Ystart;
|
|
}
|
|
// If this image is inside of the workArea, then go on
|
|
if ($Ystart < $workArea[1] + $workArea[3]) {
|
|
$im = $this->imagecopyresized($im, $cpImg, $Xstart, $Ystart, $cpImgCutX, $cpImgCutY, $w, $h, $w, $h);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Alternative function for using the similar PHP function imagecopyresized(). Used for GD2 only.
|
|
*
|
|
* OK, the reason for this stupid fix is the following story:
|
|
* GD1.x was capable of copying two images together and combining their palettes! GD2 is apparently not.
|
|
* With GD2 only the palette of the dest-image is used which mostly results in totally black images when trying to
|
|
* copy a color-ful image onto the destination.
|
|
* The GD2-fix is to
|
|
* 1) Create a blank TRUE-COLOR image
|
|
* 2) Copy the destination image onto that one
|
|
* 3) Then do the actual operation; Copying the source (top image) onto that
|
|
* 4) ... and return the result pointer.
|
|
* 5) Reduce colors (if we do not, the result may become strange!)
|
|
* It works, but the resulting images is now a true-color PNG which may be very large.
|
|
* So, why not use 'imagetruecolortopalette ($im, TRUE, 256)' - well because it does NOT WORK! So simple is that.
|
|
*
|
|
* @param \GdImage $dstImg Destination image
|
|
* @param \GdImage $srcImg Source image
|
|
* @param int $dstX Destination x-coordinate
|
|
* @param int $dstY Destination y-coordinate
|
|
* @param int $srcX Source x-coordinate
|
|
* @param int $srcY Source y-coordinate
|
|
* @param int $dstWidth Destination width
|
|
* @param int $dstHeight Destination height
|
|
* @param int $srcWidth Source width
|
|
* @param int $srcHeight Source height
|
|
*/
|
|
protected function imagecopyresized(\GdImage $dstImg, \GdImage &$srcImg, int $dstX, int $dstY, int $srcX, int $srcY, int $dstWidth, int $dstHeight, int $srcWidth, int $srcHeight): \GdImage
|
|
{
|
|
if (!$this->saveAlphaLayer) {
|
|
// Make true color image
|
|
$tmpImg = imagecreatetruecolor(imagesx($dstImg), imagesy($dstImg));
|
|
// Copy the source image onto that
|
|
imagecopyresized($tmpImg, $dstImg, 0, 0, 0, 0, imagesx($dstImg), imagesy($dstImg), imagesx($dstImg), imagesy($dstImg));
|
|
// Then copy the source image onto that (the actual operation!)
|
|
imagecopyresized($tmpImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
|
|
return $tmpImg;
|
|
}
|
|
imagecopyresized($dstImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $dstWidth, $dstHeight, $srcWidth, $srcHeight);
|
|
return $dstImg;
|
|
}
|
|
|
|
/**
|
|
* Creates some offset values in an array used to simulate a circularly applied outline around TEXT
|
|
*
|
|
* @param int $distance Distance
|
|
* @param int $iterations Iterations.
|
|
* @see makeOutline()
|
|
*/
|
|
protected function circleOffset(int $distance, int $iterations): array
|
|
{
|
|
$res = [];
|
|
if ($distance && $iterations) {
|
|
for ($a = 0; $a < $iterations; $a++) {
|
|
$yOff = round(sin(2 * M_PI / $iterations * ($a + 1)) * 100 * $distance);
|
|
if ($yOff) {
|
|
$yOff = (int)(ceil(abs($yOff / 100)) * ($yOff / abs($yOff)));
|
|
}
|
|
$xOff = round(cos(2 * M_PI / $iterations * ($a + 1)) * 100 * $distance);
|
|
if ($xOff) {
|
|
$xOff = (int)(ceil(abs($xOff / 100)) * ($xOff / abs($xOff)));
|
|
}
|
|
$res[$a] = [$xOff, $yOff];
|
|
}
|
|
}
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Creating ImageMagick parameters from TypoScript property
|
|
*
|
|
* @param string $setup A string with effect keywords=value pairs separated by "|
|
|
* @return string ImageMagick prepared parameters.
|
|
* @see makeEffect()
|
|
*/
|
|
protected function IMparams(string $setup): string
|
|
{
|
|
if (!trim($setup)) {
|
|
return '';
|
|
}
|
|
$effects = explode('|', $setup);
|
|
$commands = '';
|
|
foreach ($effects as $val) {
|
|
$pairs = explode('=', $val, 2);
|
|
$value = trim($pairs[1] ?? '');
|
|
$effect = strtolower(trim($pairs[0]));
|
|
switch ($effect) {
|
|
case 'gamma':
|
|
$commands .= ' -gamma ' . (float)$value;
|
|
break;
|
|
case 'blur':
|
|
if ($this->processorEffectsEnabled) {
|
|
$commands .= $this->imageService->v5_blur((int)$value);
|
|
}
|
|
break;
|
|
case 'sharpen':
|
|
if ($this->processorEffectsEnabled) {
|
|
$commands .= $this->imageService->v5_sharpen((int)$value);
|
|
}
|
|
break;
|
|
case 'rotate':
|
|
$commands .= ' -rotate ' . MathUtility::forceIntegerInRange((int)$value, 0, 360);
|
|
break;
|
|
case 'solarize':
|
|
$commands .= ' -solarize ' . MathUtility::forceIntegerInRange((int)$value, 0, 99);
|
|
break;
|
|
case 'swirl':
|
|
$commands .= ' -swirl ' . MathUtility::forceIntegerInRange((int)$value, 0, 1000);
|
|
break;
|
|
case 'wave':
|
|
$params = GeneralUtility::intExplode(',', $value);
|
|
$commands .= ' -wave ' . MathUtility::forceIntegerInRange($params[0], 0, 99) . 'x' . MathUtility::forceIntegerInRange($params[1], 0, 99);
|
|
break;
|
|
case 'charcoal':
|
|
$commands .= ' -charcoal ' . MathUtility::forceIntegerInRange((int)$value, 0, 100);
|
|
break;
|
|
case 'gray':
|
|
$commands .= ' -colorspace GRAY';
|
|
break;
|
|
case 'edge':
|
|
$commands .= ' -edge ' . MathUtility::forceIntegerInRange((int)$value, 0, 99);
|
|
break;
|
|
case 'emboss':
|
|
$commands .= ' -emboss';
|
|
break;
|
|
case 'flip':
|
|
$commands .= ' -flip';
|
|
break;
|
|
case 'flop':
|
|
$commands .= ' -flop';
|
|
break;
|
|
case 'colors':
|
|
$commands .= ' -colors ' . MathUtility::forceIntegerInRange((int)$value, 2, 255);
|
|
break;
|
|
case 'shear':
|
|
$commands .= ' -shear ' . MathUtility::forceIntegerInRange((int)$value, -90, 90);
|
|
break;
|
|
case 'invert':
|
|
$commands .= ' -negate';
|
|
break;
|
|
}
|
|
}
|
|
return $commands;
|
|
}
|
|
|
|
/**
|
|
* Returns the HEX color value for an RGB color array
|
|
*
|
|
* @param array $color RGB color array
|
|
* @return string HEX color value
|
|
*/
|
|
protected function hexColor(array $color): string
|
|
{
|
|
$r = dechex($color[0]);
|
|
if (strlen($r) < 2) {
|
|
$r = '0' . $r;
|
|
}
|
|
$g = dechex($color[1]);
|
|
if (strlen($g) < 2) {
|
|
$g = '0' . $g;
|
|
}
|
|
$b = dechex($color[2]);
|
|
if (strlen($b) < 2) {
|
|
$b = '0' . $b;
|
|
}
|
|
return '#' . $r . $g . $b;
|
|
}
|
|
|
|
/**
|
|
* Unifies all colors given in the colArr color array to the first color in the array.
|
|
*
|
|
* @param \GdImage $img Image resource
|
|
* @param array $colArr Array containing RGB color arrays
|
|
* @return int The index of the unified color
|
|
*/
|
|
protected function unifyColors(\GdImage &$img, array $colArr, bool $closest): int
|
|
{
|
|
$retCol = -1;
|
|
if ($colArr !== [] && function_exists('imagepng') && function_exists('imagecreatefrompng')) {
|
|
$firstCol = array_shift($colArr);
|
|
$firstColArr = $this->convertColor($firstCol);
|
|
$origName = $preName = $this->imageService->randomName() . '.png';
|
|
$postName = $this->imageService->randomName() . '.png';
|
|
$tmpImg = null;
|
|
if (count($colArr) > 1) {
|
|
$this->ImageWrite($img, $preName);
|
|
$firstCol = $this->hexColor($firstColArr);
|
|
foreach ($colArr as $transparentColor) {
|
|
$transparentColor = $this->convertColor($transparentColor);
|
|
$transparentColor = $this->hexColor($transparentColor);
|
|
$cmd = '-fill "' . $firstCol . '" -opaque "' . $transparentColor . '"';
|
|
$this->imageService->imageMagickExec($preName, $postName, $cmd);
|
|
$preName = $postName;
|
|
}
|
|
$this->imageService->imageMagickExec($postName, $origName, '');
|
|
if (@is_file($origName)) {
|
|
$tmpImg = $this->imageCreateFromFile($origName);
|
|
}
|
|
} else {
|
|
$tmpImg = $img;
|
|
}
|
|
if ($tmpImg) {
|
|
$img = $tmpImg;
|
|
if ($closest) {
|
|
$retCol = imagecolorclosest($img, $firstColArr[0], $firstColArr[1], $firstColArr[2]);
|
|
} else {
|
|
$retCol = imagecolorexact($img, $firstColArr[0], $firstColArr[1], $firstColArr[2]);
|
|
}
|
|
}
|
|
// Unlink files from process
|
|
@unlink($origName);
|
|
@unlink($postName);
|
|
}
|
|
return $retCol;
|
|
}
|
|
|
|
/**
|
|
* Converts a "HTML-color" TypoScript datatype to RGB-values.
|
|
* Default is 0,0,0
|
|
*
|
|
* @param string $string "HTML-color" data type string, eg. 'red', '#ffeedd' or '255,0,255'. You can also add a modifying operator afterwards. There are two options: "255,0,255 : 20" - will add 20 to values, result is "255,20,255". Or "255,0,255 : *1.23" which will multiply all RGB values with 1.23
|
|
* @return array RGB values in key 0/1/2 of the array
|
|
*/
|
|
protected function convertColor(string $string): array
|
|
{
|
|
$col = [];
|
|
$cParts = explode(':', $string, 2);
|
|
// Finding the RGB definitions of the color:
|
|
$string = $cParts[0];
|
|
if (str_contains($string, '#')) {
|
|
$string = preg_replace('/[^A-Fa-f0-9]*/', '', $string) ?? '';
|
|
$col[] = hexdec(substr($string, 0, 2));
|
|
$col[] = hexdec(substr($string, 2, 2));
|
|
$col[] = hexdec(substr($string, 4, 2));
|
|
} elseif (str_contains($string, ',')) {
|
|
$string = preg_replace('/[^,0-9]*/', '', $string) ?? '';
|
|
$strArr = explode(',', $string);
|
|
$col[] = (int)$strArr[0];
|
|
$col[] = (int)$strArr[1];
|
|
$col[] = (int)$strArr[2];
|
|
} else {
|
|
$string = strtolower(trim($string));
|
|
if ($this->colMap[$string] ?? false) {
|
|
$col = $this->colMap[$string];
|
|
} else {
|
|
$col = [0, 0, 0];
|
|
}
|
|
}
|
|
// ... and possibly recalculating the value
|
|
if (trim($cParts[1] ?? '')) {
|
|
$cParts[1] = trim($cParts[1]);
|
|
if ($cParts[1][0] === '*') {
|
|
$val = (float)substr($cParts[1], 1);
|
|
$col[0] = MathUtility::forceIntegerInRange((int)($col[0] * $val), 0, 255);
|
|
$col[1] = MathUtility::forceIntegerInRange((int)($col[1] * $val), 0, 255);
|
|
$col[2] = MathUtility::forceIntegerInRange((int)($col[2] * $val), 0, 255);
|
|
} else {
|
|
$val = (int)$cParts[1];
|
|
$col[0] = MathUtility::forceIntegerInRange((int)($col[0] + $val), 0, 255);
|
|
$col[1] = MathUtility::forceIntegerInRange((int)($col[1] + $val), 0, 255);
|
|
$col[2] = MathUtility::forceIntegerInRange((int)($col[2] + $val), 0, 255);
|
|
}
|
|
}
|
|
return $col;
|
|
}
|
|
|
|
/**
|
|
* Calculates text position for printing the text onto the image based on configuration like alignment and workarea.
|
|
*
|
|
* @param array $conf TypoScript array for the TEXT GIFBUILDER object
|
|
* @param array $workArea Work area definition
|
|
* @param array $BB Bounding box information, was set in start()
|
|
* @return array [0]=x, [1]=y, [2]=w, [3]=h
|
|
* @see makeText()
|
|
*/
|
|
protected function txtPosition(array $conf, array $workArea, array $BB): array
|
|
{
|
|
$angle = (int)($conf['angle'] ?? 0) / 180 * M_PI;
|
|
$conf['angle'] = 0;
|
|
$straightBB = $this->calcBBox($conf);
|
|
// offset, align, valign, workarea
|
|
// [0]=x, [1]=y, [2]=w, [3]=h
|
|
$result = [];
|
|
$result[2] = $BB[0];
|
|
$result[3] = $BB[1];
|
|
$w = $workArea[2];
|
|
$alignment = $conf['align'] ?? '';
|
|
switch ($alignment) {
|
|
case 'right':
|
|
|
|
case 'center':
|
|
$factor = abs(cos($angle));
|
|
$sign = cos($angle) < 0 ? -1 : 1;
|
|
$len1 = $sign * $factor * $straightBB[0];
|
|
$len2 = $sign * $BB[0];
|
|
$result[0] = $w - ceil($len2 * $factor + (1 - $factor) * $len1);
|
|
$factor = abs(sin($angle));
|
|
$sign = sin($angle) < 0 ? -1 : 1;
|
|
$len1 = $sign * $factor * $straightBB[0];
|
|
$len2 = $sign * $BB[1];
|
|
$result[1] = ceil($len2 * $factor + (1 - $factor) * $len1);
|
|
break;
|
|
}
|
|
switch ($alignment) {
|
|
case 'right':
|
|
break;
|
|
case 'center':
|
|
$result[0] = round($result[0] / 2);
|
|
$result[1] = round($result[1] / 2);
|
|
break;
|
|
default:
|
|
$result[0] = 0;
|
|
$result[1] = 0;
|
|
}
|
|
$result = $this->applyOffset($result, GeneralUtility::intExplode(',', (string)($conf['offset'] ?? '')));
|
|
$result = $this->applyOffset($result, $workArea);
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Calculates bounding box information for the TEXT GIFBUILDER object.
|
|
*
|
|
* @param array $conf TypoScript array for the TEXT GIFBUILDER object
|
|
* @return array Array with three keys [0]/[1] being x/y and [2] being the bounding box array
|
|
* @internal
|
|
* @see txtPosition()
|
|
* @see start()
|
|
*/
|
|
public function calcBBox(array $conf): array
|
|
{
|
|
$sF = $this->getTextScalFactor($conf);
|
|
[$spacing, $wordSpacing] = $this->calcWordSpacing($conf, $sF);
|
|
$theText = $conf['text'];
|
|
$charInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'] ?? 0, $conf['fontFile'], $theText, ($conf['splitRendering.'] ?? []), $sF);
|
|
$theBBoxInfo = $charInf;
|
|
if ($conf['angle'] ?? false) {
|
|
$xArr = [$charInf[0], $charInf[2], $charInf[4], $charInf[6]];
|
|
$yArr = [$charInf[1], $charInf[3], $charInf[5], $charInf[7]];
|
|
$x = max($xArr) - min($xArr);
|
|
$y = max($yArr) - min($yArr);
|
|
} else {
|
|
$x = $charInf[2] - $charInf[0];
|
|
$y = $charInf[1] - $charInf[7];
|
|
}
|
|
// Set original lineHeight (used by line breaks):
|
|
$theBBoxInfo['lineHeight'] = $y;
|
|
if (!empty($conf['lineHeight'])) {
|
|
$theBBoxInfo['lineHeight'] = (int)$conf['lineHeight'];
|
|
}
|
|
|
|
if ($spacing) {
|
|
$x = 0;
|
|
$utf8Chars = $this->csConvObj->utf8_to_numberarray($theText);
|
|
// For each UTF-8 char, do:
|
|
foreach ($utf8Chars as $char) {
|
|
$charInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $char, $conf['splitRendering.'], $sF);
|
|
$charW = $charInf[2] - $charInf[0];
|
|
$x += $charW + ($char === ' ' ? $wordSpacing : $spacing);
|
|
}
|
|
} elseif ($wordSpacing) {
|
|
$x = 0;
|
|
$bits = explode(' ', $theText);
|
|
foreach ($bits as $word) {
|
|
$word .= ' ';
|
|
$wordInf = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $word, $conf['splitRendering.'], $sF);
|
|
$wordW = $wordInf[2] - $wordInf[0];
|
|
$x += $wordW + $wordSpacing;
|
|
}
|
|
} elseif (isset($conf['breakWidth']) && $conf['breakWidth'] && $this->getRenderedTextWidth($conf['text'], $conf) > $conf['breakWidth']) {
|
|
$maxWidth = 0;
|
|
$currentWidth = 0;
|
|
$breakWidth = $conf['breakWidth'];
|
|
$breakSpace = $this->getBreakSpace($conf, $theBBoxInfo);
|
|
$wordPairs = $this->getWordPairsForLineBreak($conf['text']);
|
|
// Iterate through all word pairs:
|
|
foreach ($wordPairs as $index => $wordPair) {
|
|
$wordWidth = $this->getRenderedTextWidth($wordPair, $conf);
|
|
if ($index == 0 || $currentWidth + $wordWidth <= $breakWidth) {
|
|
$currentWidth += $wordWidth;
|
|
} else {
|
|
$maxWidth = max($maxWidth, $currentWidth);
|
|
$y += $breakSpace;
|
|
// Restart:
|
|
$currentWidth = $wordWidth;
|
|
}
|
|
}
|
|
$x = max($maxWidth, $currentWidth) * $sF;
|
|
}
|
|
if ($sF > 1) {
|
|
$x = ceil($x / $sF);
|
|
$y = ceil($y / $sF);
|
|
foreach ($theBBoxInfo as &$value) {
|
|
$value = ceil($value / $sF);
|
|
}
|
|
}
|
|
return [$x, $y, $theBBoxInfo];
|
|
}
|
|
|
|
/**
|
|
* Printing text onto an image like the PHP function imageTTFText does but in addition it offers options for spacing of letters and words.
|
|
* Spacing is done by printing one char at a time and this means that the spacing is rather uneven and probably not very nice.
|
|
* See
|
|
*
|
|
* @param \GdImage $im (See argument for PHP function imageTTFtext())
|
|
* @param int $fontSize (See argument for PHP function imageTTFtext())
|
|
* @param int $angle (See argument for PHP function imageTTFtext())
|
|
* @param int $x (See argument for PHP function imageTTFtext())
|
|
* @param int $y (See argument for PHP function imageTTFtext())
|
|
* @param int $Fcolor (See argument for PHP function imageTTFtext())
|
|
* @param string $fontFile (See argument for PHP function imageTTFtext())
|
|
* @param string $text (See argument for PHP function imageTTFtext()). UTF-8 string, possibly with entities in.
|
|
* @param int $spacing The spacing of letters in pixels
|
|
* @param int $wordSpacing The spacing of words in pixels
|
|
* @param array $splitRenderingConf Array
|
|
* @param int $sF Scale factor
|
|
*/
|
|
protected function SpacedImageTTFText(\GdImage &$im, int $fontSize, int $angle, int $x, int $y, int $Fcolor, string $fontFile, string $text, int $spacing, int $wordSpacing, array $splitRenderingConf, int $sF = 1): void
|
|
{
|
|
$spacing *= $sF;
|
|
$wordSpacing *= $sF;
|
|
if (!$spacing && $wordSpacing) {
|
|
$bits = explode(' ', $text);
|
|
foreach ($bits as $word) {
|
|
$word .= ' ';
|
|
$wordInf = $this->ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $word, $splitRenderingConf, $sF);
|
|
$wordW = $wordInf[2] - $wordInf[0];
|
|
$this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $word, $splitRenderingConf, $sF);
|
|
$x += $wordW + $wordSpacing;
|
|
}
|
|
} else {
|
|
$utf8Chars = $this->csConvObj->utf8_to_numberarray($text);
|
|
// For each UTF-8 char, do:
|
|
foreach ($utf8Chars as $char) {
|
|
$charInf = $this->ImageTTFBBoxWrapper($fontSize, $angle, $fontFile, $char, $splitRenderingConf, $sF);
|
|
$charW = $charInf[2] - $charInf[0];
|
|
$this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $Fcolor, $fontFile, $char, $splitRenderingConf, $sF);
|
|
$x += $charW + ($char === ' ' ? $wordSpacing : $spacing);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function that finds the right fontsize that will render the textstring within a certain width
|
|
*
|
|
* @param array $conf The TypoScript properties of the TEXT GIFBUILDER object
|
|
* @return int The new fontSize
|
|
* @see start()
|
|
*/
|
|
protected function fontResize(array $conf): int
|
|
{
|
|
// You have to use +calc options like [10.h] in 'offset' to get the right position of your text-image, if you use +calc in XY height!!!!
|
|
$maxWidth = (int)$conf['maxWidth'];
|
|
[$spacing, $wordSpacing] = $this->calcWordSpacing($conf);
|
|
if ($maxWidth) {
|
|
// If any kind of spacing applys, we use this function:
|
|
if ($spacing || $wordSpacing) {
|
|
return $conf['fontSize'];
|
|
}
|
|
do {
|
|
// Determine bounding box.
|
|
$bounds = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $conf['text'], $conf['splitRendering.']);
|
|
if ($conf['angle'] < 0) {
|
|
$pixelWidth = abs($bounds[4] - $bounds[0]);
|
|
} elseif ($conf['angle'] > 0) {
|
|
$pixelWidth = abs($bounds[2] - $bounds[6]);
|
|
} else {
|
|
$pixelWidth = abs($bounds[4] - $bounds[6]);
|
|
}
|
|
// Size is fine, exit:
|
|
if ($pixelWidth <= $maxWidth) {
|
|
break;
|
|
}
|
|
$conf['fontSize']--;
|
|
} while ($conf['fontSize'] > 1);
|
|
}
|
|
return $conf['fontSize'];
|
|
}
|
|
|
|
/**
|
|
* Wrapper for ImageTTFBBox
|
|
*
|
|
* @param int $fontSize (See argument for PHP function ImageTTFBBox())
|
|
* @param int $angle (See argument for PHP function ImageTTFBBox())
|
|
* @param string $fontFile (See argument for PHP function ImageTTFBBox())
|
|
* @param string $string (See argument for PHP function ImageTTFBBox())
|
|
* @param array $splitRendering Split-rendering configuration
|
|
* @param int $sF Scale factor
|
|
* @return array Information array.
|
|
*/
|
|
protected function ImageTTFBBoxWrapper(int $fontSize, int $angle, string $fontFile, string $string, array $splitRendering, int $sF = 1): array
|
|
{
|
|
// Initialize:
|
|
$offsetInfo = [];
|
|
$stringParts = $this->splitString($string, $splitRendering, $fontSize, $fontFile);
|
|
// Traverse string parts:
|
|
foreach ($stringParts as $strCfg) {
|
|
$fontFile = GeneralUtility::getFileAbsFileName($strCfg['fontFile']);
|
|
if (is_readable($fontFile)) {
|
|
// Calculate Bounding Box for part.
|
|
$calc = imagettfbbox($this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, $fontFile, $strCfg['str']);
|
|
// Calculate offsets:
|
|
if (empty($offsetInfo)) {
|
|
// First run, just copy over.
|
|
$offsetInfo = $calc;
|
|
} else {
|
|
$offsetInfo[2] += $calc[2] - $calc[0] + (int)$splitRendering['compX'] + (int)$strCfg['xSpaceBefore'] + (int)$strCfg['xSpaceAfter'];
|
|
$offsetInfo[3] += $calc[3] - $calc[1] - (int)$splitRendering['compY'] - (int)$strCfg['ySpaceBefore'] - (int)$strCfg['ySpaceAfter'];
|
|
$offsetInfo[4] += $calc[4] - $calc[6] + (int)$splitRendering['compX'] + (int)$strCfg['xSpaceBefore'] + (int)$strCfg['xSpaceAfter'];
|
|
$offsetInfo[5] += $calc[5] - $calc[7] - (int)$splitRendering['compY'] - (int)$strCfg['ySpaceBefore'] - (int)$strCfg['ySpaceAfter'];
|
|
}
|
|
} else {
|
|
debug('cannot read file: ' . $fontFile, self::class . '::ImageTTFBBoxWrapper()');
|
|
}
|
|
}
|
|
return $offsetInfo;
|
|
}
|
|
|
|
/**
|
|
* Wrapper for ImageTTFText
|
|
*
|
|
* @param \GdImage $im (See argument for PHP function imageTTFtext())
|
|
* @param int $fontSize (See argument for PHP function imageTTFtext())
|
|
* @param int $angle (See argument for PHP function imageTTFtext())
|
|
* @param int $x (See argument for PHP function imageTTFtext())
|
|
* @param int $y (See argument for PHP function imageTTFtext())
|
|
* @param int $color (See argument for PHP function imageTTFtext())
|
|
* @param string $fontFile (See argument for PHP function imageTTFtext())
|
|
* @param string $string (See argument for PHP function imageTTFtext()). UTF-8 string, possibly with entities in.
|
|
* @param array $splitRendering Split-rendering configuration
|
|
* @param int $sF Scale factor
|
|
*/
|
|
protected function ImageTTFTextWrapper(\GdImage &$im, int $fontSize, int $angle, int $x, int $y, int $color, string $fontFile, string $string, array $splitRendering, int $sF = 1): void
|
|
{
|
|
// Initialize:
|
|
$stringParts = $this->splitString($string, $splitRendering, $fontSize, $fontFile);
|
|
$x = (int)ceil($sF * $x);
|
|
$y = (int)ceil($sF * $y);
|
|
// Traverse string parts:
|
|
foreach ($stringParts as $i => $strCfg) {
|
|
// Initialize:
|
|
$colorIndex = $color;
|
|
// Set custom color if any (only when niceText is off):
|
|
if (($strCfg['color'] ?? false) && $sF == 1) {
|
|
$cols = $this->convertColor($strCfg['color']);
|
|
$colorIndex = imagecolorallocate($im, $cols[0], $cols[1], $cols[2]);
|
|
$colorIndex = $color >= 0 ? $colorIndex : -$colorIndex;
|
|
}
|
|
// Setting xSpaceBefore
|
|
if ($i) {
|
|
$x += (int)$strCfg['xSpaceBefore'];
|
|
$y -= (int)$strCfg['ySpaceBefore'];
|
|
}
|
|
$fontFile = GeneralUtility::getFileAbsFileName($strCfg['fontFile']);
|
|
if (is_readable($fontFile)) {
|
|
// Render part:
|
|
imagettftext($im, $this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, $x, $y, $colorIndex, $fontFile, $strCfg['str']);
|
|
// Calculate offset to apply:
|
|
$wordInf = imagettfbbox($this->compensateFontSizeiBasedOnFreetypeDpi($sF * $strCfg['fontSize']), $angle, GeneralUtility::getFileAbsFileName($strCfg['fontFile']), $strCfg['str']);
|
|
$x += $wordInf[2] - $wordInf[0] + (int)($splitRendering['compX'] ?? 0) + (int)($strCfg['xSpaceAfter'] ?? 0);
|
|
$y += $wordInf[5] - $wordInf[7] - (int)($splitRendering['compY'] ?? 0) - (int)($strCfg['ySpaceAfter'] ?? 0);
|
|
} else {
|
|
debug('cannot read file: ' . $fontFile, self::class . '::ImageTTFTextWrapper()');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Splitting a string for ImageTTFBBox up into an array where each part has its own configuration options.
|
|
*
|
|
* @param string $string UTF-8 string
|
|
* @param array $splitRendering Split-rendering configuration from GIFBUILDER TEXT object.
|
|
* @param int $fontSize Current fontsize
|
|
* @param string $fontFile Current font file
|
|
* @return array Array with input string splitted according to configuration
|
|
*/
|
|
protected function splitString(string $string, array $splitRendering, int $fontSize, string $fontFile): array
|
|
{
|
|
// Initialize by setting the whole string and default configuration as the first entry.
|
|
$result = [];
|
|
$result[] = [
|
|
'str' => $string,
|
|
'fontSize' => $fontSize,
|
|
'fontFile' => $fontFile,
|
|
];
|
|
// Traverse the split-rendering configuration:
|
|
// Splitting will create more entries in $result with individual configurations.
|
|
$sKeyArray = ArrayUtility::filterAndSortByNumericKeys($splitRendering);
|
|
// Traverse configured options:
|
|
foreach ($sKeyArray as $key) {
|
|
$cfg = $splitRendering[$key . '.'];
|
|
// Process each type of split rendering keyword:
|
|
switch ((string)$splitRendering[$key]) {
|
|
case 'highlightWord':
|
|
if ((string)$cfg['value'] !== '') {
|
|
$newResult = [];
|
|
// Traverse the current parts of the result array:
|
|
foreach ($result as $part) {
|
|
// Explode the string value by the word value to highlight:
|
|
$explodedParts = explode($cfg['value'], $part['str']);
|
|
foreach ($explodedParts as $c => $expValue) {
|
|
if ((string)$expValue !== '') {
|
|
$newResult[] = array_merge($part, ['str' => $expValue]);
|
|
}
|
|
if ($c + 1 < count($explodedParts)) {
|
|
$newResult[] = [
|
|
'str' => $cfg['value'],
|
|
'fontSize' => $cfg['fontSize'] ?: $part['fontSize'],
|
|
'fontFile' => $cfg['fontFile'] ?: $part['fontFile'],
|
|
'color' => $cfg['color'],
|
|
'xSpaceBefore' => $cfg['xSpaceBefore'],
|
|
'xSpaceAfter' => $cfg['xSpaceAfter'],
|
|
'ySpaceBefore' => $cfg['ySpaceBefore'],
|
|
'ySpaceAfter' => $cfg['ySpaceAfter'],
|
|
];
|
|
}
|
|
}
|
|
}
|
|
// Set the new result as result array:
|
|
if (!empty($newResult)) {
|
|
$result = $newResult;
|
|
}
|
|
}
|
|
break;
|
|
case 'charRange':
|
|
if ((string)$cfg['value'] !== '') {
|
|
// Initialize range:
|
|
$ranges = GeneralUtility::trimExplode(',', $cfg['value'], true);
|
|
foreach ($ranges as $i => $rangeDef) {
|
|
$ranges[$i] = GeneralUtility::intExplode('-', $rangeDef);
|
|
if (!isset($ranges[$i][1])) {
|
|
$ranges[$i][1] = $ranges[$i][0];
|
|
}
|
|
}
|
|
$newResult = [];
|
|
// Traverse the current parts of the result array:
|
|
foreach ($result as $part) {
|
|
// Initialize:
|
|
$currentState = -1;
|
|
$bankAccum = '';
|
|
// Explode the string value by the word value to highlight:
|
|
$utf8Chars = $this->csConvObj->utf8_to_numberarray($part['str']);
|
|
foreach ($utf8Chars as $utfChar) {
|
|
// Find number and evaluate position:
|
|
$uNumber = (int)$this->csConvObj->utf8CharToUnumber($utfChar);
|
|
$inRange = 0;
|
|
foreach ($ranges as $rangeDef) {
|
|
if ($uNumber >= $rangeDef[0] && (!$rangeDef[1] || $uNumber <= $rangeDef[1])) {
|
|
$inRange = 1;
|
|
break;
|
|
}
|
|
}
|
|
if ($currentState == -1) {
|
|
$currentState = $inRange;
|
|
}
|
|
// Initialize first char
|
|
// Switch bank:
|
|
if ($inRange != $currentState && $uNumber !== 9 && $uNumber !== 10 && $uNumber !== 13 && $uNumber !== 32) {
|
|
// Set result:
|
|
if ($bankAccum !== '') {
|
|
$newResult[] = [
|
|
'str' => $bankAccum,
|
|
'fontSize' => $currentState && $cfg['fontSize'] ? $cfg['fontSize'] : $part['fontSize'],
|
|
'fontFile' => $currentState && $cfg['fontFile'] ? $cfg['fontFile'] : $part['fontFile'],
|
|
'color' => $currentState ? $cfg['color'] : '',
|
|
'xSpaceBefore' => $currentState ? $cfg['xSpaceBefore'] : '',
|
|
'xSpaceAfter' => $currentState ? $cfg['xSpaceAfter'] : '',
|
|
'ySpaceBefore' => $currentState ? $cfg['ySpaceBefore'] : '',
|
|
'ySpaceAfter' => $currentState ? $cfg['ySpaceAfter'] : '',
|
|
];
|
|
}
|
|
// Initialize new settings:
|
|
$currentState = $inRange;
|
|
$bankAccum = '';
|
|
}
|
|
// Add char to bank:
|
|
$bankAccum .= $utfChar;
|
|
}
|
|
// Set result for FINAL part:
|
|
if ($bankAccum !== '') {
|
|
$newResult[] = [
|
|
'str' => $bankAccum,
|
|
'fontSize' => $currentState && $cfg['fontSize'] ? $cfg['fontSize'] : $part['fontSize'],
|
|
'fontFile' => $currentState && $cfg['fontFile'] ? $cfg['fontFile'] : $part['fontFile'],
|
|
'color' => $currentState ? $cfg['color'] : '',
|
|
'xSpaceBefore' => $currentState ? $cfg['xSpaceBefore'] : '',
|
|
'xSpaceAfter' => $currentState ? $cfg['xSpaceAfter'] : '',
|
|
'ySpaceBefore' => $currentState ? $cfg['ySpaceBefore'] : '',
|
|
'ySpaceAfter' => $currentState ? $cfg['ySpaceAfter'] : '',
|
|
];
|
|
}
|
|
}
|
|
// Set the new result as result array:
|
|
if (!empty($newResult)) {
|
|
$result = $newResult;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Calculates the spacing and wordSpacing values
|
|
*
|
|
* @param array $conf TypoScript array for the TEXT GIFBUILDER object
|
|
* @param int $scaleFactor TypoScript value from eg $conf['niceText.']['scaleFactor']
|
|
* @return array Array with two keys [0]/[1] being array($spacing,$wordSpacing)
|
|
* @see calcBBox()
|
|
*/
|
|
protected function calcWordSpacing(array $conf, int $scaleFactor = 1): array
|
|
{
|
|
$spacing = (int)($conf['spacing'] ?? 0);
|
|
$wordSpacing = (int)($conf['wordSpacing'] ?? 0);
|
|
$wordSpacing = $wordSpacing ?: $spacing * 2;
|
|
$spacing *= $scaleFactor;
|
|
$wordSpacing *= $scaleFactor;
|
|
return [$spacing, $wordSpacing];
|
|
}
|
|
|
|
/**
|
|
* Calculates and returns the niceText.scaleFactor
|
|
*
|
|
* @param array $conf TypoScript array for the TEXT GIFBUILDER object
|
|
* @return int TypoScript value from eg $conf['niceText.']['scaleFactor']
|
|
*/
|
|
protected function getTextScalFactor(array $conf): int
|
|
{
|
|
if (!($conf['niceText'] ?? false)) {
|
|
$sF = 1;
|
|
} else {
|
|
$sF = MathUtility::forceIntegerInRange(($conf['niceText.']['scaleFactor'] ?? 2), 2, 5);
|
|
}
|
|
return $sF;
|
|
}
|
|
|
|
/**
|
|
* Renders a regular text and takes care of a possible line break automatically.
|
|
*
|
|
* @param \GdImage $im (See argument for PHP function imageTTFtext())
|
|
* @param int $fontSize (See argument for PHP function imageTTFtext())
|
|
* @param int $angle (See argument for PHP function imageTTFtext())
|
|
* @param int $x (See argument for PHP function imageTTFtext())
|
|
* @param int $y (See argument for PHP function imageTTFtext())
|
|
* @param int $color (See argument for PHP function imageTTFtext())
|
|
* @param string $fontFile (See argument for PHP function imageTTFtext())
|
|
* @param string $string (See argument for PHP function imageTTFtext()). UTF-8 string, possibly with entities in.
|
|
* @param array $splitRendering Split-rendering configuration
|
|
* @param array $conf The configuration
|
|
* @param int $sF Scale factor
|
|
*/
|
|
protected function renderTTFText(\GdImage &$im, int $fontSize, int $angle, int $x, int $y, int $color, string $fontFile, string $string, array $splitRendering, array $conf, int $sF = 1): void
|
|
{
|
|
if (isset($conf['breakWidth']) && $conf['breakWidth'] && $this->getRenderedTextWidth($string, $conf) > $conf['breakWidth']) {
|
|
$phrase = '';
|
|
$currentWidth = 0;
|
|
$breakWidth = $conf['breakWidth'];
|
|
$breakSpace = $this->getBreakSpace($conf);
|
|
$wordPairs = $this->getWordPairsForLineBreak($string);
|
|
// Iterate through all word pairs:
|
|
foreach ($wordPairs as $index => $wordPair) {
|
|
$wordWidth = $this->getRenderedTextWidth($wordPair, $conf);
|
|
if ($index == 0 || $currentWidth + $wordWidth <= $breakWidth) {
|
|
$currentWidth += $wordWidth;
|
|
$phrase .= $wordPair;
|
|
} else {
|
|
// Render the current phrase that is below breakWidth:
|
|
$this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $phrase, $splitRendering, $sF);
|
|
// Calculate the news height offset:
|
|
$y += $breakSpace;
|
|
// Restart the phrase:
|
|
$currentWidth = $wordWidth;
|
|
$phrase = $wordPair;
|
|
}
|
|
}
|
|
// Render the remaining phrase:
|
|
if ($currentWidth) {
|
|
$this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $phrase, $splitRendering, $sF);
|
|
}
|
|
} else {
|
|
$this->ImageTTFTextWrapper($im, $fontSize, $angle, $x, $y, $color, $fontFile, $string, $splitRendering, $sF);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the word pairs used for automatic line breaks.
|
|
*/
|
|
protected function getWordPairsForLineBreak(string $string): array
|
|
{
|
|
$wordPairs = [];
|
|
$wordsArray = preg_split('#([- .,!:]+)#', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
|
|
$wordsArray = is_array($wordsArray) ? $wordsArray : [];
|
|
$wordsCount = count($wordsArray);
|
|
for ($index = 0; $index < $wordsCount; $index += 2) {
|
|
$wordPairs[] = $wordsArray[$index] . ($wordsArray[$index + 1] ?? '');
|
|
}
|
|
return $wordPairs;
|
|
}
|
|
|
|
/**
|
|
* Gets the rendered text width
|
|
*/
|
|
protected function getRenderedTextWidth(string $text, array $conf): int
|
|
{
|
|
$bounds = $this->ImageTTFBBoxWrapper($conf['fontSize'], $conf['angle'], $conf['fontFile'], $text, $conf['splitRendering.']);
|
|
if ($conf['angle'] < 0) {
|
|
$pixelWidth = abs($bounds[4] - $bounds[0]);
|
|
} elseif ($conf['angle'] > 0) {
|
|
$pixelWidth = abs($bounds[2] - $bounds[6]);
|
|
} else {
|
|
$pixelWidth = abs($bounds[4] - $bounds[6]);
|
|
}
|
|
return (int)$pixelWidth;
|
|
}
|
|
|
|
/**
|
|
* Gets the break space for each new line.
|
|
*
|
|
* @param array $conf TypoScript configuration for the currently rendered object
|
|
* @param array $boundingBox The bounding box for the currently rendered object
|
|
* @return int The break space
|
|
*/
|
|
protected function getBreakSpace(array $conf, array $boundingBox = []): int
|
|
{
|
|
if ($boundingBox === []) {
|
|
$boundingBox = $this->calcBBox($conf);
|
|
$boundingBox = $boundingBox[2];
|
|
}
|
|
if (isset($conf['breakSpace']) && $conf['breakSpace']) {
|
|
$breakSpace = $boundingBox['lineHeight'] * $conf['breakSpace'];
|
|
} else {
|
|
$breakSpace = $boundingBox['lineHeight'];
|
|
}
|
|
return (int)$breakSpace;
|
|
}
|
|
|
|
/**
|
|
* Function to compensate for DPI resolution.
|
|
* FreeType 2 always has 96 dpi, so it is hard-coded at this place.
|
|
*
|
|
* @param float $fontSize font size for freetype function call
|
|
* @return float compensated font size based on 96 dpi
|
|
*/
|
|
protected function compensateFontSizeiBasedOnFreetypeDpi(float $fontSize): float
|
|
{
|
|
return $fontSize / 96.0 * 72;
|
|
}
|
|
|
|
/*************************
|
|
*
|
|
* Adjustment functions
|
|
*
|
|
************************/
|
|
/**
|
|
* Apply auto-levels to input image pointer
|
|
*
|
|
* @param \GdImage $im GDlib Image Pointer
|
|
*/
|
|
protected function autolevels(\GdImage &$im): void
|
|
{
|
|
$totalCols = imagecolorstotal($im);
|
|
$grayArr = [];
|
|
for ($c = 0; $c < $totalCols; $c++) {
|
|
$cols = imagecolorsforindex($im, $c);
|
|
$grayArr[] = (int)round(($cols['red'] + $cols['green'] + $cols['blue']) / 3);
|
|
}
|
|
$min = min($grayArr);
|
|
$max = max($grayArr);
|
|
$delta = $max - $min;
|
|
if ($delta) {
|
|
for ($c = 0; $c < $totalCols; $c++) {
|
|
$cols = imagecolorsforindex($im, $c);
|
|
$cols['red'] = (int)floor(($cols['red'] - $min) / $delta * 255);
|
|
$cols['green'] = (int)floor(($cols['green'] - $min) / $delta * 255);
|
|
$cols['blue'] = (int)floor(($cols['blue'] - $min) / $delta * 255);
|
|
imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply output levels to input image pointer (decreasing contrast)
|
|
*
|
|
* @param \GdImage $im GDlib Image Pointer
|
|
* @param int $low The "low" value (close to 0)
|
|
* @param int $high The "high" value (close to 255)
|
|
*/
|
|
protected function outputLevels(\GdImage &$im, int $low, int $high): void
|
|
{
|
|
if ($low < $high) {
|
|
$low = MathUtility::forceIntegerInRange($low, 0, 255);
|
|
$high = MathUtility::forceIntegerInRange($high, 0, 255);
|
|
$delta = $high - $low;
|
|
$totalCols = imagecolorstotal($im);
|
|
for ($c = 0; $c < $totalCols; $c++) {
|
|
$cols = imagecolorsforindex($im, $c);
|
|
$cols['red'] = $low + floor($cols['red'] / 255 * $delta);
|
|
$cols['green'] = $low + floor($cols['green'] / 255 * $delta);
|
|
$cols['blue'] = $low + floor($cols['blue'] / 255 * $delta);
|
|
imagecolorset($im, $c, (int)$cols['red'], (int)$cols['green'], (int)$cols['blue']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply input levels to input image pointer (increasing contrast)
|
|
*
|
|
* @param \GdImage $im GDlib Image Pointer
|
|
* @param int $low The "low" value (close to 0)
|
|
* @param int $high The "high" value (close to 255)
|
|
*/
|
|
protected function inputLevels(\GdImage &$im, int $low, int $high): void
|
|
{
|
|
if ($low < $high) {
|
|
$low = MathUtility::forceIntegerInRange($low, 0, 255);
|
|
$high = MathUtility::forceIntegerInRange($high, 0, 255);
|
|
$delta = $high - $low;
|
|
$totalCols = imagecolorstotal($im);
|
|
for ($c = 0; $c < $totalCols; $c++) {
|
|
$cols = imagecolorsforindex($im, $c);
|
|
$cols['red'] = MathUtility::forceIntegerInRange((int)(($cols['red'] - $low) / $delta * 255), 0, 255);
|
|
$cols['green'] = MathUtility::forceIntegerInRange((int)(($cols['green'] - $low) / $delta * 255), 0, 255);
|
|
$cols['blue'] = MathUtility::forceIntegerInRange((int)(($cols['blue'] - $low) / $delta * 255), 0, 255);
|
|
imagecolorset($im, $c, $cols['red'], $cols['green'], $cols['blue']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies an ImageMagick parameter to a GDlib image pointer resource by writing the resource to file,
|
|
* performing an IM operation upon it and reading back the result into the ImagePointer.
|
|
*
|
|
* @param \GdImage $im The image pointer (reference)
|
|
* @param string $command The ImageMagick parameters. Like effects, scaling etc.
|
|
*/
|
|
protected function applyImageMagickToPHPGif(\GdImage &$im, string $command): void
|
|
{
|
|
$tmpStr = $this->imageService->randomName();
|
|
$theFile = $tmpStr . '.png';
|
|
$this->ImageWrite($im, $theFile);
|
|
$this->imageService->imageMagickExec($theFile, $theFile, $command);
|
|
$tmpImg = $this->imageCreateFromFile($theFile);
|
|
if ($tmpImg) {
|
|
$im = $tmpImg;
|
|
$this->w = imagesx($im);
|
|
$this->h = imagesy($im);
|
|
}
|
|
unlink($theFile);
|
|
}
|
|
|
|
/**
|
|
* Writes the input GDlib image pointer to file
|
|
*
|
|
* @param \GdImage $destImg The GDlib image resource pointer
|
|
* @param string $theImage The absolute file path to write to
|
|
* @param int $quality The image quality (for JPEG, WebP and AVIF files)
|
|
* @param int<-1,10> $speed The image speed (for AVIFs), 0 (slow, smaller file) to 10 (fast, larger file), -1 for default (=6)
|
|
* @return bool The output of either imageGif, imagePng, imageJpeg, imagewebp or imageavif based on the filename to write
|
|
* @see maskImageOntoImage()
|
|
* @see scale()
|
|
* @see output()
|
|
*/
|
|
public function ImageWrite(\GdImage &$destImg, string $theImage, int $quality = 0, int $speed = -1): bool
|
|
{
|
|
imageinterlace($destImg, false);
|
|
$ext = strtolower(substr($theImage, (int)strrpos($theImage, '.') + 1));
|
|
$result = false;
|
|
switch ($ext) {
|
|
case 'jpg':
|
|
case 'jpeg':
|
|
if (function_exists('imagejpeg')) {
|
|
$result = imagejpeg($destImg, $theImage, ($quality ?: $this->jpegQuality));
|
|
}
|
|
break;
|
|
case 'webp':
|
|
if (function_exists('imagewebp')) {
|
|
$result = imagewebp($destImg, $theImage, ($quality ?: $this->webpQuality));
|
|
}
|
|
break;
|
|
case 'avif':
|
|
if (function_exists('imageavif')) {
|
|
$result = imageavif($destImg, $theImage, ($quality ?: $this->avifQuality), $speed);
|
|
}
|
|
break;
|
|
case 'gif':
|
|
if (function_exists('imagegif')) {
|
|
imagetruecolortopalette($destImg, true, 256);
|
|
$result = imagegif($destImg, $theImage);
|
|
}
|
|
break;
|
|
case 'png':
|
|
if (function_exists('imagepng')) {
|
|
$result = imagepng($destImg, $theImage);
|
|
}
|
|
break;
|
|
}
|
|
if ($result) {
|
|
GeneralUtility::fixPermissions($theImage);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Creates a new GDlib image resource based on the input image filename.
|
|
* If it fails creating an image from the input file a blank gray image with the dimensions of the input image will be created instead.
|
|
*
|
|
* @param string $sourceImg Image filename
|
|
*/
|
|
public function imageCreateFromFile(string $sourceImg): \GdImage|false
|
|
{
|
|
$imgInf = pathinfo($sourceImg);
|
|
$ext = strtolower($imgInf['extension']);
|
|
switch ($ext) {
|
|
case 'gif':
|
|
if (function_exists('imagecreatefromgif')) {
|
|
return imagecreatefromgif($sourceImg);
|
|
}
|
|
break;
|
|
case 'png':
|
|
if (function_exists('imagecreatefrompng')) {
|
|
$imageHandle = imagecreatefrompng($sourceImg);
|
|
if ($this->saveAlphaLayer) {
|
|
imagesavealpha($imageHandle, true);
|
|
}
|
|
return $imageHandle;
|
|
}
|
|
break;
|
|
case 'jpg':
|
|
case 'jpeg':
|
|
if (function_exists('imagecreatefromjpeg')) {
|
|
return imagecreatefromjpeg($sourceImg);
|
|
}
|
|
break;
|
|
case 'webp':
|
|
if (function_exists('imagecreatefromwebp')) {
|
|
return imagecreatefromwebp($sourceImg);
|
|
}
|
|
break;
|
|
case 'avif':
|
|
if (function_exists('imagecreatefromavif')) {
|
|
return imagecreatefromavif($sourceImg);
|
|
}
|
|
break;
|
|
}
|
|
// If none of the above:
|
|
$imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $sourceImg);
|
|
$im = imagecreatetruecolor($imageInfo->getWidth(), $imageInfo->getHeight());
|
|
$Bcolor = imagecolorallocate($im, 128, 128, 128);
|
|
imagefilledrectangle($im, 0, 0, $imageInfo->getWidth(), $imageInfo->getHeight(), $Bcolor);
|
|
return $im;
|
|
}
|
|
|
|
/**
|
|
* Creates error image based on gfx/notfound_thumb.png
|
|
* Requires GD lib enabled, otherwise it will exit with the three
|
|
* textstrings outputted as text. Outputs the image stream to browser and exits!
|
|
*
|
|
* @param string $filename Name of the file
|
|
* @param string $textline1 Text line 1
|
|
* @param string $textline2 Text line 2
|
|
* @param string $textline3 Text line 3
|
|
* @throws \RuntimeException
|
|
* @internal will soon be renamed
|
|
*/
|
|
public function getTemporaryImageWithText(string $filename, string $textline1, string $textline2 = '', string $textline3 = ''): void
|
|
{
|
|
if (!class_exists(\GdImage::class)) {
|
|
throw new \RuntimeException('TYPO3 Fatal Error: No gdlib. ' . $textline1 . ' ' . $textline2 . ' ' . $textline3, 1270853952);
|
|
}
|
|
// Creates the basis for the error image
|
|
$basePath = ExtensionManagementUtility::extPath('core') . 'Resources/Public/Images/';
|
|
$im = imagecreatefrompng($basePath . 'NotFound.png');
|
|
// Sets background color and print color.
|
|
$white = imagecolorallocate($im, 255, 255, 255);
|
|
$black = imagecolorallocate($im, 0, 0, 0);
|
|
// Prints the text strings with the build-in font functions of GD
|
|
$x = 0;
|
|
$font = 0;
|
|
if ($textline1) {
|
|
imagefilledrectangle($im, $x, 9, 56, 16, $white);
|
|
imagestring($im, $font, $x, 9, $textline1, $black);
|
|
}
|
|
if ($textline2) {
|
|
imagefilledrectangle($im, $x, 19, 56, 26, $white);
|
|
imagestring($im, $font, $x, 19, $textline2, $black);
|
|
}
|
|
if ($textline3) {
|
|
imagefilledrectangle($im, $x, 29, 56, 36, $white);
|
|
imagestring($im, $font, $x, 29, substr($textline3, -14), $black);
|
|
}
|
|
// Outputting the image stream and exit
|
|
imagepng($im, $filename);
|
|
}
|
|
|
|
/**
|
|
* @internal Only used for ext:install, not part of TYPO3 Core API.
|
|
*/
|
|
public function getGraphicalFunctions(): GraphicalFunctions
|
|
{
|
|
return $this->imageService;
|
|
}
|
|
}
|