‪TYPO3CMS  ‪main
GraphicalFunctions.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
19 use TYPO3\CMS\Core\Type\File\ImageInfo;
25 
32 class GraphicalFunctions
33 {
39  public $addFrameSelection = true;
40 
46  protected string $colorspace = 'RGB';
47 
53  protected array $allowedColorSpaceNames = [
54  'CMY',
55  'CMYK',
56  'Gray',
57  'HCL',
58  'HSB',
59  'HSL',
60  'HWB',
61  'Lab',
62  'LCH',
63  'LMS',
64  'Log',
65  'Luv',
66  'OHTA',
67  'Rec601Luma',
68  'Rec601YCbCr',
69  'Rec709Luma',
70  'Rec709YCbCr',
71  'RGB',
72  'sRGB',
73  'Transparent',
74  'XYZ',
75  'YCbCr',
76  'YCC',
77  'YIQ',
78  'YCbCr',
79  'YUV',
80  ];
81 
88  protected array $imageFileExt = ['gif', 'jpg', 'jpeg', 'png', 'tif', 'bmp', 'tga', 'pcx', 'ai', 'pdf', 'webp'];
89 
95  protected array $webImageExt = ['gif', 'jpg', 'jpeg', 'png', 'webp'];
96 
100  public array $cmds = [
101  'jpg' => '',
102  'jpeg' => '',
103  'gif' => '',
104  'png' => '',
105  'webp' => '',
106  ];
107 
111  protected bool $processorEnabled;
112  protected bool $mayScaleUp = true;
113 
119  public $filenamePrefix = '';
120 
126  public $imageMagickConvert_forceFileNameBody = '';
127 
133  public $dontCheckForExistingTempFile = false;
134 
142  public $alternativeOutputKey = '';
143 
149  public $IM_commands = [];
150 
156  public $scalecmd = '-auto-orient -geometry';
157 
163  protected string $im5fx_blurSteps = '1x2,2x2,3x2,4x3,5x3,5x4,6x4,7x5,8x5,9x5';
164 
170  protected string $im5fx_sharpenSteps = '1x2,2x2,3x2,2x3,3x3,4x3,3x4,4x4,4x5,5x5';
171 
177  protected int $pixelLimitGif = 10000;
178 
182  protected int $jpegQuality = 85;
183 
187  protected int $webpQuality = 85;
188 
193  public function __construct()
194  {
195  $gfxConf = ‪$GLOBALS['TYPO3_CONF_VARS']['GFX'];
196  $this->colorspace = $this->getColorspaceFromConfiguration();
197 
198  $this->processorEnabled = (bool)$gfxConf['processor_enabled'];
199  $this->jpegQuality = ‪MathUtility::forceIntegerInRange($gfxConf['jpg_quality'], 1, 100, 85);
200  if (isset($gfxConf['webp_quality'])) {
201  if ($gfxConf['webp_quality'] === 'lossless') {
202  $this->webpQuality = 101;
203  } else {
204  $this->webpQuality = ‪MathUtility::forceIntegerInRange($gfxConf['webp_quality'], 1, 101, $this->webpQuality);
205  }
206  }
207  $this->addFrameSelection = (bool)$gfxConf['processor_allowFrameSelection'];
208  $this->imageFileExt = ‪GeneralUtility::trimExplode(',', $gfxConf['imagefile_ext']);
209 
210  // Processor Effects. This is necessary if using ImageMagick 5+.
211  // Effects in Imagemagick 5+ tends to render very slowly!
212  // Therefore, must be disabled in order not to perform sharpen, blurring and such.
213  // but if 'processor_effects' is set, enable effects
214  if ($gfxConf['processor_effects']) {
215  $this->cmds['jpg'] = $this->v5_sharpen(10);
216  $this->cmds['jpeg'] = $this->v5_sharpen(10);
217  }
218  // Secures that images are not scaled up.
219  $this->mayScaleUp = (bool)$gfxConf['processor_allowUpscaling'];
220  }
221 
232  public function v5_sharpen($factor)
233  {
234  $factor = ‪MathUtility::forceIntegerInRange((int)ceil($factor / 10), 0, 10);
235  $sharpenArr = explode(',', ',' . $this->im5fx_sharpenSteps);
236  $sharpenF = trim($sharpenArr[$factor]);
237  if ($sharpenF) {
238  return ' -sharpen ' . $sharpenF;
239  }
240  return '';
241  }
242 
253  public function v5_blur($factor)
254  {
255  $factor = ‪MathUtility::forceIntegerInRange((int)ceil($factor / 10), 0, 10);
256  $blurArr = explode(',', ',' . $this->im5fx_blurSteps);
257  $blurF = trim($blurArr[$factor]);
258  if ($blurF) {
259  return ' -blur ' . $blurF;
260  }
261  return '';
262  }
263 
270  public function randomName()
271  {
273  return ‪Environment::getVarPath() . '/transient/' . md5(‪StringUtility::getUniqueId());
274  }
275 
276  /***********************************
277  *
278  * Scaling, Dimensions of images
279  *
280  ***********************************/
281 
289  public function convert(string $sourceFile, string $targetFileExtension = 'web'): ?ImageProcessingResult
290  {
291  return $this->resize($sourceFile, $targetFileExtension);
292  }
293 
310  public function resize(string $sourceFile, string $targetFileExtension, int|string $width = '', int|string $height = '', string $additionalParameters = '', array $options = [], bool $forceCreation = false): ?ImageProcessingResult
311  {
312  if (!$this->processorEnabled) {
313  // Returning file info right away
314  return $this->getImageDimensions($sourceFile, true);
315  }
316  $info = $this->getImageDimensions($sourceFile, true);
317  if (!$info) {
318  return null;
319  }
320  $originalFileExtension = $info->getExtension();
321 
322  // Determine the final target file extension
323  $targetFileExtension = strtolower(trim($targetFileExtension));
324  // If no extension is given the original extension is used
325  $targetFileExtension = $targetFileExtension ?: $originalFileExtension;
326  if ($targetFileExtension === 'web') {
327  if (in_array($originalFileExtension, $this->webImageExt, true)) {
328  $targetFileExtension = $originalFileExtension;
329  } else {
330  $targetFileExtension = $this->gif_or_jpg($originalFileExtension, $info->getWidth(), $info->getHeight());
331  }
332  }
333  if (!in_array($targetFileExtension, $this->imageFileExt, true)) {
334  return null;
335  }
336  // Clean up additional $params
337  $additionalParameters = trim($additionalParameters);
338  // Refers to which frame-number to select in the image. null or 0 will select the first frame, 1 will select the next and so on...
339  $frame = $this->addFrameSelection && isset($options['frame']) ? (int)$options['frame'] : 0;
340 
341  $processingInstructions = ImageProcessingInstructions::fromCropScaleValues($info->getWidth(), $info->getHeight(), $width, $height, $options);
342 
343  $originalWidth = $info->getWidth() ?: $width;
344  $originalHeight = $info->getHeight() ?: $height;
345 
346  // Check if conversion should be performed ($noScale - no processing needed).
347  // $noScale flag is TRUE if the width / height does NOT dictate the image to be scaled. That is if no
348  // width / height is given or if the destination w/h matches the original image dimensions, or if
349  // the option to not scale the image is set.
350  $noScale = !$originalWidth && !$originalHeight || $processingInstructions->width === $info->getWidth() && $processingInstructions->height === $info->getHeight() || !empty($options['noScale']);
351  if ($noScale && !$processingInstructions->cropArea && !$additionalParameters && !$frame && $targetFileExtension === $info->getExtension() && !$forceCreation) {
352  // Set the new width and height before returning,
353  // if the noScale option is set, otherwise the incoming
354  // values are calculated.
355  if (!empty($options['noScale'])) {
356  return new ImageProcessingResult(
357  $sourceFile,
358  $processingInstructions->width,
359  $processingInstructions->height
360  );
361  }
362  return $info;
363  }
364 
365  $command = '';
366  if ($processingInstructions->cropArea) {
367  $cropArea = $processingInstructions->cropArea;
368  $command .= ' -crop ' . $cropArea->getWidth() . 'x' . $cropArea->getHeight() . '+' . $cropArea->getOffsetLeft() . '+' . $cropArea->getOffsetTop() . '! +repage ';
369  }
370 
371  // Start with the default scale command
372  // check if we should use -sample or -geometry
373  if ($options['sample'] ?? false) {
374  $command .= '-auto-orient -sample';
375  } else {
376  $command .= $this->scalecmd;
377  }
378  // from the IM docs -- https://imagemagick.org/script/command-line-processing.php
379  // "We see that ImageMagick is very good about preserving aspect ratios of images, to prevent distortion
380  // of your favorite photos and images. But you might really want the dimensions to be 100x200, thereby
381  // stretching the image. In this case just tell ImageMagick you really mean it (!) by appending an exclamation
382  // operator to the geometry. This will force the image size to exactly what you specify.
383  // So, for example, if you specify 100x200! the dimensions will become exactly 100x200"
384  $command .= ' ' . $processingInstructions->width . 'x' . $processingInstructions->height . '!';
385  // Add params
386  $additionalParameters = $this->modifyImageMagickStripProfileParameters($additionalParameters, $options);
387  $command .= ($additionalParameters ? ' ' . $additionalParameters : $this->cmds[$targetFileExtension] ?? '');
388 
389  // Add quality parameter for jpg, jpeg or webp if not already set
390  if (!str_contains($command, '-quality') && ($targetFileExtension === 'jpg' || $targetFileExtension === 'jpeg')) {
391  $command .= ' -quality ' . $this->jpegQuality;
392  }
393  // Add quality parameter for webp if not already set
394  if ($targetFileExtension === 'webp') {
395  if (!str_contains($command, '-quality') && !str_contains($command, 'webp:lossless')) {
396  if ($this->webpQuality === 101) {
397  $command .= ' -define webp:lossless=true';
398  } else {
399  $command .= ' -quality ' . $this->webpQuality;
400  }
401  }
402  }
403  // re-apply colorspace-setting for the resulting image so colors don't appear to dark (sRGB instead of RGB)
404  if (!str_contains($command, '-colorspace')) {
405  $command .= ' -colorspace ' . CommandUtility::escapeShellArgument($this->colorspace);
406  }
407  if ($this->alternativeOutputKey) {
408  $theOutputName = md5($command . $processingInstructions->cropArea . ‪PathUtility::basename($sourceFile) . $this->alternativeOutputKey . '[' . $frame . ']');
409  } else {
410  $theOutputName = md5($command . $processingInstructions->cropArea . $sourceFile . filemtime($sourceFile) . '[' . $frame . ']');
411  }
412  if ($this->imageMagickConvert_forceFileNameBody) {
413  $theOutputName = $this->imageMagickConvert_forceFileNameBody;
414  $this->imageMagickConvert_forceFileNameBody = '';
415  }
416  // Making the temporary filename
417  ‪GeneralUtility::mkdir_deep(‪Environment::getPublicPath() . '/typo3temp/assets/images/');
418  ‪$output = ‪Environment::getPublicPath() . '/typo3temp/assets/images/' . $this->filenamePrefix . $theOutputName . '.' . $targetFileExtension;
419  if ($this->dontCheckForExistingTempFile || !file_exists(‪$output)) {
420  $this->imageMagickExec($sourceFile, ‪$output, $command, $frame);
421  }
422  if (file_exists(‪$output)) {
423  // params might change some image data, so this should be calculated again
424  if ($additionalParameters) {
425  return $this->getImageDimensions(‪$output, true);
426  }
427  return new ImageProcessingResult(‪$output, $processingInstructions->width, $processingInstructions->height);
428  }
429  return null;
430  }
431 
449  public function imageMagickConvert($imagefile, $targetFileExtension = '', $w = '', $h = '', $params = '', $frame = '', $options = [], $mustCreate = false)
450  {
451  if ($frame !== '') {
452  $options['frame'] = (int)$frame;
453  }
454  $result = $this->resize($imagefile, $targetFileExtension, $w, $h, $params, $options, $mustCreate);
455  return $result?->toLegacyArray();
456  }
457 
462  public function mask(string $inputFile, string $outputFile, string $maskImage, string $maskBackgroundImage, string $params, array $options)
463  {
464  $params = $this->modifyImageMagickStripProfileParameters($params, $options);
465  $tmpStr = $this->randomName();
466  // m_mask
467  $intermediateMaskFile = $tmpStr . '_mask.png';
468  $this->imageMagickExec($maskImage, $intermediateMaskFile, $params);
469  // m_bgImg
470  $intermediateMaskBackgroundFile = $tmpStr . '_bgImg.miff';
471  $this->imageMagickExec($maskBackgroundImage, $intermediateMaskBackgroundFile, $params);
472  // The image onto the background
473  $this->combineExec($intermediateMaskBackgroundFile, $inputFile, $intermediateMaskFile, $outputFile);
474  // Unlink the temp-images...
475  @unlink($intermediateMaskFile);
476  @unlink($intermediateMaskBackgroundFile);
477  }
478 
487  public function getImageDimensions(string $imageFile, bool $useResultObject = false): ImageProcessingResult|array|null
488  {
489  preg_match('/([^\\.]*)$/', $imageFile, $reg);
490  if (!file_exists($imageFile)) {
491  return null;
492  }
493  // @todo: check if we actually need this, ass ImageInfo deals with this much more professionally
494  if (!in_array(strtolower($reg[0]), $this->imageFileExt, true)) {
495  return null;
496  }
497  $imageInfoObject = GeneralUtility::makeInstance(ImageInfo::class, $imageFile);
498  if ($imageInfoObject->isFile() && $imageInfoObject->getWidth()) {
499  $result = ImageProcessingResult::createFromImageInfo($imageInfoObject);
500  return $useResultObject ? $result : $result->toLegacyArray();
501  }
502  return null;
503  }
504 
505  /***********************************
506  *
507  * ImageMagick API functions
508  *
509  ***********************************/
516  public function imageMagickIdentify($imagefile)
517  {
518  if (!$this->processorEnabled) {
519  return null;
520  }
521 
522  $result = $this->executeIdentifyCommandForImageFile($imagefile);
523  if ($result) {
524  [$width, $height, $fileExtension, $fileType] = explode(' ', $result);
525  if ((int)$width && (int)$height) {
526  return [$width, $height, strtolower($fileExtension), $imagefile, strtolower($fileType)];
527  }
528  }
529  return null;
530  }
531 
538  protected function executeIdentifyCommandForImageFile(string $imageFile): ?string
539  {
540  $frame = $this->addFrameSelection ? 0 : null;
542  'identify',
543  '-format "%w %h %e %m" ' . ‪ImageMagickFile::fromFilePath($imageFile, $frame)
544  );
545  $returnVal = [];
546  ‪CommandUtility::exec($cmd, $returnVal);
547  $result = array_pop($returnVal);
548  $this->IM_commands[] = ['identify', $cmd, $result];
549  return $result;
550  }
551 
562  public function imageMagickExec($input, ‪$output, $params, $frame = 0)
563  {
564  if (!$this->processorEnabled) {
565  return '';
566  }
567  // If addFrameSelection is set in the Install Tool, a frame number is added to
568  // select a specific page of the image (by default this will be the first page)
569  $frame = $this->addFrameSelection ? (int)$frame : null;
571  'convert',
572  $params
573  . ' ' . ‪ImageMagickFile::fromFilePath($input, $frame)
574  . ' ' . CommandUtility::escapeShellArgument(‪$output)
575  );
576  $this->IM_commands[] = [‪$output, $cmd];
577  $ret = ‪CommandUtility::exec($cmd);
578  // Change the permissions of the file
580  return $ret;
581  }
582 
593  public function combineExec($input, $overlay, $mask, ‪$output)
594  {
595  if (!$this->processorEnabled) {
596  return '';
597  }
598  $theMask = $this->randomName() . '.png';
599  // +matte = no alpha layer in output
600  $this->imageMagickExec($mask, $theMask, '-colorspace GRAY +matte');
601 
602  $parameters = '-compose over'
603  . ' -quality ' . $this->jpegQuality
604  . ' +matte '
605  . ‪ImageMagickFile::fromFilePath($input) . ' '
606  . ‪ImageMagickFile::fromFilePath($overlay) . ' '
607  . ‪ImageMagickFile::fromFilePath($theMask) . ' '
608  . CommandUtility::escapeShellArgument(‪$output);
609  $cmd = ‪CommandUtility::imageMagickCommand('combine', $parameters);
610  $this->IM_commands[] = [‪$output, $cmd];
611  $ret = ‪CommandUtility::exec($cmd);
612  // Change the permissions of the file
614  if (is_file($theMask)) {
615  @unlink($theMask);
616  }
617  return $ret;
618  }
619 
626  protected function modifyImageMagickStripProfileParameters(string $parameters, array $options): string
627  {
628  if (!isset($options['stripProfile'])) {
629  return $parameters;
630  }
631 
632  $gfxConf = ‪$GLOBALS['TYPO3_CONF_VARS']['GFX'] ?? [];
633  // Use legacy processor_stripColorProfileCommand setting if defined, otherwise
634  // use the preferred configuration option processor_stripColorProfileParameters
635  $stripColorProfileCommand = $gfxConf['processor_stripColorProfileCommand'] ??
636  implode(' ', array_map(CommandUtility::escapeShellArgument(...), $gfxConf['processor_stripColorProfileParameters'] ?? []));
637  if ($options['stripProfile'] && $stripColorProfileCommand !== '') {
638  return $stripColorProfileCommand . ' ' . $parameters;
639  }
640 
641  return $parameters . '###SkipStripProfile###';
642  }
643 
644  /***********************************
645  *
646  * Various IO functions
647  *
648  ***********************************/
649 
659  public function gif_or_jpg($type, $w, $h)
660  {
661  if ($type === 'ai' || $w * $h < $this->pixelLimitGif) {
662  return 'png';
663  }
664  return 'jpg';
665  }
666 
670  public function isProcessingEnabled(): bool
671  {
672  return $this->processorEnabled;
673  }
674 
682  public function webpSupportAvailable(): bool
683  {
684  $cmd = ‪CommandUtility::imageMagickCommand('convert', '-list format');
686  $this->IM_commands[] = ['', $cmd];
687  foreach (‪$output as $outputLine) {
688  $outputLine = trim($outputLine);
689  if (str_starts_with($outputLine, 'WEBP') && str_contains($outputLine, ' rw')) {
690  return true;
691  }
692  }
693  return false;
694  }
695 
699  public function setImageFileExt(array $imageFileExt): void
700  {
701  $this->imageFileExt = $imageFileExt;
702  }
703 
707  public function getImageFileExt(): array
708  {
709  return $this->imageFileExt;
710  }
711 
716  protected function getColorspaceFromConfiguration(): string
717  {
718  $gfxConf = ‪$GLOBALS['TYPO3_CONF_VARS']['GFX'];
719 
720  if ($gfxConf['processor'] === 'ImageMagick' && $gfxConf['processor_colorspace'] === '') {
721  return 'sRGB';
722  }
723 
724  if ($gfxConf['processor'] === 'GraphicsMagick' && $gfxConf['processor_colorspace'] === '') {
725  return 'RGB';
726  }
727 
728  return in_array($gfxConf['processor_colorspace'], $this->allowedColorSpaceNames, true) ? $gfxConf['processor_colorspace'] : $this->colorspace;
729  }
730 }
‪TYPO3\CMS\Core\Imaging
Definition: Dimension.php:16
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:27
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static getPublicPath()
Definition: Environment.php:187
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir_deep
‪static mkdir_deep(string $directory)
Definition: GeneralUtility.php:1654
‪TYPO3\CMS\Core\Core\Environment\getVarPath
‪static getVarPath()
Definition: Environment.php:197
‪TYPO3\CMS\Core\Utility\PathUtility\basename
‪static basename(string $path)
Definition: PathUtility.php:219
‪TYPO3\CMS\Core\Utility\CommandUtility\exec
‪static exec(string $command, ?array &$output=null, int &$returnValue=0)
Definition: CommandUtility.php:85
‪TYPO3\CMS\Core\Imaging\ImageMagickFile\fromFilePath
‪static fromFilePath(string $filePath, int $frame=null)
Definition: ImageMagickFile.php:111
‪TYPO3\CMS\Core\Utility\GeneralUtility\fixPermissions
‪static bool fixPermissions(string $path, bool $recursive=false)
Definition: GeneralUtility.php:1496
‪$output
‪$output
Definition: annotationChecker.php:114
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Core\Utility\CommandUtility\imageMagickCommand
‪static string imageMagickCommand(string $command, string $parameters, string $path='')
Definition: CommandUtility.php:98
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Utility\MathUtility\forceIntegerInRange
‪static int forceIntegerInRange(mixed $theInt, int $min, int $max=2000000000, int $defaultValue=0)
Definition: MathUtility.php:34
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Utility\StringUtility
Definition: StringUtility.php:24
‪TYPO3\CMS\Core\Utility\CommandUtility
Definition: CommandUtility.php:54
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Core\Utility\StringUtility\getUniqueId
‪static getUniqueId(string $prefix='')
Definition: StringUtility.php:57