TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user