‪TYPO3CMS  ‪main
SvgManipulation.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
21 
44 {
45  private int ‪$defaultSvgDimension = 64;
46 
50  public function ‪cropScaleSvgString(string $svgString, ‪Area $cropArea, ‪ImageDimension $imageDimension): \DOMDocument
51  {
52  $offsetLeft = (int)$cropArea->‪getOffsetLeft();
53  $offsetTop = (int)$cropArea->‪getOffsetTop();
54  // Rounding is applied to preserve the same width/height that imageDimension calculates
55  $newWidth = (int)round($cropArea->‪getWidth());
56  $newHeight = (int)round($cropArea->‪getHeight());
57 
58  // Load original SVG
59  $originalSvg = new \DOMDocument();
60  $originalSvg->loadXML($svgString);
61 
62  // Create a fresh wrapping <svg> tag
63  $processedSvg = new \DOMDocument('1.0');
64  $processedSvg->preserveWhiteSpace = true;
65  $processedSvg->formatOutput = true;
66  $outerSvgElement = $processedSvg->createElement('svg');
67  $outerSvgElement->setAttribute('xmlns', 'http://www.w3.org/2000/svg');
68 
69  // Determine the SVG dimensions of the source SVG file contents
70  $dimensions = $this->‪determineSvgDimensions($originalSvg);
71 
72  // Adjust the width/height attributes of the outer SVG proxy element, if they were empty before.
73  $this->‪adjustSvgDimensions($originalSvg, $dimensions);
74 
75  // Set several attributes on the outer SVG proxy element (the "wrapper" of the real SVG)
76  $outerSvgElement->setAttribute('viewBox', $offsetLeft . ' ' . $offsetTop . ' ' . $newWidth . ' ' . $newHeight);
77  $outerSvgElement->setAttribute('width', (string)$imageDimension->getWidth());
78  $outerSvgElement->setAttribute('height', (string)$imageDimension->getHeight());
79 
80  // Possibly prevent some attributes on the "inner svg" (original input) and transport them
81  // to the new root (outerSvgElement). Currently only 'preserveAspectRatio'.
82  if ($originalSvg->documentElement->getAttribute('preserveAspectRatio') != '') {
83  $outerSvgElement->setAttribute('preserveAspectRatio', $originalSvg->documentElement->getAttribute('preserveAspectRatio'));
84  }
85 
86  // To enable some debugging for embeddding the original determined dimensions into the SVG, use:
87  // $outerSvgElement->setAttribute('data-inherit-width', (string)$dimensions['determined']['width']);
88  // $outerSvgElement->setAttribute('data-inherit-height', (string)$dimensions['determined']['height']);
89 
90  // Attach the main source SVG element into our proxy SVG element.
91  $innerSvgElement = $processedSvg->importNode($originalSvg->documentElement, true);
92 
93  // Stitch together the wrapper plus the old root element plus children,
94  // so that $processedSvg contains the full XML tree
95  $outerSvgElement->appendChild($innerSvgElement);
96  $processedSvg->appendChild($outerSvgElement);
97 
98  return $processedSvg;
99  }
100 
108  protected function ‪adjustSvgDimensions(\DOMDocument $originalSvg, array $determinedDimensions): bool
109  {
110  $isAltered = false;
111 
112  if ($determinedDimensions['original']['width'] === '') {
113  $originalSvg->documentElement->setAttribute('width', $determinedDimensions['determined']['width']);
114  $originalSvg->documentElement->setAttribute('data-manipulated-width', 'true');
115  $isAltered = true;
116  }
117 
118  if ($determinedDimensions['original']['height'] === '') {
119  $originalSvg->documentElement->setAttribute('height', $determinedDimensions['determined']['height']);
120  $originalSvg->documentElement->setAttribute('data-manipulated-height', 'true');
121  $isAltered = true;
122  }
123 
124  return $isAltered;
125  }
126 
133  protected function ‪determineSvgDimensions(\DOMDocument $originalSvg): array
134  {
135  // A default used when SVG neither uses width, height nor viewBox
136  // Files falling back to this are probably broken.
137  $width = $height = null;
138 
139  $originalSvgViewBox = $originalSvg->documentElement->getAttribute('viewBox');
140  $originalSvgWidth = $originalSvg->documentElement->getAttribute('width');
141  $originalSvgHeight = $originalSvg->documentElement->getAttribute('height');
142 
143  // width/height can easily be used if they are numeric. Else, viewBox attribute dimensions
144  // are evaluated. These are used as better fallback here, overridden if width/height exist.
145  if ($originalSvgViewBox !== '') {
146  $viewBoxParts = explode(' ', $originalSvgViewBox);
147  if (isset($viewBoxParts[2]) && is_numeric($viewBoxParts[2])) {
148  $width = $viewBoxParts[2];
149  }
150 
151  if (isset($viewBoxParts[3]) && is_numeric($viewBoxParts[3])) {
152  $height = $viewBoxParts[3];
153  }
154  }
155 
156  // width/height may contain percentages or units like "mm", "cm"
157  // When non-numeric, we only use the width/height when no viewBox
158  // exists (because the size of a viewBox would be preferred
159  // to a non-numeric value), and then unify the unit as "1".
160  if ($originalSvgWidth !== '') {
161  if (is_numeric($originalSvgWidth)) {
162  $width = $originalSvgWidth;
163  } elseif ($width === null) {
164  // contains a unit like "cm", "mm", "%", ...
165  // Currently just stripped because without knowing the output
166  // device, no pixel size can be calculated (missing dpi).
167  // So we regard the unit to be "1" - this is how TYPO3
168  // already did it when SVG file metadata was evaluated (before
169  // cropping).
170  $width = (int)$originalSvgWidth;
171  }
172  }
173 
174  if ($originalSvgHeight !== '') {
175  if (is_numeric($originalSvgHeight)) {
176  $height = $originalSvgHeight;
177  } elseif ($height === null) {
178  $height = (int)$originalSvgHeight;
179  }
180  }
181 
182  return [
183  // The "proper" image dimensions (with viewBox preference)
184  'determined' => [
185  'width' => $width ?? ‪$this->defaultSvgDimension,
186  'height' => $height ?? ‪$this->defaultSvgDimension,
187  ],
188 
189  // Possible original "width/height" attributes (may not correlate with the viewBox, could be empty)
190  'original' => [
191  'width' => $originalSvgWidth,
192  'height' => $originalSvgHeight,
193  ],
194  ];
195  }
196 }
‪TYPO3\CMS\Core\Imaging\ImageManipulation\Area\getWidth
‪getWidth()
Definition: Area.php:86
‪TYPO3\CMS\Core\Imaging
Definition: Dimension.php:16
‪TYPO3\CMS\Core\Imaging\SvgManipulation\adjustSvgDimensions
‪adjustSvgDimensions(\DOMDocument $originalSvg, array $determinedDimensions)
Definition: SvgManipulation.php:108
‪TYPO3\CMS\Core\Imaging\ImageManipulation\Area
Definition: Area.php:23
‪TYPO3\CMS\Core\Imaging\ImageManipulation\Area\getHeight
‪getHeight()
Definition: Area.php:91
‪TYPO3\CMS\Core\Imaging\SvgManipulation\$defaultSvgDimension
‪int $defaultSvgDimension
Definition: SvgManipulation.php:45
‪TYPO3\CMS\Core\Imaging\SvgManipulation\determineSvgDimensions
‪determineSvgDimensions(\DOMDocument $originalSvg)
Definition: SvgManipulation.php:133
‪TYPO3\CMS\Core\Imaging\SvgManipulation\cropScaleSvgString
‪cropScaleSvgString(string $svgString, Area $cropArea, ImageDimension $imageDimension)
Definition: SvgManipulation.php:50
‪TYPO3\CMS\Core\Imaging\ImageManipulation\Area\getOffsetTop
‪getOffsetTop()
Definition: Area.php:101
‪TYPO3\CMS\Core\Imaging\ImageManipulation\Area\getOffsetLeft
‪getOffsetLeft()
Definition: Area.php:96
‪TYPO3\CMS\Core\Imaging\SvgManipulation
Definition: SvgManipulation.php:44
‪TYPO3\CMS\Core\Imaging\ImageDimension
Definition: ImageDimension.php:28