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 */ protected array $combinedTextStrings = []; /** * Contains all filenames (basename without extension) used on this image * * @var list */ 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 */ protected array $charRangeMap = []; /** * @var array{0?: int<0, max>, 1?: int<0, max>} */ protected array $XY = []; protected ?ContentObjectRenderer $cObj = null; /** * @var list */ protected array $workArea = []; /** * @var list */ 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, 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 */ protected array $offset; /** * File formats supported by gdlib. This variable gets filled in the constructor * * @var list */ 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; } }