TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Imaging\Svg;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Imaging\ImageDimension;
use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
/**
* Stateless operations on {@see SvgDocument} instances: dimension
* resolution, serialization and crop-scaling.
*
* The document to operate on is always passed in as the first argument,
* no instance state is kept. Obtain documents via {@see SvgDocumentFactory}.
*
* @internal not part of TYPO3 Core API.
*/
#[Autoconfigure(public: true)]
final readonly class SvgDocumentService
{
private const int DEFAULT_DIMENSION = 64;
/**
* Resolve pixel dimensions of the SVG.
*
* Preference order:
* 1. `viewBox` attribute (width = index 2, height = index 3)
* 2. numeric `width` / `height` attributes (override viewBox values)
* 3. non-numeric `width` / `height` (e.g. "100mm", "50%") only when
* no viewBox-derived value is present, stripped of their unit
* 4. 64x64 fallback when nothing usable is found
*/
public function getDimensions(SvgDocument $document): ImageDimension
{
$root = $document->documentElement;
$viewBox = $root->getAttribute('viewBox');
$widthAttr = $root->getAttribute('width');
$heightAttr = $root->getAttribute('height');
$width = null;
$height = null;
if ($viewBox !== '') {
// SVG spec allows whitespace or comma separators.
$parts = preg_split('/[\s,]+/', trim($viewBox)) ?: [];
if (isset($parts[2]) && is_numeric($parts[2])) {
$width = (int)(float)$parts[2];
}
if (isset($parts[3]) && is_numeric($parts[3])) {
$height = (int)(float)$parts[3];
}
}
if ($widthAttr !== '') {
if (is_numeric($widthAttr)) {
$width = (int)(float)$widthAttr;
} elseif ($width === null) {
// Unit like "mm", "cm", "%" - stripped because without an
// output device (dpi) we cannot translate to pixels.
$width = (int)$widthAttr;
}
}
if ($heightAttr !== '') {
if (is_numeric($heightAttr)) {
$height = (int)(float)$heightAttr;
} elseif ($height === null) {
$height = (int)$heightAttr;
}
}
return new ImageDimension(
max(0, $width ?? self::DEFAULT_DIMENSION),
max(0, $height ?? self::DEFAULT_DIMENSION),
);
}
/**
* Serialize the document as plain XML markup of its root element,
* without an `<?xml ?>` prolog.
*/
public function toXml(SvgDocument $document): string
{
return (string)$document->saveXML($document->documentElement);
}
/**
* Serialize the document as inline HTML5-ready markup.
*
* Emits only the root `<svg>` element (no `<?xml ?>` prolog) and
* applies the clean-ups needed when inlining SVG into an HTML
* document:
*
* - drops `xmlns="http://www.w3.org/2000/svg"` (HTML5 auto-places
* `<svg>` in the SVG namespace),
* - drops the legacy `version` attribute,
* - synthesizes a `viewBox` from `width`/`height` when missing so
* the SVG scales with CSS instead of rendering at intrinsic
* pixel size.
*
* Operates on a detached clone; the passed document is not mutated.
*/
public function toInlineMarkup(SvgDocument $document): string
{
$clone = new \DOMDocument();
$clone->appendChild($clone->importNode($document->documentElement, true));
/** @var \DOMElement $root */
$root = $clone->documentElement;
$root->removeAttributeNS('http://www.w3.org/2000/svg', '');
if ($root->hasAttribute('version')) {
$root->removeAttribute('version');
}
if (!$root->hasAttribute('viewBox')
&& $root->hasAttribute('width')
&& $root->hasAttribute('height')
) {
$root->setAttribute(
'viewBox',
sprintf('0 0 %d %d', (int)$root->getAttribute('width'), (int)$root->getAttribute('height')),
);
}
return (string)$clone->saveXML($root);
}
/**
* Wrap the source SVG in an outer `<svg>` that carries the crop viewBox
* and target dimensions. The passed document is not mutated, a new
* {@see SvgDocument} is returned.
*/
public function cropScale(SvgDocument $document, Area $cropArea, ImageDimension $targetDimension): SvgDocument
{
$offsetLeft = (int)$cropArea->getOffsetLeft();
$offsetTop = (int)$cropArea->getOffsetTop();
// Rounding matches ImageDimension's width/height calculation.
$newWidth = (int)round($cropArea->getWidth());
$newHeight = (int)round($cropArea->getHeight());
$sourceRoot = $document->documentElement;
$intrinsic = $this->getDimensions($document);
$wrapper = new SvgDocument('1.0');
$wrapper->preserveWhiteSpace = true;
$wrapper->formatOutput = true;
// Deep-copy the source tree into the wrapper document so we never
// mutate the passed document.
/** @var \DOMElement $innerSvg */
$innerSvg = $wrapper->importNode($sourceRoot, true);
// Ensure the inner <svg> carries width/height; without them the
// crop cannot render correctly when the file is embedded via <img>.
if ($sourceRoot->getAttribute('width') === '') {
$innerSvg->setAttribute('width', (string)$intrinsic->getWidth());
$innerSvg->setAttribute('data-manipulated-width', 'true');
}
if ($sourceRoot->getAttribute('height') === '') {
$innerSvg->setAttribute('height', (string)$intrinsic->getHeight());
$innerSvg->setAttribute('data-manipulated-height', 'true');
}
$outerSvg = $wrapper->createElement('svg');
$outerSvg->setAttribute('xmlns', 'http://www.w3.org/2000/svg');
$outerSvg->setAttribute('viewBox', $offsetLeft . ' ' . $offsetTop . ' ' . $newWidth . ' ' . $newHeight);
$outerSvg->setAttribute('width', (string)$targetDimension->getWidth());
$outerSvg->setAttribute('height', (string)$targetDimension->getHeight());
// Propagate preserveAspectRatio from the inner root onto the wrapper.
$preserveAspectRatio = $sourceRoot->getAttribute('preserveAspectRatio');
if ($preserveAspectRatio !== '') {
$outerSvg->setAttribute('preserveAspectRatio', $preserveAspectRatio);
}
$outerSvg->appendChild($innerSvg);
$wrapper->appendChild($outerSvg);
return $wrapper;
}
}