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