47c561288ba5642d7e357b281b3b566f218ca164
[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 $gifBuilder->absPrefix = PATH_site;
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(',', array((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 // the result info is an array with 0=width,1=height,2=extension,3=filename
96 $result = $gifBuilder->imageMagickConvert(
97 $originalFileName,
98 '',
99 '',
100 '',
101 sprintf('-crop %dx%d+%d+%d', $newWidth, $newHeight, $offsetLeft, $offsetTop),
102 '',
103 ['noScale' => true],
104 true
105 );
106 $gifBuilder->filenamePrefix = $backupPrefix;
107
108 if ($result !== null) {
109 $originalFileName = $croppedImage = $result[3];
110 }
111 }
112
113 // Normal situation (no masking)
114 if (!(is_array($configuration['maskImages']) && $GLOBALS['TYPO3_CONF_VARS']['GFX']['im'])) {
115
116 // SVG
117 if ($croppedImage === null && $sourceFile->getExtension() === 'svg') {
118 $newDimensions = $this->getNewSvgDimensions($sourceFile, $configuration, $options, $gifBuilder);
119 $result = array(
120 0 => $newDimensions['width'],
121 1 => $newDimensions['height'],
122 3 => '' // no file = use original
123 );
124
125 // all other images
126 } else {
127 // the result info is an array with 0=width,1=height,2=extension,3=filename
128 $result = $gifBuilder->imageMagickConvert(
129 $originalFileName,
130 $configuration['fileExtension'],
131 $configuration['width'],
132 $configuration['height'],
133 $configuration['additionalParameters'],
134 $configuration['frame'],
135 $options
136 );
137 }
138 } else {
139 $targetFileName = $this->getFilenameForImageCropScaleMask($task);
140 $temporaryFileName = $gifBuilder->tempPath . $targetFileName;
141 $maskImage = $configuration['maskImages']['maskImage'];
142 $maskBackgroundImage = $configuration['maskImages']['backgroundImage'];
143 if ($maskImage instanceof Resource\FileInterface && $maskBackgroundImage instanceof Resource\FileInterface) {
144 $temporaryExtension = 'png';
145 if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['im_mask_temp_ext_gif']) {
146 // If ImageMagick version 5+
147 $temporaryExtension = $gifBuilder->gifExtension;
148 }
149 $tempFileInfo = $gifBuilder->imageMagickConvert(
150 $originalFileName,
151 $temporaryExtension,
152 $configuration['width'],
153 $configuration['height'],
154 $configuration['additionalParameters'],
155 $configuration['frame'],
156 $options
157 );
158 if (is_array($tempFileInfo)) {
159 $maskBottomImage = $configuration['maskImages']['maskBottomImage'];
160 if ($maskBottomImage instanceof Resource\FileInterface) {
161 $maskBottomImageMask = $configuration['maskImages']['maskBottomImageMask'];
162 } else {
163 $maskBottomImageMask = null;
164 }
165
166 // Scaling: ****
167 $tempScale = array();
168 $command = '-geometry ' . $tempFileInfo[0] . 'x' . $tempFileInfo[1] . '!';
169 $command = $this->modifyImageMagickStripProfileParameters($command, $configuration);
170 $tmpStr = $gifBuilder->randomName();
171 // m_mask
172 $tempScale['m_mask'] = $tmpStr . '_mask.' . $temporaryExtension;
173 $gifBuilder->imageMagickExec($maskImage->getForLocalProcessing(true), $tempScale['m_mask'], $command);
174 // m_bgImg
175 $tempScale['m_bgImg'] = $tmpStr . '_bgImg.miff';
176 $gifBuilder->imageMagickExec($maskBackgroundImage->getForLocalProcessing(), $tempScale['m_bgImg'], $command);
177 // m_bottomImg / m_bottomImg_mask
178 if ($maskBottomImage instanceof Resource\FileInterface && $maskBottomImageMask instanceof Resource\FileInterface) {
179 $tempScale['m_bottomImg'] = $tmpStr . '_bottomImg.' . $temporaryExtension;
180 $gifBuilder->imageMagickExec($maskBottomImage->getForLocalProcessing(), $tempScale['m_bottomImg'], $command);
181 $tempScale['m_bottomImg_mask'] = ($tmpStr . '_bottomImg_mask.') . $temporaryExtension;
182 $gifBuilder->imageMagickExec($maskBottomImageMask->getForLocalProcessing(), $tempScale['m_bottomImg_mask'], $command);
183 // BEGIN combining:
184 // The image onto the background
185 $gifBuilder->combineExec($tempScale['m_bgImg'], $tempScale['m_bottomImg'], $tempScale['m_bottomImg_mask'], $tempScale['m_bgImg']);
186 }
187 // The image onto the background
188 $gifBuilder->combineExec($tempScale['m_bgImg'], $tempFileInfo[3], $tempScale['m_mask'], $temporaryFileName);
189 $tempFileInfo[3] = $temporaryFileName;
190 // Unlink the temp-images...
191 foreach ($tempScale as $tempFile) {
192 if (@is_file($tempFile)) {
193 unlink($tempFile);
194 }
195 }
196 }
197 $result = $tempFileInfo;
198 }
199 }
200
201 // check if the processing really generated a new file (scaled and/or cropped)
202 if ($result !== null) {
203 if ($result[3] !== $originalFileName || $originalFileName === $croppedImage) {
204 $result = array(
205 'width' => $result[0],
206 'height' => $result[1],
207 'filePath' => $result[3],
208 );
209 } else {
210 // No file was generated
211 $result = null;
212 }
213 }
214
215 // Cleanup temp file if it isn't used as result
216 if ($croppedImage && ($result === null || $croppedImage !== $result['filePath'])) {
217 GeneralUtility::unlink_tempfile($croppedImage);
218 }
219
220 return $result;
221 }
222
223 /**
224 * Calculate new dimensions for SVG image
225 * No cropping, if cropped info present image is scaled down
226 *
227 * @param Resource\FileInterface $file
228 * @param array $configuration
229 * @param array $options
230 * @param GifBuilder $gifBuilder
231 * @return array width,height
232 */
233 protected function getNewSvgDimensions($file, array $configuration, array $options, GifBuilder $gifBuilder)
234 {
235 $info = array($file->getProperty('width'), $file->getProperty('height'));
236 $data = $gifBuilder->getImageScale($info, $configuration['width'], $configuration['height'], $options);
237
238 // Turn cropScaling into scaling
239 if ($data['crs']) {
240 if (!$data['origW']) {
241 $data['origW'] = $data[0];
242 }
243 if (!$data['origH']) {
244 $data['origH'] = $data[1];
245 }
246 if ($data[0] > $data['origW']) {
247 $data[1] = (int)(($data['origW'] * $data[1]) / $data[0]);
248 $data[0] = $data['origW'];
249 } else {
250 $data[0] = (int)(($data['origH'] * $data[0]) / $data[1]);
251 $data[1] = $data['origH'];
252 }
253 }
254
255 return array(
256 'width' => $data[0],
257 'height' => $data[1]
258 );
259 }
260
261 /**
262 * @param Resource\ProcessedFile $processedFile
263 * @param \TYPO3\CMS\Frontend\Imaging\GifBuilder $gifBuilder
264 *
265 * @return array
266 */
267 protected function getConfigurationForImageCropScaleMask(Resource\ProcessedFile $processedFile, \TYPO3\CMS\Frontend\Imaging\GifBuilder $gifBuilder)
268 {
269 $configuration = $processedFile->getProcessingConfiguration();
270
271 if ($configuration['useSample']) {
272 $gifBuilder->scalecmd = '-sample';
273 }
274 $options = array();
275 if ($configuration['maxWidth']) {
276 $options['maxW'] = $configuration['maxWidth'];
277 }
278 if ($configuration['maxHeight']) {
279 $options['maxH'] = $configuration['maxHeight'];
280 }
281 if ($configuration['minWidth']) {
282 $options['minW'] = $configuration['minWidth'];
283 }
284 if ($configuration['minHeight']) {
285 $options['minH'] = $configuration['minHeight'];
286 }
287
288 $options['noScale'] = $configuration['noScale'];
289
290 return $options;
291 }
292
293 /**
294 * Returns the filename for a cropped/scaled/masked file.
295 *
296 * @param TaskInterface $task
297 * @return string
298 */
299 protected function getFilenameForImageCropScaleMask(TaskInterface $task)
300 {
301 $configuration = $task->getTargetFile()->getProcessingConfiguration();
302 $targetFileExtension = $task->getSourceFile()->getExtension();
303 $processedFileExtension = $GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png'] ? 'png' : 'gif';
304 if (is_array($configuration['maskImages']) && $GLOBALS['TYPO3_CONF_VARS']['GFX']['im'] && $task->getSourceFile()->getExtension() != $processedFileExtension) {
305 $targetFileExtension = 'jpg';
306 } elseif ($configuration['fileExtension']) {
307 $targetFileExtension = $configuration['fileExtension'];
308 }
309
310 return $task->getTargetFile()->generateProcessedFileNameWithoutExtension() . '.' . ltrim(trim($targetFileExtension), '.');
311 }
312
313 /**
314 * Modifies the parameters for ImageMagick for stripping of profile information.
315 *
316 * @param string $parameters The parameters to be modified (if required)
317 * @param array $configuration The TypoScript configuration of [IMAGE].file
318 * @return string
319 */
320 protected function modifyImageMagickStripProfileParameters($parameters, array $configuration)
321 {
322 // Strips profile information of image to save some space:
323 if (isset($configuration['stripProfile'])) {
324 if ($configuration['stripProfile']) {
325 $parameters = $GLOBALS['TYPO3_CONF_VARS']['GFX']['im_stripProfileCommand'] . $parameters;
326 } else {
327 $parameters .= '###SkipStripProfile###';
328 }
329 }
330 return $parameters;
331 }
332 }