[BUGFIX] Add cropVariant support for broken GalleryProcessor
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / DataProcessing / GalleryProcessor.php
1 <?php
2 namespace TYPO3\CMS\Frontend\DataProcessing;
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\Imaging\ImageManipulation\CropVariantCollection;
18 use TYPO3\CMS\Core\Resource\FileInterface;
19 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
20 use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
21 use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
22
23 /**
24 * This data processor will calculate rows, columns and dimensions for a gallery
25 * based on several settings and can be used for f.i. the CType "textmedia"
26 *
27 * The output will be an array which contains the rows and columns,
28 * including the file references and the calculated width and height for each media element,
29 * but also some more information of the gallery, like position, width and counters
30 *
31 * Example TypoScript configuration:
32 *
33 * 10 = TYPO3\CMS\Frontend\DataProcessing\GalleryProcessor
34 * 10 {
35 * filesProcessedDataKey = files
36 * mediaOrientation.field = imageorient
37 * numberOfColumns.field = imagecols
38 * equalMediaHeight.field = imageheight
39 * equalMediaWidth.field = imagewidth
40 * columnSpacing = 0
41 * borderEnabled.field = imageborder
42 * borderPadding = 0
43 * borderWidth = 0
44 * maxGalleryWidth = {$styles.content.mediatext.maxW}
45 * maxGalleryWidthInText = {$styles.content.mediatext.maxWInText}
46 * as = gallery
47 * }
48 *
49 * Output example:
50 *
51 * gallery {
52 * position {
53 * horizontal = center
54 * vertical = above
55 * noWrap = FALSE
56 * }
57 * width = 600
58 * count {
59 * files = 2
60 * columns = 1
61 * rows = 2
62 * }
63 * rows {
64 * 1 {
65 * columns {
66 * 1 {
67 * media = TYPO3\CMS\Core\Resource\FileReference
68 * dimensions {
69 * width = 600
70 * height = 400
71 * }
72 * }
73 * }
74 * }
75 * 2 {
76 * columns {
77 * 1 {
78 * media = TYPO3\CMS\Core\Resource\FileReference
79 * dimensions {
80 * width = 600
81 * height = 400
82 * }
83 * }
84 * }
85 * }
86 * }
87 * columnSpacing = 0
88 * border {
89 * enabled = FALSE
90 * width = 0
91 * padding = 0
92 * }
93 * }
94 *
95 */
96 class GalleryProcessor implements DataProcessorInterface
97 {
98 /**
99 * The content object renderer
100 *
101 * @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
102 */
103 protected $contentObjectRenderer;
104
105 /**
106 * The processor configuration
107 *
108 * @var array
109 */
110 protected $processorConfiguration;
111
112 /**
113 * Matching the tt_content field towards the imageOrient option
114 *
115 * @var array
116 */
117 protected $availableGalleryPositions = [
118 'horizontal' => [
119 'center' => [0, 8],
120 'right' => [1, 9, 17, 25],
121 'left' => [2, 10, 18, 26]
122 ],
123 'vertical' => [
124 'above' => [0, 1, 2],
125 'intext' => [17, 18, 25, 26],
126 'below' => [8, 9, 10]
127 ]
128 ];
129
130 /**
131 * Storage for processed data
132 *
133 * @var array
134 */
135 protected $galleryData = [
136 'position' => [
137 'horizontal' => '',
138 'vertical' => '',
139 'noWrap' => false
140 ],
141 'width' => 0,
142 'count' => [
143 'files' => 0,
144 'columns' => 0,
145 'rows' => 0,
146 ],
147 'columnSpacing' => 0,
148 'border' => [
149 'enabled' => false,
150 'width' => 0,
151 'padding' => 0,
152 ],
153 'rows' => []
154 ];
155
156 /**
157 * @var int
158 */
159 protected $numberOfColumns;
160
161 /**
162 * @var int
163 */
164 protected $mediaOrientation;
165
166 /**
167 * @var int
168 */
169 protected $maxGalleryWidth;
170
171 /**
172 * @var int
173 */
174 protected $maxGalleryWidthInText;
175
176 /**
177 * @var int
178 */
179 protected $equalMediaHeight;
180
181 /**
182 * @var int
183 */
184 protected $equalMediaWidth;
185
186 /**
187 * @var int
188 */
189 protected $columnSpacing;
190
191 /**
192 * @var bool
193 */
194 protected $borderEnabled;
195
196 /**
197 * @var int
198 */
199 protected $borderWidth;
200
201 /**
202 * @var int
203 */
204 protected $borderPadding;
205
206 /**
207 * @var string
208 */
209 protected $cropVariant = 'default';
210
211 /**
212 * The (filtered) media files to be used in the gallery
213 *
214 * @var FileInterface[]
215 */
216 protected $fileObjects = [];
217
218 /**
219 * The calculated dimensions for each media element
220 *
221 * @var array
222 */
223 protected $mediaDimensions = [];
224
225 /**
226 * Process data for a gallery, for instance the CType "textmedia"
227 *
228 * @param ContentObjectRenderer $cObj The content object renderer, which contains data of the content element
229 * @param array $contentObjectConfiguration The configuration of Content Object
230 * @param array $processorConfiguration The configuration of this processor
231 * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
232 * @return array the processed data as key/value store
233 * @throws ContentRenderingException
234 */
235 public function process(
236 ContentObjectRenderer $cObj,
237 array $contentObjectConfiguration,
238 array $processorConfiguration,
239 array $processedData
240 ) {
241 if (isset($processorConfiguration['if.']) && !$cObj->checkIf($processorConfiguration['if.'])) {
242 return $processedData;
243 }
244
245 $this->contentObjectRenderer = $cObj;
246 $this->processorConfiguration = $processorConfiguration;
247
248 $filesProcessedDataKey = (string)$cObj->stdWrapValue(
249 'filesProcessedDataKey',
250 $processorConfiguration,
251 'files'
252 );
253 if (isset($processedData[$filesProcessedDataKey]) && is_array($processedData[$filesProcessedDataKey])) {
254 $this->fileObjects = $processedData[$filesProcessedDataKey];
255 $this->galleryData['count']['files'] = count($this->fileObjects);
256 } else {
257 throw new ContentRenderingException('No files found for key ' . $filesProcessedDataKey . ' in $processedData.', 1436809789);
258 }
259
260 $this->numberOfColumns = (int)$this->getConfigurationValue('numberOfColumns', 'imagecols');
261 $this->mediaOrientation = (int)$this->getConfigurationValue('mediaOrientation', 'imageorient');
262 $this->maxGalleryWidth = (int)$this->getConfigurationValue('maxGalleryWidth') ?: 600;
263 $this->maxGalleryWidthInText = (int)$this->getConfigurationValue('maxGalleryWidthInText') ?: 300;
264 $this->equalMediaHeight = (int)$this->getConfigurationValue('equalMediaHeight', 'imageheight');
265 $this->equalMediaWidth = (int)$this->getConfigurationValue('equalMediaWidth', 'imagewidth');
266 $this->columnSpacing = (int)$this->getConfigurationValue('columnSpacing');
267 $this->borderEnabled = (bool)$this->getConfigurationValue('borderEnabled', 'imageborder');
268 $this->borderWidth = (int)$this->getConfigurationValue('borderWidth');
269 $this->borderPadding = (int)$this->getConfigurationValue('borderPadding');
270 $this->cropVariant = (int)$this->getConfigurationValue('cropVariant') ?: 'default';
271
272 $this->determineGalleryPosition();
273 $this->determineMaximumGalleryWidth();
274
275 $this->calculateRowsAndColumns();
276 $this->calculateMediaWidthsAndHeights();
277
278 $this->prepareGalleryData();
279
280 $targetFieldName = (string)$cObj->stdWrapValue(
281 'as',
282 $processorConfiguration,
283 'gallery'
284 );
285
286 $processedData[$targetFieldName] = $this->galleryData;
287
288 return $processedData;
289 }
290
291 /**
292 * Get configuration value from processorConfiguration
293 * with when $dataArrayKey fallback to value from cObj->data array
294 *
295 * @param string $key
296 * @param string|NULL $dataArrayKey
297 * @return string
298 */
299 protected function getConfigurationValue($key, $dataArrayKey = null)
300 {
301 $defaultValue = '';
302 if ($dataArrayKey && isset($this->contentObjectRenderer->data[$dataArrayKey])) {
303 $defaultValue = $this->contentObjectRenderer->data[$dataArrayKey];
304 }
305 return $this->contentObjectRenderer->stdWrapValue(
306 $key,
307 $this->processorConfiguration,
308 $defaultValue
309 );
310 }
311
312 /**
313 * Define the gallery position
314 *
315 * Gallery has a horizontal and a vertical position towards the text
316 * and a possible wrapping of the text around the gallery.
317 *
318 * @return void
319 */
320 protected function determineGalleryPosition()
321 {
322 foreach ($this->availableGalleryPositions as $positionDirectionKey => $positionDirectionValue) {
323 foreach ($positionDirectionValue as $positionKey => $positionArray) {
324 if (in_array($this->mediaOrientation, $positionArray, true)) {
325 $this->galleryData['position'][$positionDirectionKey] = $positionKey;
326 }
327 }
328 }
329
330 if ($this->mediaOrientation === 25 || $this->mediaOrientation === 26) {
331 $this->galleryData['position']['noWrap'] = true;
332 }
333 }
334
335 /**
336 * Get the gallery width based on vertical position
337 *
338 * @return void
339 */
340 protected function determineMaximumGalleryWidth()
341 {
342 if ($this->galleryData['position']['vertical'] === 'intext') {
343 $this->galleryData['width'] = $this->maxGalleryWidthInText;
344 } else {
345 $this->galleryData['width'] = $this->maxGalleryWidth;
346 }
347 }
348
349 /**
350 * Calculate the amount of rows and columns
351 *
352 * @return void
353 */
354 protected function calculateRowsAndColumns()
355 {
356
357 // If no columns defined, set it to 1
358 $columns = max((int)$this->numberOfColumns, 1);
359
360 // When more columns than media elements, set the columns to the amount of media elements
361 if ($columns > $this->galleryData['count']['files']) {
362 $columns = $this->galleryData['count']['files'];
363 }
364
365 if ($columns === 0) {
366 $columns = 1;
367 }
368
369 // Calculate the rows from the amount of files and the columns
370 $rows = ceil($this->galleryData['count']['files'] / $columns);
371
372 $this->galleryData['count']['columns'] = $columns;
373 $this->galleryData['count']['rows'] = (int)$rows;
374 }
375
376 /**
377 * Calculate the width/height of the media elements
378 *
379 * Based on the width of the gallery, defined equal width or height by a user, the spacing between columns and
380 * the use of a border, defined by user, where the border width and padding are taken into account
381 *
382 * File objects MUST already be filtered. They need a height and width to be shown in the gallery
383 *
384 * @return void
385 */
386 protected function calculateMediaWidthsAndHeights()
387 {
388 $columnSpacingTotal = ($this->galleryData['count']['columns'] - 1) * $this->columnSpacing;
389
390 $galleryWidthMinusBorderAndSpacing = max($this->galleryData['width'] - $columnSpacingTotal, 1);
391
392 if ($this->borderEnabled) {
393 $borderPaddingTotal = ($this->galleryData['count']['columns'] * 2) * $this->borderPadding;
394 $borderWidthTotal = ($this->galleryData['count']['columns'] * 2) * $this->borderWidth;
395 $galleryWidthMinusBorderAndSpacing = $galleryWidthMinusBorderAndSpacing - $borderPaddingTotal - $borderWidthTotal;
396 }
397
398 // User entered a predefined height
399 if ($this->equalMediaHeight) {
400 $mediaScalingCorrection = 1;
401 $maximumRowWidth = 0;
402
403 // Calculate the scaling correction when the total of media elements is wider than the gallery width
404 for ($row = 1; $row <= $this->galleryData['count']['rows']; $row++) {
405 $totalRowWidth = 0;
406 for ($column = 1; $column <= $this->galleryData['count']['columns']; $column++) {
407 $fileKey = (($row - 1) * $this->galleryData['count']['columns']) + $column - 1;
408 if ($fileKey > $this->galleryData['count']['files'] - 1) {
409 break 2;
410 }
411 $currentMediaScaling = $this->equalMediaHeight / max($this->getCroppedDimensionalProperty($this->fileObjects[$fileKey], 'height'), 1);
412 $totalRowWidth += $this->getCroppedDimensionalProperty($this->fileObjects[$fileKey], 'width') * $currentMediaScaling;
413 }
414 $maximumRowWidth = max($totalRowWidth, $maximumRowWidth);
415 $mediaInRowScaling = $totalRowWidth / $galleryWidthMinusBorderAndSpacing;
416 $mediaScalingCorrection = max($mediaInRowScaling, $mediaScalingCorrection);
417 }
418
419 // Set the corrected dimensions for each media element
420 foreach ($this->fileObjects as $key => $fileObject) {
421 $mediaHeight = floor($this->equalMediaHeight / $mediaScalingCorrection);
422 $mediaWidth = floor(
423 $this->getCroppedDimensionalProperty($fileObject, 'width') * ($mediaHeight / max($this->getCroppedDimensionalProperty($fileObject, 'height'), 1))
424 );
425 $this->mediaDimensions[$key] = [
426 'width' => $mediaWidth,
427 'height' => $mediaHeight
428 ];
429 }
430
431 // Recalculate gallery width
432 $this->galleryData['width'] = floor($maximumRowWidth / $mediaScalingCorrection);
433
434 // User entered a predefined width
435 } elseif ($this->equalMediaWidth) {
436 $mediaScalingCorrection = 1;
437
438 // Calculate the scaling correction when the total of media elements is wider than the gallery width
439 $totalRowWidth = $this->galleryData['count']['columns'] * $this->equalMediaWidth;
440 $mediaInRowScaling = $totalRowWidth / $galleryWidthMinusBorderAndSpacing;
441 $mediaScalingCorrection = max($mediaInRowScaling, $mediaScalingCorrection);
442
443 // Set the corrected dimensions for each media element
444 foreach ($this->fileObjects as $key => $fileObject) {
445 $mediaWidth = floor($this->equalMediaWidth / $mediaScalingCorrection);
446 $mediaHeight = floor(
447 $this->getCroppedDimensionalProperty($fileObject, 'height') * ($mediaWidth / max($this->getCroppedDimensionalProperty($fileObject, 'width'), 1))
448 );
449 $this->mediaDimensions[$key] = [
450 'width' => $mediaWidth,
451 'height' => $mediaHeight
452 ];
453 }
454
455 // Recalculate gallery width
456 $this->galleryData['width'] = floor($totalRowWidth / $mediaScalingCorrection);
457
458 // Automatic setting of width and height
459 } else {
460 $maxMediaWidth = (int)($galleryWidthMinusBorderAndSpacing / $this->galleryData['count']['columns']);
461 foreach ($this->fileObjects as $key => $fileObject) {
462 $mediaWidth = min($maxMediaWidth, $this->getCroppedDimensionalProperty($fileObject, 'width'));
463 $mediaHeight = floor(
464 $this->getCroppedDimensionalProperty($fileObject, 'height') * ($mediaWidth / max($this->getCroppedDimensionalProperty($fileObject, 'width'), 1))
465 );
466 $this->mediaDimensions[$key] = [
467 'width' => $mediaWidth,
468 'height' => $mediaHeight
469 ];
470 }
471 }
472 }
473
474 /**
475 * When retrieving the height or width for a media file
476 * a possible cropping needs to be taken into account.
477 *
478 * @param FileInterface $fileObject
479 * @param string $dimensionalProperty 'width' or 'height'
480 *
481 * @return int
482 */
483 protected function getCroppedDimensionalProperty(FileInterface $fileObject, $dimensionalProperty)
484 {
485 if (!$fileObject->hasProperty('crop') || empty($fileObject->getProperty('crop'))) {
486 return $fileObject->getProperty($dimensionalProperty);
487 }
488
489 $croppingConfiguration = $fileObject->getProperty('crop');
490 $cropVariantCollection = CropVariantCollection::create((string)$croppingConfiguration);
491 return (int) $cropVariantCollection->getCropArea($this->cropVariant)->makeAbsoluteBasedOnFile($fileObject)->asArray()[$dimensionalProperty];
492 }
493
494 /**
495 * Prepare the gallery data
496 *
497 * Make an array for rows, columns and configuration
498 *
499 * @return void
500 */
501 protected function prepareGalleryData()
502 {
503 for ($row = 1; $row <= $this->galleryData['count']['rows']; $row++) {
504 for ($column = 1; $column <= $this->galleryData['count']['columns']; $column++) {
505 $fileKey = (($row - 1) * $this->galleryData['count']['columns']) + $column - 1;
506
507 $this->galleryData['rows'][$row]['columns'][$column] = [
508 'media' => $this->fileObjects[$fileKey],
509 'dimensions' => [
510 'width' => $this->mediaDimensions[$fileKey]['width'],
511 'height' => $this->mediaDimensions[$fileKey]['height']
512 ]
513 ];
514 }
515 }
516
517 $this->galleryData['columnSpacing'] = $this->columnSpacing;
518 $this->galleryData['border']['enabled'] = $this->borderEnabled;
519 $this->galleryData['border']['width'] = $this->borderWidth;
520 $this->galleryData['border']['padding'] = $this->borderPadding;
521 }
522 }