[BUGFIX] Respect GFX/jpg_quality when cropping
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / Processing / LocalCropScaleMaskHelper.php
1 <?php
2 namespace TYPO3\CMS\Core\Resource\Processing;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Resource;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19 use TYPO3\CMS\Core\Utility\MathUtility;
20 use TYPO3\CMS\Frontend\Imaging\GifBuilder;
21
22 /**
23 * Helper class to locally perform a crop/scale/mask task with the TYPO3 image processing classes.
24 */
25 class LocalCropScaleMaskHelper
26 {
27 /**
28 * @var LocalImageProcessor
29 */
30 protected $processor;
31
32 /**
33 * @param LocalImageProcessor $processor
34 */
35 public function __construct(LocalImageProcessor $processor)
36 {
37 $this->processor = $processor;
38 }
39
40 /**
41 * This method actually does the processing of files locally
42 *
43 * Takes the original file (for remote storages this will be fetched from the remote server),
44 * does the IM magic on the local server by creating a temporary typo3temp/ file,
45 * copies the typo3temp/ file to the processing folder of the target storage and
46 * removes the typo3temp/ file.
47 *
48 * The returned array has the following structure:
49 * width => 100
50 * height => 200
51 * filePath => /some/path
52 *
53 * If filePath isn't set but width and height are the original file is used as ProcessedFile
54 * with the returned width and height. This is for example useful for SVG images.
55 *
56 * @param TaskInterface $task
57 * @return array|null
58 */
59 public function process(TaskInterface $task)
60 {
61 $result = null;
62 $targetFile = $task->getTargetFile();
63 $sourceFile = $task->getSourceFile();
64
65 $originalFileName = $sourceFile->getForLocalProcessing(false);
66 /** @var $gifBuilder GifBuilder */
67 $gifBuilder = GeneralUtility::makeInstance(GifBuilder::class);
68 $gifBuilder->init();
69
70 $configuration = $targetFile->getProcessingConfiguration();
71 $configuration['additionalParameters'] = $this->modifyImageMagickStripProfileParameters($configuration['additionalParameters'], $configuration);
72
73 if (empty($configuration['fileExtension'])) {
74 $configuration['fileExtension'] = $task->getTargetFileExtension();
75 }
76
77 $options = $this->getConfigurationForImageCropScaleMask($targetFile, $gifBuilder);
78
79 $croppedImage = null;
80 if (!empty($configuration['crop'])) {
81
82 // check if it is a json object
83 $cropData = json_decode($configuration['crop']);
84 if ($cropData) {
85 $crop = implode(',', [(int)$cropData->x, (int)$cropData->y, (int)$cropData->width, (int)$cropData->height]);
86 } else {
87 $crop = $configuration['crop'];
88 }
89
90 list($offsetLeft, $offsetTop, $newWidth, $newHeight) = explode(',', $crop, 4);
91
92 $backupPrefix = $gifBuilder->filenamePrefix;
93 $gifBuilder->filenamePrefix = 'crop_';
94
95 $jpegQuality = MathUtility::forceIntegerInRange($GLOBALS['TYPO3_CONF_VARS']['GFX']['jpg_quality'], 10, 100, 85);
96
97 // the result info is an array with 0=width,1=height,2=extension,3=filename
98 $result = $gifBuilder->imageMagickConvert(
99 $originalFileName,
100 $configuration['fileExtension'],
101 '',
102 '',
103 sprintf('-crop %dx%d+%d+%d +repage -quality %d', $newWidth, $newHeight, $offsetLeft, $offsetTop, $jpegQuality),
104 '',
105 ['noScale' => true],
106 true
107 );
108 $gifBuilder->filenamePrefix = $backupPrefix;
109
110 if ($result !== null) {
111 $originalFileName = $croppedImage = $result[3];
112 }
113 }
114
115 // Normal situation (no masking)
116 if (!(is_array($configuration['maskImages']) && $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled'])) {
117
118 // SVG
119 if ($croppedImage === null && $sourceFile->getExtension() === 'svg') {
120 $newDimensions = $this->getNewSvgDimensions($sourceFile, $configuration, $options, $gifBuilder);
121 $result = [
122 0 => $newDimensions['width'],
123 1 => $newDimensions['height'],
124 3 => '' // no file = use original
125 ];
126 } else {
127 // all other images
128 // the result info is an array with 0=width,1=height,2=extension,3=filename
129 $result = $gifBuilder->imageMagickConvert(
130 $originalFileName,
131 $configuration['fileExtension'],
132 $configuration['width'],
133 $configuration['height'],
134 $configuration['additionalParameters'],
135 $configuration['frame'],
136 $options
137 );
138 }
139 } else {
140 $targetFileName = $this->getFilenameForImageCropScaleMask($task);
141 $temporaryFileName = PATH_site . 'typo3temp/' . $targetFileName;
142 $maskImage = $configuration['maskImages']['maskImage'];
143 $maskBackgroundImage = $configuration['maskImages']['backgroundImage'];
144 if ($maskImage instanceof Resource\FileInterface && $maskBackgroundImage instanceof Resource\FileInterface) {
145 $temporaryExtension = 'png';
146 if (!$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowTemporaryMasksAsPng']) {
147 // If ImageMagick version 5+
148 $temporaryExtension = $gifBuilder->gifExtension;
149 }
150 $tempFileInfo = $gifBuilder->imageMagickConvert(
151 $originalFileName,
152 $temporaryExtension,
153 $configuration['width'],
154 $configuration['height'],
155 $configuration['additionalParameters'],
156 $configuration['frame'],
157 $options
158 );
159 if (is_array($tempFileInfo)) {
160 $maskBottomImage = $configuration['maskImages']['maskBottomImage'];
161 if ($maskBottomImage instanceof Resource\FileInterface) {
162 $maskBottomImageMask = $configuration['maskImages']['maskBottomImageMask'];
163 } else {
164 $maskBottomImageMask = null;
165 }
166
167 // Scaling: ****
168 $tempScale = [];
169 $command = '-geometry ' . $tempFileInfo[0] . 'x' . $tempFileInfo[1] . '!';
170 $command = $this->modifyImageMagickStripProfileParameters($command, $configuration);
171 $tmpStr = $gifBuilder->randomName();
172 // m_mask
173 $tempScale['m_mask'] = $tmpStr . '_mask.' . $temporaryExtension;
174 $gifBuilder->imageMagickExec($maskImage->getForLocalProcessing(true), $tempScale['m_mask'], $command);
175 // m_bgImg
176 $tempScale['m_bgImg'] = $tmpStr . '_bgImg.miff';
177 $gifBuilder->imageMagickExec($maskBackgroundImage->getForLocalProcessing(), $tempScale['m_bgImg'], $command);
178 // m_bottomImg / m_bottomImg_mask
179 if ($maskBottomImage instanceof Resource\FileInterface && $maskBottomImageMask instanceof Resource\FileInterface) {
180 $tempScale['m_bottomImg'] = $tmpStr . '_bottomImg.' . $temporaryExtension;
181 $gifBuilder->imageMagickExec($maskBottomImage->getForLocalProcessing(), $tempScale['m_bottomImg'], $command);
182 $tempScale['m_bottomImg_mask'] = ($tmpStr . '_bottomImg_mask.') . $temporaryExtension;
183 $gifBuilder->imageMagickExec($maskBottomImageMask->getForLocalProcessing(), $tempScale['m_bottomImg_mask'], $command);
184 // BEGIN combining:
185 // The image onto the background
186 $gifBuilder->combineExec($tempScale['m_bgImg'], $tempScale['m_bottomImg'], $tempScale['m_bottomImg_mask'], $tempScale['m_bgImg']);
187 }
188 // The image onto the background
189 $gifBuilder->combineExec($tempScale['m_bgImg'], $tempFileInfo[3], $tempScale['m_mask'], $temporaryFileName);
190 $tempFileInfo[3] = $temporaryFileName;
191 // Unlink the temp-images...
192 foreach ($tempScale as $tempFile) {
193 if (@is_file($tempFile)) {
194 unlink($tempFile);
195 }
196 }
197 }
198 $result = $tempFileInfo;
199 }
200 }
201
202 // check if the processing really generated a new file (scaled and/or cropped)
203 if ($result !== null) {
204 if ($result[3] !== $originalFileName || $originalFileName === $croppedImage) {
205 $result = [
206 'width' => $result[0],
207 'height' => $result[1],
208 'filePath' => $result[3],
209 ];
210 } else {
211 // No file was generated
212 $result = null;
213 }
214 }
215
216 // Cleanup temp file if it isn't used as result
217 if ($croppedImage && ($result === null || $croppedImage !== $result['filePath'])) {
218 GeneralUtility::unlink_tempfile($croppedImage);
219 }
220
221 return $result;
222 }
223
224 /**
225 * Calculate new dimensions for SVG image
226 * No cropping, if cropped info present image is scaled down
227 *
228 * @param Resource\FileInterface $file
229 * @param array $configuration
230 * @param array $options
231 * @param GifBuilder $gifBuilder
232 * @return array width,height
233 */
234 protected function getNewSvgDimensions($file, array $configuration, array $options, GifBuilder $gifBuilder)
235 {
236 $info = [$file->getProperty('width'), $file->getProperty('height')];
237 $data = $gifBuilder->getImageScale($info, $configuration['width'], $configuration['height'], $options);
238
239 // Turn cropScaling into scaling
240 if ($data['crs']) {
241 if (!$data['origW']) {
242 $data['origW'] = $data[0];
243 }
244 if (!$data['origH']) {
245 $data['origH'] = $data[1];
246 }
247 if ($data[0] > $data['origW']) {
248 $data[1] = (int)(($data['origW'] * $data[1]) / $data[0]);
249 $data[0] = $data['origW'];
250 } else {
251 $data[0] = (int)(($data['origH'] * $data[0]) / $data[1]);
252 $data[1] = $data['origH'];
253 }
254 }
255
256 return [
257 'width' => $data[0],
258 'height' => $data[1]
259 ];
260 }
261
262 /**
263 * @param Resource\ProcessedFile $processedFile
264 * @param \TYPO3\CMS\Frontend\Imaging\GifBuilder $gifBuilder
265 *
266 * @return array
267 */
268 protected function getConfigurationForImageCropScaleMask(Resource\ProcessedFile $processedFile, \TYPO3\CMS\Frontend\Imaging\GifBuilder $gifBuilder)
269 {
270 $configuration = $processedFile->getProcessingConfiguration();
271
272 if ($configuration['useSample']) {
273 $gifBuilder->scalecmd = '-sample';
274 }
275 $options = [];
276 if ($configuration['maxWidth']) {
277 $options['maxW'] = $configuration['maxWidth'];
278 }
279 if ($configuration['maxHeight']) {
280 $options['maxH'] = $configuration['maxHeight'];
281 }
282 if ($configuration['minWidth']) {
283 $options['minW'] = $configuration['minWidth'];
284 }
285 if ($configuration['minHeight']) {
286 $options['minH'] = $configuration['minHeight'];
287 }
288
289 $options['noScale'] = $configuration['noScale'];
290
291 return $options;
292 }
293
294 /**
295 * Returns the filename for a cropped/scaled/masked file.
296 *
297 * @param TaskInterface $task
298 * @return string
299 */
300 protected function getFilenameForImageCropScaleMask(TaskInterface $task)
301 {
302 $configuration = $task->getTargetFile()->getProcessingConfiguration();
303 $targetFileExtension = $task->getSourceFile()->getExtension();
304 $processedFileExtension = $GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png'] ? 'png' : 'gif';
305 if (is_array($configuration['maskImages']) && $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled'] && $task->getSourceFile()->getExtension() != $processedFileExtension) {
306 $targetFileExtension = 'jpg';
307 } elseif ($configuration['fileExtension']) {
308 $targetFileExtension = $configuration['fileExtension'];
309 }
310
311 return $task->getTargetFile()->generateProcessedFileNameWithoutExtension() . '.' . ltrim(trim($targetFileExtension), '.');
312 }
313
314 /**
315 * Modifies the parameters for ImageMagick for stripping of profile information.
316 *
317 * @param string $parameters The parameters to be modified (if required)
318 * @param array $configuration The TypoScript configuration of [IMAGE].file
319 * @return string
320 */
321 protected function modifyImageMagickStripProfileParameters($parameters, array $configuration)
322 {
323 // Strips profile information of image to save some space:
324 if (isset($configuration['stripProfile'])) {
325 if (
326 $configuration['stripProfile']
327 && $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileCommand'] !== ''
328 ) {
329 $parameters = $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripProfileCommand'] . $parameters;
330 } else {
331 $parameters .= '###SkipStripProfile###';
332 }
333 }
334 return $parameters;
335 }
336 }