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