[BUGFIX] Add fileSuffix option to GeneralUtility::tempnam()
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Sprite / SpriteGenerator.php
1 <?php
2 namespace TYPO3\CMS\Backend\Sprite;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2010-2013 Steffen Ritter <info@steffen-ritter.net>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the text file GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29
30 use TYPO3\CMS\Core\Html\HtmlParser;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32
33 /**
34 * Sprite generator
35 *
36 * @author Steffen Ritter <info@steffen-ritter.net>
37 */
38 class SpriteGenerator {
39
40 /**
41 * Template creating CSS for the spritefile
42 *
43 * @var string
44 */
45 protected $templateSprite = '
46 .###NAMESPACE###-###SPRITENAME### {
47 background-image: url(\'###SPRITEURL###\') !important;
48 height: ###DEFAULTHEIGHT###px;
49 width: ###DEFAULTWIDTH###px;
50 }
51 ';
52
53 /**
54 * Template creating CSS for the high density spritefile
55 *
56 * @var string
57 */
58 protected $templateSpriteHighDensity = '
59 .backgroundsize .###NAMESPACE###-###SPRITENAME### {
60 background-image: url(\'###SPRITEURL###\') !important;
61 background-size:###BGWIDTH### ###BGHEIGHT###;
62 }
63 ';
64 /**
65 * Template creating CSS for position and size of a single icon
66 *
67 * @var string
68 */
69 protected $templateIcon = '.###NAMESPACE###-###ICONNAME### {
70 background-position: -###LEFT###px -###TOP###px !important;
71 ###SIZE_INFO###
72 }
73 ';
74
75 /**
76 * @var boolean
77 */
78 protected $enableHighDensitySprite = TRUE;
79
80 /**
81 * Most common icon-width in the sprite
82 *
83 * @var integer
84 */
85 protected $defaultWidth = 0;
86
87 /**
88 * Most common icon-height in the sprite
89 *
90 * @var integer
91 */
92 protected $defaultHeight = 0;
93
94 /**
95 * Calculated width of the sprite
96 *
97 * @var integer
98 */
99 protected $spriteWidth = 0;
100
101 /**
102 * Calculated height of the sprite
103 *
104 * @var integer
105 */
106 protected $spriteHeight = 0;
107
108 /**
109 * Sprite name, will be the filename, too
110 *
111 * @var string
112 */
113 protected $spriteName = '';
114
115 /**
116 * The folder the sprite-images will be saved (relative to PATH_site)
117 *
118 * @var string
119 */
120 protected $spriteFolder = 'typo3temp/sprites/';
121
122 /**
123 * The folder the sprite-cs will be saved (relative to PATH_site)
124 *
125 * @var string
126 */
127 protected $cssFolder = 'typo3temp/sprites/';
128
129 /**
130 * The spriteName will not be included in icon names
131 *
132 * @var boolean
133 */
134 protected $omitSpriteNameInIconName = FALSE;
135
136 /**
137 * Namespace of css classes
138 *
139 * @var string
140 */
141 protected $nameSpace = 't3-icon';
142
143 /**
144 * Setting this to TRUE, the timestamp of the creation will be included to the background import
145 * helps to easily rebuild sprites without cache problems
146 *
147 * @var boolean
148 */
149 protected $includeTimestampInCSS = TRUE;
150
151 /**
152 * All bases/root-names included in the sprite which has to be in css
153 * as sprite to include the background-image
154 *
155 * @var array
156 */
157 protected $spriteBases = array();
158
159 /**
160 * Collects data about all icons to process
161 *
162 * @var array
163 */
164 protected $iconsData = array();
165
166 /**
167 * Collects all sizes of icons within this sprite and there count
168 *
169 * @var array
170 */
171 protected $iconSizes = array();
172
173 /**
174 * Maps icon-sizes to iconnames
175 *
176 * @var array
177 */
178 protected $iconNamesPerSize = array();
179
180 /**
181 * space in px between to icons in the sprite (gap)
182 *
183 * @var integer
184 */
185 protected $space = 2;
186
187 /**
188 * Initializes the configuration of the spritegenerator
189 *
190 * @param string $spriteName The name of the sprite to be generated
191 */
192 public function __construct($spriteName) {
193 $this->spriteName = $spriteName;
194 }
195
196 /**
197 * Sets namespace of css code
198 *
199 * @param string $nameSpace
200 * @return SpriteGenerator An instance of $this, to enable "chaining".
201 */
202 public function setNamespace($nameSpace) {
203 $this->nameSpace = $nameSpace;
204 return $this;
205 }
206
207 /**
208 * Sets the spritename
209 *
210 * @param string $spriteName The name of the sprite to be generated
211 * @return SpriteGenerator An instance of $this, to enable "chaining".
212 */
213 public function setSpriteName($spriteName) {
214 $this->spriteName = $spriteName;
215 return $this;
216 }
217
218 /**
219 * Sets the sprite-graphics target-folder
220 *
221 * @param string $folder The target folder where the generated sprite is stored
222 * @return SpriteGenerator An instance of $this, to enable "chaining".
223 */
224 public function setSpriteFolder($folder) {
225 $this->spriteFolder = $folder;
226 return $this;
227 }
228
229 /**
230 * Sets the sprite-css target-folder
231 *
232 * @param string $folder the target folder where the generated CSS files are stored
233 * @return SpriteGenerator An instance of $this, to enable "chaining".
234 */
235 public function setCSSFolder($folder) {
236 $this->cssFolder = $folder;
237 return $this;
238 }
239
240 /**
241 * Enables/Disables HighDensitySprite Generation
242 *
243 * @param boolean $enable
244 * @return SpriteGenerator An instance of $this, to enable "chaining".
245 */
246 public function setEnableHighDensitySprite($enable = TRUE) {
247 $this->enableHighDensitySprite = $enable;
248 return $this;
249 }
250
251 /**
252 * Setter do enable the exclusion of the sprites-name from iconnames
253 *
254 * @param boolean $value
255 * @return SpriteGenerator An instance of $this, to enable "chaining".
256 * @deprecated since 6.2, will be removed two versions later - use setOmitSpriteNameInIconName() instead
257 */
258 public function setOmmitSpriteNameInIconName($value) {
259 GeneralUtility::logDeprecatedFunction();
260 return $this->setOmitSpriteNameInIconName($value);
261 }
262
263 /**
264 * Setter do enable the exclusion of the sprites-name from iconnames
265 *
266 * @param boolean $value
267 * @return SpriteGenerator An instance of $this, to enable "chaining".
268 */
269 public function setOmitSpriteNameInIconName($value) {
270 $this->omitSpriteNameInIconName = is_bool($value) ? $value : FALSE;
271 return $this;
272 }
273
274 /**
275 * Setter to adjust how much space is between to icons in the sprite
276 *
277 * @param integer $value
278 * @return SpriteGenerator An instance of $this, to enable "chaining".
279 */
280 public function setIconSpace($value) {
281 $this->space = intval($value);
282 return $this;
283 }
284
285 /**
286 * Setter for timestamp inclusion: imageFiles will be included with ?timestamp
287 *
288 * @param boolean $value
289 * @return SpriteGenerator An instance of $this, to enable "chaining".
290 */
291 public function setIncludeTimestampInCSS($value) {
292 $this->includeTimestampInCSS = is_bool($value) ? $value : TRUE;
293 return $this;
294 }
295
296 /**
297 * Reads all png,gif,jpg files from the passed folder name (including 1 subfolder level)
298 * extracts size information and stores data in internal array,
299 * afterwards triggers sprite generation.
300 *
301 * @param array $inputFolder Folder from which files are read
302 * @return array
303 */
304 public function generateSpriteFromFolder(array $inputFolder) {
305 $iconArray = array();
306 foreach ($inputFolder as $folder) {
307 // Detect all files to be included in sprites
308 $iconArray = array_merge($iconArray, $this->getFolder($folder));
309 }
310 return $this->generateSpriteFromArray($iconArray);
311 }
312
313 /**
314 * Method processes an array of files into an sprite,
315 * the array can be build from files within an folder or
316 * by hand (as the SpriteManager does)
317 *
318 * @param array $files array(icon name => icon file)
319 * @return array
320 */
321 public function generateSpriteFromArray(array $files) {
322 if (!$this->omitSpriteNameInIconName) {
323 $this->spriteBases[] = $this->spriteName;
324 }
325 $this->buildFileInformationCache($files);
326 // Calculate Icon Position in sprite
327 $this->calculateSpritePositions();
328 $this->generateGraphic();
329 if ($this->enableHighDensitySprite) {
330 $this->generateHighDensityGraphic();
331 }
332 $this->generateCSS();
333 $iconNames = array_keys($this->iconsData);
334 natsort($iconNames);
335 return array(
336 'spriteImage' => PATH_site . $this->spriteFolder . $this->spriteName . '.png',
337 'cssFile' => PATH_site . $this->cssFolder . $this->spriteName . '.css',
338 'iconNames' => $iconNames
339 );
340 }
341
342 /**
343 * Generates the css files
344 *
345 * @return void
346 */
347 protected function generateCSS() {
348 $cssData = '';
349 if ($this->includeTimestampInCSS) {
350 $timestamp = '?' . time();
351 } else {
352 $timestamp = '';
353 }
354 $spritePathForCSS = $this->resolveSpritePath();
355 $markerArray = array(
356 '###NAMESPACE###' => $this->nameSpace,
357 '###DEFAULTWIDTH###' => $this->defaultWidth,
358 '###DEFAULTHEIGHT###' => $this->defaultHeight,
359 '###SPRITENAME###' => '',
360 '###SPRITEURL###' => $spritePathForCSS ? $spritePathForCSS . '/' : ''
361 );
362 $markerArray['###SPRITEURL###'] .= $this->spriteName . '.png' . $timestamp;
363 foreach ($this->spriteBases as $base) {
364 $markerArray['###SPRITENAME###'] = $base;
365 $cssData .= HtmlParser::substituteMarkerArray($this->templateSprite, $markerArray);
366
367 if ($this->enableHighDensitySprite) {
368 $highDensityMarkerArray = array_merge($markerArray, array(
369 '###BGWIDTH###' => $this->spriteWidth . 'px',
370 '###BGHEIGHT###' => $this->spriteHeight . 'px',
371 '###SPRITEURL###' => str_replace(
372 $this->spriteName . '.png',
373 $this->spriteName . '@x2.png',
374 $markerArray['###SPRITEURL###']
375 )
376 ));
377 $cssData .= HtmlParser::substituteMarkerArray($this->templateSpriteHighDensity, $highDensityMarkerArray);
378 }
379 }
380
381 foreach ($this->iconsData as $data) {
382 $temp = $data['iconNameParts'];
383 array_shift($temp);
384 $cssName = implode('-', $temp);
385 $markerArrayIcons = array(
386 '###NAMESPACE###' => $this->nameSpace,
387 '###ICONNAME###' => $cssName,
388 '###LEFT###' => $data['left'],
389 '###TOP###' => $data['top'],
390 '###SIZE_INFO###' => ''
391 );
392 if ($data['height'] != $this->defaultHeight) {
393 $markerArrayIcons['###SIZE_INFO###'] .= TAB . 'height: ' . $data['height'] . 'px;' . LF;
394 }
395 if ($data['width'] != $this->defaultWidth) {
396 $markerArrayIcons['###SIZE_INFO###'] .= TAB . 'width: ' . $data['width'] . 'px;' . LF;
397 }
398 $cssData .= HtmlParser::substituteMarkerArray($this->templateIcon, $markerArrayIcons);
399 }
400 GeneralUtility::writeFile(PATH_site . $this->cssFolder . $this->spriteName . '.css', $cssData);
401 }
402
403 /**
404 * Compares image path to CSS path and creates the relative backpath from css to the sprites
405 *
406 * @return string
407 */
408 protected function resolveSpritePath() {
409 // Fix window paths
410 $this->cssFolder = str_replace('\\', '/', $this->cssFolder);
411 $this->spriteFolder = str_replace('\\', '/', $this->spriteFolder);
412 $cssPathSegments = GeneralUtility::trimExplode('/', trim($this->cssFolder, '/'));
413 $graphicPathSegments = GeneralUtility::trimExplode('/', trim($this->spriteFolder, '/'));
414 $i = 0;
415 while (isset($cssPathSegments[$i]) && isset($graphicPathSegments[$i]) && $cssPathSegments[$i] == $graphicPathSegments[$i]) {
416 unset($cssPathSegments[$i]);
417 unset($graphicPathSegments[$i]);
418 ++$i;
419 }
420 foreach ($cssPathSegments as $key => $value) {
421 $cssPathSegments[$key] = '..';
422 }
423 $completePath = array_merge($cssPathSegments, $graphicPathSegments);
424 $path = implode('/', $completePath);
425 return GeneralUtility::resolveBackPath($path);
426 }
427
428 /**
429 * The actual sprite generator, renders the command for Im/GM and executes
430 *
431 * @return void
432 */
433 protected function generateGraphic() {
434 $tempSprite = GeneralUtility::tempnam($this->spriteName, '.png');
435 $filePath = PATH_site . $this->spriteFolder . $this->spriteName . '.png';
436
437 // Create black true color image with given size
438 $newSprite = imagecreatetruecolor($this->spriteWidth, $this->spriteHeight);
439 imagesavealpha($newSprite, TRUE);
440 // Make it transparent
441 imagefill($newSprite, 0, 0, imagecolorallocatealpha($newSprite, 0, 255, 255, 127));
442 foreach ($this->iconsData as $icon) {
443 $function = 'imagecreatefrom' . strtolower($icon['fileExtension']);
444 if (function_exists($function)) {
445 $currentIcon = $function($icon['fileName']);
446 imagecopy($newSprite, $currentIcon, $icon['left'], $icon['top'], 0, 0, $icon['width'], $icon['height']);
447 }
448 }
449 imagepng($newSprite, $tempSprite);
450 GeneralUtility::upload_copy_move($tempSprite, $filePath);
451 GeneralUtility::unlink_tempfile($tempSprite);
452 }
453
454 /**
455 * The actual sprite generator, renders the command for IM/GM and executes
456 *
457 * @return void
458 */
459 protected function generateHighDensityGraphic() {
460 $tempSprite = GeneralUtility::tempnam($this->spriteName . '@x2', '.png');
461 $filePath = PATH_site . $this->spriteFolder . $this->spriteName . '@x2.png';
462
463 // Create black true color image with given size
464 $newSprite = imagecreatetruecolor($this->spriteWidth * 2, $this->spriteHeight * 2);
465 imagesavealpha($newSprite, TRUE);
466 // Make it transparent
467 imagefill($newSprite, 0, 0, imagecolorallocatealpha($newSprite, 0, 255, 255, 127));
468 foreach ($this->iconsData as $icon) {
469 $function = 'imagecreatefrom' . strtolower($icon['fileExtension']);
470 if (function_exists($function)) {
471 if ($icon['fileNameHighDensity'] !== FALSE) {
472 // copy HighDensity file
473 $currentIcon = $function($icon['fileNameHighDensity']);
474 imagecopy($newSprite, $currentIcon, $icon['left'] * 2, $icon['top'] * 2, 0, 0, $icon['width'] * 2, $icon['height'] * 2);
475 } else {
476 // scale up normal file
477 $currentIcon = $function($icon['fileName']);
478 imagecopyresized($newSprite, $currentIcon, $icon['left'] * 2, $icon['top'] * 2, 0, 0, $icon['width'] * 2, $icon['height'] * 2, $icon['width'], $icon['height']);
479 }
480 }
481 }
482 imagepng($newSprite, $tempSprite);
483 GeneralUtility::upload_copy_move($tempSprite, $filePath);
484 GeneralUtility::unlink_tempfile($tempSprite);
485 }
486 /**
487 * Arranges icons in sprites,
488 * afterwards all icons have information about the position in sprite
489 */
490 protected function calculateSpritePositions() {
491 // Calculate width of every icon-size-group
492 $sizes = array();
493 foreach ($this->iconSizes as $sizeTag => $count) {
494 $size = $this->explodeSizeTag($sizeTag);
495 $rowWidth = (int)ceil(sqrt($count)) * $size['width'];
496 while (isset($sizes[$rowWidth])) {
497 $rowWidth++;
498 }
499 $sizes[$rowWidth] = $sizeTag;
500 }
501 // Reverse sorting: widest group to top
502 krsort($sizes);
503 $currentTop = 0;
504 // Integrate all icons grouped by icons size into the sprite
505 foreach ($sizes as $sizeTag) {
506 $size = $this->explodeSizeTag($sizeTag);
507 $currentLeft = 0;
508 $rowCounter = 0;
509 $rowSize = ceil(sqrt($this->iconSizes[$sizeTag]));
510 $rowWidth = $rowSize * $size['width'] + ($rowSize - 1) * $this->space;
511 $this->spriteWidth = $rowWidth > $this->spriteWidth ? $rowWidth : $this->spriteWidth;
512 $firstLine = TRUE;
513 natsort($this->iconNamesPerSize[$sizeTag]);
514 foreach ($this->iconNamesPerSize[$sizeTag] as $iconName) {
515 if ($rowCounter == $rowSize - 1) {
516 $rowCounter = -1;
517 } elseif ($rowCounter == 0) {
518 if (!$firstLine) {
519 $currentTop += $size['height'];
520 $currentTop += $this->space;
521 }
522 $firstLine = FALSE;
523 $currentLeft = 0;
524 }
525 $this->iconsData[$iconName]['left'] = $currentLeft;
526 $this->iconsData[$iconName]['top'] = $currentTop;
527 $currentLeft += $size['width'];
528 $currentLeft += $this->space;
529 $rowCounter++;
530 }
531 $currentTop += $size['height'];
532 $currentTop += $this->space;
533 }
534 $this->spriteHeight = $currentTop;
535 }
536
537 /**
538 * Function getFolder traverses the target directory,
539 * locates all iconFiles and collects them into an array
540 *
541 * @param string $directoryPath Path to an folder which contains images
542 * @return array Returns an array with all files key: iconname, value: fileName
543 */
544 protected function getFolder($directoryPath) {
545 $subFolders = GeneralUtility::get_dirs(PATH_site . $directoryPath);
546 if (!$this->omitSpriteNameInIconName) {
547 $subFolders[] = '';
548 }
549 $resultArray = array();
550 foreach ($subFolders as $folder) {
551 if ($folder !== '.svn') {
552 $icons = GeneralUtility::getFilesInDir(PATH_site . $directoryPath . $folder . '/', 'gif,png,jpg');
553 if (!in_array($folder, $this->spriteBases) && count($icons) && $folder !== '') {
554 $this->spriteBases[] = $folder;
555 }
556 foreach ($icons as $icon) {
557 $fileInfo = pathinfo($icon);
558 $iconName = ($folder ? $folder . '-' : '') . $fileInfo['filename'];
559 if (!$this->omitSpriteNameInIconName) {
560 $iconName = $this->spriteName . '-' . $iconName;
561 }
562 $resultArray[$iconName] = $directoryPath . $folder . '/' . $icon;
563 }
564 }
565 }
566 return $resultArray;
567 }
568
569 /**
570 * Generates file information cache from file array
571 *
572 * @param array $files List of all files with their icon name
573 * @return void
574 */
575 protected function buildFileInformationCache(array $files) {
576 foreach ($files as $iconName => $iconFile) {
577 $iconNameParts = GeneralUtility::trimExplode('-', $iconName);
578 if (!in_array($iconNameParts[0], $this->spriteBases)) {
579 $this->spriteBases[] = $iconNameParts[0];
580 }
581 $fileInfo = @pathinfo((PATH_site . $iconFile));
582 $imageInfo = @getimagesize((PATH_site . $iconFile));
583 $this->iconsData[$iconName] = array(
584 'iconName' => $iconName,
585 'iconNameParts' => $iconNameParts,
586 'singleName' => $fileInfo['filename'],
587 'fileExtension' => $fileInfo['extension'],
588 'fileName' => PATH_site . $iconFile,
589 'width' => $imageInfo[0],
590 'height' => $imageInfo[1],
591 'left' => 0,
592 'top' => 0,
593 'fileNameHighDensity' => FALSE
594 );
595 if ($this->enableHighDensitySprite) {
596 $highDensityFile = str_replace('.' . $fileInfo['extension'], '@x2.' . $fileInfo['extension'], $iconFile);
597 if (@file_exists(PATH_site . $highDensityFile)) {
598 $this->iconsData[$iconName]['fileNameHighDensity'] = $highDensityFile;
599 }
600 }
601 $sizeTag = $imageInfo[0] . 'x' . $imageInfo[1];
602 if (isset($this->iconSizes[$sizeTag])) {
603 $this->iconSizes[$sizeTag] += 1;
604 } else {
605 $this->iconSizes[$sizeTag] = 1;
606 $this->iconNamesPerSize[$sizeTag] = array();
607 }
608 $this->iconNamesPerSize[$sizeTag][] = $iconName;
609 }
610 // Find most common image size, save it as default
611 asort($this->iconSizes);
612 $defaultSize = $this->explodeSizeTag(array_pop(array_keys($this->iconSizes)));
613 $this->defaultWidth = $defaultSize['width'];
614 $this->defaultHeight = $defaultSize['height'];
615 }
616
617 /**
618 * Transforms size tag into size array
619 *
620 * @param string $tag A size tag at the cache arrays
621 * @return array
622 */
623 protected function explodeSizeTag($tag = '') {
624 $size = GeneralUtility::trimExplode('x', $tag);
625 return array(
626 'width' => $size[0],
627 'height' => $size[1]
628 );
629 }
630
631 }