<?php

declare(strict_types=1);

/**
 * SVG.php
 *
 * @since     2002-08-03
 * @category  Library
 * @package   Pdf
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD
 * @license   https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
 * @link      https://github.com/tecnickcom/tc-lib-pdf
 *
 * This file is part of tc-lib-pdf software library.
 */

namespace Com\Tecnick\Pdf;

use Com\Tecnick\Pdf\Exception as PdfException;
use TSVGAttribs;
use TSVGStyle;

/**
 * Com\Tecnick\Pdf\SVG
 *
 * SVG PDF class
 *
 * @since     2002-08-03
 * @category  Library
 * @package   Pdf
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD
 * @license   https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
 * @link      https://github.com/tecnickcom/tc-lib-pdf
 *
 * @phpstan-import-type TTMatrix from \Com\Tecnick\Pdf\Graph\Base
 * @phpstan-import-type TRefUnitValues from \Com\Tecnick\Pdf\Base
 *
 * @phpstan-type TSVGSize array{
 *    'x': float,
 *    'y': float,
 *    'width': float,
 *    'height': float,
 *    'viewBox': array{
 *       float,
 *       float,
 *       float,
 *       float
 *    },
 *    'ar_align': string,
 *    'ar_ms': string,
 * }
 *
 * @phpstan-type TSVGCoord array{
 *    'x': float,
 *    'y': float,
 *    'x0': float,
 *    'y0': float,
 *    'xmin': float,
 *    'xmax': float,
 *    'ymin': float,
 *    'ymax': float,
 *    'xinit': float,
 *    'yinit': float,
 *    'xoffset': float,
 *    'yoffset': float,
 *    'relcoord': bool,
 *    'firstcmd': bool,
 *    'cpx': float,
 *    'cpy': float,
 *    'cptype': string,
 * }
 *
 * @phpstan-type TSVGGradient array{
 *    'xref': string,
 *    'type': int,
 *    'gradientUnits': string,
 *    'mode': string,
 *    'coords': array<float>,
 *    'stops': array<int, array{
 *            'color': string,
 *            'exponent'?: float,
 *            'opacity'?: float,
 *            'offset'?: float,
 *        }>,
 *    'gradientTransform': array<float>,
 * }
 *
 * @phpstan-type TSVGStyle array{
 *    'alignment-baseline': string,
 *    'baseline-shift': string,
 *    'clip': string,
 *    'clip-path': string,
 *    'clip-rule': string,
 *    'color': string,
 *    'color-interpolation': string,
 *    'color-interpolation-filters': string,
 *    'color-profile': string,
 *    'color-rendering': string,
 *    'cursor': string,
 *    'direction': string,
 *    'display': string,
 *    'dominant-baseline': string,
 *    'enable-background': string,
 *    'fill': string,
 *    'fill-opacity': float,
 *    'fill-rule': string,
 *    'filter': string,
 *    'flood-color': string,
 *    'flood-opacity': float,
 *    'font': string,
 *    'font-family': string,
 *    'font-size': string,
 *    'font-size-val': float,
 *    'font-size-adjust': string,
 *    'font-stretch': string,
 *    'font-stretch-val': float,
 *    'font-style': string,
 *    'font-variant': string,
 *    'font-weight': string,
 *    'font-mode': string,
 *    'glyph-orientation-horizontal': string,
 *    'glyph-orientation-vertical': string,
 *    'image-rendering': string,
 *    'kerning': string,
 *    'letter-spacing': string,
 *    'letter-spacing-val': float,
 *    'lighting-color': string,
 *    'marker': string,
 *    'marker-end': string,
 *    'marker-mid': string,
 *    'marker-start': string,
 *    'mask': string,
 *    'mix-blend-mode': string,
 *    'objstyle': string,
 *    'opacity': float,
 *    'overflow': string,
 *    'pointer-events': string,
 *    'shape-rendering': string,
 *    'stop-color': string,
 *    'stop-opacity': float,
 *    'stroke': string,
 *    'stroke-dasharray': string,
 *    'stroke-dashoffset': float,
 *    'stroke-linecap': string,
 *    'stroke-linejoin': string,
 *    'stroke-miterlimit': float,
 *    'stroke-opacity': float,
 *    'stroke-width': float,
 *    'text-anchor': string,
 *    'text-decoration': string,
 *    'text-rendering': string,
 *    'unicode-bidi': string,
 *    'visibility': string,
 *    'word-spacing': string,
 *    'writing-mode': string,
 *    'text-color': string,
 *    'transfmatrix': TTMatrix,
 *  }
 *
 * @phpstan-type TSVGTextMode array{
 *    'rtl': bool,
 *    'invisible': bool,
 *    'stroke': int,
 *    'text-anchor': string,
 *    'spacepreserve'?: bool,
 *    'textstarted'?: bool,
 *    'vertical'?: bool,
 *    'linkhref'?: string,
 *    'linkx'?: float,
 *    'linky'?: float,
 *    'baseline'?: string,
 *    'rotate'?: float,
 *    'textlength'?: float,
 *    'lengthadjust'?: string,
 *    'xlist'?: array<int, float>,
 *    'ylist'?: array<int, float>,
 *    'rotlist'?: array<int, float>,
 *    'textpathpoints'?: array<int, array{
 *       0: float,
 *       1: float
 *    }>,
 *    'textpathoffset'?: float,
 *    'textpathmethod'?: string,
 *    'textpathspacing'?: string,
 * }
 *
 * @phpstan-type TSVGAttributes array{
 *    'accumulate'?: string,
 *    'additive'?: string,
 *    'alignment-baseline'?: string,
 *    'amplitude'?: string,
 *    'attributeName'?: string,
 *    'attributeType'?: string,
 *    'azimuth'?: string,
 *    'baseFrequency'?: string,
 *    'baseProfile'?: string,
 *    'baseline-shift'?: string,
 *    'begin'?: string,
 *    'bias'?: string,
 *    'by'?: string,
 *    'calcMode'?: string,
 *    'class'?: string,
 *    'clip'?: string,
 *    'clip-path'?: string,
 *    'clip-rule'?: string,
 *    'clipPathUnits'?: string,
 *    'closing_tag'?: bool,
 *    'color'?: string,
 *    'color-interpolation'?: string,
 *    'color-interpolation-filters'?: string,
 *    'content'?: string,
 *    'crossorigin'?: string,
 *    'cursor'?: string,
 *    'cx'?: string,
 *    'cy'?: string,
 *    'd'?: string,
 *    'data-*'?: string,
 *    'decoding'?: string,
 *    'diffuseConstant'?: string,
 *    'direction'?: string,
 *    'display'?: string,
 *    'divisor'?: string,
 *    'dominant-baseline'?: string,
 *    'dur'?: string,
 *    'dx'?: string,
 *    'dy'?: string,
 *    'edgeMode'?: string,
 *    'elevation'?: string,
 *    'end'?: string,
 *    'exponent'?: string,
 *    'fill'?: string,
 *    'fill-opacity'?: string,
 *    'fill-rule'?: string,
 *    'filter'?: string,
 *    'filterUnits'?: string,
 *    'flood-color'?: string,
 *    'flood-opacity'?: string,
 *    'font-family'?: string,
 *    'font-size'?: string,
 *    'font-size-adjust'?: string,
 *    'font-stretch'?: string,
 *    'font-style'?: string,
 *    'font-variant'?: string,
 *    'font-weight'?: string,
 *    'fr'?: string,
 *    'from'?: string,
 *    'fx'?: string,
 *    'fy'?: string,
 *    'glyph-orientation-horizontal'?: string,
 *    'glyph-orientation-vertical'?: string,
 *    'gradientTransform'?: string,
 *    'gradientUnits'?: string,
 *    'height'?: string,
 *    'href'?: string,
 *    'hreflang'?: string,
 *    'id'?: string,
 *    'image-rendering'?: string,
 *    'in'?: string,
 *    'in2'?: string,
 *    'intercept'?: string,
 *    'k1'?: string,
 *    'k2'?: string,
 *    'k3'?: string,
 *    'k4'?: string,
 *    'kernelMatrix'?: string,
 *    'kernelUnitLength'?: string,
 *    'keyPoints'?: string,
 *    'keySplines'?: string,
 *    'keyTimes'?: string,
 *    'lang'?: string,
 *    'lengthAdjust'?: string,
 *    'letter-spacing'?: string,
 *    'lighting-color'?: string,
 *    'limitingConeAngle'?: string,
 *    'local'?: string,
 *    'marker-end'?: string,
 *    'marker-mid'?: string,
 *    'marker-start'?: string,
 *    'markerHeight'?: string,
 *    'markerUnits'?: string,
 *    'markerWidth'?: string,
 *    'mask'?: string,
 *    'maskContentUnits'?: string,
 *    'maskUnits'?: string,
 *    'max'?: string,
 *    'media'?: string,
 *    'method'?: string,
 *    'min'?: string,
 *    'mode'?: string,
 *    'numOctaves'?: string,
 *    'offset'?: string,
 *    'opacity'?: string,
 *    'operator'?: string,
 *    'order'?: string,
 *    'orient'?: string,
 *    'origin'?: string,
 *    'overflow'?: string,
 *    'paint-order'?: string,
 *    'path'?: string,
 *    'pathLength'?: string,
 *    'patternContentUnits'?: string,
 *    'patternTransform'?: string,
 *    'patternUnits'?: string,
 *    'ping'?: string,
 *    'pointer-events'?: string,
 *    'points'?: string,
 *    'pointsAtX'?: string,
 *    'pointsAtY'?: string,
 *    'pointsAtZ'?: string,
 *    'preserveAlpha'?: string,
 *    'preserveAspectRatio'?: string,
 *    'primitiveUnits'?: string,
 *    'r'?: string,
 *    'radius'?: string,
 *    'refX'?: string,
 *    'refY'?: string,
 *    'referrerPolicy'?: string,
 *    'rel'?: string,
 *    'rendering-intent'?: string,
 *    'repeatCount'?: string,
 *    'repeatDur'?: string,
 *    'requiredExtensions'?: string,
 *    'requiredFeatures'?: string,
 *    'restart'?: string,
 *    'result'?: string,
 *    'rotate'?: string,
 *    'rx'?: string,
 *    'ry'?: string,
 *    'scale'?: string,
 *    'seed'?: string,
 *    'shape-rendering'?: string,
 *    'side'?: string,
 *    'slope'?: string,
 *    'spacing'?: string,
 *    'specularConstant'?: string,
 *    'specularExponent'?: string,
 *    'speed'?: string,
 *    'spreadMethod'?: string,
 *    'startOffset'?: string,
 *    'stdDeviation'?: string,
 *    'stitchTiles'?: string,
 *    'stop-color'?: string,
 *    'stop-opacity'?: string,
 *    'stroke'?: string,
 *    'stroke-dasharray'?: string,
 *    'stroke-dashoffset'?: string,
 *    'stroke-linecap'?: string,
 *    'stroke-linejoin'?: string,
 *    'stroke-miterlimit'?: string,
 *    'stroke-opacity'?: string,
 *    'stroke-width'?: string,
 *    'style'?: string,
 *    'surfaceScale'?: string,
 *    'systemLanguage'?: string,
 *    'tabindex'?: string,
 *    'tableValues'?: string,
 *    'target'?: string,
 *    'targetX'?: string,
 *    'targetY'?: string,
 *    'text-anchor'?: string,
 *    'text-decoration'?: string,
 *    'text-rendering'?: string,
 *    'textLength'?: string,
 *    'to'?: string,
 *    'transform'?: string,
 *    'transform-origin'?: string,
 *    'type'?: string,
 *    'unicode-bidi'?: string,
 *    'values'?: string,
 *    'vector-effect'?: string,
 *    'version'?: string,
 *    'viewBox'?: string,
 *    'visibility'?: string,
 *    'width'?: string,
 *    'word-spacing'?: string,
 *    'writing-mode'?: string,
 *    'x'?: string,
 *    'x1'?: string,
 *    'x2'?: string,
 *    'xChannelSelector'?: string,
 *    'xlink:actuate'?: string,
 *    'xlink:arcrole'?: string,
 *    'xlink:href Deprecated'?: string,
 *    'xlink:href'?: string,
 *    'xlink:role'?: string,
 *    'xlink:show'?: string,
 *    'xlink:title'?: string,
 *    'xlink:type'?: string,
 *    'xml:lang'?: string,
 *    'xml:space'?: string,
 *    'y'?: string,
 *    'y1'?: string,
 *    'y2'?: string,
 *    'yChannelSelector'?: string,
 *    'z'?: string,
 *    'zoomAndPan'?: string,
 * }
 *
 * @phpstan-type TSVGAttribChild array{
 *    'name': string,
 *    'attr': TSVGAttributes,
 * }
 *
 * @phpstan-type TSVGAttribs array{
 *    'name': string,
 *    'attr': TSVGAttributes,
 *    'tm'?: TTMatrix,
 *    'child'?: array<string, TSVGAttribChild>,
 * }
 *
 * @phpstan-type TSVGSwitchState array{
 *    'depth': int,
 *    'selected': bool,
 *    'skipdepth': int,
 * }
 *
 * @phpstan-type TSVGObj array{
 *    'defsmode': bool,
 *    'clipmode': bool,
 *    'clipid': int|string,
 *    'tagdepth': int,
 *    'x0': float,
 *    'y0': float,
 *    'x': float,
 *    'y': float,
 *    'refunitval': TRefUnitValues,
 *    'gradientid': string,
 *    'gradients': array<string, TSVGGradient>,
 *    'clippaths': array<string, TSVGAttribs>,
 *    'defs': array<string, TSVGAttribs>,
 *    'cliptm': TTMatrix,
 *    'styles': array<int, TSVGStyle>,
 *    'child': array<int>,
 *    'xmldepth': int,
 *    'switchstack'?: array<int, TSVGSwitchState>,
 *    'usechain': array<string, bool>,
 *    'markermode': int,
 *    'patternmode': int,
 *    'textmode': TSVGTextMode,
 *    'charskip': int,
 *    'textdepth': int,
 *    'text': string,
 *    'markupresources': bool,
 *    'dir': string,
 *    'out': string,
 * }
 *
 * @SuppressWarnings("PHPMD.DepthOfInheritance")
 */
abstract class SVG extends \Com\Tecnick\Pdf\Text
{
    /**
     * Create a link annotation in the specified rectangular area.
     *
     * Implemented by descendants that provide annotation/link support.
     *
     * @param float $posx Abscissa of upper-left corner.
     * @param float $posy Ordinate of upper-left corner.
     * @param float $width Link width.
     * @param float $height Link height.
     * @param string $link External URL or internal link identifier.
     *
     * @return int Annotation object ID.
     */
    abstract public function setLink(float $posx, float $posy, float $width, float $height, string $link): int;

    /**
     * Tags whose character data must not be rendered as drawing text.
     *
     * @var array<int, string>
     */
    protected const SVGCHARDATASKIPTAGS = [
        'desc',
        'title',
        'metadata',
        'style',
        'script',
        // SVG filter primitives: the whole subtree content is discarded.
        'filter',
        'feBlend',
        'feColorMatrix',
        'feComponentTransfer',
        'feComposite',
        'feConvolveMatrix',
        'feDiffuseLighting',
        'feDisplacementMap',
        'feDistantLight',
        'feDropShadow',
        'feFlood',
        'feFuncA',
        'feFuncB',
        'feFuncG',
        'feFuncR',
        'feGaussianBlur',
        'feImage',
        'feMerge',
        'feMergeNode',
        'feMorphology',
        'feOffset',
        'fePointLight',
        'feSpecularLighting',
        'feSpotLight',
        'feTile',
        'feTurbulence',
    ];

    /**
     * Tags whose character data is rendered as text.
     *
     * @var array<int, string>
     */
    protected const SVGTEXTCONTENTTAGS = [
        'text',
        'tspan',
        'textPath',
        'tref',
        'altGlyph',
    ];

    /**
     * Default unit of measure for SVG (px = pixels).
     *
     * @var string
     */
    protected const SVGUNIT = 'px';

    /**
     * Pattern matching a single number in SVG path or point data.
     *
     * Covers the optional sign, the forms with no leading digit ('.5', '-.5')
     * and no trailing digit ('5.'), and scientific notation ('1e-3').
     *
     * @var string
     */
    protected const SVGNUMPATTERN = '[-+]?(?:[0-9]*\.[0-9]+|[0-9]+\.?)(?:[eE][-+]?[0-9]+)?';

    /**
     * Regular expression matching a single number in SVG path or point data.
     *
     * @var string
     */
    protected const SVGNUMREGEX = '/' . self::SVGNUMPATTERN . '/';

    /**
     * Regular expression matching one parameter set of a path 'A' command:
     * rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y.
     *
     * The two flags are single digits, so they may be packed with the values
     * around them ('0120 0' = flags 0 and 1 followed by 20 and 0).
     *
     * @var string
     */
    protected const SVGARCREGEX =
        '/('
            . self::SVGNUMPATTERN
            . ')[\s,]*'
            . '('
            . self::SVGNUMPATTERN
            . ')[\s,]*'
            . '('
            . self::SVGNUMPATTERN
            . ')[\s,]*'
            . '([01])[\s,]*'
            . '([01])[\s,]*'
            . '('
            . self::SVGNUMPATTERN
            . ')[\s,]*'
            . '('
            . self::SVGNUMPATTERN
            . ')/';

    /**
     * Default SVG minimum length in points.
     *
     * @var float
     */
    protected const SVGMINPNTLEN = 0.01;

    /**
     * Default SVG minimum float diff.
     *
     * @var float
     */
    protected const SVGMINFLOATDIFF = 0.000_01;

    /**
     * Default SVG maximum value for float.
     *
     * @var float
     */
    protected const SVGMAXVAL = 2_147_483_647.0;

    /**
     * Maximum depth of a <use> resolution chain.
     *
     * Bounds the recursion in parseSVGTagSTARTuse().
     *
     * @var int
     */
    protected const SVGMAXUSEDEPTH = 64;

    /**
     * Identity Transofrmation matrix.
     *
     * @var TTMatrix
     */
    protected const TMXID = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];

    /**
     * Array of inheritable SVG properties.
     *
     * @var array<string>
     */
    protected const SVGINHPROP = [
        'clip-rule',
        'color',
        'color-interpolation',
        'color-interpolation-filters',
        'color-profile',
        'color-rendering',
        'cursor',
        'direction',
        'display',
        'fill',
        'fill-opacity',
        'fill-rule',
        'font',
        'font-family',
        'font-size',
        'font-size-adjust',
        'font-stretch',
        'font-style',
        'font-variant',
        'font-weight',
        'glyph-orientation-horizontal',
        'glyph-orientation-vertical',
        'image-rendering',
        'kerning',
        'letter-spacing',
        'marker',
        'marker-end',
        'marker-mid',
        'marker-start',
        'mix-blend-mode',
        'pointer-events',
        'shape-rendering',
        'stroke',
        'stroke-dasharray',
        'stroke-dashoffset',
        'stroke-linecap',
        'stroke-linejoin',
        'stroke-miterlimit',
        'stroke-opacity',
        'stroke-width',
        'text-anchor',
        'text-rendering',
        'visibility',
        'word-spacing',
        'writing-mode',
    ];

    /**
     * Default SVG style.
     *
     * @var TSVGStyle
     */
    protected const DEFSVGSTYLE = [
        'alignment-baseline' => 'auto',
        'baseline-shift' => 'baseline',
        'clip' => 'auto',
        'clip-path' => 'none',
        'clip-rule' => 'nonzero',
        'color' => 'black',
        'color-interpolation' => 'sRGB',
        'color-interpolation-filters' => 'linearRGB',
        'color-profile' => 'auto',
        'color-rendering' => 'auto',
        'cursor' => 'auto',
        'direction' => 'ltr',
        'display' => 'inline',
        'dominant-baseline' => 'auto',
        'enable-background' => 'accumulate',
        'fill' => 'black',
        'fill-opacity' => 1.0,
        'fill-rule' => 'nonzero',
        'filter' => 'none',
        'flood-color' => 'black',
        'flood-opacity' => 1.0,
        'font' => '',
        'font-family' => 'helvetica',
        'font-size' => 'medium',
        'font-size-val' => 10.0,
        'font-size-adjust' => 'none',
        'font-stretch' => 'normal',
        'font-stretch-val' => 100.0,
        'font-style' => 'normal',
        'font-variant' => 'normal',
        'font-weight' => 'normal',
        'font-mode' => '',
        'glyph-orientation-horizontal' => '0deg',
        'glyph-orientation-vertical' => 'auto',
        'image-rendering' => 'auto',
        'kerning' => 'auto',
        'letter-spacing' => 'normal',
        'letter-spacing-val' => 0.0,
        'lighting-color' => 'white',
        'marker' => '',
        'marker-end' => 'none',
        'marker-mid' => 'none',
        'marker-start' => 'none',
        'mask' => 'none',
        'mix-blend-mode' => 'normal',
        'objstyle' => '',
        'opacity' => 1.0,
        'overflow' => 'auto',
        'pointer-events' => 'visiblePainted',
        'shape-rendering' => 'auto',
        'stop-color' => 'black',
        'stop-opacity' => 1.0,
        'stroke' => 'none',
        'stroke-dasharray' => 'none',
        'stroke-dashoffset' => 0.0,
        'stroke-linecap' => 'butt',
        'stroke-linejoin' => 'miter',
        'stroke-miterlimit' => 4.0,
        'stroke-opacity' => 1.0,
        'stroke-width' => 1.0,
        'text-anchor' => 'start',
        'text-decoration' => 'none',
        'text-rendering' => 'auto',
        'unicode-bidi' => 'normal',
        'visibility' => 'visible',
        'word-spacing' => 'normal',
        'writing-mode' => 'lr-tb',
        'text-color' => 'black',
        'transfmatrix' => self::TMXID,
    ];

    /**
     * List of possible SVG font attributes to parse.
     *
     * @var array<string>
     *
     */
    protected const FONTATTRIBS = [
        'font-family',
        'font-size-adjust',
        'font-size',
        'font-stretch',
        'font-style',
        'font-variant',
        'font-weight',
        'letter-spacing',
        'text-decoration',
    ];

    /**
     * Modes to check when processing SVG start tags.
     *
     * @var array<int, string>
     */
    protected const SVGDEFSMODESTART = [
        'clipPath',
        'linearGradient',
        'radialGradient',
        'stop',
        'symbol',
        'marker',
        'pattern',
        'mask',
        'filter',
    ];

    /**
     * Modes to check when processing SVG end tags.
     *
     * @var array<int, string>
     */
    protected const SVGDEFSMODEEND = [
        'defs',
        'clipPath',
        'linearGradient',
        'radialGradient',
        'stop',
        'symbol',
        'marker',
        'pattern',
        'mask',
        'filter',
    ];

    /**
     * Default SVG object properties.
     *
     * @var TSVGObj
     */
    protected const SVGDEFOBJ = [
        'defsmode' => false,
        'clipmode' => false,
        'clipid' => '',
        'tagdepth' => 0,
        'x0' => 0.0,
        'y0' => 0.0,
        'x' => 0.0,
        'y' => 0.0,
        'refunitval' => self::REFUNITVAL,
        'gradientid' => '',
        'gradients' => [],
        'clippaths' => [],
        'cliptm' => self::TMXID,
        'defs' => [],
        'styles' => [0 => self::DEFSVGSTYLE],
        'child' => [],
        'xmldepth' => 0,
        'switchstack' => [],
        'usechain' => [],
        'markermode' => 0,
        'patternmode' => 0,
        'textmode' => [
            'rtl' => false,
            'invisible' => false,
            'stroke' => 0,
            'text-anchor' => 'start',
            'spacepreserve' => false,
            'textstarted' => false,
            'vertical' => false,
            'linkhref' => '',
            'linkx' => 0.0,
            'linky' => 0.0,
        ],
        'charskip' => 0,
        'textdepth' => 0,
        'text' => '',
        'markupresources' => false,
        'dir' => '',
        'out' => '',
    ];

    /**
     * Map SVG blend mode names to PDF names.
     *
     * @var array<string, string>
     */
    protected const SVGBLENDMODE = [
        'color-dodge' => 'ColorDodge',
        'color-burn' => 'ColorBurn',
        'hard-light' => 'HardLight',
        'soft-light' => 'SoftLight',
        'normal' => 'Normal',
        'multiply' => 'Multiply',
        'screen' => 'Screen',
        'overlay' => 'Overlay',
        'darken' => 'Darken',
        'lighten' => 'Lighten',
        'difference' => 'Difference',
        'exclusion' => 'Exclusion',
        'hue' => 'Hue',
        'saturation' => 'Saturation',
        'color' => 'Color',
        'luminosity' => 'Luminosity',
    ];

    /**
     * SVG gradient attributes.
     *
     * @var array<string>
     */
    protected const SVGGRADIENTATTRIB = [
        'id',
        'x1',
        'y1',
        'x2',
        'y2',
        'cx',
        'cy',
        'fx',
        'fy',
        'r',
        'offset',
        'gradientUnits',
        'gradientTransform',
        'xlink:href',
        'href',
        'stop-color',
        'stop-opacity',
        'style',
    ];

    /**
     * SVG object properties.
     *
     * @var array<int, TSVGObj>
     */
    protected array $svgobjs = [];

    /**
     * Convert value from SVG units to internal points.
     *
     * @param string|float|int $val Value to convert in user units.
     * @param int $soid SVG object ID.
     * @param ?TRefUnitValues $ref overrides the svg reference unit values.
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function svgUnitToPoints(string|float|int $val, int $soid = -1, ?array $ref = null): float
    {
        if ($ref === null) {
            if ($soid > 0 && isset($this->svgobjs[$soid]['refunitval']) && $this->svgobjs[$soid]['refunitval'] !== []) {
                $ref = $this->svgobjs[$soid]['refunitval'];
            } else {
                $ref = self::REFUNITVAL;
            }
        }
        return $this->getUnitValuePoints($val, $ref, self::SVGUNIT);
    }

    /**
     * Convert value from SVG units to user units.
     *
     * @param string|float|int $val Value to convert in user units.
     * @param int $soid SVG object ID.
     * @param ?TRefUnitValues $ref overrides the svg reference unit values.
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function svgUnitToUnit(string|float|int $val, int $soid = -1, ?array $ref = null): float
    {
        return $this->toUnit($this->svgUnitToPoints($val, $soid, $ref));
    }

    /**
     * Convert a length from user units back to SVG user units.
     *
     * Inverse of svgUnitToUnit(): used where a value already converted to user
     * units has to be fed back into an SVG coordinate system.
     *
     * @param float $val Length in user units.
     * @param int $soid SVG object ID.
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function unitToSVGUnit(float $val, int $soid = -1): float
    {
        $ratio = $this->svgUnitToUnit(1.0, $soid);
        return $ratio > 0.0 ? $val / $ratio : $val;
    }

    /**
     * Parse the SVG transformation 'matrix'.
     *
     * @param string $val Transformation matrix string to parse.
     *
     * @return TTMatrix Transformation matrix.
     */
    protected function parseSVGTMmatrix(string $val): array
    {
        $tmb = $this->graph::IDMATRIX;
        $regs = [];
        if (\preg_match(
            '/([a-z0-9\-\.]+)[\,\s]+'
            . '([a-z0-9\-\.]+)[\,\s]+'
            . '([a-z0-9\-\.]+)[\,\s]+'
            . '([a-z0-9\-\.]+)[\,\s]+'
            . '([a-z0-9\-\.]+)[\,\s]+'
            . '([a-z0-9\-\.]+)/si',
            $val,
            $regs,
        )) {
            $tmb[0] = \floatval($regs[1] ?? 0);
            $tmb[1] = \floatval($regs[2] ?? 0);
            $tmb[2] = \floatval($regs[3] ?? 0);
            $tmb[3] = \floatval($regs[4] ?? 0);
            $tmb[4] = \floatval($regs[5] ?? 0);
            $tmb[5] = \floatval($regs[6] ?? 0);
        }
        return $tmb;
    }

    /**
     * Parse the SVG transformation 'translate'.
     *
     * @param string $val Transformation matrix string to parse.
     *
     * @return TTMatrix Transformation matrix.
     */
    protected function parseSVGTMtranslate(string $val): array
    {
        $tmb = $this->graph::IDMATRIX;
        $regs = [];
        if (\preg_match('/([a-z0-9\-\.]+)[\,\s]+([a-z0-9\-\.]+)/si', $val, $regs)) {
            $tmb[4] = \floatval($regs[1] ?? 0);
            $tmb[5] = \floatval($regs[2] ?? 0);
            return $tmb;
        }
        if (\preg_match('/([a-z0-9\-\.]+)/si', $val, $regs)) {
            $tmb[4] = \floatval($regs[1] ?? 0);
        }
        return $tmb;
    }

    /**
     * Parse the SVG transformation 'scale'.
     *
     * @param string $val Transformation matrix string to parse.
     *
     * @return TTMatrix Transformation matrix.
     */
    protected function parseSVGTMscale(string $val): array
    {
        $tmb = $this->graph::IDMATRIX;
        $regs = [];
        if (\preg_match('/([a-z0-9\-\.]+)[\,\s]+([a-z0-9\-\.]+)/si', $val, $regs)) {
            $tmb[0] = \floatval($regs[1] ?? 0);
            $tmb[3] = \floatval($regs[2] ?? 0);
            return $tmb;
        }
        if (\preg_match('/([a-z0-9\-\.]+)/si', $val, $regs)) {
            $tmb[0] = \floatval($regs[1] ?? 0);
            $tmb[3] = $tmb[0];
        }
        return $tmb;
    }

    /**
     * Parse the SVG transformation 'rotate'.
     *
     * @param string $val Transformation matrix string to parse.
     *
     * @return TTMatrix Transformation matrix.
     */
    protected function parseSVGTMrotate(string $val): array
    {
        $tmb = $this->graph::IDMATRIX;
        $regs = [];
        if (\preg_match('/([0-9\-\.]+)[\,\s]+([a-z0-9\-\.]+)[\,\s]+([a-z0-9\-\.]+)/si', $val, $regs)) {
            $ang = \deg2rad(\floatval($regs[1] ?? 0));
            $trx = \floatval($regs[2] ?? 0);
            $try = \floatval($regs[3] ?? 0);
            $tmb[0] = \cos($ang);
            $tmb[1] = \sin($ang);
            $tmb[2] = -$tmb[1];
            $tmb[3] = $tmb[0];
            $tm0 = $tmb[0];
            $tm1 = $tmb[1];
            $tm2 = $tmb[2];
            $tm3 = $tmb[3];
            $tmb[4] = ($trx * (1 - $tm0)) - ($try * $tm2);
            $tmb[5] = ($try * (1 - $tm3)) - ($trx * $tm1);
            return $tmb;
        }
        if (\preg_match('/([0-9\-\.]+)/si', $val, $regs)) {
            $ang = \deg2rad(\floatval($regs[1] ?? 0));
            $tmb[0] = \cos($ang);
            $tmb[1] = \sin($ang);
            $tmb[2] = -$tmb[1];
            $tmb[3] = $tmb[0];
            $tmb[4] = 0.0;
            $tmb[5] = 0.0;
        }
        return $tmb;
    }

    /**
     * Parse the SVG transformation 'skewX'.
     *
     * @param string $val Transformation matrix string to parse.
     *
     * @return TTMatrix Transformation matrix.
     */
    protected function parseSVGTMskewX(string $val): array
    {
        $tmb = $this->graph::IDMATRIX;
        $regs = [];
        if (\preg_match('/([0-9\-\.]+)/si', $val, $regs)) {
            $tmb[2] = \tan(\deg2rad(\floatval($regs[1] ?? 0)));
        }
        return $tmb;
    }

    /**
     * Parse the SVG transformation 'skewY'.
     *
     * @param string $val Transformation matrix string to parse.
     *
     * @return TTMatrix Transformation matrix.
     */
    protected function parseSVGTMskewY(string $val): array
    {
        $tmb = $this->graph::IDMATRIX;
        $regs = [];
        if (\preg_match('/([0-9\-\.]+)/si', $val, $regs)) {
            $tmb[1] = \tan(\deg2rad(\floatval($regs[1] ?? 0)));
        }
        return $tmb;
    }

    /**
     * Get the tranformation matrix from the SVG 'transform' attribute.
     *
     * @param string $attr Transformation attribute.
     *
     * @return TTMatrix Transformation matrix.
     */
    protected function getSVGTransformMatrix(string $attr): array
    {
        $transform = [];
        $tma = $this->graph::IDMATRIX;

        $matchCount = \preg_match_all(
            '/(matrix|translate|scale|rotate|skewX|skewY)[\s]*+\(([^\)]+)\)/si',
            $attr,
            $transform,
            PREG_SET_ORDER,
        );
        if ($matchCount === false || $matchCount === 0) {
            return $tma;
        }

        foreach ($transform as $data) {
            if (!isset($data[2]) || $data[2] === '') {
                continue;
            }

            $val = $data[2];

            $tmb = match ($data[1]) {
                'matrix' => $this->parseSVGTMmatrix($val),
                'translate' => $this->parseSVGTMtranslate($val),
                'scale' => $this->parseSVGTMscale($val),
                'rotate' => $this->parseSVGTMrotate($val),
                'skewX' => $this->parseSVGTMskewX($val),
                'skewY' => $this->parseSVGTMskewY($val),
                default => $this->graph::IDMATRIX,
            };
            $tma = $this->graph->getCtmProduct($tma, $tmb);
        }

        return $tma;
    }

    /**
     * Convert SVG transformation matrix to PDF.
     *
     * @param TTMatrix $trm original SVG transformation matrix.
     * @param int $soid SVG object ID.
     *
     * @return TTMatrix Transformation matrix.
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function convertSVGMatrix(array $trm, int $soid = 0): array
    {
        $ref = $this->svgobjs[$soid]['refunitval'] ?? self::REFUNITVAL;
        $pheight = $ref['page']['height'];
        $tm1 = $trm[1];
        $tm2 = $trm[2];
        $tm3 = $trm[3];
        $tm4 = $trm[4];
        $tm5 = $trm[5];
        $trm[1] = -$tm1;
        $trm[2] = -$tm2;
        $trm[4] = $this->svgUnitToPoints($tm4, $soid) - ($pheight * $trm[2]);
        $trm[5] = ($pheight * (1 - $tm3)) - $this->svgUnitToPoints($tm5, $soid);
        return $trm;
    }

    /**
     * Get the SVG tranformation matrix (CTM) PDF string.
     *
     * @param TTMatrix $trm original SVG transformation matrix.
     * @param int $soid SVG object ID.
     *
     * @return string Transformation matrix (PDF string).
     */
    protected function getOutSVGTransformation(array $trm, int $soid = 0): string
    {
        try {
            return $this->graph->getTransformation($this->convertSVGMatrix($trm, $soid));
        } catch (\Com\Tecnick\Pdf\Exception) {
            return '';
        }
    }

    /**
     * Return the tag name without the namespace
     *
     * @param string $name Tag name
     *
     * @return string Tag name without the namespace
     */
    protected function removeTagNamespace(string $name): string
    {
        $parts = \explode(':', $name);
        return \end($parts);
    }

    /**
     * Extract the raw parameters of an SVG path command.
     *
     * @param string $cmd Path command letter.
     * @param string $data Parameters of the command.
     *
     * @return array<int, string> Raw parameters.
     */
    protected function getSVGPathParams(string $cmd, string $data): array
    {
        $params = [];

        if (\strtoupper($cmd) !== 'A') {
            $nums = [];
            \preg_match_all(self::SVGNUMREGEX, $data, $nums);
            foreach ($nums[0] ?? [] as $num) {
                $params[] = $num;
            }

            return $params;
        }

        // The arc flags are single digits that may be packed with the values
        // around them, so each parameter set is matched as a whole.
        $sets = [];
        \preg_match_all(self::SVGARCREGEX, $data, $sets, PREG_SET_ORDER);
        foreach ($sets as $set) {
            foreach (\array_slice($set, 1, 7) as $prm) {
                $params[] = $prm;
            }
        }

        return $params;
    }

    /**
     * Draw the SVG path.
     *
     * @param int $soid ID of the current SVG object.
     * @param string $attrd
     * @param string $mode
     * @return string
     */
    protected function getSVGPath(int $soid, string $attrd, string $mode = ''): string
    {
        // set paint operator
        $pop = $this->graph->getPathPaintOp($mode, '');
        if ($pop === '') {
            return '';
        }

        // extract paths
        if ($attrd === '') {
            return '';
        }

        $paths = [];
        \preg_match_all('/([ACHLMQSTVZ])[\s]*+([^ACHLMQSTVZ\"]*+)/si', $attrd, $paths, PREG_SET_ORDER);

        // initialize variables
        $out = '';

        $coord = [
            'x' => 0.0,
            'y' => 0.0,
            'x0' => 0.0,
            'y0' => 0.0,
            'xmin' => self::SVGMAXVAL,
            'xmax' => 0.0,
            'ymin' => self::SVGMAXVAL,
            'ymax' => 0.0,
            'xinit' => 0.0,
            'yinit' => 0.0,
            'xoffset' => 0.0,
            'yoffset' => 0.0,
            'relcoord' => false,
            'firstcmd' => true,
            'cpx' => 0.0,
            'cpy' => 0.0,
            'cptype' => '',
        ];

        // draw curve pieces
        foreach ($paths as $key => $val) {
            if (!isset($val[1], $val[2])) {
                continue;
            }
            // get curve type
            $cmd = \trim($val[1]);

            // relative or absolute coordinates
            $coord['relcoord'] = \strtolower($cmd) === $cmd;
            if ($coord['relcoord']) {
                // use relative coordinated instead of absolute
                $coord['xoffset'] = $coord['x'];
                $coord['yoffset'] = $coord['y'];
            } else {
                $coord['xoffset'] = 0.0;
                $coord['yoffset'] = 0.0;
            }

            $params = [];

            // get curve parameters
            $rawparams = $this->getSVGPathParams($cmd, \trim($val[2]));

            foreach ($rawparams as $prk => $prv) {
                try {
                    $params[$prk] = $this->svgUnitToUnit($prv, $soid);
                } catch (\Com\Tecnick\Pdf\Exception) {
                    return '';
                }
                if (\abs($params[$prk]) < $this->svgminunitlen) {
                    // approximate little values to zero
                    $params[$prk] = 0.0;
                }
            }

            // store current origin point
            $coord['x0'] = $coord['x'];
            $coord['y0'] = $coord['y'];

            $ucmd = \strtoupper($cmd);

            if (!\in_array($ucmd, ['C', 'S', 'Q', 'T'], true)) {
                // Only a curve leaves a control point for the smooth commands
                // 'S' and 'T' to mirror.
                $coord['cptype'] = '';
            }

            $out .= match ($ucmd) {
                'A' => $this->svgPathCmdA($params, $coord, $paths, $key, $rawparams),
                'C' => $this->svgPathCmdC($params, $coord),
                'H' => $this->svgPathCmdH($params, $coord),
                'L' => $this->svgPathCmdL($params, $coord),
                'M' => $this->svgPathCmdM($params, $coord),
                'Q' => $this->svgPathCmdQ($params, $coord),
                'S' => $this->svgPathCmdS($params, $coord),
                'T' => $this->svgPathCmdT($params, $coord),
                'V' => $this->svgPathCmdV($params, $coord),
                'Z' => $this->svgPathCmdZ($coord),
                default => '',
            };

            $coord['firstcmd'] = false;
        }

        if ($out === '') {
            return '';
        }

        $this->bbox[] = [
            'x' => $coord['xmin'],
            'y' => $coord['ymin'],
            'w' => $coord['xmax'] - $coord['xmin'],
            'h' => $coord['ymax'] - $coord['ymin'],
        ];

        $out .= $pop;

        return $out;
    }

    /**
     * Process SVG path command 'A' (elliptical arc).
     *
     * @param array<float> $prm Parameters.
     * @param TSVGCoord $crd Current coordinates.
     * @param array<array<string>> $paths All paths.
     * @param int $key Current key.
     * @param array<string> $rawparams Raw parameters.
     *
     * @return string
     */
    protected function svgPathCmdA(array $prm, array &$crd, array $paths, int $key, array $rawparams): string
    {
        $out = '';

        foreach ($prm as $prk => $prv) {
            $idx = (int) $prk;
            if ((($idx + 1) % 7) !== 0) {
                continue;
            }

            $crd['x0'] = $crd['x'];
            $crd['y0'] = $crd['y'];
            $rpx = \max(\abs($prm[$idx - 6] ?? 0.0), .000_000_001);
            $rpy = \max(\abs($prm[$idx - 5] ?? 0.0), .000_000_001);
            $ang = -\intval($rawparams[$idx - 4] ?? '0');
            $angle = \deg2rad($ang);
            $laf = (int) ($rawparams[$idx - 3] ?? 0); // large-arc-flag
            $swf = (int) ($rawparams[$idx - 2] ?? 0); // sweep-flag
            $crd['x'] = ($prm[$idx - 1] ?? 0.0) + $crd['xoffset'];
            $crd['y'] = $prv + $crd['yoffset'];

            if (
                \abs($crd['x0'] - $crd['x']) < $this->svgminunitlen
                && \abs($crd['y0'] - $crd['y']) < $this->svgminunitlen
            ) {
                // endpoints are almost identical
                $crd['xmin'] = \min($crd['xmin'], $crd['x']);
                $crd['ymin'] = \min($crd['ymin'], $crd['y']);
                $crd['xmax'] = \max($crd['xmax'], $crd['x']);
                $crd['ymax'] = \max($crd['ymax'], $crd['y']);
            } else {
                $cos_ang = \cos($angle);
                $sin_ang = \sin($angle);
                $cra = ($crd['x0'] - $crd['x']) / 2;
                $crb = ($crd['y0'] - $crd['y']) / 2;
                $pxa = ($cra * $cos_ang) - ($crb * $sin_ang);
                $pya = ($cra * $sin_ang) + ($crb * $cos_ang);
                $rx2 = $rpx * $rpx;
                $ry2 = $rpy * $rpy;
                $pxa2 = $pxa * $pxa;
                $pya2 = $pya * $pya;
                $delta = ($pxa2 / $rx2) + ($pya2 / $ry2);
                if ($delta > 1) {
                    $rpx *= \sqrt($delta);
                    $rpy *= \sqrt($delta);
                    $rx2 = $rpx * $rpx;
                    $ry2 = $rpy * $rpy;
                }
                $numerator = ($rx2 * $ry2) - ($rx2 * $pya2) - ($ry2 * $pxa2);
                $root = 0;
                if ($numerator > 0) {
                    $root = \sqrt($numerator / (($rx2 * $pya2) + ($ry2 * $pxa2)));
                }
                if ($laf === $swf) {
                    $root *= -1;
                }
                $cax = $root * (($rpx * $pya) / $rpy);
                $cay = -$root * (($rpy * $pxa) / $rpx);
                // coordinates of ellipse center
                $pcx = ($cax * $cos_ang) - ($cay * $sin_ang) + (($crd['x0'] + $crd['x']) / 2);
                $pcy = ($cax * $sin_ang) + ($cay * $cos_ang) + (($crd['y0'] + $crd['y']) / 2);
                // get angles
                $angs = $this->graph->getVectorsAngle(1, 0, ($pxa - $cax) / $rpx, ($cay - $pya) / $rpy);
                $dang = $this->graph->getVectorsAngle(
                    ($pxa - $cax) / $rpx,
                    ($pya - $cay) / $rpy,
                    (-$pxa - $cax) / $rpx,
                    (-$pya - $cay) / $rpy,
                );
                if ($swf === 0 && $dang > 0) {
                    $dang -= 2 * M_PI;
                } elseif ($swf === 1 && $dang < 0) {
                    $dang += 2 * M_PI;
                }
                $angf = $angs - $dang;
                if ($swf === 0 && $angs > $angf || $swf === 1 && $angs < $angf) {
                    // reverse angles
                    $tmp = $angs;
                    $angs = $angf;
                    $angf = $tmp;
                }
                $angs = \round(\rad2deg($angs), 6);
                $angf = \round(\rad2deg($angf), 6);
                // convert angles to positive values
                if ($angs < 0 && $angf < 0) {
                    $angs += 360;
                    $angf += 360;
                }
                $pie = false;
                $nextCmd = $paths[$key + 1][1] ?? '';
                if ($key === 0 && \trim($nextCmd) === 'z') {
                    $pie = true;
                }
                // list($axmin, $aymin, $axmax, $aymax)
                $bbox = [0, 0, 0, 0];
                $out .= $this->graph->getRawEllipticalArc(
                    $pcx,
                    $pcy,
                    $rpx,
                    $rpy,
                    $ang,
                    $angs,
                    $angf,
                    $pie,
                    2,
                    false,
                    $swf === 0,
                    true,
                    $bbox,
                );
                $crd['xmin'] = \min($crd['xmin'], $crd['x'], $bbox[0] ?? $crd['x']);
                $crd['ymin'] = \min($crd['ymin'], $crd['y'], $bbox[1] ?? $crd['y']);
                $crd['xmax'] = \max($crd['xmax'], $crd['x'], $bbox[2] ?? $crd['x']);
                $crd['ymax'] = \max($crd['ymax'], $crd['y'], $bbox[3] ?? $crd['y']);
            }

            if ($crd['relcoord']) {
                $crd['xoffset'] = $crd['x'];
                $crd['yoffset'] = $crd['y'];
            }
        }

        return $out;
    }

    /**
     * Process SVG path command 'C' (curveto).
     *
     * @param array<float> $prm Parameters.
     * @param TSVGCoord $crd Current coordinates.
     *
     * @return string
     */
    protected function svgPathCmdC(array $prm, array &$crd): string
    {
        $out = '';

        foreach ($prm as $prk => $prv) {
            $idx = (int) $prk;
            if ((($idx + 1) % 6) !== 0) {
                continue;
            }
            $px1 = ($prm[$idx - 5] ?? 0.0) + $crd['xoffset'];
            $py1 = ($prm[$idx - 4] ?? 0.0) + $crd['yoffset'];
            $px2 = ($prm[$idx - 3] ?? 0.0) + $crd['xoffset'];
            $py2 = ($prm[$idx - 2] ?? 0.0) + $crd['yoffset'];
            $crd['x'] = ($prm[$idx - 1] ?? 0.0) + $crd['xoffset'];
            $crd['y'] = $prv + $crd['yoffset'];
            $out .= $this->graph->getRawCurve($px1, $py1, $px2, $py2, $crd['x'], $crd['y']);
            // Second control point mirrored by a following 'S' command.
            $crd['cpx'] = $px2;
            $crd['cpy'] = $py2;
            $crd['cptype'] = 'C';
            $crd['xmin'] = \min($crd['xmin'], $crd['x'], $px1, $px2);
            $crd['ymin'] = \min($crd['ymin'], $crd['y'], $py1, $py2);
            $crd['xmax'] = \max($crd['xmax'], $crd['x'], $px1, $px2);
            $crd['ymax'] = \max($crd['ymax'], $crd['y'], $py1, $py2);
            if ($crd['relcoord']) {
                $crd['xoffset'] = $crd['x'];
                $crd['yoffset'] = $crd['y'];
            }
        }

        return $out;
    }

    /**
     * Process SVG path command 'H' (horizontal lineto).
     *
     * @param array<float> $prm Parameters.
     * @param TSVGCoord $crd Current coordinates.
     *
     * @return string
     */
    protected function svgPathCmdH(array $prm, array &$crd): string
    {
        $out = '';

        foreach ($prm as $prv) {
            $crd['x'] = $prv + $crd['xoffset'];
            if (
                \abs($crd['x0'] - $crd['x']) >= $this->svgminunitlen
                || \abs($crd['y0'] - $crd['y']) >= $this->svgminunitlen
            ) {
                $out .= $this->graph->getRawLine($crd['x'], $crd['y']);
                $crd['x0'] = $crd['x'];
                $crd['y0'] = $crd['y'];
            }
            $crd['xmin'] = \min($crd['xmin'], $crd['x']);
            $crd['xmax'] = \max($crd['xmax'], $crd['x']);
            if ($crd['relcoord']) {
                $crd['xoffset'] = $crd['x'];
            }
        }

        return $out;
    }

    /**
     * Process SVG path command 'L' (lineto).
     *
     * @param array<float> $prm Parameters.
     * @param TSVGCoord $crd Current coordinates.
     *
     * @return string
     */
    protected function svgPathCmdL(array $prm, array &$crd): string
    {
        $out = '';

        foreach ($prm as $prk => $prv) {
            if (!\is_int($prk)) {
                continue;
            }

            if (($prk % 2) === 0) {
                $crd['x'] = $prv + $crd['xoffset'];
                continue;
            }

            $crd['y'] = $prv + $crd['yoffset'];
            if (
                \abs($crd['x0'] - $crd['x']) >= $this->svgminunitlen
                || \abs($crd['y0'] - $crd['y']) >= $this->svgminunitlen
            ) {
                $out .= $this->graph->getRawLine($crd['x'], $crd['y']);
                $crd['x0'] = $crd['x'];
                $crd['y0'] = $crd['y'];
            }
            $crd['xmin'] = \min($crd['xmin'], $crd['x']);
            $crd['ymin'] = \min($crd['ymin'], $crd['y']);
            $crd['xmax'] = \max($crd['xmax'], $crd['x']);
            $crd['ymax'] = \max($crd['ymax'], $crd['y']);
            if ($crd['relcoord']) {
                $crd['xoffset'] = $crd['x'];
                $crd['yoffset'] = $crd['y'];
            }
        }

        return $out;
    }

    /**
     * Process SVG path command 'M' (moveto)
     *
     * @param array<float> $prm Parameters.
     * @param TSVGCoord $crd Current coordinates.
     *
     * @return string
     */
    protected function svgPathCmdM(array $prm, array &$crd): string
    {
        $out = '';

        foreach ($prm as $prk => $prv) {
            if (!\is_int($prk)) {
                continue;
            }

            if (($prk % 2) === 0) {
                $crd['x'] = $prv + $crd['xoffset'];
                continue;
            }

            $crd['y'] = $prv + $crd['yoffset'];
            if (
                $crd['firstcmd']
                || \abs($crd['x0'] - $crd['x']) >= $this->svgminunitlen
                || \abs($crd['y0'] - $crd['y']) >= $this->svgminunitlen
            ) {
                if ($prk === 1) {
                    $out .= $this->graph->getRawPoint($crd['x'], $crd['y']);
                    $crd['firstcmd'] = false;
                    $crd['xinit'] = $crd['x'];
                    $crd['yinit'] = $crd['y'];
                } else {
                    $out .= $this->graph->getRawLine($crd['x'], $crd['y']);
                }
                $crd['x0'] = $crd['x'];
                $crd['y0'] = $crd['y'];
            }
            $crd['xmin'] = \min($crd['xmin'], $crd['x']);
            $crd['ymin'] = \min($crd['ymin'], $crd['y']);
            $crd['xmax'] = \max($crd['xmax'], $crd['x']);
            $crd['ymax'] = \max($crd['ymax'], $crd['y']);
            if ($crd['relcoord']) {
                $crd['xoffset'] = $crd['x'];
                $crd['yoffset'] = $crd['y'];
            }
        }

        return $out;
    }

    /**
     * Process SVG path command 'Q' (quadratic Bezier curveto).
     *
     * @param array<float> $prm Parameters.
     * @param TSVGCoord $crd Current coordinates.
     *
     * @return string
     */
    protected function svgPathCmdQ(array $prm, array &$crd): string
    {
        $out = '';

        foreach ($prm as $prk => $prv) {
            if (!\is_int($prk)) {
                continue;
            }

            if ((($prk + 1) % 4) !== 0) {
                continue;
            }

            // convert quadratic points to cubic points
            $px1 = ($prm[$prk - 3] ?? 0.0) + $crd['xoffset'];
            $py1 = ($prm[$prk - 2] ?? 0.0) + $crd['yoffset'];
            $pxa = ($crd['x'] + (2 * $px1)) / 3;
            $pya = ($crd['y'] + (2 * $py1)) / 3;
            $crd['x'] = ($prm[$prk - 1] ?? 0.0) + $crd['xoffset'];
            $crd['y'] = $prv + $crd['yoffset'];
            $pxb = ($crd['x'] + (2 * $px1)) / 3;
            $pyb = ($crd['y'] + (2 * $py1)) / 3;
            $out .= $this->graph->getRawCurve($pxa, $pya, $pxb, $pyb, $crd['x'], $crd['y']);
            // Quadratic control point mirrored by a following 'T' command.
            $crd['cpx'] = $px1;
            $crd['cpy'] = $py1;
            $crd['cptype'] = 'Q';
            $crd['xmin'] = \min($crd['xmin'], $crd['x'], $pxa, $pxb);
            $crd['ymin'] = \min($crd['ymin'], $crd['y'], $pya, $pyb);
            $crd['xmax'] = \max($crd['xmax'], $crd['x'], $pxa, $pxb);
            $crd['ymax'] = \max($crd['ymax'], $crd['y'], $pya, $pyb);
            if ($crd['relcoord']) {
                $crd['xoffset'] = $crd['x'];
                $crd['yoffset'] = $crd['y'];
            }
        }

        return $out;
    }

    /**
     * Process SVG path command 'S' (shorthand/smooth curveto).
     *
     * The first control point is the reflection of the second control point of
     * the previous command about the current point, or the current point itself
     * when the previous command is not a cubic curve.
     *
     * @param array<float> $prm Parameters.
     * @param TSVGCoord $crd Current coordinates.
     *
     * @return string
     */
    protected function svgPathCmdS(array $prm, array &$crd): string
    {
        $out = '';

        foreach ($prm as $prk => $prv) {
            if (!\is_int($prk)) {
                continue;
            }

            if ((($prk + 1) % 4) !== 0) {
                continue;
            }

            if ($crd['cptype'] === 'C') {
                $px1 = (2 * $crd['x']) - $crd['cpx'];
                $py1 = (2 * $crd['y']) - $crd['cpy'];
            } else {
                $px1 = $crd['x'];
                $py1 = $crd['y'];
            }

            $px2 = ($prm[$prk - 3] ?? 0.0) + $crd['xoffset'];
            $py2 = ($prm[$prk - 2] ?? 0.0) + $crd['yoffset'];
            $crd['x'] = ($prm[$prk - 1] ?? 0.0) + $crd['xoffset'];
            $crd['y'] = $prv + $crd['yoffset'];
            $out .= $this->graph->getRawCurve($px1, $py1, $px2, $py2, $crd['x'], $crd['y']);
            $crd['cpx'] = $px2;
            $crd['cpy'] = $py2;
            $crd['cptype'] = 'C';
            $crd['xmin'] = \min($crd['xmin'], $crd['x'], $px1, $px2);
            $crd['ymin'] = \min($crd['ymin'], $crd['y'], $py1, $py2);
            $crd['xmax'] = \max($crd['xmax'], $crd['x'], $px1, $px2);
            $crd['ymax'] = \max($crd['ymax'], $crd['y'], $py1, $py2);
            if ($crd['relcoord']) {
                $crd['xoffset'] = $crd['x'];
                $crd['yoffset'] = $crd['y'];
            }
        }

        return $out;
    }

    /**
     * Process SVG path command 'T' (shorthand/smooth quadratic Bezier curveto).
     *
     * The control point is the reflection of the control point of the previous
     * command about the current point, or the current point itself when the
     * previous command is not a quadratic curve.
     *
     * @param array<float> $prm Parameters.
     * @param TSVGCoord $crd Current coordinates.
     *
     * @return string
     */
    protected function svgPathCmdT(array $prm, array &$crd): string
    {
        $out = '';

        foreach ($prm as $prk => $prv) {
            if (!\is_int($prk)) {
                continue;
            }

            if (($prk % 2) === 0) {
                continue;
            }

            if ($crd['cptype'] === 'Q') {
                $px1 = (2 * $crd['x']) - $crd['cpx'];
                $py1 = (2 * $crd['y']) - $crd['cpy'];
            } else {
                $px1 = $crd['x'];
                $py1 = $crd['y'];
            }

            $crd['cpx'] = $px1;
            $crd['cpy'] = $py1;
            $crd['cptype'] = 'Q';

            // convert quadratic points to cubic points
            $pxa = ($crd['x'] + (2 * $px1)) / 3;
            $pya = ($crd['y'] + (2 * $py1)) / 3;
            $crd['x'] = ($prm[$prk - 1] ?? 0.0) + $crd['xoffset'];
            $crd['y'] = $prv + $crd['yoffset'];
            $pxb = ($crd['x'] + (2 * $px1)) / 3;
            $pyb = ($crd['y'] + (2 * $py1)) / 3;
            $out .= $this->graph->getRawCurve($pxa, $pya, $pxb, $pyb, $crd['x'], $crd['y']);
            $crd['xmin'] = \min($crd['xmin'], $crd['x'], $pxa, $pxb);
            $crd['ymin'] = \min($crd['ymin'], $crd['y'], $pya, $pyb);
            $crd['xmax'] = \max($crd['xmax'], $crd['x'], $pxa, $pxb);
            $crd['ymax'] = \max($crd['ymax'], $crd['y'], $pya, $pyb);
            if ($crd['relcoord']) {
                $crd['xoffset'] = $crd['x'];
                $crd['yoffset'] = $crd['y'];
            }
        }

        return $out;
    }

    /**
     * Process SVG path command 'V' (vertical lineto).
     *
     * @param array<float> $prm Parameters.
     * @param TSVGCoord $crd Current coordinates.
     *
     * @return string
     */
    protected function svgPathCmdV(array $prm, array &$crd): string
    {
        $out = '';

        foreach ($prm as $prv) {
            $crd['y'] = $prv + $crd['yoffset'];
            if (
                \abs($crd['x0'] - $crd['x']) >= $this->svgminunitlen
                || \abs($crd['y0'] - $crd['y']) >= $this->svgminunitlen
            ) {
                $out .= $this->graph->getRawLine($crd['x'], $crd['y']);
                $crd['x0'] = $crd['x'];
                $crd['y0'] = $crd['y'];
            }
            $crd['ymin'] = \min($crd['ymin'], $crd['y']);
            $crd['ymax'] = \max($crd['ymax'], $crd['y']);
            if ($crd['relcoord']) {
                $crd['yoffset'] = $crd['y'];
            }
        }

        return $out;
    }

    /**
     * Process SVG path command 'Z'.
     *
     * @param TSVGCoord $crd Current coordinates.
     *
     * @return string
     */
    protected function svgPathCmdZ(array &$crd): string
    {
        $crd['x'] = $crd['xinit'];
        $crd['x0'] = $crd['xinit'];
        $crd['y'] = $crd['yinit'];
        $crd['y0'] = $crd['yinit'];
        return "h\n";
    }

    // ----------------------------------------

    /**
     * Returns the letter-spacing value.
     *
     * @param string $spacing letter-spacing value.
     * @param float $parent font spacing (tracking) value of the parent element.
     *
     * @return float Quantity to increases or decreases the space between characters in a text.
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function getTALetterSpacing(string $spacing, float $parent = 0.0): float
    {
        $spacing = \trim($spacing);
        return match ($spacing) {
            'normal' => 0.0,
            'inherit' => $parent,
            default => $this->svgUnitToPoints($spacing, -1, \array_merge(self::REFUNITVAL, ['parent' => $parent])),
        };
    }

    /**
     * Returns the percentage of font stretching.
     *
     * @param string $stretch stretch mode
     * @param float $parent stretch value of the parent element
     *
     * @return float font stretching percentage
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function getTAFontStretching(string $stretch, float $parent = 100): float
    {
        $stretch = \trim($stretch);
        return match ($stretch) {
            'ultra-condensed' => 40,
            'extra-condensed' => 55,
            'condensed' => 70,
            'semi-condensed' => 85,
            'normal' => 100,
            'semi-expanded' => 115,
            'expanded' => 130,
            'extra-expanded' => 145,
            'ultra-expanded' => 160,
            'wider' => $parent + 10,
            'narrower' => $parent - 10,
            'inherit' => $parent,
            default => $this->getUnitValuePoints($stretch, \array_merge(self::REFUNITVAL, ['parent' => $parent]), '%'),
        };
    }

    /**
     * Returns the font weight letter.
     *
     * @param string $weight Font weight Description.
     *
     * @return string Font weight Letter('B'|'').
     */
    protected function getTAFontWeight(string $weight): string
    {
        $weight = \trim($weight);
        return match ($weight) {
            'bold', 'bolder' => 'B',
            // default to 'normal'
            default => '',
        };
    }

    /**
     * Returns the font style letter.
     *
     * @param string $style Font style Description.
     *
     * @return string Font style Letter ('I'|'').
     */
    protected function getTAFontStyle(string $style): string
    {
        $style = \trim($style);
        return match ($style) {
            'italic', 'oblique' => 'I',
            // default to 'normal'
            default => '',
        };
    }

    /**
     * Returns the font decoration letter
     *
     * @param string $decoration Font decoration Description.
     *
     * @return string Font decoration Letter('U'|'O'|'D'|'').
     */
    protected function getTAFontDecoration(string $decoration): string
    {
        $decoration = \trim($decoration);
        return match ($decoration) {
            'underline' => 'U',
            'overline' => 'O',
            'line-through' => 'D',
            default => '',
        };
    }

    /**
     * Normalize a CSS mix-blend-mode value to the PDF blend mode name.
     *
     * CSS uses kebab-case; PDF uses CamelCase. Unknown values fall back to 'Normal'.
     *
     * @param string $mode CSS blend mode value (e.g. 'multiply', 'color-dodge').
     *
     * @return string PDF blend mode name (e.g. 'Multiply', 'ColorDodge').
     */
    protected function normalizeSVGBlendMode(string $mode): string
    {
        $mode = \strtolower(\trim($mode));
        return isset(self::SVGBLENDMODE[$mode]) ? self::SVGBLENDMODE[$mode] : 'Normal';
    }

    /**
     * Normalize an SVG opacity value to the 0..1 range.
     *
     * @param string|float|int $alpha Opacity value from parsed SVG style.
     */
    protected function normalizeSVGAlphaValue(string|float|int $alpha): float
    {
        return \max(0.0, \min(1.0, (float) $alpha));
    }

    /**
     * Normalize a generic style float value without implicit non-numeric casts.
     *
     * @param mixed $value Raw style value.
     * @param float $default Fallback value.
     */
    protected function normalizeSVGStyleFloatValue(mixed $value, float $default): float
    {
        if (\is_float($value) || \is_int($value)) {
            return (float) $value;
        }

        if (\is_string($value)) {
            $value = \trim($value);
            if ($value !== '' && \is_numeric($value)) {
                return (float) $value;
            }
        }

        return $default;
    }

    /**
     * Normalize an SVG style array to the strict TSVGStyle shape.
     *
     * @param array<string, mixed> $style Raw style array (normalized in place).
     */
    protected function normalizeSVGStyleArray(array &$style): void
    {
        $normalized = self::DEFSVGSTYLE;

        foreach ($normalized as $key => $default) {
            if ($key === 'transfmatrix') {
                if (
                    isset($style[$key])
                    && \is_array($style[$key])
                    && isset(
                        $style[$key][0],
                        $style[$key][1],
                        $style[$key][2],
                        $style[$key][3],
                        $style[$key][4],
                        $style[$key][5],
                    )
                ) {
                    $normalized[$key] = [
                        $this->normalizeSVGStyleFloatValue($style[$key][0], self::TMXID[0]),
                        $this->normalizeSVGStyleFloatValue($style[$key][1], self::TMXID[1]),
                        $this->normalizeSVGStyleFloatValue($style[$key][2], self::TMXID[2]),
                        $this->normalizeSVGStyleFloatValue($style[$key][3], self::TMXID[3]),
                        $this->normalizeSVGStyleFloatValue($style[$key][4], self::TMXID[4]),
                        $this->normalizeSVGStyleFloatValue($style[$key][5], self::TMXID[5]),
                    ];
                }

                continue;
            }

            if (\is_float($default)) {
                $normalized[$key] = $this->normalizeSVGStyleFloatValue($style[$key] ?? $default, $default);
                continue;
            }

            if (isset($style[$key]) && \is_string($style[$key])) {
                $normalized[$key] = $style[$key];
                continue;
            }

            if (isset($style[$key]) && (\is_bool($style[$key]) || \is_int($style[$key]) || \is_float($style[$key]))) {
                $normalized[$key] = (string) $style[$key];
            }
        }

        $style = $normalized;
    }

    /**
     * Emit a partial graphics state update for SVG alpha/blend settings.
     *
     * @param ?float  $strokingAlpha    Stroked alpha (CA), when provided.
     * @param ?float  $nonstrokingAlpha Non-stroked alpha (ca), when provided.
     * @param string  $blendMode        PDF blend mode name.
     */
    protected function getSVGExtGState(
        ?float $strokingAlpha = null,
        ?float $nonstrokingAlpha = null,
        string $blendMode = 'Normal',
    ): string {
        if (!$this->isTransparencyAllowed()) {
            return '';
        }

        $parms = [];
        if ($strokingAlpha !== null) {
            $parms['CA'] = $strokingAlpha;
        }
        if ($nonstrokingAlpha !== null) {
            $parms['ca'] = $nonstrokingAlpha;
        }
        if ($blendMode !== '') {
            $parms['BM'] = '/' . $blendMode;
        }

        if ($parms === []) {
            return '';
        }

        return $this->graph->getExtGState($parms);
    }

    /**
     * Parse the SVG style font attributes.
     *
     * @param string $tag Font tag content.
     * @param string $attr Attribute name.
     * @param string $default Default value.
     *
     * @return string
     */
    protected function parseCSSAttrib(string $tag, string $attr, string $default = ''): string
    {
        $regs = [];
        if (\preg_match('/' . $attr . '[\s]*+:[\s]*+([^\;\"]*+)/si', $tag, $regs)) {
            return \trim($regs[1] ?? '');
        }
        return $default;
    }

    /**
     * Parse the SVG font style.
     *
     * @param TSVGStyle $svgstyle SVG style.
     * @param TSVGStyle $parent Parent SVG style.
     *
     * @return string the Raw PDF command to insert the font.
     *
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     */
    protected function parseSVGStyleFont(array &$svgstyle, array $parent = self::DEFSVGSTYLE): string
    {
        if ($svgstyle['font'] !== '') {
            // get font attributes from CSS style
            $font = $svgstyle['font'];
            $svgstyle['font-family'] = $this->parseCSSAttrib($font, 'font-family', $svgstyle['font-family']);
            $svgstyle['font-size-adjust'] = $this->parseCSSAttrib(
                $font,
                'font-size-adjust',
                $svgstyle['font-size-adjust'],
            );
            $svgstyle['font-size'] = $this->parseCSSAttrib($font, 'font-size', $svgstyle['font-size']);
            $svgstyle['font-stretch'] = $this->parseCSSAttrib($font, 'font-stretch', $svgstyle['font-stretch']);
            $svgstyle['font-style'] = $this->parseCSSAttrib($font, 'font-style', $svgstyle['font-style']);
            $svgstyle['font-variant'] = $this->parseCSSAttrib($font, 'font-variant', $svgstyle['font-variant']);
            $svgstyle['font-weight'] = $this->parseCSSAttrib($font, 'font-weight', $svgstyle['font-weight']);
            $svgstyle['letter-spacing'] = $this->parseCSSAttrib($font, 'letter-spacing', $svgstyle['letter-spacing']);
            $svgstyle['text-decoration'] = $this->parseCSSAttrib(
                $font,
                'text-decoration',
                $svgstyle['text-decoration'],
            );
        }

        if ($svgstyle['font-family'] === '') {
            $svgstyle['font-family'] = $parent['font-family'];
        } elseif ($this->font->hasCurrentFont()) {
            // Without a font on the stack there is no family to fall back to:
            // keep the requested one and let the insert() call below load it.
            $svgstyle['font-family'] = $this->font->getFontFamilyName($svgstyle['font-family']);
        }

        $svgstyle['letter-spacing-val'] = $this->getTALetterSpacing(
            $svgstyle['letter-spacing'],
            $parent['letter-spacing-val'],
        );
        $svgstyle['font-stretch-val'] = $this->getTAFontStretching(
            $svgstyle['font-stretch'],
            $parent['font-stretch-val'],
        );

        $ref = self::REFUNITVAL;
        $ref['parent'] = $parent['font-size-val'];
        $ref['font']['rootsize'] = $parent['font-size-val'];
        $ref['font']['size'] = $parent['font-size-val'];
        $ref['font']['xheight'] = $parent['font-size-val'] / 2;
        $ref['font']['zerowidth'] = $parent['font-size-val'] / 3;
        $svgstyle['font-size-val'] = $this->getFontValuePoints($svgstyle['font-size'], $ref);

        $svgstyle['font-mode'] = '';
        $svgstyle['font-mode'] .= $this->getTAFontWeight($svgstyle['font-weight']);
        $svgstyle['font-mode'] .= $this->getTAFontStyle($svgstyle['font-style']);
        $svgstyle['font-mode'] .= $this->getTAFontDecoration($svgstyle['text-decoration']);

        $fontmetric = $this->font->insert(
            $this->pon,
            $svgstyle['font-family'],
            $svgstyle['font-mode'],
            $svgstyle['font-size-val'],
        );

        return $fontmetric['out'];
    }

    /**
     * Parse the SVG stroke style.
     *
     * @param int $soid SVG object ID.
     * @param TSVGStyle $svgstyle SVG style.
     *
     * @return string the Raw PDF command to set the stroke.
     * @throws \Com\Tecnick\Color\Exception
     */
    protected function parseSVGStyleStroke(int $soid, array &$svgstyle): string
    {
        try {
            if ($svgstyle['stroke'] === '' || $svgstyle['stroke'] === 'none') {
                return '';
            }

            $strokestyle = $this->graph->getDefaultStyle();

            $col = $this->color->getColorObj($svgstyle['stroke']);
            if ($col === null) {
                return '';
            }

            $out = '';
            $blendMode = $this->normalizeSVGBlendMode($svgstyle['mix-blend-mode']);
            $baseOpacity = $this->normalizeSVGAlphaValue($svgstyle['opacity']);
            $strokeOpacity = $this->normalizeSVGAlphaValue($svgstyle['stroke-opacity']);
            $strokeAlpha = $baseOpacity * $strokeOpacity;
            $rgba = $col->toRgbArray();
            $alpha = $rgba['alpha'] ?? null;
            if (\is_float($alpha) && $alpha < 1) {
                $strokeAlpha *= $this->normalizeSVGAlphaValue($alpha);
            }

            if (\abs($strokeAlpha - $baseOpacity) > self::SVGMINFLOATDIFF) {
                $out .= $this->getSVGExtGState($strokeAlpha, null, $blendMode);
            }

            if (!isset($this->svgobjs[$soid]['refunitval'])) {
                return '';
            }

            $ref = $this->svgobjs[$soid]['refunitval'];
            $ref['parent'] = 0.0;
            $strokestyle['lineWidth'] = $this->svgUnitToUnit($svgstyle['stroke-width'], -1, $ref);

            $strokestyle['lineCap'] = $svgstyle['stroke-linecap'];
            $strokestyle['lineJoin'] = $svgstyle['stroke-linejoin'];
            //  $strokestyle['miterLimit'] = (10.0 / $this->kunit),
            if ($svgstyle['stroke-dasharray'] === '' || $svgstyle['stroke-dasharray'] === 'none') {
                $strokestyle['dashArray'] = [];
            } else {
                // Normalise each dash/gap token to user units so that values with
                // unit suffixes (px, pt, mm, %, …) produce correct dash lengths.
                $dashRef = $ref;
                $dashRef['parent'] = 0.0;
                $dashArray = [];
                foreach (\explode(' ', $svgstyle['stroke-dasharray'], 100) as $tok) {
                    $dashArray[] = (int) \round($this->svgUnitToUnit(\trim($tok), -1, $dashRef));
                }

                $strokestyle['dashArray'] = $dashArray;
            }
            // $strokestyle['dashPhase'] = 0,
            $strokestyle['lineColor'] = $svgstyle['stroke'];
            unset($strokestyle['fillColor']);

            $out .= $this->graph->getStyleCmd($strokestyle);

            $objstyle = 'D';
            if (!\str_contains($svgstyle['objstyle'], $objstyle)) {
                $svgstyle['objstyle'] .= $objstyle;
            }

            return $out;
        } catch (\Com\Tecnick\Pdf\Exception) {
            return '';
        }
    }

    /**
     * Parse the SVG opacity, color and text-color styles.
     *
     * @param TSVGStyle $svgstyle SVG style.
     *
     * @return string the Raw PDF command to set the stroke.
     */
    protected function parseSVGStyleColor(array &$svgstyle): string
    {
        $out = '';
        $blendMode = $this->normalizeSVGBlendMode($svgstyle['mix-blend-mode']);

        if ($this->isTransparencyAllowed() && ($svgstyle['opacity'] < 1 || $blendMode !== 'Normal')) {
            $out .= $this->graph->getAlpha($svgstyle['opacity'], $blendMode);
        }

        if ($svgstyle['color'] !== '') {
            $this->graph->add(['fillColor' => $svgstyle['color']], true);
        }

        if ($svgstyle['text-color'] !== '') {
            $out .= $this->color->getPdfFillColor($svgstyle['text-color']);
        }

        return $out;
    }

    /**
     * Apply an SVG mask="url(#id)" as a PDF SMask ExtGState.
     *
     * Renders the mask child content to a stream emitted as a Form XObject with
     * a DeviceGray transparency group, referenced by an SMask dict, which is in
     * turn referenced by an ExtGState. Returns the command activating that
     * ExtGState.
     *
     * @param int $soid SVG object ID.
     * @param TSVGStyle $svgstyle Current SVG style.
     *
     * @return string Raw PDF command or empty string.
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGStyleMask(int $soid, array $svgstyle): string
    {
        if (!$this->isTransparencyAllowed()) {
            return '';
        }

        $svgobjs = &$this->svgobjs;
        if (!\array_key_exists($soid, $svgobjs)) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        if ((int) $svgobj['patternmode'] > 0) {
            return '';
        }

        $maskRef = $svgstyle['mask'];
        if ($maskRef === 'none' || $maskRef === '') {
            return '';
        }

        $regs = [];
        if (!\preg_match('/url\(\s*#([^)]+)\s*\)/i', $maskRef, $regs)) {
            return '';
        }

        $maskId = \trim($regs[1] ?? '');
        if ($maskId === '') {
            return '';
        }

        $maskDef = $svgobj['defs'][$maskId] ?? null;
        if ($maskDef === null) {
            return '';
        }

        if ($maskDef['name'] !== 'mask') {
            return '';
        }

        $maskKey = 'MSK_' . \strtoupper(\substr(\md5($maskId), 0, 16));

        if (!isset($this->svgmasks[$maskKey])) {
            $pheight = $svgobj['refunitval']['page']['height'];
            $pwidth = $svgobj['refunitval']['page']['width'];

            $maskParser = \xml_parser_create('UTF-8');
            $stream = '';
            $svgobj['patternmode']++;
            try {
                if (isset($maskDef['child']) && $maskDef['child'] !== []) {
                    $maskChildren = $maskDef['child'];
                    foreach ($maskChildren as $child) {
                        /** @var mixed $child */
                        if (!\is_array($child)) {
                            continue;
                        }

                        if (
                            !isset($child['name'], $child['attr'])
                            || !\is_string($child['name'])
                            || !\is_array($child['attr'])
                        ) {
                            continue;
                        }

                        $childName = $child['name'];
                        $childAttr = $child['attr'];

                        /** @var TSVGAttributes $childAttr */

                        $prevOut = $svgobj['out'];
                        $prevLen = \strlen($prevOut);
                        if (isset($childAttr['closing_tag'])) {
                            $childContentRaw = $childAttr['content'] ?? null;
                            if (\is_string($childContentRaw) && $childContentRaw !== '') {
                                $svgobj['text'] .= $childContentRaw;
                            }

                            $this->handleSVGTagEnd($maskParser, $childName);
                        } else {
                            $this->handleSVGTagStart($maskParser, $childName, $childAttr, $soid);
                        }

                        $currOut = $svgobj['out'];
                        $currLen = \strlen($currOut);
                        if ($currLen > $prevLen) {
                            $stream .= \substr($currOut, $prevLen);
                            $svgobj['out'] = $prevOut;
                        }
                    }
                }
            } finally {
                $svgobj['patternmode'] = \max(0, $svgobj['patternmode'] - 1);
                unset($maskParser);
            }

            $this->svgmasks[$maskKey] = [
                'id' => $maskKey,
                'stream' => $stream,
                'bbox' => [0.0, 0.0, $pwidth, $pheight],
                'gs_n' => 0,
            ];
        }
        return '/' . $maskKey . ' gs' . "\n";
    }

    /**
     * Parse an SVG glyph-orientation angle value.
     *
     * Accepts bare numbers (degrees) and explicit `deg`, `rad`, `grad` units.
     *
     * @param string $value Raw CSS value.
     * @param float $default Default angle in degrees when parsing fails.
     *
     * @return float Angle in degrees.
     */
    protected function parseSVGGlyphOrientationAngle(string $value, float $default): float
    {
        $val = \trim(\strtolower($value));
        if ($val === '') {
            return $default;
        }

        $regs = [];
        if (!\preg_match('/^([-+]?\d*\.?\d+)(deg|rad|grad)?$/', $val, $regs)) {
            return $default;
        }

        $angStr = $regs[1] ?? '';
        if (!\is_numeric($angStr)) {
            return $default;
        }

        $ang = (float) $angStr;
        $unit = $regs[2] ?? 'deg';

        return match ($unit) {
            'rad' => \rad2deg($ang),
            'grad' => $ang * 0.9,
            default => $ang,
        };
    }

    /**
     * Resolve text rotation implied by writing-mode and glyph-orientation.
     *
     * @param TSVGStyle $svgstyle SVG style.
     * @param bool $isVertical True when writing-mode is vertical.
     *
     * @return float Rotation in degrees.
     */
    protected function getSVGGlyphOrientationRotation(array $svgstyle, bool $isVertical): float
    {
        if ($isVertical) {
            // Vertical writing stacks glyphs and rotates them 90°.
            $default = 90.0;
            $gvert = \strtolower($svgstyle['glyph-orientation-vertical']);
            if ($gvert === 'auto') {
                return $default;
            }

            return $this->parseSVGGlyphOrientationAngle($gvert, $default);
        }

        $default = 0.0;
        $ghorz = \strtolower($svgstyle['glyph-orientation-horizontal']);
        if ($ghorz === 'auto') {
            return $default;
        }

        return $this->parseSVGGlyphOrientationAngle($ghorz, $default);
    }

    /**
     * Map SVG rendering-hint properties to PDF rendering-intent operators.
     *
     * The SVG `color-rendering`, `image-rendering`, `shape-rendering` and
     * `text-rendering` hints map onto the PDF /ri operator:
     *
     *  - `optimizeQuality`  / `crispEdges`     → /RelativeColorimetric (default)
     *  - `optimizeSpeed`    / `pixelated`      → /AbsoluteColorimetric
     *  - `auto`             (any)              → no operator emitted
     *
     * The `color-interpolation` and `color-interpolation-filters` values
     * (`sRGB`, `linearRGB`) are accepted and not forwarded.
     *
     * @param TSVGStyle $svgstyle SVG style.
     *
     * @return string Raw PDF command or empty string.
     */
    protected function parseSVGStyleRenderingHints(array $svgstyle): string
    {
        // Collect the most-demanding rendering hint across the four properties.
        $hints = [
            $svgstyle['color-rendering'],
            $svgstyle['image-rendering'],
            $svgstyle['shape-rendering'],
            $svgstyle['text-rendering'],
        ];

        $intent = '';
        foreach ($hints as $h) {
            switch (\strtolower($h)) {
                case 'optimizequality':
                case 'crispedges':
                case 'geometricprecision':
                    // Quality-first hints → RelativeColorimetric (PDF default, but
                    // emit explicitly so the value is recorded in the content stream).
                    if ($intent === '') {
                        $intent = 'RelativeColorimetric';
                    }

                    break;
                case 'optimizespeed':
                case 'pixelated':
                    // Speed-first hints → AbsoluteColorimetric.
                    $intent = 'AbsoluteColorimetric';
                    break;
            }
        }

        if ($intent === '') {
            return '';
        }

        return '/' . $intent . ' ri' . "\n";
    }

    /**
     * Parse the SVG clip style.
     *
     * @param TSVGStyle $svgstyle SVG style.
     * @param float $posx X position in user units.
     * @param float $posy Y position in user units.
     * @param float $width Width in user units.
     * @param float $height Height in user units.
     *
     * @return string the Raw PDF command to set the stroke.
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function parseSVGStyleClip(
        array &$svgstyle,
        float $posx,
        float $posy,
        float $width,
        float $height,
    ): string {
        $regs = [];
        if (!\preg_match(
            '/rect\(([a-z0-9\-\.]*)[\s]*([a-z0-9\-\.]*)[\s]*([a-z0-9\-\.]*)[\s]*([a-z0-9\-\.]*)\)/si',
            $svgstyle['clip'],
            $regs,
        )) {
            return '';
        }

        $regTop = $regs[1] ?? '';
        $regRight = $regs[2] ?? '';
        $regBottom = $regs[3] ?? '';
        $regLeft = $regs[4] ?? '';

        $top = $this->toUnit($regTop !== '' ? $this->svgUnitToPoints($regTop) : 0.0);
        $right = $this->toUnit($regRight !== '' ? $this->svgUnitToPoints($regRight) : 0.0);
        $bottom = $this->toUnit($regBottom !== '' ? $this->svgUnitToPoints($regBottom) : 0.0);
        $left = $this->toUnit($regLeft !== '' ? $this->svgUnitToPoints($regLeft) : 0.0);

        $clx = $posx + $left;
        $cly = $posy + $top;
        $clw = $width - $left - $right;
        $clh = $height - $top - $bottom;
        $eoclip = $svgstyle['clip-rule'] === 'evenodd';

        return $this->graph->getClippingRect($clx, $cly, $clw, $clh, $eoclip);
    }

    /**
     * Parse the SVG fill style.
     *
     * @parma int $soid SVG object ID.
     * @param array<string, TSVGGradient> $gradients Gradients.
     * @param string $xref Gradient ID.
     * @param float $grx X position in user units.
     * @param float $gry Y position in user units.
     * @param float $grw Width in user units.
     * @param float $grh Height in user units.
     * @param string $clip_fnc Optional clipping function name.
     * @param array<mixed> $clip_par Optional clipping function parameters.
     *
     * @return string the Raw PDF command.
     *
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function parseSVGStyleGradient(
        int $soid,
        array $gradients,
        string $xref,
        float $grx,
        float $gry,
        float $grw,
        float $grh,
        string $clip_fnc = '',
        array $clip_par = [],
    ): string {
        $svgobj = $this->svgobjs[$soid] ?? null;
        if ($svgobj === null) {
            return '';
        }

        $gradient = $gradients[$xref] ?? null;
        if ($gradient === null) {
            return '';
        }
        $gradientXref = $gradient['xref'] ?? '';
        if ($gradientXref !== '' && isset($gradients[$gradientXref])) {
            // reference to another gradient definition
            $newgradient = $gradients[$gradientXref];
            $newgradient['coords'] = $gradient['coords'];
            $newgradient['mode'] = $gradient['mode'];
            $newgradient['type'] = $gradient['type'] ?? 2;
            $newgradient['gradientUnits'] = $gradient['gradientUnits'];
            if (isset($gradient['gradientTransform'])) {
                $newgradient['gradientTransform'] = $gradient['gradientTransform'];
            }
            $gradient = $newgradient;
        }

        $gradient['coords'] = [
            0 => $gradient['coords'][0] ?? 0.0,
            1 => $gradient['coords'][1] ?? 0.0,
            2 => $gradient['coords'][2] ?? 0.0,
            3 => $gradient['coords'][3] ?? 0.0,
            4 => $gradient['coords'][4] ?? 0.0,
        ];
        $gradientType = (int) ($gradient['type'] ?? 2);

        $out = '';
        $out .= $this->graph->getStartTransform();

        if ($clip_fnc !== '') {
            $bboxid_start = \array_key_last($this->bbox);
            $bboxidStart = \is_int($bboxid_start) ? $bboxid_start : -1;
            $out .= $this->applySVGClipFunction($clip_fnc, $clip_par);
            $bboxid_last = \array_key_last($this->bbox);
            $bboxidLast = \is_int($bboxid_last) ? $bboxid_last : -1;

            if ($bboxidLast > $bboxidStart && isset($this->bbox[$bboxidLast]) && $gradientType !== 3) {
                $bbox = $this->bbox[$bboxidLast];
                $grx = $bbox['x'];
                $gry = $bbox['y'];
                $grw = $bbox['w'];
                $grh = $bbox['h'];
            }
        }

        switch ($gradient['mode']) {
            case 'percentage':
                foreach ($gradient['coords'] as $key => $val) {
                    $gradient['coords'][$key] = \intval($val) / 100;
                    if ($val < 0) {
                        $gradient['coords'][$key] = 0;
                    } elseif ($val > 1) {
                        $gradient['coords'][$key] = 1;
                    }
                }
                break;
            case 'measure':
                if (isset($gradient['gradientTransform'])) {
                    $gtm0 = $gradient['gradientTransform'][0] ?? 0.0;
                    $gtm1 = $gradient['gradientTransform'][1] ?? 0.0;
                    $gtm2 = $gradient['gradientTransform'][2] ?? 0.0;
                    $gtm3 = $gradient['gradientTransform'][3] ?? 0.0;
                    $gtm4 = $gradient['gradientTransform'][4] ?? 0.0;
                    $gtm5 = $gradient['gradientTransform'][5] ?? 0.0;
                    $coord4 = $gradient['coords'][4];
                    // apply transformation matrix
                    $gxa = ($gtm0 * $gradient['coords'][0]) + ($gtm2 * $gradient['coords'][1]) + $gtm4;
                    $gya = ($gtm1 * $gradient['coords'][0]) + ($gtm3 * $gradient['coords'][1]) + $gtm5;
                    $gxb = ($gtm0 * $gradient['coords'][2]) + ($gtm2 * $gradient['coords'][3]) + $gtm4;
                    $gyb = ($gtm1 * $gradient['coords'][2]) + ($gtm3 * $gradient['coords'][3]) + $gtm5;
                    $grr = \sqrt(($gtm0 * $coord4) ** 2 + ($gtm1 * $coord4) ** 2);
                    $gradient['coords'][0] = $gxa;
                    $gradient['coords'][1] = $gya;
                    $gradient['coords'][2] = $gxb;
                    $gradient['coords'][3] = $gyb;
                    $gradient['coords'][4] = $grr;
                }
                // convert SVG coordinates to user units
                $gradient['coords'][0] = $this->svgUnitToUnit($gradient['coords'][0], $soid);
                $gradient['coords'][1] = $this->svgUnitToUnit($gradient['coords'][1], $soid);
                $gradient['coords'][2] = $this->svgUnitToUnit($gradient['coords'][2], $soid);
                $gradient['coords'][3] = $this->svgUnitToUnit($gradient['coords'][3], $soid);
                $gradient['coords'][4] = $this->svgUnitToUnit($gradient['coords'][4], $soid);
                if ($grw <= $this->svgminunitlen) {
                    $grw = $this->svgminunitlen;
                }
                if ($grh <= $this->svgminunitlen) {
                    $grh = $this->svgminunitlen;
                }
                // shift units
                if ($gradient['gradientUnits'] === 'objectBoundingBox') {
                    // convert to SVG coordinate system
                    $gradient['coords'][0] += $grx;
                    $gradient['coords'][1] += $gry;
                    $gradient['coords'][2] += $grx;
                    $gradient['coords'][3] += $gry;
                }
                // calculate percentages
                $gradient['coords'][0] = ($gradient['coords'][0] - $grx) / $grw;
                $gradient['coords'][1] = ($gradient['coords'][1] - $gry) / $grh;
                $gradient['coords'][2] = ($gradient['coords'][2] - $grx) / $grw;
                $gradient['coords'][3] = ($gradient['coords'][3] - $gry) / $grh;
                $gradient['coords'][4] /= $grw;
                break;
        }

        if (
            $gradientType === 2
            && $gradient['coords'][0] === $gradient['coords'][2]
            && $gradient['coords'][1] === $gradient['coords'][3]
        ) {
            // single color (no shading)
            $gradient['coords'][0] = 1;
            $gradient['coords'][1] = 0;
            $gradient['coords'][2] = 0.999;
            $gradient['coords'][3] = 0;
        }

        // swap Y coordinates
        $tmp = $gradient['coords'][1];
        $gradient['coords'][1] = $gradient['coords'][3];
        $gradient['coords'][3] = $tmp;

        // set transformation map for gradient
        $gry = $this->toUnit($svgobj['refunitval']['page']['height']) - $gry;
        if ($gradientType === 3) {
            // circular gradient
            $gry -= $gradient['coords'][1] * ($grw + $grh);
            $maxGradSpan = \max($grw, $grh);
            $grw = $maxGradSpan;
            $grh = $maxGradSpan;
        } else {
            $gry -= $grh;
        }

        $out .= \sprintf(
            "%F 0 0 %F %F %F cm\n",
            $this->toPoints($grw),
            $this->toPoints($grh),
            $this->toPoints($grx),
            $this->toPoints($gry),
        );

        if (\count($gradient['stops']) > 1) {
            $out .= $this->graph->getGradient($gradientType, $gradient['coords'], $gradient['stops'], '', false);
        }

        $out .= $this->graph->getStopTransform();

        return $out;
    }

    /**
     * Resolve and invoke supported clip/path functions with strict argument checks.
     *
     * @param string $clip_fnc Function name.
     * @param array<mixed> $clip_par Function parameters.
     *
     * @return string
     */
    protected function applySVGClipFunction(string $clip_fnc, array $clip_par): string
    {
        return match ($clip_fnc) {
            'getSVGPath' => isset($clip_par[0], $clip_par[1], $clip_par[2])
                && \is_int($clip_par[0])
                && \is_string($clip_par[1])
                && \is_string($clip_par[2])
                    ? $this->getSVGPath($clip_par[0], $clip_par[1], $clip_par[2])
                    : '',
            'getRoundedRect' => isset(
                $clip_par[0],
                $clip_par[1],
                $clip_par[2],
                $clip_par[3],
                $clip_par[4],
                $clip_par[5],
                $clip_par[6],
                $clip_par[7],
            )
                && (\is_int($clip_par[0]) || \is_float($clip_par[0]))
                && (\is_int($clip_par[1]) || \is_float($clip_par[1]))
                && (\is_int($clip_par[2]) || \is_float($clip_par[2]))
                && (\is_int($clip_par[3]) || \is_float($clip_par[3]))
                && (\is_int($clip_par[4]) || \is_float($clip_par[4]))
                && (\is_int($clip_par[5]) || \is_float($clip_par[5]))
                && \is_string($clip_par[6])
                && \is_string($clip_par[7])
                    ? $this->graph->getRoundedRect(
                        (float) $clip_par[0],
                        (float) $clip_par[1],
                        (float) $clip_par[2],
                        (float) $clip_par[3],
                        (float) $clip_par[4],
                        (float) $clip_par[5],
                        $clip_par[6],
                        $clip_par[7],
                    )
                    : '',
            'getCircle' => isset($clip_par[0], $clip_par[1], $clip_par[2], $clip_par[3], $clip_par[4], $clip_par[5])
                && (\is_int($clip_par[0]) || \is_float($clip_par[0]))
                && (\is_int($clip_par[1]) || \is_float($clip_par[1]))
                && (\is_int($clip_par[2]) || \is_float($clip_par[2]))
                && (\is_int($clip_par[3]) || \is_float($clip_par[3]))
                && (\is_int($clip_par[4]) || \is_float($clip_par[4]))
                && \is_string($clip_par[5])
                    ? $this->graph->getCircle(
                        (float) $clip_par[0],
                        (float) $clip_par[1],
                        (float) $clip_par[2],
                        (float) $clip_par[3],
                        (float) $clip_par[4],
                        $clip_par[5],
                    )
                    : '',
            'getEllipse' => isset(
                $clip_par[0],
                $clip_par[1],
                $clip_par[2],
                $clip_par[3],
                $clip_par[4],
                $clip_par[5],
                $clip_par[6],
                $clip_par[7],
            )
                && (\is_int($clip_par[0]) || \is_float($clip_par[0]))
                && (\is_int($clip_par[1]) || \is_float($clip_par[1]))
                && (\is_int($clip_par[2]) || \is_float($clip_par[2]))
                && (\is_int($clip_par[3]) || \is_float($clip_par[3]))
                && (\is_int($clip_par[4]) || \is_float($clip_par[4]))
                && (\is_int($clip_par[5]) || \is_float($clip_par[5]))
                && (\is_int($clip_par[6]) || \is_float($clip_par[6]))
                && \is_string($clip_par[7])
                    ? $this->graph->getEllipse(
                        (float) $clip_par[0],
                        (float) $clip_par[1],
                        (float) $clip_par[2],
                        (float) $clip_par[3],
                        (float) $clip_par[4],
                        (float) $clip_par[5],
                        (float) $clip_par[6],
                        $clip_par[7],
                    )
                    : '',
            'getLine' => isset($clip_par[0], $clip_par[1], $clip_par[2], $clip_par[3])
                && (\is_int($clip_par[0]) || \is_float($clip_par[0]))
                && (\is_int($clip_par[1]) || \is_float($clip_par[1]))
                && (\is_int($clip_par[2]) || \is_float($clip_par[2]))
                && (\is_int($clip_par[3]) || \is_float($clip_par[3]))
                    ? $this->graph->getLine(
                        (float) $clip_par[0],
                        (float) $clip_par[1],
                        (float) $clip_par[2],
                        (float) $clip_par[3],
                    )
                    : '',
            'getPolygon' => isset($clip_par[0], $clip_par[1]) && \is_array($clip_par[0]) && \is_string($clip_par[1])
                ? $this->graph->getPolygon(\array_map('floatval', $clip_par[0]), $clip_par[1])
                : '',
            'getClippingRect' => isset($clip_par[0], $clip_par[1], $clip_par[2], $clip_par[3])
                && (\is_int($clip_par[0]) || \is_float($clip_par[0]))
                && (\is_int($clip_par[1]) || \is_float($clip_par[1]))
                && (\is_int($clip_par[2]) || \is_float($clip_par[2]))
                && (\is_int($clip_par[3]) || \is_float($clip_par[3]))
                    ? $this->graph->getClippingRect(
                        (float) $clip_par[0],
                        (float) $clip_par[1],
                        (float) $clip_par[2],
                        (float) $clip_par[3],
                        $this->normalizeSVGBoolLike($clip_par[4] ?? false),
                    )
                    : '',
            default => '',
        };
    }

    /**
     * Normalize scalar-like values into strict booleans.
     */
    protected function normalizeSVGBoolLike(mixed $value): bool
    {
        if (\is_bool($value)) {
            return $value;
        }

        if (\is_int($value) || \is_float($value)) {
            return (float) $value !== 0.0;
        }

        if (\is_string($value)) {
            return \in_array(\strtolower(\trim($value)), ['1', 'true', 'yes', 'on'], true);
        }

        return false;
    }

    /**
     * Resolve a pattern length optionally expressed as percentage.
     *
     * @param string $raw Raw pattern length value.
     * @param float $base Base value for percentages.
     * @param int $soid SVG object ID.
     *
     * @return float
     */
    protected function resolveSVGPatternLength(string $raw, float $base, int $soid): float
    {
        $raw = \trim($raw);
        if ($raw === '') {
            return 0.0;
        }

        if (\str_ends_with($raw, '%')) {
            $pctRaw = \trim(\substr($raw, 0, -1));
            if (!\is_numeric($pctRaw)) {
                return 0.0;
            }
            $pct = (float) $pctRaw;
            return ($pct / 100.0) * $base;
        }

        try {
            return $this->svgUnitToUnit($raw, $soid);
        } catch (\Com\Tecnick\Pdf\Exception) {
            return 0.0;
        }
    }

    /**
     * Resolve a pattern definition, including href inheritance chain.
     *
     * Child pattern attributes override inherited parent values.
     * Child pattern content is inherited only when the child has no own content.
     *
     * @param int $soid SVG object ID.
     * @param string $patternId Pattern ID without '#'.
     *
     * @return ?TSVGAttribs
     */
    protected function resolveSVGPatternDef(int $soid, string $patternId): ?array
    {
        $resolved = $this->svgobjs[$soid]['defs'][$patternId] ?? null;
        if ($resolved === null) {
            return null;
        }

        if ($resolved['name'] !== 'pattern') {
            return null;
        }

        $resolvedAttr = $resolved['attr'];
        $resolvedChild = $resolved['child'] ?? [];

        $seen = [$patternId => true];
        $href = $resolvedAttr['xlink:href'] ?? $resolvedAttr['href'] ?? '';
        while ($href !== '' && $href[0] === '#') {
            $parentId = \substr($href, 1);
            if ($parentId === '' || isset($seen[$parentId])) {
                break;
            }
            $seen[$parentId] = true;

            $parent = $this->svgobjs[$soid]['defs'][$parentId] ?? null;
            if ($parent === null) {
                break;
            }

            if ($parent['name'] !== 'pattern') {
                break;
            }

            $parentAttr = $parent['attr'];
            $resolvedAttr = \array_replace($parentAttr, $resolvedAttr);
            /** @var TSVGAttributes $resolvedAttr */

            if ($resolvedChild === [] && isset($parent['child']) && $parent['child'] !== []) {
                $resolvedChild = $parent['child'];
            }

            $href = $parentAttr['xlink:href'] ?? $parentAttr['href'] ?? '';
        }

        return [
            'name' => 'pattern',
            'attr' => $resolvedAttr,
            'child' => $resolvedChild,
        ];
    }

    /**
     * Register a PDF tiling pattern resource for an SVG pattern definition.
     *
     * @param int $soid SVG object ID.
     * @param string $patternId Pattern ID without '#'.
     * @param TSVGAttribs $patterndef Resolved pattern definition.
     * @param TSVGAttributes $attr Resolved pattern attributes.
     * @param float $tileX Tile origin x in user units.
     * @param float $tileY Tile origin y in user units.
     * @param float $tileW Tile width in user units.
     * @param float $tileH Tile height in user units.
     * @param float $width Target object width in user units.
     * @param float $height Target object height in user units.
     *
     * @return string Pattern resource ID or empty string on failure.
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     */
    protected function registerSVGPatternResource(
        int $soid,
        string $patternId,
        array $patterndef,
        array $attr,
        float $tileX,
        float $tileY,
        float $tileW,
        float $tileH,
        float $width,
        float $height,
    ): string {
        if ($tileW <= $this->svgminunitlen || $tileH <= $this->svgminunitlen) {
            return '';
        }

        try {
            $contentUnits = $attr['patternContentUnits'] ?? 'userSpaceOnUse';
            $patternTransform = isset($attr['patternTransform']) && $attr['patternTransform'] !== ''
                ? $this->getSVGTransformMatrix($attr['patternTransform'])
                : self::TMXID;
            $viewBoxTm = self::TMXID;
            $hasViewBox = false;
            if (isset($attr['viewBox']) && $attr['viewBox'] !== '') {
                $vals = \preg_split('/[\s,]+/', \trim($attr['viewBox']), -1, \PREG_SPLIT_NO_EMPTY);
                if (\is_array($vals) && isset($vals[0], $vals[1], $vals[2], $vals[3])) {
                    $vbx = $this->svgUnitToUnit($vals[0], $soid);
                    $vby = $this->svgUnitToUnit($vals[1], $soid);
                    $vbw = \abs($this->svgUnitToUnit($vals[2], $soid));
                    $vbh = \abs($this->svgUnitToUnit($vals[3], $soid));

                    if ($vbw > 0.0 && $vbh > 0.0) {
                        $hasViewBox = true;
                        $viewScaleX = $tileW / $vbw;
                        $viewScaleY = $tileH / $vbh;
                        $viewOffsetX = 0.0;
                        $viewOffsetY = 0.0;

                        $aspectRaw = $attr['preserveAspectRatio'] ?? 'xMidYMid meet';
                        $aspectFit = 'meet';
                        $aspectX = 'xMid';
                        $aspectY = 'YMid';
                        if (\trim($aspectRaw) === 'none') {
                            $aspectFit = 'none';
                        } else {
                            $aspectMatches = [];
                            \preg_match_all('/[a-zA-Z]+/', $aspectRaw, $aspectMatches);
                            $tokens = $aspectMatches[0] ?? [];
                            $firstToken = $tokens[0] ?? '';
                            if (\strtolower($firstToken) === 'defer') {
                                \array_shift($tokens);
                                $firstToken = $tokens[0] ?? '';
                            }

                            if ($firstToken !== '' && \strlen($firstToken) === 8) {
                                $alignToken = $firstToken;
                                $aspectX = \substr($alignToken, 0, 4);
                                $aspectY = \substr($alignToken, 4, 4);
                                $aspectMode = $tokens[1] ?? '';
                                if ($aspectMode !== '' && \in_array($aspectMode, ['meet', 'slice', 'none'], true)) {
                                    $aspectFit = $aspectMode;
                                }
                            } elseif ($firstToken !== '' && \in_array($firstToken, ['meet', 'slice', 'none'], true)) {
                                $aspectFit = $firstToken;
                            }
                        }

                        if ($aspectFit !== 'none') {
                            $scaleX = $tileW / $vbw;
                            $scaleY = $tileH / $vbh;
                            $scale = $aspectFit === 'slice' ? \max($scaleX, $scaleY) : \min($scaleX, $scaleY);
                            $viewScaleX = $scale;
                            $viewScaleY = $scale;
                            $scaledW = $vbw * $scale;
                            $scaledH = $vbh * $scale;
                            $viewOffsetX = match ($aspectX) {
                                'xMax' => $tileW - $scaledW,
                                'xMid' => ($tileW - $scaledW) / 2.0,
                                default => 0.0,
                            };
                            $viewOffsetY = match ($aspectY) {
                                'YMax' => $tileH - $scaledH,
                                'YMid' => ($tileH - $scaledH) / 2.0,
                                default => 0.0,
                            };
                        }

                        $viewBoxTm = [
                            $viewScaleX,
                            0.0,
                            0.0,
                            $viewScaleY,
                            $viewOffsetX - ($viewScaleX * $vbx),
                            $viewOffsetY - ($viewScaleY * $vby),
                        ];
                    }
                }
            }

            $contentTm = self::TMXID;
            // SVG2: patternContentUnits has no effect when a viewBox is specified.
            if ($contentUnits === 'objectBoundingBox' && !$hasViewBox) {
                $contentTm = $this->graph->getCtmProduct($contentTm, [$width, 0.0, 0.0, $height, 0.0, 0.0]);
            }
            $contentTm = $this->graph->getCtmProduct($contentTm, $viewBoxTm);

            $stream = $this->graph->getStartTransform();
            $stream .= $this->getOutSVGTransformation($contentTm, $soid);

            $patParser = \xml_parser_create('UTF-8');

            $this->svgobjs[$soid]['patternmode'] = (int) ($this->svgobjs[$soid]['patternmode'] ?? 0) + 1;
            try {
                if (isset($patterndef['child']) && $patterndef['child'] !== []) {
                    foreach ($patterndef['child'] as $child) {
                        $prevOut = $this->svgobjs[$soid]['out'];
                        $prevLen = \strlen($prevOut);
                        if (isset($child['attr']['closing_tag'])) {
                            if (isset($child['attr']['content']) && $child['attr']['content'] !== '') {
                                $this->svgobjs[$soid]['text'] .= $child['attr']['content'];
                            }
                            $this->handleSVGTagEnd($patParser, $child['name']);
                        } else {
                            /** @var TSVGAttributes $childAttr */
                            $childAttr = $child['attr'];
                            $this->handleSVGTagStart($patParser, $child['name'], $childAttr, $soid);
                        }

                        $currOut = $this->svgobjs[$soid]['out'];
                        $currLen = \strlen($currOut);
                        if ($currLen > $prevLen) {
                            $stream .= \substr($currOut, $prevLen);

                            $this->svgobjs[$soid]['out'] = $prevOut;
                        }
                    }
                }
            } finally {
                $this->svgobjs[$soid]['patternmode'] = \max(0, (int) $this->svgobjs[$soid]['patternmode'] - 1);
                unset($patParser);
            }

            $stream .= $this->graph->getStopTransform();
            if (\trim($stream) === '') {
                return '';
            }

            $patternMatrix = $this->graph->getCtmProduct([1.0, 0.0, 0.0, 1.0, $tileX, $tileY], $patternTransform);
            $pid =
                'PTN_'
                . \strtoupper(\substr(
                    \md5(\sprintf(
                        '%s|%F|%F|%F|%F|%F|%F|%F|%F',
                        $patternId,
                        $tileX,
                        $tileY,
                        $tileW,
                        $tileH,
                        $width,
                        $height,
                        $patternMatrix[4],
                        $patternMatrix[5],
                    )),
                    0,
                    16,
                ));

            if (!isset($this->patterns[$pid])) {
                $this->patterns[$pid] = [
                    'id' => $pid,
                    'n' => 0,
                    'outdata' => $stream,
                    'bbox' => [0.0, 0.0, $tileW, $tileH],
                    'xstep' => $tileW,
                    'ystep' => $tileH,
                    'matrix' => [
                        $patternMatrix[0],
                        $patternMatrix[1],
                        $patternMatrix[2],
                        $patternMatrix[3],
                        $patternMatrix[4],
                        $patternMatrix[5],
                    ],
                ];
            }

            return $pid;
        } catch (\Com\Tecnick\Pdf\Exception|\Com\Tecnick\Unicode\Exception) {
            return '';
        }
    }

    /**
     * Parse SVG pattern fill style from defs.
     *
     * @param int $soid SVG object ID.
     * @param string $patternId Pattern ID without '#'.
     * @param float $posx X position in user units.
     * @param float $posy Y position in user units.
     * @param float $width Width in user units.
     * @param float $height Height in user units.
     * @param string $clip_fnc Optional clipping function name.
     * @param array<mixed> $clip_par Optional clipping function parameters.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGStylePattern(
        int $soid,
        string $patternId,
        float $posx,
        float $posy,
        float $width,
        float $height,
        string $clip_fnc = '',
        array $clip_par = [],
    ): string {
        try {
            $svgobjs = &$this->svgobjs;
            if (!\array_key_exists($soid, $svgobjs)) {
                return '';
            }
            $svgobj = &$this->getSVGObjRef($soid);

            if ((int) $svgobj['patternmode'] > 0) {
                return '';
            }

            $patterndef = $this->resolveSVGPatternDef($soid, $patternId);
            if ($patterndef === null) {
                return '';
            }

            $attr = $patterndef['attr'];
            $units = $attr['patternUnits'] ?? 'objectBoundingBox';
            $isObjectBox = $units !== 'userSpaceOnUse';

            $tileX = $this->resolveSVGPatternLength($attr['x'] ?? '0', $isObjectBox ? $width : 1.0, $soid);
            $tileY = $this->resolveSVGPatternLength($attr['y'] ?? '0', $isObjectBox ? $height : 1.0, $soid);
            $tileW = $this->resolveSVGPatternLength($attr['width'] ?? '0', $isObjectBox ? $width : 1.0, $soid);
            $tileH = $this->resolveSVGPatternLength($attr['height'] ?? '0', $isObjectBox ? $height : 1.0, $soid);

            if ($isObjectBox) {
                $tileX += $posx;
                $tileY += $posy;
            }

            if ($tileW <= $this->svgminunitlen || $tileH <= $this->svgminunitlen) {
                return '';
            }

            $patternResId = $this->registerSVGPatternResource(
                $soid,
                $patternId,
                $patterndef,
                $attr,
                $tileX,
                $tileY,
                $tileW,
                $tileH,
                $width,
                $height,
            );
            if ($patternResId !== '') {
                return '/Pattern cs /' . $patternResId . " scn\n";
            }

            $ixStart = (int) \floor(($posx - $tileX) / $tileW) - 1;
            $ixEnd = (int) \ceil(($posx + $width - $tileX) / $tileW) + 1;
            $iyStart = (int) \floor(($posy - $tileY) / $tileH) - 1;
            $iyEnd = (int) \ceil(($posy + $height - $tileY) / $tileH) + 1;

            $out = '';
            $out .= $this->graph->getStartTransform();

            if ($clip_fnc !== '') {
                $out .= $this->applySVGClipFunction($clip_fnc, $clip_par);
            }

            $contentUnits = $attr['patternContentUnits'] ?? 'userSpaceOnUse';
            $patternTransform = isset($attr['patternTransform']) && $attr['patternTransform'] !== ''
                ? $this->getSVGTransformMatrix($attr['patternTransform'])
                : self::TMXID;
            $viewBoxTm = self::TMXID;
            $hasViewBox = false;
            if (isset($attr['viewBox']) && $attr['viewBox'] !== '') {
                $vals = \preg_split('/[\s,]+/', \trim($attr['viewBox']), -1, \PREG_SPLIT_NO_EMPTY);
                if (\is_array($vals) && isset($vals[0], $vals[1], $vals[2], $vals[3])) {
                    $vbx = $this->svgUnitToUnit($vals[0], $soid);
                    $vby = $this->svgUnitToUnit($vals[1], $soid);
                    $vbw = \abs($this->svgUnitToUnit($vals[2], $soid));
                    $vbh = \abs($this->svgUnitToUnit($vals[3], $soid));

                    if ($vbw > 0.0 && $vbh > 0.0) {
                        $hasViewBox = true;
                        $viewScaleX = $tileW / $vbw;
                        $viewScaleY = $tileH / $vbh;
                        $viewOffsetX = 0.0;
                        $viewOffsetY = 0.0;

                        $aspectRaw = $attr['preserveAspectRatio'] ?? 'xMidYMid meet';
                        $aspectFit = 'meet';
                        $aspectX = 'xMid';
                        $aspectY = 'YMid';
                        if (\trim($aspectRaw) === 'none') {
                            $aspectFit = 'none';
                        } else {
                            $aspectMatches = [];
                            \preg_match_all('/[a-zA-Z]+/', $aspectRaw, $aspectMatches);
                            $tokens = $aspectMatches[0] ?? [];
                            $firstToken = $tokens[0] ?? '';
                            if (\strtolower($firstToken) === 'defer') {
                                \array_shift($tokens);
                                $firstToken = $tokens[0] ?? '';
                            }

                            if ($firstToken !== '' && \strlen($firstToken) === 8) {
                                $alignToken = $firstToken;
                                $aspectX = \substr($alignToken, 0, 4);
                                $aspectY = \substr($alignToken, 4, 4);
                                $aspectMode = $tokens[1] ?? '';
                                $aspectFit = $aspectMode !== ''
                                && \in_array($aspectMode, ['meet', 'slice', 'none'], true)
                                    ? $aspectMode
                                    : $aspectFit;
                            } elseif ($firstToken !== '' && \in_array($firstToken, ['meet', 'slice', 'none'], true)) {
                                $aspectFit = $firstToken;
                            }
                        }

                        if ($aspectFit !== 'none') {
                            $scaleX = $tileW / $vbw;
                            $scaleY = $tileH / $vbh;
                            $scale = $aspectFit === 'slice' ? \max($scaleX, $scaleY) : \min($scaleX, $scaleY);
                            $viewScaleX = $scale;
                            $viewScaleY = $scale;
                            $scaledW = $vbw * $scale;
                            $scaledH = $vbh * $scale;
                            $viewOffsetX = match ($aspectX) {
                                'xMax' => $tileW - $scaledW,
                                'xMid' => ($tileW - $scaledW) / 2.0,
                                default => 0.0,
                            };
                            $viewOffsetY = match ($aspectY) {
                                'YMax' => $tileH - $scaledH,
                                'YMid' => ($tileH - $scaledH) / 2.0,
                                default => 0.0,
                            };
                        }

                        $viewBoxTm = [
                            $viewScaleX,
                            0.0,
                            0.0,
                            $viewScaleY,
                            $viewOffsetX - ($viewScaleX * $vbx),
                            $viewOffsetY - ($viewScaleY * $vby),
                        ];
                    }
                }
            }
            $patParser = \xml_parser_create('UTF-8');

            $svgobj['patternmode'] = (int) $svgobj['patternmode'] + 1;
            try {
                for ($iy = $iyStart; $iy <= $iyEnd; ++$iy) {
                    for ($ix = $ixStart; $ix <= $ixEnd; ++$ix) {
                        $tilePosX = $tileX + ($ix * $tileW);
                        $tilePosY = $tileY + ($iy * $tileH);
                        if (
                            $tilePosX > ($posx + $width)
                            || ($tilePosX + $tileW) < $posx
                            || $tilePosY > ($posy + $height)
                            || ($tilePosY + $tileH) < $posy
                        ) {
                            continue;
                        }

                        $out .= $this->graph->getStartTransform();
                        $tileTm = [1.0, 0.0, 0.0, 1.0, $tilePosX, $tilePosY];
                        // SVG2: patternContentUnits has no effect when a viewBox is specified.
                        if ($contentUnits === 'objectBoundingBox' && !$hasViewBox) {
                            $tileTm = $this->graph->getCtmProduct($tileTm, [$width, 0.0, 0.0, $height, 0.0, 0.0]);
                        }
                        $tileTm = $this->graph->getCtmProduct($tileTm, $patternTransform);
                        $tileTm = $this->graph->getCtmProduct($tileTm, $viewBoxTm);
                        $out .= $this->getOutSVGTransformation($tileTm, $soid);

                        if (isset($patterndef['child']) && $patterndef['child'] !== []) {
                            foreach ($patterndef['child'] as $child) {
                                $childName = $child['name'];
                                $childAttr = $child['attr'];
                                $prevOut = $svgobj['out'];
                                $prevLen = \strlen($prevOut);
                                if (!isset($childAttr['closing_tag'])) {
                                    $this->handleSVGTagStart($patParser, $childName, $childAttr, $soid);
                                    continue;
                                }

                                $childContent = $childAttr['content'] ?? null;
                                if (\is_string($childContent) && $childContent !== '') {
                                    // Replay text captured in defs before closing the element.

                                    $svgobj['text'] .= $childContent;
                                }
                                $this->handleSVGTagEnd($patParser, $childName);

                                $currOut = $svgobj['out'];
                                $currLen = \strlen($currOut);
                                if ($currLen > $prevLen) {
                                    $out .= \substr($currOut, $prevLen);
                                    // keep replay output scoped to this pattern fill stream

                                    $svgobj['out'] = $prevOut;
                                }
                            }
                        }

                        $out .= $this->graph->getStopTransform();
                    }
                }
            } finally {
                $svgobj['patternmode'] = \max(0, (int) $svgobj['patternmode'] - 1);
                unset($patParser);
            }

            $out .= $this->graph->getStopTransform();
            return $out;
        } catch (\Com\Tecnick\Pdf\Exception) {
            return '';
        }
    }

    /**
     * Parse the SVG fill style.
     *
     * @param int $soid SVG object ID.
     * @param TSVGStyle $svgstyle SVG style.
     * @param array<string, TSVGGradient> $gradients Gradients.
     * @param float $posx X position in user units.
     * @param float $posy Y position in user units.
     * @param float $width Width in user units.
     * @param float $height Height in user units.
     * @param string $clip_fnc Optional clipping function name.
     * @param array<mixed> $clip_par Optional clipping function parameters.
     *
     * @return string the Raw PDF command.
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     */
    protected function parseSVGStyleFill(
        int $soid,
        array &$svgstyle,
        array $gradients,
        float $posx,
        float $posy,
        float $width,
        float $height,
        string $clip_fnc = '',
        array $clip_par = [],
    ): string {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        if ($svgstyle['fill'] === '' || $svgstyle['fill'] === 'none') {
            return '';
        }

        if ((int) ($this->svgobjs[$soid]['patternmode'] ?? 0) > 0) {
            return '';
        }

        $out = '';
        $blendMode = $this->normalizeSVGBlendMode($svgstyle['mix-blend-mode']);
        $baseOpacity = $this->normalizeSVGAlphaValue($svgstyle['opacity']);
        $fillOpacity = $this->normalizeSVGAlphaValue($svgstyle['fill-opacity']);
        $fillAlpha = $baseOpacity * $fillOpacity;

        $regs = [];
        if (\preg_match('/url\([\s]*\#([^\)]*)\)/si', $svgstyle['fill'], $regs)) {
            $fillRef = $regs[1] ?? null;
            if (!\is_string($fillRef) || $fillRef === '') {
                return $out;
            }

            if (\abs($fillAlpha - $baseOpacity) > self::SVGMINFLOATDIFF) {
                $out .= $this->getSVGExtGState(null, $fillAlpha, $blendMode);
            }

            $filldef = $this->svgobjs[$soid]['defs'][$fillRef] ?? null;
            if ($filldef !== null && $filldef !== []) {
                if ($filldef['name'] === 'pattern') {
                    try {
                        $patternOut = $this->parseSVGStylePattern(
                            $soid,
                            $fillRef,
                            $posx,
                            $posy,
                            $width,
                            $height,
                            $clip_fnc,
                            $clip_par,
                        );
                    } catch (
                        \Com\Tecnick\Pdf\Page\Exception|\Com\Tecnick\Pdf\Font\Exception|\Com\Tecnick\Unicode\Exception
                    ) {
                        return $out;
                    }
                    if (\str_contains($patternOut, '/Pattern cs /')) {
                        $objstyle = $svgstyle['fill-rule'] === 'evenodd' ? 'F*' : 'F';
                        if (!\str_contains($svgstyle['objstyle'], $objstyle)) {
                            $svgstyle['objstyle'] .= $objstyle;
                        }
                    }

                    return $out . $patternOut;
                }
            }

            try {
                return $out
                . $this->parseSVGStyleGradient(
                    $soid,
                    $gradients,
                    $fillRef,
                    $posx,
                    $posy,
                    $width,
                    $height,
                    $clip_fnc,
                    $clip_par,
                );
            } catch (\Com\Tecnick\Pdf\Exception) {
                return $out;
            }
        }

        $col = $this->color->getColorObj($svgstyle['fill']);
        if ($col === null) {
            return $out;
        }

        $rgba = $col->toRgbArray();
        $fillAlphaAlpha = isset($rgba['alpha']) ? $rgba['alpha'] : 1.0;
        if ($fillAlphaAlpha < 1) {
            $fillAlpha *= $this->normalizeSVGAlphaValue($fillAlphaAlpha);
        }

        if (\abs($fillAlpha - $baseOpacity) > self::SVGMINFLOATDIFF) {
            $out .= $this->getSVGExtGState(null, $fillAlpha, $blendMode);
        }

        $objstyle = $svgstyle['fill-rule'] === 'evenodd' ? 'F*' : 'F';
        if (!\str_contains($svgstyle['objstyle'], $objstyle)) {
            $svgstyle['objstyle'] .= $objstyle;
        }

        $out .= $col->getPdfColor();

        return $out;
    }

    /**
     * Parse the SVG style clip-path.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid SVG object ID.
     * @param array<string, TSVGAttribs> $clippaths Clipping paths.
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGStyleClipPath(\XMLParser $parser, int $soid, array $clippaths = []): void
    {
        foreach ($clippaths as $cp) {
            $this->handleSVGTagStart($parser, $cp['name'], $cp['attr'], $soid, true, $cp['tm'] ?? self::TMXID);
        }
    }

    /**
     * Parse the SVG style.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid SVG object ID.
     * @param TSVGStyle $svgstyle SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     * @param float $posx X position in user units.
     * @param float $posy Y position in user units.
     * @param float $width Width in user units.
     * @param float $height Height in user units.
     * @param string $objstyle Style to return.
     * @param string $clip_fnc Optional clipping function name.
     * @param array<mixed> $clip_par Optional clipping function parameters.
     *
     * @return string the object style.
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Color\Exception
     */
    protected function parseSVGStyle(
        \XMLParser $parser,
        int $soid,
        array $svgstyle,
        array $prev_svgstyle,
        float $posx = 0,
        float $posy = 0,
        float $width = 1,
        float $height = 1,
        string &$objstyle = '',
        string $clip_fnc = '',
        array $clip_par = [],
    ): string {
        try {
            $svgobj = $this->svgobjs[$soid] ?? null;
            if ($svgobj === null) {
                return '';
            }

            $clipPaths = $svgobj['clippaths'] ?? [];
            $gradients = $svgobj['gradients'] ?? [];

            $svgstyle['opacity'] = $this->normalizeSVGAlphaValue($svgstyle['opacity']);
            if ($svgstyle['opacity'] <= 0.0) {
                return '';
            }

            $this->parseSVGStyleClipPath($parser, $soid, $clipPaths);

            $out = '';
            $out .= $this->parseSVGStyleColor($svgstyle);
            $out .= $this->parseSVGStyleMask($soid, $svgstyle);
            $out .= $this->parseSVGStyleClip($svgstyle, $posx, $posy, $width, $height);
            $out .= $this->parseSVGStyleFill(
                $soid,
                $svgstyle,
                $gradients,
                $posx,
                $posy,
                $width,
                $height,
                $clip_fnc,
                $clip_par,
            );
            $out .= $this->parseSVGStyleStroke($soid, $svgstyle);
            $out .= $this->parseSVGStyleFont($svgstyle, $prev_svgstyle);
            $out .= $this->parseSVGStyleRenderingHints($svgstyle);

            $objstyle = $svgstyle['objstyle'];

            return $out;
        } catch (\Com\Tecnick\Pdf\Page\Exception|\Com\Tecnick\Unicode\Exception) {
            return '';
        }
    }

    /**
     * Handler for the SVG character data.
     *
     * @param \XMLParser $_parser The XML parser calling the handler.
     * @param string $data Character data.
     *
     * @return void
     *
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     * @throws \Com\Tecnick\Color\Exception
     *
     * @SuppressWarnings("PHPMD.UnusedFormalParameter")
     */
    protected function handlerSVGCharacter(\XMLParser $_parser, string $data): void
    {
        $soid = (int) \array_key_last($this->svgobjs);
        if ($soid < 0 || !isset($this->svgobjs[$soid]['text'])) {
            return;
        }
        if ((int) $this->svgobjs[$soid]['charskip'] > 0) {
            return;
        }
        if ((int) $this->svgobjs[$soid]['textdepth'] < 1) {
            // Character data outside a text-content element is not rendered.
            return;
        }

        $this->svgobjs[$soid]['text'] .= $data;
    }

    /**
     * Apply the XML white space processing rules to a chunk of SVG text content.
     *
     * With the default xml:space handling the newlines are removed, the tabs are
     * turned into spaces, the leading and trailing spaces of the text element are
     * stripped and the remaining space runs are collapsed to a single space.
     * With xml:space='preserve' only the newlines and tabs become spaces.
     *
     * @param int $soid ID of the current SVG object.
     * @param string $txt Raw text content.
     * @param bool $atend True when the chunk ends the outermost text element.
     *
     * @return string
     */
    protected function normalizeSVGTextContent(int $soid, string $txt, bool $atend): string
    {
        if ($txt === '' || !isset($this->svgobjs[$soid])) {
            return $txt;
        }

        $svgobj = &$this->getSVGObjRef($soid);

        if ($svgobj['textmode']['spacepreserve'] ?? false) {
            $out = \strtr($txt, ["\r\n" => ' ', "\r" => ' ', "\n" => ' ', "\t" => ' ']);
        } else {
            $out = \strtr($txt, ["\r\n" => '', "\r" => '', "\n" => '', "\t" => ' ']);
            $out = (string) \preg_replace('/ {2,}/', ' ', $out);
            if (!($svgobj['textmode']['textstarted'] ?? false)) {
                $out = \ltrim($out, ' ');
            }
            if ($atend) {
                $out = \rtrim($out, ' ');
            }
        }

        if ($out !== '') {
            $svgobj['textmode']['textstarted'] = true;
        }

        return $out;
    }

    /**
     * Handler for the end of an SVG tag.
     *
     * @param \XMLParser $_parser The XML parser calling the handler.
     * @param string $name Name of the element for which this handler is called.
     *
     * @return void
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     *
     * @SuppressWarnings("PHPMD.UnusedFormalParameter")
     */
    protected function handleSVGTagEnd(\XMLParser $_parser, string $name): void
    {
        $name = $this->removeTagNamespace($name);

        $svgobjs = &$this->svgobjs;
        $soidkey = \array_key_last($svgobjs);
        if ($soidkey === null || !isset($svgobjs[(int) $soidkey])) {
            return;
        }
        $soid = (int) $soidkey;

        $svgobj = &$this->getSVGObjRef($soid);

        $xmldepth = (int) $svgobj['xmldepth'];
        if ($xmldepth <= 0) {
            $xmldepth = 1;
        }

        try {
            if (\in_array($name, self::SVGCHARDATASKIPTAGS, true)) {
                $svgobj['charskip'] = \max(0, (int) $svgobj['charskip'] - 1);
                return;
            }

            if (\in_array($name, self::SVGTEXTCONTENTTAGS, true)) {
                $svgobj['textdepth'] = \max(0, (int) $svgobj['textdepth'] - 1);
            }

            // Skip subtree ends for non-selected <switch> siblings.
            if (isset($svgobj['switchstack']) && $svgobj['switchstack'] !== []) {
                $switchkey = \array_key_last($svgobj['switchstack']);
                if (!isset($svgobj['switchstack'][$switchkey])) {
                    return;
                }

                $switchctx = $svgobj['switchstack'][$switchkey];
                $skipDepth = (int) $switchctx['skipdepth'];

                if ($skipDepth > 0 && $xmldepth > $skipDepth) {
                    return;
                }

                if ($skipDepth > 0 && $xmldepth === $skipDepth) {
                    $switchctx['skipdepth'] = 0;

                    $svgobj['switchstack'][$switchkey] = $switchctx;
                    return;
                }
            }

            if ($svgobj['defsmode'] && !\in_array($name, self::SVGDEFSMODEEND, true)) {
                $lastDefsId = \array_key_last($svgobj['defs']);
                if ($lastDefsId !== null) {
                    $last_svgdefs_id = $lastDefsId;
                    if (
                        isset($svgobj['defs'][$last_svgdefs_id]['child'])
                        && $svgobj['defs'][$last_svgdefs_id]['child'] !== []
                    ) {
                        foreach ($svgobj['defs'][$last_svgdefs_id]['child'] as $child) {
                            if (!(isset($child['attr']['id']) && $child['name'] === $name)) {
                                continue;
                            }

                            $closeKey = $child['attr']['id'] . '_CLOSE';

                            $svgobj['defs'][$last_svgdefs_id]['child'][$closeKey] = [
                                'name' => $name,
                                'attr' => [
                                    'closing_tag' => true,
                                    'content' => $svgobj['text'],
                                ],
                            ];
                            return;
                        }
                        if ($svgobj['defs'][$last_svgdefs_id]['name'] === $name) {
                            $closeKey = $last_svgdefs_id . '_CLOSE';

                            $svgobj['defs'][$last_svgdefs_id]['child'][$closeKey] = [
                                'name' => $name,
                                'attr' => [
                                    'closing_tag' => true,
                                    'content' => $svgobj['text'],
                                ],
                            ];
                            return;
                        }
                    }
                }
                return;
            }

            $svgobj['out'] .= match ($name) {
                'defs' => $this->parseSVGTagENDdefs($soid),
                'clipPath' => $this->parseSVGTagENDclipPath($soid),
                'svg' => $this->parseSVGTagENDsvg($soid),
                'g' => $this->parseSVGTagENDg($soid),
                'text' => $this->parseSVGTagENDtext($soid),
                'tspan' => $this->parseSVGTagENDtspan($soid),
                'textPath' => $this->parseSVGTagENDtextPath($soid),
                'symbol' => $this->parseSVGTagENDsymbol($soid),
                'marker' => $this->parseSVGTagENDmarker($soid),
                'pattern' => $this->parseSVGTagENDpattern($soid),
                'mask' => $this->parseSVGTagENDmask($soid),
                'filter' => $this->parseSVGTagENDfilter($soid),
                'a' => $this->parseSVGTagENDa($soid),
                'switch' => $this->parseSVGTagENDswitch($soid),
                default => '',
            };

            // Pop completed switch context.
            if (isset($svgobj['switchstack']) && $svgobj['switchstack'] !== [] && $name === 'switch') {
                $switchkey = \array_key_last($svgobj['switchstack']);
                if (isset($svgobj['switchstack'][$switchkey])) {
                    $switchctx = $svgobj['switchstack'][$switchkey];
                    if ($xmldepth === (int) $switchctx['depth']) {
                        unset($svgobj['switchstack'][$switchkey]);
                    }
                }
            }
        } finally {
            $svgobj['xmldepth'] = \max(0, (int) $svgobj['xmldepth'] - 1);
        }
    }

    /**
     * Parse the SVG End tag 'defs'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     */
    protected function parseSVGTagENDdefs(int $soid): string
    {
        return $this->setSVGDefsMode($soid, false);
    }

    /**
     * Parse the SVG End tag 'clipPath'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     */
    protected function parseSVGTagENDclipPath(int $soid): string
    {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        $this->svgobjs[$soid]['clipmode'] = false;
        return '';
    }

    /**
     * Parse the SVG End tag 'svg'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     */
    protected function parseSVGTagENDsvg(int $soid): string
    {
        if (!isset($this->svgobjs[$soid]['tagdepth'])) {
            return '';
        }

        if (--$this->svgobjs[$soid]['tagdepth'] <= 0) {
            return '';
        }
        return $this->parseSVGTagENDg($soid);
    }

    /**
     * Parse the SVG End tag 'g'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     */
    protected function parseSVGTagENDg(int $soid): string
    {
        if (isset($this->svgobjs[$soid]['styles'])) {
            \array_pop($this->svgobjs[$soid]['styles']);
        }

        return $this->graph->getStopTransform();
    }

    /**
     * Parse the SVG End tag 'tspan'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGTagENDtspan(int $soid): string
    {
        try {
            return $this->parseSVGTagENDtext($soid);
        } catch (\Com\Tecnick\Pdf\Page\Exception) {
            return '';
        }
    }

    /**
     * Parse the SVG End tag 'textPath'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGTagENDtextPath(int $soid): string
    {
        try {
            return $this->parseSVGTagENDtext($soid);
        } catch (\Com\Tecnick\Pdf\Page\Exception) {
            return '';
        }
    }

    /**
     * Render an SVG text run, optionally wrapped in a rotation transform.
     *
     * @param int $soid ID of the current SVG object.
     * @param string $text Text to render.
     * @param float $posx X coordinate.
     * @param float $posy Y coordinate.
     * @param float $width Forced text width.
     * @param float $strokeWidth Text stroke width.
     * @param string $txtanchor Text anchor mode.
     * @param float $rotate Rotation in degrees.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Unicode\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     */
    protected function getSVGTextRunOutput(
        int $soid,
        string $text,
        float $posx,
        float $posy,
        float $width,
        float $strokeWidth,
        string $txtanchor,
        float $rotate = 0.0,
    ): string {
        $svgobjs = $this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = $this->getSVGObjRef($soid);

        $out = '';
        if ($rotate !== 0.0) {
            $rad = \deg2rad(-$rotate);
            $cos = \cos($rad);
            $sin = \sin($rad);
            $out .= $this->graph->getStartTransform();
            $out .= $this->getOutSVGTransformation([$cos, $sin, -$sin, $cos, $posx, $posy], $soid);
            $posx = 0.0;
            $posy = 0.0;
        }

        $out .= $this->getTextLine(
            $text,
            $posx,
            $posy,
            $width,
            $strokeWidth,
            0,
            0,
            0,
            true,
            $strokeWidth > 0,
            false,
            false,
            false,
            false,
            $svgobj['textmode']['rtl'] ? 'R' : '',
            $txtanchor,
            null,
        );

        if ($rotate !== 0.0) {
            $out .= $this->graph->getStopTransform();
        }

        return $out;
    }

    /**
     * Parse the SVG End tag 'text'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGTagENDtext(int $soid): string
    {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        // The nesting counter is decreased before this handler runs, so a zero
        // value marks the end of the outermost text element.
        $svgobj['text'] = $this->normalizeSVGTextContent($soid, $svgobj['text'], (int) $svgobj['textdepth'] === 0);

        if ($svgobj['textmode']['invisible']) {
            // Per SVG spec, visibility:hidden text is invisible but still consumes layout space.
            // Advance the cursor by the text width without emitting any drawing operators.
            $txt = $svgobj['text'];
            if ($txt !== '') {
                if ($svgobj['textmode']['vertical'] ?? false) {
                    $svgobj['y'] += $this->getStringWidth($txt);
                } else {
                    $svgobj['x'] += $this->getStringWidth($txt);
                }
            }

            $svgobj['text'] = '';
            if (!$svgobj['defsmode']) {
                \array_pop($svgobj['styles']);
            }

            return '';
        }

        $curx = $svgobj['x'];
        $cury = $svgobj['y'];

        $anchor = $svgobj['textmode']['text-anchor'];
        $txtanchor = match ($anchor) {
            'end' => 'E',
            'middle' => 'M',
            default => 'S',
        };

        // Compute the Y offset for dominant-baseline / alignment-baseline.
        $baselineOffset = 0.0;
        $baselineKw = $svgobj['textmode']['baseline'] ?? 'auto';
        if ($baselineKw !== 'auto' && $baselineKw !== 'alphabetic') {
            $curfont = $this->font->getCurrentFont();
            $ascent = $this->toUnit($curfont['ascent']);
            $descent = $this->toUnit($curfont['descent']);
            $baselineOffset = match ($baselineKw) {
                'hanging' => -$ascent,
                'text-before-edge' => -$ascent,
                'middle', 'central' => -($ascent / 2.0),
                'mathematical' => -($ascent * 0.5),
                'ideographic', 'text-after-edge' => $descent,
                default => 0.0,
            };
        }

        // First-angle rotation for the text run.
        $rotate = $svgobj['textmode']['rotate'] ?? 0.0;

        // When a textPath is active, derive per-glyph x/y/angle lists
        // from the sampled path and glyph advances before rendering.
        $this->applyTextPathGlyphLayout($soid);

        // textLength adjustment.
        $textLengthTarget = $svgobj['textmode']['textlength'] ?? 0.0;
        $lengthAdjust = $svgobj['textmode']['lengthadjust'] ?? 'spacing';
        $forcedWidth = 0.0; // passed to getTextLine for spacing-only adjust
        $scaleX = 1.0; // used for spacingAndGlyphs

        if ($textLengthTarget > 0.0) {
            $actualWidth = $this->getStringWidth($svgobj['text']);
            if ($actualWidth > 0.0) {
                if ($lengthAdjust === 'spacingAndGlyphs') {
                    $scaleX = $textLengthTarget / $actualWidth;
                } else {
                    // 'spacing': pass target width to getTextLine for word/letter spacing.
                    $forcedWidth = $textLengthTarget;
                }
            }
        }

        // Multi-value x / y lists: emit one call per character when lists are present.
        $xlist = $svgobj['textmode']['xlist'] ?? [];
        $ylist = $svgobj['textmode']['ylist'] ?? [];
        $rotlist = $svgobj['textmode']['rotlist'] ?? [];
        $isVertical = $svgobj['textmode']['vertical'] ?? false;

        $out = '';

        if ($xlist !== [] || $ylist !== [] || $rotlist !== []) {
            // Emit individual character positions for multi-value coordinate lists.
            $chars = \mb_str_split($svgobj['text'], 1, 'UTF-8');
            foreach ($chars as $idx => $ch) {
                $charX = $xlist[$idx] ?? $curx;
                $charY = ($ylist[$idx] ?? $cury) + $baselineOffset;
                $charRotate = $rotlist[$idx] ?? $rotate;
                $originX = $charX;
                $originY = $charY;
                $out .= $this->getSVGTextRunOutput(
                    $soid,
                    $ch,
                    $charX,
                    $charY,
                    0,
                    $svgobj['textmode']['stroke'],
                    $txtanchor,
                    $charRotate,
                );
                if ($isVertical) {
                    $cury = $originY + $this->getStringWidth($ch);
                } else {
                    $curx = $originX + $this->getStringWidth($ch);
                }
            }
        } elseif ($isVertical) {
            // Vertical writing mode: stack glyphs along the Y axis.
            $chars = \mb_str_split($svgobj['text'], 1, 'UTF-8');
            $strokeWidth = (float) $svgobj['textmode']['stroke'];
            foreach ($chars as $ch) {
                $charX = $curx;
                $charY = $cury + $baselineOffset;
                $out .= $this->getSVGTextRunOutput($soid, $ch, $charX, $charY, 0, $strokeWidth, $txtanchor, $rotate);
                $cury += $this->getStringWidth($ch);
            }
        } else {
            $renderY = $cury + $baselineOffset;
            if ($scaleX !== 1.0) {
                $out .= $this->graph->getStartTransform();
                $out .= $this->getOutSVGTransformation([$scaleX, 0.0, 0.0, 1.0, 0.0, 0.0], $soid);
            }
            if ($rotate !== 0.0) {
                $out .= $this->getSVGTextRunOutput(
                    $soid,
                    $svgobj['text'],
                    $curx,
                    $renderY,
                    $forcedWidth,
                    $svgobj['textmode']['stroke'],
                    $txtanchor,
                    $rotate,
                );
            } else {
                $out .= $this->getSVGTextRunOutput(
                    $soid,
                    $svgobj['text'],
                    $curx,
                    $renderY,
                    $forcedWidth,
                    $svgobj['textmode']['stroke'],
                    $txtanchor,
                );
            }
            if ($scaleX !== 1.0) {
                $out .= $this->graph->getStopTransform();
            }
            // The vertical writing mode is handled by the branch above.
            $curx += $this->getStringWidth($svgobj['text']);
        }

        // Leave the cursor at the end of the rendered run so that the following
        // sibling text chunk starts where this one ends.
        $svgobj['x'] = $curx;
        $svgobj['y'] = $cury;

        $svgobj['text'] = ''; // reset text buffer
        $out .= $this->graph->getStopTransform();

        if (!$svgobj['defsmode']) {
            \array_pop($svgobj['styles']);
        }

        return $out;
    }

    /**
     * Handler for the start of an SVG tag.
     *
     * @param \XMLParser $parser The XML parser calling the handler.
     * @param string $name Name of the element for which this handler is called.
     * @param TSVGAttributes $attr Associative array with the element's attributes.
     * @param int $soid ID of the current SVG object.
     * @param bool $clipmode Clip-path mode (optional).
     * @param TTMatrix $ctm Current transformation matrix (optional).
     *
     * @return void
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     * @SuppressWarnings("PHPMD.UnusedFormalParameter")
     */
    protected function handleSVGTagStart(
        \XMLParser $parser,
        string $name,
        array $attr,
        int $soid = -1,
        bool $clipmode = false,
        array $ctm = self::TMXID, // identity matrix
    ): void {
        $svgobjs = &$this->svgobjs;
        if ($soid < 0) {
            $soid = (int) \array_key_last($svgobjs);
        }
        if (!isset($svgobjs[$soid])) {
            return;
        }

        $svgobj = &$this->getSVGObjRef($soid);

        $name = $this->removeTagNamespace($name);

        // Track absolute XML nesting depth for switch child selection.

        $svgobj['xmldepth'] = (int) $svgobj['xmldepth'] + 1;
        $xmldepth = (int) $svgobj['xmldepth'];

        if (\in_array($name, self::SVGCHARDATASKIPTAGS, true)) {
            $svgobj['charskip'] = (int) $svgobj['charskip'] + 1;
            return;
        }

        // Track the text-content element nesting: character data outside of
        // these elements is not rendered.
        if (\in_array($name, self::SVGTEXTCONTENTTAGS, true)) {
            $svgobj['textdepth'] = (int) $svgobj['textdepth'] + 1;
        }

        // Render only the first direct child of each <switch>.
        if (isset($svgobj['switchstack']) && $svgobj['switchstack'] !== []) {
            $switchkey = \array_key_last($svgobj['switchstack']);
            if (!isset($svgobj['switchstack'][$switchkey])) {
                return;
            }

            $switchctx = $svgobj['switchstack'][$switchkey];
            $skipDepth = (int) $switchctx['skipdepth'];

            if ($skipDepth > 0 && $xmldepth > $skipDepth) {
                return;
            }

            if ($xmldepth === ((int) $switchctx['depth'] + 1)) {
                if ($switchctx['selected']) {
                    $switchctx['skipdepth'] = $xmldepth;

                    $svgobj['switchstack'][$switchkey] = $switchctx;
                    return;
                }

                $switchctx['selected'] = true;

                $svgobj['switchstack'][$switchkey] = $switchctx;
            }
        }

        if ($name === 'switch') {
            $svgobj['switchstack'][] = [
                'depth' => $xmldepth,
                'selected' => false,
                'skipdepth' => 0,
            ];
        }

        if ($svgobj['clipmode']) {
            $clipKey = 'CP_' . (string) (\count($svgobj['clippaths']) + 1);
            $svgobj['clippaths'][$clipKey] = [
                'name' => $name,
                'attr' => $attr,
                'tm' => $svgobj['cliptm'],
            ];
            return;
        }

        if ($svgobj['defsmode'] && !\in_array($name, self::SVGDEFSMODESTART, true)) {
            if (isset($attr['id'])) {
                $svgobj['defs'][$attr['id']] = [
                    'name' => $name,
                    'attr' => $attr,
                ];
                return;
            }

            $lastDefsId = \array_key_last($svgobj['defs']);
            if ($lastDefsId !== null) {
                $last_svgdefs_id = $lastDefsId;
                $defsEntry = $svgobj['defs'][$last_svgdefs_id] ?? null;
                if (!\is_array($defsEntry)) {
                    return;
                }
                if (!isset($defsEntry['child'])) {
                    $defsEntry['child'] = [];
                }

                $attr['id'] = 'DF_' . (\count($defsEntry['child']) + 1);
                $defsEntry['child'][$attr['id']] = [
                    'name' => $name,
                    'attr' => $attr,
                ];
                $svgobj['defs'][$last_svgdefs_id] = $defsEntry;
                return;
            }

            return;
        }

        $svgobj['clipmode'] = $clipmode;

        // default style
        $svgstyle = self::DEFSVGSTYLE;
        if (isset($svgobj['styles'][0])) {
            $svgstyle = \array_merge(self::DEFSVGSTYLE, $svgobj['styles'][0]);
        }

        // Style of the enclosing element: the top of the stack, since only the
        // container tags ('svg', 'g' and the text tags) push their own style.
        $psid = (int) (\array_key_last($svgobj['styles']) ?? 0);
        $prev_svgstyle = self::DEFSVGSTYLE;
        if (isset($this->svgobjs[$soid]['styles'][$psid])) {
            $prev_svgstyle = \array_merge(self::DEFSVGSTYLE, $this->svgobjs[$soid]['styles'][$psid]);
        }
        $attrval = [];

        if (
            $this->svgobjs[$soid]['clipmode']
            && !isset($attr['fill'])
            && (
                !isset($attr['style'])
                || !\preg_match('/[;\"\s]{1}fill[\s]*:[\s]*([^;\"]*)/si', $attr['style'], $attrval)
            )
        ) {
            // default fill attribute for clipping
            $attr['fill'] = 'none';
        }

        if (isset($attr['style']) && $attr['style'] !== '' && $attr['style'][0] !== ';') {
            // fix style for regular expression
            $attr['style'] = ';' . $attr['style'];
        }

        foreach ($prev_svgstyle as $key => $val) {
            if (\in_array($key, self::SVGINHPROP, true)) {
                // inherit previous value
                $svgstyle[$key] = $val;
            }
            if (isset($attr[$key]) && $attr[$key] !== '') {
                // specific attribute settings
                if ($attr[$key] === 'inherit') {
                    $svgstyle[$key] = $val;
                } else {
                    $svgstyle[$key] = $attr[$key];
                }
            } elseif (isset($attr['style']) && $attr['style'] !== '') {
                // CSS style syntax
                $attrval = [];
                if (\preg_match('/[;\"\s]{1}' . $key . '[\s]*:[\s]*([^;\"]*)/si', $attr['style'], $attrval)) {
                    if (isset($attrval[1])) {
                        if ($attrval[1] === 'inherit') {
                            $svgstyle[$key] = $val;
                        } else {
                            $svgstyle[$key] = $attrval[1];
                        }
                    }
                }
            }
        }

        $tmx = $ctm;
        if (isset($attr['transform']) && $attr['transform'] !== '') {
            $tmx = $this->graph->getCtmProduct($tmx, $this->getSVGTransformMatrix($attr['transform']));
        }

        $svgstyle['transfmatrix'] = $tmx;
        $this->normalizeSVGStyleArray($svgstyle);
        $this->normalizeSVGStyleArray($prev_svgstyle);
        /** @var TSVGStyle $svgstyle */
        /** @var TSVGStyle $prev_svgstyle */

        $visibility = $svgstyle['visibility'];
        $display = $svgstyle['display'];
        $this->svgobjs[$soid]['textmode']['invisible'] =
            $visibility === 'hidden' || $visibility === 'collapse' || $display === 'none';

        // push new style
        //$this->svgobjs[$soid]['styles'][] = $svgstyle;

        // process tags

        $this->svgobjs[$soid]['out'] .= match ($name) {
            'defs' => $this->parseSVGTagSTARTdefs($soid),
            'clipPath' => $this->parseSVGTagSTARTclipPath($soid, $tmx),
            'svg' => $this->parseSVGTagSTARTsvg($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'g' => $this->parseSVGTagSTARTg($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'linearGradient' => $this->parseSVGTagSTARTlinearGradient($soid, $attr),
            'radialGradient' => $this->parseSVGTagSTARTradialGradient($soid, $attr),
            'stop' => $this->parseSVGTagSTARTstop($soid, $attr, $svgstyle),
            'path' => $this->parseSVGTagSTARTpath($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'rect' => $this->parseSVGTagSTARTrect($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'circle' => $this->parseSVGTagSTARTcircle($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'ellipse' => $this->parseSVGTagSTARTellipse($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'line' => $this->parseSVGTagSTARTline($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'polyline' => $this->parseSVGTagSTARTpolygon($parser, $soid, $attr, $svgstyle, $prev_svgstyle, true),
            'polygon' => $this->parseSVGTagSTARTpolygon($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'image' => $this->parseSVGTagSTARTimage($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'text' => $this->parseSVGTagSTARTtext($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'tspan' => $this->parseSVGTagSTARTtspan($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'textPath' => $this->parseSVGTagSTARTtextPath($parser, $soid, $attr, $svgstyle, $prev_svgstyle),
            'use' => $this->parseSVGTagSTARTuse($parser, $soid, $attr),
            'symbol' => $this->parseSVGTagSTARTsymbol($soid, $attr),
            'marker' => $this->parseSVGTagSTARTmarker($soid, $attr),
            'pattern' => $this->parseSVGTagSTARTpattern($soid, $attr),
            'mask' => $this->parseSVGTagSTARTmask($soid, $attr),
            'filter' => $this->parseSVGTagSTARTfilter($soid, $attr),
            'a' => $this->parseSVGTagSTARTa($soid, $attr),
            'switch' => $this->parseSVGTagSTARTswitch($soid),
            default => '',
        };
    }

    /**
     * Parse the SVG Start tag 'defs'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     */
    protected function parseSVGTagSTARTdefs(int $soid): string
    {
        return $this->setSVGDefsMode($soid, true);
    }

    /**
     * Return a reference to an existing SVG object entry.
     *
     * Callers must verify the key exists before invoking this helper.
     *
     * @param int $soid SVG object ID.
     *
     * @phpstan-return TSVGObj
     */
    protected function &getSVGObjRef(int $soid): array
    {
        \assert(
            isset($this->svgobjs[$soid]),
            'Existing SVG object key is required before requesting by-reference access.',
        );
        return $this->svgobjs[$soid];
    }

    /**
     * Toggle defs capture mode.
     *
     * @param int $soid ID of the current SVG object.
     * @param bool $enabled Whether defs capture is enabled.
     *
     * @return string
     */
    protected function setSVGDefsMode(int $soid, bool $enabled): string
    {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        $this->svgobjs[$soid]['defsmode'] = $enabled;
        return '';
    }

    /**
     * Register a defs-backed container and enable defs capture mode.
     *
     * @param int $soid ID of the current SVG object.
     * @param string $name SVG tag name.
     * @param TSVGAttributes $attr SVG attributes.
     *
     * @return string
     */
    protected function registerSVGDefsContainer(int $soid, string $name, array $attr): string
    {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        if (isset($attr['id'])) {
            $this->svgobjs[$soid]['defs'][$attr['id']] = [
                'name' => $name,
                'attr' => $attr,
                'child' => [],
            ];
        }

        return $this->setSVGDefsMode($soid, true);
    }

    /**
     * Parse the SVG Start tag 'clipPath'.
     *
     * @param int $soid ID of the current SVG object.
     * @param array<float> $tmx Current transformation matrix (optional).
     *
     * @return string
     */
    protected function parseSVGTagSTARTclipPath(int $soid, array $tmx = []): string
    {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);
        if ($svgobj['textmode']['invisible']) {
            return '';
        }

        $svgobj['clipmode'] = true;
        $svgobj['cliptm'] = $tmx === [] ? self::TMXID : $tmx;
        return '';
    }

    /**
     * Parse the SVG Start tag 'svg'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    protected function parseSVGTagSTARTsvg(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
    ): string {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        $svgobj['tagdepth']++;
        if ($svgobj['tagdepth'] <= 1) {
            // root SVG
            return '';
        }
        // inner SVG
        $out = '';
        \array_push($svgobj['styles'], $svgstyle);
        $out .= $this->graph->getStartTransform();
        $svgX = isset($attr['x']) ? $this->svgUnitToUnit($attr['x'], $soid) : 0.0;
        $svgY = isset($attr['y']) ? $this->svgUnitToUnit($attr['y'], $soid) : 0.0;
        $svgW = isset($attr['width']) ? $this->svgUnitToUnit($attr['width'], $soid) : 0.0;
        $svgH = isset($attr['height']) ? $this->svgUnitToUnit($attr['height'], $soid) : 0.0;
        // set x, y position using transform matrix
        $tmx = $this->graph->getCtmProduct($svgstyle['transfmatrix'], [1.0, 0.0, 0.0, 1.0, $svgX, $svgY]);
        $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
        // set clipping for width and height
        $page = $this->page->getPage();
        $posx = 0;
        $posy = 0;
        $pageWidth = $page['width'];
        $pageHeight = $page['height'];
        $width = $svgW <= 0.0 ? $pageWidth - $svgX : $svgW;
        $height = $svgH <= 0.0 ? $pageHeight - $svgY : $svgH;
        // draw clipping rect
        $out .= $this->graph->getRawRect($posx, $posy, $width, $height, 'CNZ');
        // parse viewbox, calculate extra transformation matrix
        if (!isset($attr['viewBox']) || $attr['viewBox'] === '') {
            return $out
            . $this->parseSVGStyle($parser, $soid, $svgstyle, $prev_svgstyle, $posx, $posy, $width, $height);
        }
        // Parse "min-x min-y width height" on whitespace and commas, keeping
        // decimals and negative origins intact.
        $vbvals = \preg_split('/[\s,]+/', \trim($attr['viewBox']), -1, \PREG_SPLIT_NO_EMPTY);
        if (!\is_array($vbvals) || !isset($vbvals[0], $vbvals[1], $vbvals[2], $vbvals[3])) {
            return $out
            . $this->parseSVGStyle($parser, $soid, $svgstyle, $prev_svgstyle, $posx, $posy, $width, $height);
        }
        $vbx = \floatval($vbvals[0]);
        $vby = \floatval($vbvals[1]);
        $vbw = \abs(\floatval($vbvals[2]));
        $vbh = \abs(\floatval($vbvals[3]));
        if ($vbw <= 0.0 || $vbh <= 0.0) {
            // Degenerate viewBox (zero or non-positive extent): render without
            // the viewBox transform.
            return $out
            . $this->parseSVGStyle($parser, $soid, $svgstyle, $prev_svgstyle, $posx, $posy, $width, $height);
        }
        // get aspect ratio
        $tmp = [];
        $aspectX = 'xMid';
        $aspectY = 'YMid';
        $fit = 'meet';
        if (isset($attr['preserveAspectRatio']) && $attr['preserveAspectRatio'] !== '') {
            if ($attr['preserveAspectRatio'] === 'none') {
                $fit = 'none';
            } else {
                \preg_match_all('/[a-zA-Z]+/', $attr['preserveAspectRatio'], $tmp);
                $tmp = $tmp[0] ?? [];
                $alignToken = $tmp[0] ?? null;
                $fitToken = $tmp[1] ?? null;
                if (
                    \count($tmp) === 2
                    && \is_string($alignToken)
                    && \strlen($alignToken) === 8
                    && \is_string($fitToken)
                    && \in_array($fitToken, ['meet', 'slice', 'none'], true)
                ) {
                    $aspectX = \substr($alignToken, 0, 4);
                    $aspectY = \substr($alignToken, 4, 4);
                    $fit = $fitToken;
                }
            }
        }
        $wsr = $svgW / $vbw;
        $hsr = $svgH / $vbh;
        $asx = 0;
        $asy = 0;
        if ($fit === 'meet' && $hsr < $wsr || $fit === 'slice' && $hsr > $wsr) {
            if ($aspectX === 'xMax') {
                $asx = ($vbw * ($wsr / $hsr)) - $vbw;
            }
            if ($aspectX === 'xMid') {
                $asx = (($vbw * ($wsr / $hsr)) - $vbw) / 2;
            }
            $wsr = $hsr;
        } elseif ($fit === 'meet' && $hsr > $wsr || $fit === 'slice' && $hsr < $wsr) {
            if ($aspectY === 'YMax') {
                $asy = ($vbh * ($hsr / $wsr)) - $vbh;
            }
            if ($aspectY === 'YMid') {
                $asy = (($vbh * ($hsr / $wsr)) - $vbh) / 2;
            }
            $hsr = $wsr;
        }
        $newtmx = [$wsr, 0.0, 0.0, $hsr, ($wsr * ($asx - $vbx)) - $svgX, ($hsr * ($asy - $vby)) - $svgY];
        $tmx = $this->graph->getCtmProduct($tmx, $newtmx);
        $out .= $this->getOutSVGTransformation($tmx, $soid);
        $out .= $this->parseSVGStyle($parser, $soid, $svgstyle, $prev_svgstyle, $posx, $posy, $width, $height);

        return $out;
    }

    /**
     * Parse the SVG Start tag 'g'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    protected function parseSVGTagSTARTg(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
    ): string {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        $out = '';

        \array_push($svgobj['styles'], $svgstyle);
        $out .= $this->graph->getStartTransform();
        $posx = isset($attr['x']) ? $this->svgUnitToUnit($attr['x'], $soid) : 0.0;
        $posy = isset($attr['y']) ? $this->svgUnitToUnit($attr['y'], $soid) : 0.0;
        $width = 1.0; // isset($attr['width']) ? $this->svgUnitToUnit($attr['width'], $soid) : 1.0;
        $height = 1.0; // isset($attr['height']) ? $this->svgUnitToUnit($attr['height'], $soid) : 1.0;
        $tmx = $this->graph->getCtmProduct($svgstyle['transfmatrix'], [$width, 0.0, 0.0, $height, $posx, $posy]);
        $out .= $this->getOutSVGTransformation($tmx, $soid);
        $out .= $this->parseSVGStyle($parser, $soid, $svgstyle, $prev_svgstyle, $posx, $posy, $width, $height);

        return $out;
    }

    /**
     * Parse the SVG Start tag 'linearGradient'.
     *
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     *
     * @return string
     */
    protected function parseSVGTagSTARTlinearGradient(int $soid, array $attr): string
    {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        if ($this->pdfa === 1 || $this->pdfa === 2) {
            return '';
        }

        if (!isset($attr['id'])) {
            $attr['id'] = 'GR_' . (\count($svgobj['gradients']) + 1);
        }
        $gid = $attr['id'];

        $svgobj['gradientid'] = $gid;
        $svgobj['gradients'][$gid] = [];
        $svgobj['gradients'][$gid]['type'] = 2;
        $svgobj['gradients'][$gid]['stops'] = [];
        if (isset($attr['gradientUnits'])) {
            $svgobj['gradients'][$gid]['gradientUnits'] = $attr['gradientUnits'];
        } else {
            $svgobj['gradients'][$gid]['gradientUnits'] = 'objectBoundingBox';
        }
        // $attr['spreadMethod']
        if (
            !isset($attr['x1']) && !isset($attr['y1']) && !isset($attr['x2']) && !isset($attr['y2'])
            || (
                isset($attr['x1'])
                && \substr($attr['x1'], -1) === '%'
                || isset($attr['y1'])
                && \substr($attr['y1'], -1) === '%'
                || isset($attr['x2'])
                && \substr($attr['x2'], -1) === '%'
                || isset($attr['y2'])
                && \substr($attr['y2'], -1) === '%'
            )
        ) {
            $svgobj['gradients'][$gid]['mode'] = 'percentage';
        } else {
            $svgobj['gradients'][$gid]['mode'] = 'measure';
        }
        $px1 = $attr['x1'] ?? 0.0;
        $py1 = $attr['y1'] ?? 0.0;
        $px2 = $attr['x2'] ?? 100.0;
        $py2 = $attr['y2'] ?? 0.0;
        if (isset($attr['gradientTransform'])) {
            $svgobj['gradients'][$gid]['gradientTransform'] = $this->getSVGTransformMatrix($attr['gradientTransform']);
        }
        $svgobj['gradients'][$gid]['coords'] = [$px1, $py1, $px2, $py2];
        $gradHref = $attr['xlink:href'] ?? $attr['href'] ?? '';
        if ($gradHref !== '') {
            // gradient is defined on another place
            $svgobj['gradients'][$gid]['xref'] = \substr($gradHref, 1);
        }
        return '';
    }

    /**
     * Parse the SVG Start tag 'radialGradient'.
     *
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     *
     * @return string
     */
    protected function parseSVGTagSTARTradialGradient(int $soid, array $attr): string
    {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        if ($this->pdfa === 1 || $this->pdfa === 2) {
            return '';
        }

        if (!isset($attr['id'])) {
            $attr['id'] = 'GR_' . (\count($svgobj['gradients']) + 1);
        }
        $gid = $attr['id'];

        $svgobj['gradientid'] = $gid;
        $svgobj['gradients'][$gid] = [];
        $svgobj['gradients'][$gid]['type'] = 3;
        $svgobj['gradients'][$gid]['stops'] = [];
        if (isset($attr['gradientUnits'])) {
            $svgobj['gradients'][$gid]['gradientUnits'] = $attr['gradientUnits'];
        } else {
            $svgobj['gradients'][$gid]['gradientUnits'] = 'objectBoundingBox';
        }
        // $attr['spreadMethod']
        if (
            !isset($attr['cx']) && !isset($attr['cy'])
            || (
                isset($attr['cx'])
                && \substr($attr['cx'], -1) === '%'
                || isset($attr['cy'])
                && \substr($attr['cy'], -1) === '%'
            )
        ) {
            $svgobj['gradients'][$gid]['mode'] = 'percentage';
        } elseif (isset($attr['r']) && \is_numeric($attr['r']) && $attr['r'] <= 1) {
            $svgobj['gradients'][$gid]['mode'] = 'ratio';
        } else {
            $svgobj['gradients'][$gid]['mode'] = 'measure';
        }
        $pcx = $attr['cx'] ?? 0.5;
        $pcy = $attr['cy'] ?? 0.5;
        $pfx = $attr['fx'] ?? $pcx;
        $pfy = $attr['fy'] ?? $pcy;
        $grr = $attr['r'] ?? 0.5;
        if (isset($attr['gradientTransform'])) {
            $svgobj['gradients'][$gid]['gradientTransform'] = $this->getSVGTransformMatrix($attr['gradientTransform']);
        }
        $svgobj['gradients'][$gid]['coords'] = [$pcx, $pcy, $pfx, $pfy, $grr];
        $gradHref = $attr['xlink:href'] ?? $attr['href'] ?? '';
        if ($gradHref !== '') {
            // gradient is defined on another place
            $svgobj['gradients'][$gid]['xref'] = \substr($gradHref, 1);
        }
        return '';
    }

    /**
     * Parse the SVG Start tag 'stop'.
     *
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function parseSVGTagSTARTstop(int $soid, array $attr, array $svgstyle): string
    {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        $offset = isset($attr['offset']) ? $this->svgUnitToUnit($attr['offset'], $soid) : 0.0;
        $stop_color = $svgstyle['stop-color'];
        // Normalize stop colors to hex RGB so all gradient stops share one
        // color space.
        $colobj = $this->color->getColorObj($stop_color);
        if ($colobj !== null) {
            $stop_color = $colobj->getRgbHexColor();
        }
        $opacity = \max(0.0, \min(1.0, \floatval($svgstyle['stop-opacity'])));
        $gid = $svgobj['gradientid'];

        $svgobj['gradients'][$gid]['stops'][] = [
            'offset' => $offset,
            'color' => $stop_color,
            'opacity' => $opacity,
        ];
        return '';
    }

    /**
     * Parse the SVG Start tag 'path'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGTagSTARTpath(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
    ): string {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        if ($svgobj['textmode']['invisible']) {
            return '';
        }
        if (!isset($attr['d']) || $attr['d'] === '') {
            return '';
        }

        $ptd = \trim($attr['d']);

        $posx = isset($attr['x']) ? $this->svgUnitToUnit($attr['x'], $soid) : 0.0;
        $posy = isset($attr['y']) ? $this->svgUnitToUnit($attr['y'], $soid) : 0.0;
        $width = isset($attr['width']) ? $this->svgUnitToUnit($attr['width'], $soid) : 1.0;
        $height = isset($attr['height']) ? $this->svgUnitToUnit($attr['height'], $soid) : 1.0;
        $tmx = $this->graph->getCtmProduct($svgstyle['transfmatrix'], [$width, 0.0, 0.0, $height, $posx, $posy]);

        $out = '';

        if ($svgobj['clipmode']) {
            $out .= $this->getOutSVGTransformation($tmx, $soid);
            $out .= $this->getSVGPath($soid, $ptd, 'CNZ');
            return $out;
        }

        $out .= $this->graph->getStartTransform();
        $out .= $this->getOutSVGTransformation($tmx, $soid);
        $obstyle = '';
        $out .= $this->parseSVGStyle(
            $parser,
            $soid,
            $svgstyle,
            $prev_svgstyle,
            $posx,
            $posy,
            $width,
            $height,
            $obstyle,
            'getSVGPath',
            [$soid, $ptd, 'CNZ'],
        );

        if ($obstyle !== '') {
            $out .= $this->getSVGPath($soid, $ptd, $obstyle);
        }

        $segments = $this->getSVGPathMarkerSegments($soid, $ptd);
        $out .= $this->renderSVGMarkersForSegments($parser, $soid, $svgstyle, $segments);

        $out .= $this->graph->getStopTransform();

        return $out;
    }

    /**
     * Parse the SVG Start tag 'rect'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    protected function parseSVGTagSTARTrect(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
    ): string {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        if ($svgobj['textmode']['invisible']) {
            return '';
        }
        $posx = isset($attr['x']) ? $this->svgUnitToUnit($attr['x'], $soid) : 0.0;
        $posy = isset($attr['y']) ? $this->svgUnitToUnit($attr['y'], $soid) : 0.0;
        $width = isset($attr['width']) ? $this->svgUnitToUnit($attr['width'], $soid) : 0.0;
        $height = isset($attr['height']) ? $this->svgUnitToUnit($attr['height'], $soid) : 0.0;
        $prx = isset($attr['rx']) ? $this->svgUnitToUnit($attr['rx'], $soid) : 0.0;
        $pry = isset($attr['ry']) ? $this->svgUnitToUnit($attr['ry'], $soid) : $prx;
        $out = '';
        if ($svgobj['clipmode']) {
            $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
            $out .= $this->graph->getRoundedRect($posx, $posy, $width, $height, $prx, $pry, '1111', 'CNZ');
            return $out;
        }
        $out .= $this->graph->getStartTransform();
        $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
        $obstyle = '';
        $out .= $this->parseSVGStyle(
            $parser,
            $soid,
            $svgstyle,
            $prev_svgstyle,
            $posx,
            $posy,
            $width,
            $height,
            $obstyle,
            'getRoundedRect',
            [$posx, $posy, $width, $height, $prx, $pry, '1111', 'CNZ'],
        );
        if ($obstyle !== '') {
            $out .= $this->graph->getRoundedRect($posx, $posy, $width, $height, $prx, $pry, '1111', $obstyle);
        }

        $out .= $this->graph->getStopTransform();

        return $out;
    }

    /**
     * Parse the SVG Start tag 'circle'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    protected function parseSVGTagSTARTcircle(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
    ): string {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        if ($svgobj['textmode']['invisible']) {
            return '';
        }
        $crr = isset($attr['r']) ? $this->svgUnitToUnit($attr['r'], $soid) : 0.0;
        if (isset($attr['cx'])) {
            $ctx = $this->svgUnitToUnit($attr['cx'], $soid);
        } elseif (isset($attr['x'])) {
            $ctx = $this->svgUnitToUnit($attr['x'], $soid);
        } else {
            $ctx = 0.0;
        }
        if (isset($attr['cy'])) {
            $cty = $this->svgUnitToUnit($attr['cy'], $soid);
        } elseif (isset($attr['y'])) {
            $cty = $this->svgUnitToUnit($attr['y'], $soid);
        } else {
            $cty = 0.0;
        }
        $posx = $ctx - $crr;
        $posy = $cty - $crr;
        $width = 2 * $crr;
        $height = $width;
        $out = '';
        if ($svgobj['clipmode']) {
            $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
            $out .= $this->graph->getCircle($ctx, $cty, $crr, 0, 360, 'CNZ', [], 8);
            return $out;
        }
        $out .= $this->graph->getStartTransform();
        $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
        $obstyle = '';
        $out .= $this->parseSVGStyle(
            $parser,
            $soid,
            $svgstyle,
            $prev_svgstyle,
            $posx,
            $posy,
            $width,
            $height,
            $obstyle,
            'getCircle',
            [$ctx, $cty, $crr, 0, 360, 'CNZ'],
        );
        if ($obstyle !== '') {
            $out .= $this->graph->getCircle($ctx, $cty, $crr, 0, 360, $obstyle, [], 8);
        }
        $out .= $this->graph->getStopTransform();
        return $out;
    }

    /**
     * Parse the SVG Start tag 'ellipse'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    protected function parseSVGTagSTARTellipse(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
    ): string {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        $svgobjs = &$this->svgobjs;
        $svgobj = &$this->getSVGObjRef($soid);

        if ($svgobj['textmode']['invisible']) {
            return '';
        }
        $erx = isset($attr['rx']) ? $this->svgUnitToUnit($attr['rx'], $soid) : 0.0;
        $ery = isset($attr['ry']) ? $this->svgUnitToUnit($attr['ry'], $soid) : 0.0;
        if (isset($attr['cx'])) {
            $ecx = $this->svgUnitToUnit($attr['cx'], $soid);
        } elseif (isset($attr['x'])) {
            $ecx = $this->svgUnitToUnit($attr['x'], $soid);
        } else {
            $ecx = 0.0;
        }
        if (isset($attr['cy'])) {
            $ecy = $this->svgUnitToUnit($attr['cy'], $soid);
        } elseif (isset($attr['y'])) {
            $ecy = $this->svgUnitToUnit($attr['y'], $soid);
        } else {
            $ecy = 0.0;
        }
        $posx = $ecx - $erx;
        $posy = $ecy - $ery;
        $width = 2 * $erx;
        $height = 2 * $ery;
        $out = '';
        if ($svgobj['clipmode']) {
            $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
            $out .= $this->graph->getEllipse($ecx, $ecy, $erx, $ery, 0, 0, 360, 'CNZ', [], 8);
            return $out;
        }
        $out .= $this->graph->getStartTransform();
        $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
        $obstyle = '';
        $out .= $this->parseSVGStyle(
            $parser,
            $soid,
            $svgstyle,
            $prev_svgstyle,
            $posx,
            $posy,
            $width,
            $height,
            $obstyle,
            'getEllipse',
            [$ecx, $ecy, $erx, $ery, 0, 0, 360, 'CNZ'],
        );
        if ($obstyle !== '') {
            $out .= $this->graph->getEllipse($ecx, $ecy, $erx, $ery, 0, 0, 360, $obstyle, [], 8);
        }
        $out .= $this->graph->getStopTransform();

        return $out;
    }

    /**
     * Parse the SVG Start tag 'line'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGTagSTARTline(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
    ): string {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        $svgobjs = &$this->svgobjs;
        $svgobj = &$this->getSVGObjRef($soid);

        if ($svgobj['textmode']['invisible']) {
            return '';
        }
        if ($svgobj['clipmode']) {
            return '';
        }
        $posx1 = isset($attr['x1']) ? $this->svgUnitToUnit($attr['x1'], $soid) : 0.0;
        $posy1 = isset($attr['y1']) ? $this->svgUnitToUnit($attr['y1'], $soid) : 0.0;
        $posx2 = isset($attr['x2']) ? $this->svgUnitToUnit($attr['x2'], $soid) : 0.0;
        $posy2 = isset($attr['y2']) ? $this->svgUnitToUnit($attr['y2'], $soid) : 0.0;
        $posx = $posx1;
        $posy = $posy1;
        $width = \abs($posx2 - $posx1);
        $height = \abs($posy2 - $posy1);
        $out = '';
        $out .= $this->graph->getStartTransform();
        $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
        $obstyle = '';
        $out .= $this->parseSVGStyle(
            $parser,
            $soid,
            $svgstyle,
            $prev_svgstyle,
            $posx,
            $posy,
            $width,
            $height,
            $obstyle,
            'getLine',
            [$posx1, $posy1, $posx2, $posy2],
        );
        $out .= $this->graph->getLine($posx1, $posy1, $posx2, $posy2);
        $out .= $this->parseSVGLineMarkers($parser, $soid, $svgstyle, $posx1, $posy1, $posx2, $posy2);
        $out .= $this->graph->getStopTransform();
        return $out;
    }

    /**
     * Parse marker URLs and render start/end markers for a straight line segment.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param float $startX Segment start X.
     * @param float $startY Segment start Y.
     * @param float $endX Segment end X.
     * @param float $endY Segment end Y.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGLineMarkers(
        \XMLParser $parser,
        int $soid,
        array $svgstyle,
        float $startX,
        float $startY,
        float $endX,
        float $endY,
    ): string {
        if ((int) ($this->svgobjs[$soid]['markermode'] ?? 0) > 0) {
            return '';
        }

        return $this->renderSVGMarkersForSegments($parser, $soid, $svgstyle, [[
            'x1' => $startX,
            'y1' => $startY,
            'x2' => $endX,
            'y2' => $endY,
            'angle' => \rad2deg(\atan2($endY - $startY, $endX - $startX)),
        ]]);
    }

    /**
     * Render marker-start/marker-mid/marker-end for an array of path segments.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param array<int, array{x1: float, y1: float, x2: float, y2: float, angle: float, startAngle?: float, endAngle?: float}> $segments
     *
     * @return string
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function renderSVGMarkersForSegments(
        \XMLParser $parser,
        int $soid,
        array $svgstyle,
        array $segments,
    ): string {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        if ($segments === [] || (int) ($this->svgobjs[$soid]['markermode'] ?? 0) > 0) {
            return '';
        }

        $strokeWidth = $this->svgUnitToUnit($svgstyle['stroke-width'], $soid);
        if ($strokeWidth <= 0.0) {
            $strokeWidth = 1.0;
        }

        $first = $segments[0] ?? null;
        $last = $segments[\count($segments) - 1] ?? null;
        if ($first === null || $last === null) {
            return '';
        }

        $markerAll = $svgstyle['marker'];
        $markerStartRaw = $svgstyle['marker-start'];
        $markerMidRaw = $svgstyle['marker-mid'];
        $markerEndRaw = $svgstyle['marker-end'];

        // If any specific marker anchor is explicitly set to a non-default
        // value, respect explicit 'none' on other anchors instead of falling
        // back from shorthand.
        $hasExplicitSpecific =
            $markerStartRaw !== '' && $markerStartRaw !== 'none'
            || $markerMidRaw !== '' && $markerMidRaw !== 'none'
            || $markerEndRaw !== '' && $markerEndRaw !== 'none';

        $markerStart = $this->getSVGResolvedMarker($markerStartRaw, $markerAll, !$hasExplicitSpecific);
        $markerMid = $this->getSVGResolvedMarker($markerMidRaw, $markerAll, !$hasExplicitSpecific);
        $markerEnd = $this->getSVGResolvedMarker($markerEndRaw, $markerAll, !$hasExplicitSpecific);

        $out = '';
        $out .= $this->renderSVGMarker(
            $parser,
            $soid,
            $markerStart,
            $first['x1'],
            $first['y1'],
            $first['angle'],
            $strokeWidth,
            true,
        );

        $midMarker = $markerMid;
        if ($midMarker !== '' && $midMarker !== 'none' && \count($segments) > 1) {
            for ($idx = 1, $max = \count($segments); $idx < $max; ++$idx) {
                $prev = $segments[$idx - 1] ?? [];
                $next = $segments[$idx] ?? [];
                $prevAngle = $prev['angle'] ?? 0.0;
                $nextAngle = $next['angle'] ?? 0.0;
                $prevAngleRad = \deg2rad($prevAngle);
                $nextAngleRad = \deg2rad($nextAngle);
                $vectorX = \cos($prevAngleRad) + \cos($nextAngleRad);
                $vectorY = \sin($prevAngleRad) + \sin($nextAngleRad);
                $midAngle =
                    \abs($vectorX) < self::SVGMINFLOATDIFF && \abs($vectorY) < self::SVGMINFLOATDIFF
                        ? $nextAngle
                        : \rad2deg(\atan2($vectorY, $vectorX));
                $nextX1 = $next['x1'] ?? 0.0;
                $nextY1 = $next['y1'] ?? 0.0;
                $out .= $this->renderSVGMarker(
                    $parser,
                    $soid,
                    $midMarker,
                    $nextX1,
                    $nextY1,
                    $midAngle,
                    $strokeWidth,
                    false,
                );
            }

            $closedPath =
                \abs($first['x1'] - $last['x2']) < self::SVGMINFLOATDIFF
                && \abs($first['y1'] - $last['y2']) < self::SVGMINFLOATDIFF;
            if ($closedPath) {
                $prevAngleRad = \deg2rad($last['angle']);
                $nextAngleRad = \deg2rad($first['angle']);
                $vectorX = \cos($prevAngleRad) + \cos($nextAngleRad);
                $vectorY = \sin($prevAngleRad) + \sin($nextAngleRad);
                $midAngle =
                    \abs($vectorX) < self::SVGMINFLOATDIFF && \abs($vectorY) < self::SVGMINFLOATDIFF
                        ? $first['angle']
                        : \rad2deg(\atan2($vectorY, $vectorX));
                $out .= $this->renderSVGMarker(
                    $parser,
                    $soid,
                    $midMarker,
                    $first['x1'],
                    $first['y1'],
                    $midAngle,
                    $strokeWidth,
                    false,
                );
            }
        }

        $out .= $this->renderSVGMarker(
            $parser,
            $soid,
            $markerEnd,
            $last['x2'],
            $last['y2'],
            $last['angle'],
            $strokeWidth,
            false,
        );

        return $out;
    }

    /**
     * Resolve marker shorthand fallback for marker-start/mid/end.
     *
     * @param string $specific Marker-specific value.
     * @param string $markerAll Shorthand marker value.
     * @param bool $fallbackFromNone If true, 'none' can fallback to shorthand.
     *
     * @return string
     */
    protected function getSVGResolvedMarker(string $specific, string $markerAll, bool $fallbackFromNone = true): string
    {
        if ($specific !== '') {
            if ($specific === 'none' && !$fallbackFromNone) {
                return 'none';
            }
            if ($specific !== 'none') {
                return $specific;
            }
        }

        if ($specific === 'none' && $fallbackFromNone && ($markerAll === '' || $markerAll === 'none')) {
            return $specific;
        }

        if ($markerAll !== '' && $markerAll !== 'none') {
            return $markerAll;
        }

        return $specific;
    }

    /**
     * Build drawable segment anchors from an SVG path string for marker placement.
     *
     * @param int $soid SVG object ID.
     * @param string $attrd Path data.
     *
     * @return array<
     *   int,
     *   array{x1: float, y1: float, x2: float, y2: float, angle: float, startAngle: float, endAngle: float}
     * >
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function getSVGPathMarkerSegments(int $soid, string $attrd): array
    {
        if ($attrd === '') {
            return [];
        }

        $paths = [];
        \preg_match_all('/([ACHLMQSTVZ])[\s]*+([^ACHLMQSTVZ\"]*+)/si', $attrd, $paths, PREG_SET_ORDER);
        if ($paths === []) {
            return [];
        }

        $segments = [];
        $currentX = 0.0;
        $currentY = 0.0;
        $subpathStartX = 0.0;
        $subpathStartY = 0.0;
        $prevCmd = '';
        $cp2x = 0.0;
        $cp2y = 0.0;
        $qp1x = 0.0;
        $qp1y = 0.0;

        foreach ($paths as $path) {
            $cmd = \trim($path[1] ?? '');
            if ($cmd === '') {
                continue;
            }

            $upper = \strtoupper($cmd);
            $rel = \strtolower($cmd) === $cmd;
            $rawparams = $this->getSVGPathParams($cmd, \trim($path[2] ?? ''));
            $params = [];
            foreach ($rawparams as $prv) {
                $val = $this->svgUnitToUnit($prv, $soid);
                $params[] = \abs($val) < $this->svgminunitlen ? 0.0 : $val;
            }

            $addSegment = static function (float $startX, float $startY, float $endX, float $endY) use (
                &$segments,
            ): void {
                if (\abs($endX - $startX) < self::SVGMINFLOATDIFF && \abs($endY - $startY) < self::SVGMINFLOATDIFF) {
                    return;
                }
                $segments[] = [
                    'x1' => $startX,
                    'y1' => $startY,
                    'x2' => $endX,
                    'y2' => $endY,
                    'angle' => \rad2deg(\atan2($endY - $startY, $endX - $startX)),
                    'startAngle' => \rad2deg(\atan2($endY - $startY, $endX - $startX)),
                    'endAngle' => \rad2deg(\atan2($endY - $startY, $endX - $startX)),
                ];
            };

            $setLastSegmentAngles = static function (float $startAngle, float $endAngle) use (&$segments): void {
                $last = \array_key_last($segments);
                if (!\is_int($last)) {
                    return;
                }

                $lastSegment = $segments[$last] ?? null;
                if (!\is_array($lastSegment)) {
                    return;
                }
                $lastSegment['startAngle'] = $startAngle;
                $lastSegment['endAngle'] = $endAngle;
                $segments[$last] = $lastSegment;
            };

            if ($upper === 'M') {
                for ($i = 0; ($i + 1) < \count($params); $i += 2) {
                    $nextX = ($params[$i] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $nextY = ($params[$i + 1] ?? 0.0) + ($rel ? $currentY : 0.0);
                    if ($i === 0) {
                        $currentX = $nextX;
                        $currentY = $nextY;
                        $subpathStartX = $nextX;
                        $subpathStartY = $nextY;
                    } else {
                        $addSegment($currentX, $currentY, $nextX, $nextY);
                        $currentX = $nextX;
                        $currentY = $nextY;
                    }
                }
                $prevCmd = 'M';
                continue;
            }

            if ($upper === 'L') {
                for ($i = 0; ($i + 1) < \count($params); $i += 2) {
                    $nextX = ($params[$i] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $nextY = ($params[$i + 1] ?? 0.0) + ($rel ? $currentY : 0.0);
                    $addSegment($currentX, $currentY, $nextX, $nextY);
                    $currentX = $nextX;
                    $currentY = $nextY;
                }
                $prevCmd = 'L';
                continue;
            }

            if ($upper === 'H') {
                foreach ($params as $val) {
                    $nextX = $val + ($rel ? $currentX : 0.0);
                    $addSegment($currentX, $currentY, $nextX, $currentY);
                    $currentX = $nextX;
                }
                $prevCmd = 'H';
                continue;
            }

            if ($upper === 'V') {
                foreach ($params as $val) {
                    $nextY = $val + ($rel ? $currentY : 0.0);
                    $addSegment($currentX, $currentY, $currentX, $nextY);
                    $currentY = $nextY;
                }
                $prevCmd = 'V';
                continue;
            }

            if ($upper === 'C') {
                for ($i = 0; ($i + 5) < \count($params); $i += 6) {
                    $cp1x = ($params[$i] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $cp1y = ($params[$i + 1] ?? 0.0) + ($rel ? $currentY : 0.0);
                    $cp2x = ($params[$i + 2] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $cp2y = ($params[$i + 3] ?? 0.0) + ($rel ? $currentY : 0.0);
                    $nextX = ($params[$i + 4] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $nextY = ($params[$i + 5] ?? 0.0) + ($rel ? $currentY : 0.0);
                    $addSegment($currentX, $currentY, $nextX, $nextY);
                    $setLastSegmentAngles(
                        \rad2deg(\atan2($cp1y - $currentY, $cp1x - $currentX)),
                        \rad2deg(\atan2($nextY - $cp2y, $nextX - $cp2x)),
                    );
                    $currentX = $nextX;
                    $currentY = $nextY;
                }
                $prevCmd = 'C';
                continue;
            }

            if ($upper === 'S') {
                for ($i = 0; ($i + 3) < \count($params); $i += 4) {
                    $cp1x = $currentX;
                    $cp1y = $currentY;
                    if ($prevCmd === 'C' || $prevCmd === 'S') {
                        $cp1x = (2 * $currentX) - $cp2x;
                        $cp1y = (2 * $currentY) - $cp2y;
                    }
                    $cp2x = ($params[$i] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $cp2y = ($params[$i + 1] ?? 0.0) + ($rel ? $currentY : 0.0);
                    $nextX = ($params[$i + 2] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $nextY = ($params[$i + 3] ?? 0.0) + ($rel ? $currentY : 0.0);
                    $addSegment($currentX, $currentY, $nextX, $nextY);
                    $setLastSegmentAngles(
                        \rad2deg(\atan2($cp1y - $currentY, $cp1x - $currentX)),
                        \rad2deg(\atan2($nextY - $cp2y, $nextX - $cp2x)),
                    );
                    $currentX = $nextX;
                    $currentY = $nextY;
                    // Each further set of this command mirrors the previous one.
                    $prevCmd = 'S';
                }
                $prevCmd = 'S';
                continue;
            }

            if ($upper === 'Q') {
                for ($i = 0; ($i + 3) < \count($params); $i += 4) {
                    $qp1x = ($params[$i] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $qp1y = ($params[$i + 1] ?? 0.0) + ($rel ? $currentY : 0.0);
                    $nextX = ($params[$i + 2] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $nextY = ($params[$i + 3] ?? 0.0) + ($rel ? $currentY : 0.0);
                    $addSegment($currentX, $currentY, $nextX, $nextY);
                    $setLastSegmentAngles(
                        \rad2deg(\atan2($qp1y - $currentY, $qp1x - $currentX)),
                        \rad2deg(\atan2($nextY - $qp1y, $nextX - $qp1x)),
                    );
                    $currentX = $nextX;
                    $currentY = $nextY;
                }
                $prevCmd = 'Q';
                continue;
            }

            if ($upper === 'T') {
                for ($i = 0; ($i + 1) < \count($params); $i += 2) {
                    if ($prevCmd === 'Q' || $prevCmd === 'T') {
                        $qp1x = (2 * $currentX) - $qp1x;
                        $qp1y = (2 * $currentY) - $qp1y;
                    } else {
                        $qp1x = $currentX;
                        $qp1y = $currentY;
                    }
                    $nextX = ($params[$i] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $nextY = ($params[$i + 1] ?? 0.0) + ($rel ? $currentY : 0.0);
                    $addSegment($currentX, $currentY, $nextX, $nextY);
                    $setLastSegmentAngles(
                        \rad2deg(\atan2($qp1y - $currentY, $qp1x - $currentX)),
                        \rad2deg(\atan2($nextY - $qp1y, $nextX - $qp1x)),
                    );
                    $currentX = $nextX;
                    $currentY = $nextY;
                    // Each further set of this command mirrors the previous one.
                    $prevCmd = 'T';
                }
                $prevCmd = 'T';
                continue;
            }

            if ($upper === 'A') {
                for ($i = 0; ($i + 6) < \count($params); $i += 7) {
                    $startX0 = $currentX;
                    $startY0 = $currentY;
                    $radiusXValue = $params[$i] ?? 0.0;
                    $radiusYValue = $params[$i + 1] ?? 0.0;
                    $radiusX = \max(\abs($radiusXValue), .000_000_001);
                    $radiusY = \max(\abs($radiusYValue), .000_000_001);
                    $rawXAxisRot = $rawparams[$i + 2] ?? null;
                    $xAxisRot = \is_numeric($rawXAxisRot) ? (float) $rawXAxisRot : 0.0;
                    $rawLargeArc = $rawparams[$i + 3] ?? null;
                    $largeArcFlag = \is_numeric($rawLargeArc) && (float) $rawLargeArc >= 0.5;
                    $rawSweep = $rawparams[$i + 4] ?? null;
                    $sweepFlag = \is_numeric($rawSweep) && (float) $rawSweep >= 0.5;
                    $nextX = ($params[$i + 5] ?? 0.0) + ($rel ? $currentX : 0.0);
                    $nextY = ($params[$i + 6] ?? 0.0) + ($rel ? $currentY : 0.0);
                    $addSegment($currentX, $currentY, $nextX, $nextY);
                    $samples = $this->sampleTextPathArc(
                        $startX0,
                        $startY0,
                        $radiusX,
                        $radiusY,
                        $xAxisRot,
                        $largeArcFlag,
                        $sweepFlag,
                        $nextX,
                        $nextY,
                    );

                    $startPx = $nextX;
                    $startPy = $nextY;
                    if ($samples !== []) {
                        $firstSample = $samples[0] ?? null;
                        if (\is_array($firstSample)) {
                            $startPx = $firstSample[0];
                            $startPy = $firstSample[1];
                        }
                    }
                    $startDx = $startPx - $startX0;
                    $startDy = $startPy - $startY0;
                    if (\abs($startDx) < self::SVGMINFLOATDIFF && \abs($startDy) < self::SVGMINFLOATDIFF) {
                        $startDx = $nextX - $startX0;
                        $startDy = $nextY - $startY0;
                    }

                    $endAx = $startX0;
                    $endAy = $startY0;
                    if ($samples !== []) {
                        $sampleCount = \count($samples);
                        if ($sampleCount >= 2) {
                            $endSample = $samples[$sampleCount - 2] ?? null;
                            if (\is_array($endSample)) {
                                $endAx = $endSample[0];
                                $endAy = $endSample[1];
                            }
                        }
                    }

                    $setLastSegmentAngles(
                        \rad2deg(\atan2($startDy, $startDx)),
                        \rad2deg(\atan2($nextY - $endAy, $nextX - $endAx)),
                    );
                    $currentX = $nextX;
                    $currentY = $nextY;
                }
                $prevCmd = 'A';
                continue;
            }

            if ($upper === 'Z') {
                $addSegment($currentX, $currentY, $subpathStartX, $subpathStartY);
                $currentX = $subpathStartX;
                $currentY = $subpathStartY;
                $prevCmd = 'Z';
            }
        }

        return $segments;
    }

    /**
     * Build line segments from a polygon/polyline numeric point list.
     *
     * @param array<int, float> $pset Point list (x1,y1,x2,y2,...).
     * @param bool $closed Whether to close back to the first point.
     *
     * @return array<int, array{x1: float, y1: float, x2: float, y2: float, angle: float, startAngle: float, endAngle: float}>
     */
    protected function getSVGPolylineSegments(array $pset, bool $closed = false): array
    {
        $segments = [];
        $pointCount = (int) \floor(\count($pset) / 2);
        if ($pointCount < 2) {
            return $segments;
        }

        for ($i = 0; $i < ($pointCount - 1); ++$i) {
            $startX = $pset[2 * $i] ?? 0.0;
            $startY = $pset[(2 * $i) + 1] ?? 0.0;
            $endX = $pset[2 * ($i + 1)] ?? 0.0;
            $endY = $pset[(2 * ($i + 1)) + 1] ?? 0.0;
            if (\abs($endX - $startX) < self::SVGMINFLOATDIFF && \abs($endY - $startY) < self::SVGMINFLOATDIFF) {
                continue;
            }
            $segments[] = [
                'x1' => $startX,
                'y1' => $startY,
                'x2' => $endX,
                'y2' => $endY,
                'angle' => \rad2deg(\atan2($endY - $startY, $endX - $startX)),
                'startAngle' => \rad2deg(\atan2($endY - $startY, $endX - $startX)),
                'endAngle' => \rad2deg(\atan2($endY - $startY, $endX - $startX)),
            ];
        }

        if ($closed) {
            $startX = $pset[2 * ($pointCount - 1)] ?? 0.0;
            $startY = $pset[(2 * ($pointCount - 1)) + 1] ?? 0.0;
            $endX = $pset[0] ?? 0.0;
            $endY = $pset[1] ?? 0.0;
            if (\abs($endX - $startX) >= self::SVGMINFLOATDIFF || \abs($endY - $startY) >= self::SVGMINFLOATDIFF) {
                $segments[] = [
                    'x1' => $startX,
                    'y1' => $startY,
                    'x2' => $endX,
                    'y2' => $endY,
                    'angle' => \rad2deg(\atan2($endY - $startY, $endX - $startX)),
                    'startAngle' => \rad2deg(\atan2($endY - $startY, $endX - $startX)),
                    'endAngle' => \rad2deg(\atan2($endY - $startY, $endX - $startX)),
                ];
            }
        }

        return $segments;
    }

    /**
     * Extract marker definition ID from a marker style value.
     *
     * @param string $marker Marker style value.
     *
     * @return string
     */
    protected function getSVGMarkerId(string $marker): string
    {
        if ($marker === '' || $marker === 'none') {
            return '';
        }

        $matches = [];
        if (!\preg_match('/^url\([\s]*#([^)\s]+)[\s]*\)$/', \trim($marker), $matches)) {
            return '';
        }

        return $matches[1] ?? '';
    }

    /**
     * Render a marker definition at a given anchor point.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param string $marker Marker style value.
     * @param float $anchorX Anchor X.
     * @param float $anchorY Anchor Y.
     * @param float $segmentAngle Segment angle in degrees.
     * @param float $strokeWidth Segment stroke width.
     * @param bool $isStart Whether this is a start marker.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function renderSVGMarker(
        \XMLParser $parser,
        int $soid,
        string $marker,
        float $anchorX,
        float $anchorY,
        float $segmentAngle,
        float $strokeWidth,
        bool $isStart,
    ): string {
        $markerId = $this->getSVGMarkerId($marker);
        if ($markerId === '') {
            return '';
        }

        $svgObj = $this->svgobjs[$soid] ?? [];
        $defs = $svgObj['defs'] ?? [];
        $markerdef = $defs[$markerId] ?? null;
        if (!\is_array($markerdef)) {
            return '';
        }
        if ($markerdef['name'] !== 'marker') {
            return '';
        }

        $markerAttr = $markerdef['attr'];

        $mkw = isset($markerAttr['markerWidth']) ? $this->svgUnitToUnit($markerAttr['markerWidth'], $soid) : 3.0;
        $mkh = isset($markerAttr['markerHeight']) ? $this->svgUnitToUnit($markerAttr['markerHeight'], $soid) : 3.0;

        $vbx = 0.0;
        $vby = 0.0;
        $vbw = $mkw;
        $vbh = $mkh;
        if (isset($markerAttr['viewBox']) && $markerAttr['viewBox'] !== '') {
            $vals = \preg_split('/[\s,]+/', \trim($markerAttr['viewBox']), -1, \PREG_SPLIT_NO_EMPTY);
            if (\is_array($vals) && \count($vals) >= 4) {
                $vbx = $this->svgUnitToUnit($vals[0], $soid);
                $vby = $this->svgUnitToUnit($vals[1] ?? '0', $soid);
                $vbw = \abs($this->svgUnitToUnit($vals[2] ?? '0', $soid));
                $vbh = \abs($this->svgUnitToUnit($vals[3] ?? '0', $soid));
            }
        }

        $refX = $this->resolveSVGMarkerRefCoordinate($markerAttr['refX'] ?? '', $vbx, $vbw, $soid);
        $refY = $this->resolveSVGMarkerRefCoordinate($markerAttr['refY'] ?? '', $vby, $vbh, $soid);
        $markerUnits = $markerAttr['markerUnits'] ?? 'strokeWidth';
        // The stroke width is a scale factor here, so it is taken in the SVG
        // user space the marker viewport lives in.
        $markerScale = $markerUnits === 'userSpaceOnUse' ? 1.0 : $this->unitToSVGUnit($strokeWidth, $soid);

        $viewScaleX = 1.0;
        $viewScaleY = 1.0;
        $viewOffsetX = 0.0;
        $viewOffsetY = 0.0;
        $aspectRaw = $markerAttr['preserveAspectRatio'] ?? 'xMidYMid meet';
        $aspectFit = 'meet';
        $aspectX = 'xMid';
        $aspectY = 'YMid';
        if (\trim($aspectRaw) === 'none') {
            $aspectFit = 'none';
        } else {
            $aspectMatches = [];
            \preg_match_all('/[a-zA-Z]+/', $aspectRaw, $aspectMatches);
            $tokens = \array_values($aspectMatches[0] ?? []);
            if ($tokens !== []) {
                if (\strtolower($tokens[0]) === 'defer') {
                    \array_shift($tokens);
                }

                if ($tokens !== [] && \strlen($tokens[0]) === 8) {
                    $alignToken = $tokens[0];
                    $aspectX = \substr($alignToken, 0, 4);
                    $aspectY = \substr($alignToken, 4, 4);
                    if (isset($tokens[1]) && \in_array($tokens[1], ['meet', 'slice', 'none'], true)) {
                        $aspectFit = $tokens[1];
                    }
                } elseif ($tokens !== [] && \in_array($tokens[0], ['meet', 'slice', 'none'], true)) {
                    $aspectFit = $tokens[0];
                }
            }
        }

        if ($vbw > 0.0 && $vbh > 0.0 && $mkw > 0.0 && $mkh > 0.0) {
            if ($aspectFit === 'none') {
                $viewScaleX = $mkw / $vbw;
                $viewScaleY = $mkh / $vbh;
            } else {
                $scaleX = $mkw / $vbw;
                $scaleY = $mkh / $vbh;
                $scale = $aspectFit === 'slice' ? \max($scaleX, $scaleY) : \min($scaleX, $scaleY);
                $viewScaleX = $scale;
                $viewScaleY = $scale;
                $scaledW = $vbw * $scale;
                $scaledH = $vbh * $scale;
                $viewOffsetX = match ($aspectX) {
                    'xMax' => $mkw - $scaledW,
                    'xMid' => ($mkw - $scaledW) / 2.0,
                    default => 0.0,
                };
                $viewOffsetY = match ($aspectY) {
                    'YMax' => $mkh - $scaledH,
                    'YMid' => ($mkh - $scaledH) / 2.0,
                    default => 0.0,
                };
            }
        }

        $orient = \trim($markerAttr['orient'] ?? '0');
        $angle = 0.0;
        if ($orient === 'auto' || $orient === 'auto-start-reverse') {
            $angle = $segmentAngle;
            if ($isStart && $orient === 'auto-start-reverse') {
                $angle += 180.0;
            }
        } else {
            $omatch = [];
            if (\preg_match('/^([+-]?\d+(?:\.\d+)?)(deg)?$/i', $orient, $omatch) === 1) {
                if (isset($omatch[1]) && \is_numeric($omatch[1])) {
                    $angle = (float) $omatch[1];
                }
            }
        }

        $rad = \deg2rad($angle);
        $cos = \cos($rad);
        $sin = \sin($rad);

        $transformMatrix = $this->graph->getCtmProduct([1.0, 0.0, 0.0, 1.0, $anchorX, $anchorY], [
            $cos,
            $sin,
            -$sin,
            $cos,
            0.0,
            0.0,
        ]);
        $transformMatrix = $this->graph->getCtmProduct($transformMatrix, [
            1.0,
            0.0,
            0.0,
            1.0,
            $viewOffsetX,
            $viewOffsetY,
        ]);
        $transformMatrix = $this->graph->getCtmProduct($transformMatrix, [
            $markerScale * $viewScaleX,
            0.0,
            0.0,
            $markerScale * $viewScaleY,
            0.0,
            0.0,
        ]);
        $transformMatrix = $this->graph->getCtmProduct($transformMatrix, [1.0, 0.0, 0.0, 1.0, -$vbx, -$vby]);
        $transformMatrix = $this->graph->getCtmProduct($transformMatrix, [1.0, 0.0, 0.0, 1.0, -$refX, -$refY]);

        // The offsets above are lengths in user units while the emitted matrix
        // is read as SVG user units, so the translation is converted back.
        $transformMatrix[4] = $this->unitToSVGUnit($transformMatrix[4], $soid);
        $transformMatrix[5] = $this->unitToSVGUnit($transformMatrix[5], $soid);

        $out = $this->graph->getStartTransform();
        $out .= $this->getOutSVGTransformation($transformMatrix, $soid);

        // Prevent marker content from recursively emitting nested markers.

        $this->svgobjs[$soid]['markermode'] = (int) ($this->svgobjs[$soid]['markermode'] ?? 0) + 1;

        // The replayed children append to the object output buffer, so their
        // stream is moved here to keep it inside the marker transform.
        $prevOut = $this->svgobjs[$soid]['out'];
        $prevLen = \strlen($prevOut);

        try {
            if (isset($markerdef['child']) && $markerdef['child'] !== []) {
                foreach ($markerdef['child'] as $child) {
                    if (isset($child['attr']['closing_tag'])) {
                        $this->handleSVGTagEnd($parser, $child['name']);
                    } else {
                        /** @var TSVGAttributes $childAttr */
                        $childAttr = $child['attr'];
                        $this->handleSVGTagStart($parser, $child['name'], $childAttr, $soid);
                    }
                }
            }
        } finally {
            $this->svgobjs[$soid]['markermode'] = \max(0, (int) $this->svgobjs[$soid]['markermode'] - 1);
        }

        $currOut = $this->svgobjs[$soid]['out'];
        if (\strlen($currOut) > $prevLen) {
            $out .= \substr($currOut, $prevLen);
            $this->svgobjs[$soid]['out'] = $prevOut;
        }

        $out .= $this->graph->getStopTransform();
        return $out;
    }

    /**
     * Resolve marker refX/refY values, including percentage coordinates.
     *
     * @param string $raw Ref attribute value.
     * @param float $viewBoxMin ViewBox minimum coordinate (x or y).
     * @param float $viewBoxSize ViewBox size (width or height).
     * @param int $soid SVG object ID.
     *
     * @return float
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function resolveSVGMarkerRefCoordinate(
        string $raw,
        float $viewBoxMin,
        float $viewBoxSize,
        int $soid,
    ): float {
        $raw = \trim($raw);
        if ($raw === '') {
            return 0.0;
        }

        if (\str_ends_with($raw, '%')) {
            $pct = \substr($raw, 0, -1);
            if (!\is_numeric($pct)) {
                return 0.0;
            }
            $pct = (float) $pct;
            return $viewBoxMin + (($pct / 100.0) * $viewBoxSize);
        }

        return $this->svgUnitToUnit($raw, $soid);
    }

    /**
     * Parse the SVG Start tag 'polygon'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGTagSTARTpolygon(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
        bool $isPolyline = false,
    ): string {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        $svgobjs = &$this->svgobjs;
        $svgobj = &$this->getSVGObjRef($soid);

        if ($svgobj['textmode']['invisible']) {
            return '';
        }
        $attrpoints = isset($attr['points']) && $attr['points'] !== '' ? \trim($attr['points']) : '0 0';
        // note that point may use a complex syntax not covered here
        $points = \preg_split('/[\,\s]+/si', $attrpoints);
        if (!\is_array($points) || \count($points) < 4) {
            return '';
        }
        $pset = [];
        $xmin = self::SVGMAXVAL;
        $xmax = 0.0;
        $ymin = self::SVGMAXVAL;
        $ymax = 0.0;
        foreach ($points as $key => $val) {
            $pset[$key] = $this->svgUnitToUnit($val, $soid);
            if (($key % 2) === 0) {
                // X coordinate
                $xmin = \min($xmin, $pset[$key]);
                $xmax = \max($xmax, $pset[$key]);
            } else {
                // Y coordinate
                $ymin = \min($ymin, $pset[$key]);
                $ymax = \max($ymax, $pset[$key]);
            }
        }
        $posx = $xmin;
        $posy = $ymin;
        $width = $xmax - $xmin;
        $height = $ymax - $ymin;
        $out = '';
        if ($svgobj['clipmode']) {
            $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
            $out .= $this->graph->getPolygon($pset, 'CNZ');
            return $out;
        }
        $out .= $this->graph->getStartTransform();
        $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
        $obstyle = '';
        $out .= $this->parseSVGStyle(
            $parser,
            $soid,
            $svgstyle,
            $prev_svgstyle,
            $posx,
            $posy,
            $width,
            $height,
            $obstyle,
            'getPolygon',
            [$pset, 'CNZ'],
        );
        if ($obstyle !== '') {
            $out .= $this->graph->getPolygon($pset, $obstyle);
        }

        $segments = $this->getSVGPolylineSegments($pset, !$isPolyline);
        $out .= $this->renderSVGMarkersForSegments($parser, $soid, $svgstyle, $segments);

        $out .= $this->graph->getStopTransform();
        return $out;
    }

    /**
     * Parse the SVG Start tag 'image'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    protected function parseSVGTagSTARTimage(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
    ): string {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        $svgobjs = &$this->svgobjs;
        $svgobj = &$this->getSVGObjRef($soid);

        if ($svgobj['textmode']['invisible']) {
            return '';
        }
        if ($svgobj['clipmode']) {
            return '';
        }
        // SVG 2 uses plain 'href'; fall back from xlink:href for compatibility.
        $img = $attr['xlink:href'] ?? $attr['href'] ?? '';
        if ($img === '') {
            return '';
        }
        $posx = isset($attr['x']) ? $this->svgUnitToUnit($attr['x'], $soid) : 0.0;
        $posy = isset($attr['y']) ? $this->svgUnitToUnit($attr['y'], $soid) : 0.0;
        $width = isset($attr['width']) ? $this->svgUnitToUnit($attr['width'], $soid) : 0.0;
        $height = isset($attr['height']) ? $this->svgUnitToUnit($attr['height'], $soid) : 0.0;
        $out = '';
        $out .= $this->graph->getStartTransform();
        $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
        $out .= $this->parseSVGStyle($parser, $soid, $svgstyle, $prev_svgstyle, $posx, $posy, $width, $height);
        if (
            'svg' === \strtolower(\trim(\pathinfo(
                ($purl = \parse_url($img, PHP_URL_PATH)) ? $purl : '',
                PATHINFO_EXTENSION,
            )))
        ) {
            try {
                $child = $this->addSVG(
                    $img,
                    $posx,
                    $posy,
                    $width,
                    $height,
                    0.0,
                    $this->svgobjs[$soid]['markupresources'] ?? false,
                );
            } catch (Exception $e) {
                return '';
            }

            $this->svgobjs[$soid]['child'][] = $child;
            return $out;
        }
        $match = [];
        if (\preg_match('/^data:image\/[^;]+;base64,/', $img, $match) === 1) {
            // embedded image encoded as base64
            $matchPrefix = $match[0] ?? '';
            $raw = \base64_decode(\substr($img, \strlen($matchPrefix)), true);
            if ($raw === false) {
                return $out;
            }
            $img = '@' . $raw;
        }

        if (
            isset($this->svgobjs[$soid]['dir'])
            && $this->svgobjs[$soid]['dir'] !== ''
            && ($img[0] === '.' || \basename($img) === $img)
        ) {
            // replace relative path with full server path
            $img = $this->svgobjs[$soid]['dir'] . '/' . $img;
        }

        try {
            $imgid = $this->svgobjs[$soid]['markupresources'] ?? false
                ? $this->addMarkupImage($img)
                : $this->image->add($img);
        } catch (\Throwable) {
            return '';
        }
        // Honour preserveAspectRatio for raster images.
        $renderX = $posx;
        $renderY = $posy;
        $renderW = $width;
        $renderH = $height;
        $par = $attr['preserveAspectRatio'] ?? 'xMidYMid meet';
        if ($par !== 'none' && $width > 0.0 && $height > 0.0) {
            try {
                $imgdim = $this->image->getImageDimensionsByKey($this->image->getKey($img));
                $intrW = (float) $imgdim['width'];
                $intrH = (float) $imgdim['height'];
                if ($intrW > 0.0 && $intrH > 0.0) {
                    $parTokens = [];
                    \preg_match_all('/[a-zA-Z]+/', $par, $parTokens);
                    $parTokens = \array_values($parTokens[0] ?? []);
                    $fit = \count($parTokens) >= 2 ? $parTokens[\count($parTokens) - 1] ?? 'meet' : 'meet';
                    $scaleW = $width / $intrW;
                    $scaleH = $height / $intrH;
                    $scale = $fit === 'slice' ? \max($scaleW, $scaleH) : \min($scaleW, $scaleH);
                    $scaledW = $intrW * $scale;
                    $scaledH = $intrH * $scale;
                    $alignStr = \count($parTokens) >= 2 ? $parTokens[0] : 'xMidYMid';
                    $offX = match (\substr($alignStr, 0, 4)) {
                        'xMax' => $width - $scaledW,
                        'xMid' => ($width - $scaledW) / 2.0,
                        default => 0.0,
                    };
                    $offY = match (\substr($alignStr, 4, 4)) {
                        'YMax' => $height - $scaledH,
                        'YMid' => ($height - $scaledH) / 2.0,
                        default => 0.0,
                    };
                    $renderX = $posx + $offX;
                    $renderY = $posy + $offY;
                    $renderW = $scaledW;
                    $renderH = $scaledH;
                }
            } catch (\Throwable $e) {
                // Image metadata unavailable; use original dimensions unchanged.
                unset($e);
            }
        }
        $out .= $this->image->getSetImage(
            $imgid,
            $renderX,
            $renderY,
            $renderW,
            $renderH,
            $this->page->getPage()['height'],
        );
        $out .= $this->graph->getStopTransform();
        return $out;
    }

    /**
     * Parse the SVG Start tag 'text'.
     * Basic support only.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     * @param bool $is_tspan True if the tag is 'tspan'.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    protected function parseSVGTagSTARTtext(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
        bool $is_tspan = false,
    ): string {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        $svgobjs = &$this->svgobjs;
        $svgobj = &$this->getSVGObjRef($soid);

        $out = '';
        $svgobj['text'] = $this->normalizeSVGTextContent($soid, $svgobj['text'], false);
        if ($svgobj['text'] !== '') {
            // Flush the text accumulated between an outer <text> start and this
            // nested <text>/<tspan> start: emit the text-line operator and clear
            // the buffer. The outer transform and the styles stack are left to
            // the matching </text> end handler.
            $anchor = $svgobj['textmode']['text-anchor'];
            $txtanchor = match ($anchor) {
                'end' => 'E',
                'middle' => 'M',
                default => 'S',
            };
            if (!$svgobj['textmode']['invisible']) {
                $out .= $this->getTextLine(
                    $svgobj['text'],
                    $svgobj['x'],
                    $svgobj['y'],
                    0,
                    $svgobj['textmode']['stroke'],
                    0,
                    0,
                    0,
                    true,
                    $svgobj['textmode']['stroke'] > 0,
                    false,
                    false,
                    false,
                    false,
                    $svgobj['textmode']['rtl'] ? 'R' : '',
                    $txtanchor,
                    null,
                );
            }

            // Advance the cursor by the text width: invisible text also
            // consumes layout space.
            if ($svgobj['textmode']['vertical'] ?? false) {
                $svgobj['y'] += $this->getStringWidth($svgobj['text']);
            } else {
                $svgobj['x'] += $this->getStringWidth($svgobj['text']);
            }

            $svgobj['text'] = '';
        }

        if ($svgobj['textmode']['invisible']) {
            return $out;
        }

        $this->normalizeSVGStyleArray($svgstyle);
        $this->normalizeSVGStyleArray($prev_svgstyle);
        /** @var TSVGStyle $svgstyle */
        /** @var TSVGStyle $prev_svgstyle */

        \array_push($svgobj['styles'], $svgstyle);
        $posx = 0.0;
        $posy = 0.0;
        if (isset($attr['x'])) {
            $posx = $this->svgUnitToUnit($attr['x'], $soid);
        } elseif ($is_tspan) {
            $posx = $svgobj['x'];
        }
        if (isset($attr['dx'])) {
            $posx += $this->svgUnitToUnit($attr['dx'], $soid);
        }
        if (isset($attr['y'])) {
            $posy = $this->svgUnitToUnit($attr['y'], $soid);
        } elseif ($is_tspan) {
            $posy = $svgobj['y'];
        }
        if (isset($attr['dy'])) {
            $posy += $this->svgUnitToUnit($attr['dy'], $soid);
        }
        $svgstyle['text-color'] = $svgstyle['fill'];
        $svgobj['text'] = '';
        if (isset($attr['xml:space'])) {
            $svgobj['textmode']['spacepreserve'] = \trim($attr['xml:space']) === 'preserve';
        } elseif (!$is_tspan) {
            // A tspan inherits the xml:space value of the enclosing text element.
            $svgobj['textmode']['spacepreserve'] = false;
        }
        if (!$is_tspan) {
            $svgobj['textmode']['textstarted'] = false;
        }
        $svgobj['textmode']['text-anchor'] = $svgstyle['text-anchor'];
        $direction = $svgstyle['direction'];
        $svgobj['textmode']['rtl'] = $direction === 'rtl';
        $wmode = $svgstyle['writing-mode'];
        $svgobj['textmode']['vertical'] = \str_starts_with($wmode, 'tb') || \str_starts_with($wmode, 'vertical');
        $svgobj['textmode']['rotate'] = $this->getSVGGlyphOrientationRotation(
            $svgstyle,
            $svgobj['textmode']['vertical'] ?? false,
        );
        if ($svgstyle['stroke'] !== 'none' && $svgstyle['stroke-width'] > 0) {
            $svgobj['textmode']['stroke'] = $this->svgUnitToUnit($svgstyle['stroke-width'], $soid);
        } else {
            $svgobj['textmode']['stroke'] = 0.0;
        }

        // Dominant-baseline / alignment-baseline Y offset.
        $svgobj['textmode']['baseline'] = $svgstyle['dominant-baseline'];

        // textLength and lengthAdjust.
        $svgobj['textmode']['textlength'] = isset($attr['textLength'])
            ? $this->svgUnitToUnit($attr['textLength'], $soid)
            : 0.0;
        $svgobj['textmode']['lengthadjust'] = $attr['lengthAdjust'] ?? 'spacing';

        // Parse the rotate list; the first angle is the run fallback.
        $svgobj['textmode']['rotlist'] = [];
        if (isset($attr['rotate']) && $attr['rotate'] !== '') {
            $rotvals = \preg_split('/[\s,]+/', \trim($attr['rotate']), -1, \PREG_SPLIT_NO_EMPTY);
            if (\is_array($rotvals) && $rotvals !== []) {
                if (\is_numeric($rotvals[0])) {
                    $svgobj['textmode']['rotate'] = (float) $rotvals[0];
                }
                foreach ($rotvals as $rotval) {
                    if (!\is_numeric($rotval)) {
                        continue;
                    }

                    $svgobj['textmode']['rotlist'][] = (float) $rotval;
                }
            }
        }

        // Multi-value x / y coordinate lists.
        $svgobj['textmode']['xlist'] = [];
        $svgobj['textmode']['ylist'] = [];
        $svgobj['textmode']['textpathpoints'] = [];
        $svgobj['textmode']['textpathoffset'] = 0.0;
        $svgobj['textmode']['textpathmethod'] = 'align';
        $svgobj['textmode']['textpathspacing'] = 'exact';
        if (isset($attr['x']) && \str_contains($attr['x'], ' ')) {
            $xvals = \preg_split('/[\s,]+/', \trim($attr['x']), -1, \PREG_SPLIT_NO_EMPTY);
            if (\is_array($xvals)) {
                foreach ($xvals as $xv) {
                    $svgobj['textmode']['xlist'][] = $this->svgUnitToUnit($xv, $soid);
                }
            }
        }
        if (isset($attr['y']) && \str_contains($attr['y'], ' ')) {
            $yvals = \preg_split('/[\s,]+/', \trim($attr['y']), -1, \PREG_SPLIT_NO_EMPTY);
            if (\is_array($yvals)) {
                foreach ($yvals as $yv) {
                    $svgobj['textmode']['ylist'][] = $this->svgUnitToUnit($yv, $soid);
                }
            }
        }
        $out .= $this->graph->getStartTransform();
        $out .= $this->getOutSVGTransformation($svgstyle['transfmatrix'], $soid);
        $out .= $this->parseSVGStyle($parser, $soid, $svgstyle, $prev_svgstyle, $posx, $posy, 1, 1);
        $svgobj['x'] = $posx;
        $svgobj['y'] = $posy;
        return $out;
    }

    /**
     * Parse the SVG Start tag 'tspan'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGTagSTARTtspan(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
    ): string {
        return $this->parseSVGTagSTARTtext($parser, $soid, $attr, $svgstyle, $prev_svgstyle, true);
    }

    /**
     * Resolve textPath reference to a sequence of points.
     *
     * @param int $soid ID of the current SVG object.
     * @param string $href Reference URI (typically '#id').
     *
     * @return array<int, array{0: float, 1: float}>|null
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function getTextPathPoints(int $soid, string $href): ?array
    {
        $pathDef = $this->resolveTextPathDef($soid, $href);
        if ($pathDef === null) {
            return null;
        }

        return $this->getTextPathPointsFromDef($soid, $pathDef['name'], $pathDef['attr']);
    }

    /**
     * Resolve textPath href to a defs entry.
     *
     * @param int $soid ID of the current SVG object.
     * @param string $href Reference URI (typically '#id').
     *
     * @return array{name: string, attr: TSVGAttributes}|null
     */
    protected function resolveTextPathDef(int $soid, string $href): ?array
    {
        if ($href === '' || $href[0] !== '#') {
            return null;
        }

        $pathId = \substr($href, 1);
        $defs = $this->svgobjs[$soid]['defs'] ?? [];
        if ($pathId === '' || !\array_key_exists($pathId, $defs)) {
            return null;
        }

        $def = null;
        foreach ($defs as $key => $value) {
            if ($key !== $pathId) {
                continue;
            }

            $def = $value;
            break;
        }

        if (!\is_array($def)) {
            return null;
        }

        if ($def['name'] === '' || $def['attr'] === []) {
            return null;
        }

        return [
            'name' => $def['name'],
            'attr' => $def['attr'],
        ];
    }

    /**
     * Resolve shape-specific points for textPath layout.
     *
     * @param int $soid ID of the current SVG object.
     * @param string $defName Defs element name.
     * @param TSVGAttributes $defAttr Defs element attributes.
     *
     * @return array<int, array{0: float, 1: float}>|null
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function getTextPathPointsFromDef(int $soid, string $defName, array $defAttr): ?array
    {
        if ($defName === 'line') {
            $startX = $this->svgUnitToUnit($defAttr['x1'] ?? '0', $soid);
            $startY = $this->svgUnitToUnit($defAttr['y1'] ?? '0', $soid);
            $endX = $this->svgUnitToUnit($defAttr['x2'] ?? '0', $soid);
            $endY = $this->svgUnitToUnit($defAttr['y2'] ?? '0', $soid);
            return [[$startX, $startY], [$endX, $endY]];
        }

        if ($defName === 'polyline' || $defName === 'polygon') {
            $attrPoints = $defAttr['points'] ?? '';
            $points = \preg_split('/[\,\s]+/si', \trim($attrPoints), -1, \PREG_SPLIT_NO_EMPTY);
            if (!\is_array($points) || \count($points) < 4) {
                return null;
            }
            $ptlist = [];
            for ($idx = 0; ($idx + 1) < \count($points); $idx += 2) {
                $ptlist[] = [
                    $this->svgUnitToUnit($points[$idx] ?? '0', $soid),
                    $this->svgUnitToUnit($points[$idx + 1] ?? '0', $soid),
                ];
            }
            if ($defName === 'polygon') {
                $firstPoint = $ptlist[0] ?? [0.0, 0.0];
                $ptlist[] = [$firstPoint[0], $firstPoint[1]];
            }
            return \count($ptlist) >= 2 ? $ptlist : null;
        }

        if ($defName === 'path') {
            return $this->getTextPathPointsFromPathData($soid, $defAttr['d'] ?? '');
        }

        return null;
    }

    /**
     * Parse path data into sampled points for textPath layout.
     *
     * @param int $soid ID of the current SVG object.
     * @param string $pathData Path d attribute.
     *
     * @return array<int, array{0: float, 1: float}>|null
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function getTextPathPointsFromPathData(int $soid, string $pathData): ?array
    {
        $tokenMatch = [];
        \preg_match_all('/[MmLlHhVvCcSsQqTtAaZz]|-?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/', $pathData, $tokenMatch);
        $tokens = \array_values($tokenMatch[0] ?? []);
        if ($tokens === []) {
            return null;
        }

        $ptlist = [];
        $curX = 0.0;
        $curY = 0.0;
        $subX = 0.0;
        $subY = 0.0;
        $command = '';
        $lastCurveCtrlX = 0.0;
        $lastCurveCtrlY = 0.0;
        $lastQuadCtrlX = 0.0;
        $lastQuadCtrlY = 0.0;
        $hasCurveCtrl = false;
        $hasQuadCtrl = false;
        $tokCount = \count($tokens);
        $idx = 0;

        while ($idx < $tokCount) {
            $token = $tokens[$idx] ?? '';
            if (\preg_match('/^[A-Za-z]$/', $token) === 1) {
                $command = $token;
                ++$idx;
                if ($command === 'Z' || $command === 'z') {
                    if ($ptlist !== []) {
                        $ptlist[] = [$subX, $subY];
                        $curX = $subX;
                        $curY = $subY;
                        $hasCurveCtrl = false;
                        $hasQuadCtrl = false;
                    }
                }
                continue;
            }

            if ($command === '') {
                ++$idx;
                continue;
            }

            $isRel = \ctype_lower($command);
            $cmd = \strtolower($command);

            if ($cmd === 'm' || $cmd === 'l' || $cmd === 't') {
                if (($idx + 1) >= $tokCount) {
                    break;
                }
                $t0 = $tokens[$idx] ?? '';
                $t1 = $tokens[$idx + 1] ?? '';
                $endX = $this->svgUnitToUnit($t0, $soid);
                $endY = $this->svgUnitToUnit($t1, $soid);
                if ($isRel) {
                    $endX += $curX;
                    $endY += $curY;
                }
                if ($cmd === 't') {
                    $ctrlX = $curX;
                    $ctrlY = $curY;
                    if ($hasQuadCtrl) {
                        $ctrlX = (2.0 * $curX) - $lastQuadCtrlX;
                        $ctrlY = (2.0 * $curY) - $lastQuadCtrlY;
                    }
                    $samples = $this->sampleTextPathQuadratic($curX, $curY, $ctrlX, $ctrlY, $endX, $endY, 12);
                    foreach ($samples as $point) {
                        $ptlist[] = $point;
                    }
                    $lastQuadCtrlX = $ctrlX;
                    $lastQuadCtrlY = $ctrlY;
                    $hasQuadCtrl = true;
                    $hasCurveCtrl = false;
                } else {
                    $ptlist[] = [$endX, $endY];
                    $hasCurveCtrl = false;
                    $hasQuadCtrl = false;
                }
                $curX = $endX;
                $curY = $endY;
                if ($cmd === 'm') {
                    $subX = $curX;
                    $subY = $curY;
                    $command = $isRel ? 'l' : 'L';
                }
                $idx += 2;
                continue;
            }

            if ($cmd === 'h') {
                $t0 = $tokens[$idx] ?? '';
                $valX = $this->svgUnitToUnit($t0, $soid);
                $curX = $isRel ? $curX + $valX : $valX;
                $ptlist[] = [$curX, $curY];
                $hasCurveCtrl = false;
                $hasQuadCtrl = false;
                ++$idx;
                continue;
            }

            if ($cmd === 'v') {
                $t0 = $tokens[$idx] ?? '';
                $valY = $this->svgUnitToUnit($t0, $soid);
                $curY = $isRel ? $curY + $valY : $valY;
                $ptlist[] = [$curX, $curY];
                $hasCurveCtrl = false;
                $hasQuadCtrl = false;
                ++$idx;
                continue;
            }

            if ($cmd === 'c') {
                if (($idx + 5) >= $tokCount) {
                    break;
                }
                $t0 = $tokens[$idx] ?? '';
                $t1 = $tokens[$idx + 1] ?? '';
                $t2 = $tokens[$idx + 2] ?? '';
                $t3 = $tokens[$idx + 3] ?? '';
                $t4 = $tokens[$idx + 4] ?? '';
                $t5 = $tokens[$idx + 5] ?? '';
                $ctrl1X = $this->svgUnitToUnit($t0, $soid);
                $ctrl1Y = $this->svgUnitToUnit($t1, $soid);
                $ctrl2X = $this->svgUnitToUnit($t2, $soid);
                $ctrl2Y = $this->svgUnitToUnit($t3, $soid);
                $endX = $this->svgUnitToUnit($t4, $soid);
                $endY = $this->svgUnitToUnit($t5, $soid);
                if ($isRel) {
                    $ctrl1X += $curX;
                    $ctrl1Y += $curY;
                    $ctrl2X += $curX;
                    $ctrl2Y += $curY;
                    $endX += $curX;
                    $endY += $curY;
                }
                $samples = $this->sampleTextPathCubic(
                    $curX,
                    $curY,
                    $ctrl1X,
                    $ctrl1Y,
                    $ctrl2X,
                    $ctrl2Y,
                    $endX,
                    $endY,
                    12,
                );
                foreach ($samples as $point) {
                    $ptlist[] = $point;
                }
                $curX = $endX;
                $curY = $endY;
                $lastCurveCtrlX = $ctrl2X;
                $lastCurveCtrlY = $ctrl2Y;
                $hasCurveCtrl = true;
                $hasQuadCtrl = false;
                $idx += 6;
                continue;
            }

            if ($cmd === 's') {
                if (($idx + 3) >= $tokCount) {
                    break;
                }
                $ctrl1X = $curX;
                $ctrl1Y = $curY;
                if ($hasCurveCtrl) {
                    $ctrl1X = (2.0 * $curX) - $lastCurveCtrlX;
                    $ctrl1Y = (2.0 * $curY) - $lastCurveCtrlY;
                }
                $t0 = $tokens[$idx] ?? '';
                $t1 = $tokens[$idx + 1] ?? '';
                $t2 = $tokens[$idx + 2] ?? '';
                $t3 = $tokens[$idx + 3] ?? '';
                $ctrl2X = $this->svgUnitToUnit($t0, $soid);
                $ctrl2Y = $this->svgUnitToUnit($t1, $soid);
                $endX = $this->svgUnitToUnit($t2, $soid);
                $endY = $this->svgUnitToUnit($t3, $soid);
                if ($isRel) {
                    $ctrl2X += $curX;
                    $ctrl2Y += $curY;
                    $endX += $curX;
                    $endY += $curY;
                }
                $samples = $this->sampleTextPathCubic(
                    $curX,
                    $curY,
                    $ctrl1X,
                    $ctrl1Y,
                    $ctrl2X,
                    $ctrl2Y,
                    $endX,
                    $endY,
                    12,
                );
                foreach ($samples as $point) {
                    $ptlist[] = $point;
                }
                $curX = $endX;
                $curY = $endY;
                $lastCurveCtrlX = $ctrl2X;
                $lastCurveCtrlY = $ctrl2Y;
                $hasCurveCtrl = true;
                $hasQuadCtrl = false;
                $idx += 4;
                continue;
            }

            if ($cmd === 'q') {
                if (($idx + 3) >= $tokCount) {
                    break;
                }
                $t0 = $tokens[$idx] ?? '';
                $t1 = $tokens[$idx + 1] ?? '';
                $t2 = $tokens[$idx + 2] ?? '';
                $t3 = $tokens[$idx + 3] ?? '';
                $ctrlX = $this->svgUnitToUnit($t0, $soid);
                $ctrlY = $this->svgUnitToUnit($t1, $soid);
                $endX = $this->svgUnitToUnit($t2, $soid);
                $endY = $this->svgUnitToUnit($t3, $soid);
                if ($isRel) {
                    $ctrlX += $curX;
                    $ctrlY += $curY;
                    $endX += $curX;
                    $endY += $curY;
                }
                $samples = $this->sampleTextPathQuadratic($curX, $curY, $ctrlX, $ctrlY, $endX, $endY, 12);
                foreach ($samples as $point) {
                    $ptlist[] = $point;
                }
                $curX = $endX;
                $curY = $endY;
                $lastQuadCtrlX = $ctrlX;
                $lastQuadCtrlY = $ctrlY;
                $hasQuadCtrl = true;
                $hasCurveCtrl = false;
                $idx += 4;
                continue;
            }

            if ($cmd === 'a') {
                if (($idx + 6) >= $tokCount) {
                    break;
                }
                $t0 = $tokens[$idx] ?? '';
                $t1 = $tokens[$idx + 1] ?? '';
                $t2 = $tokens[$idx + 2] ?? '';
                $t3 = $tokens[$idx + 3] ?? '';
                $t4 = $tokens[$idx + 4] ?? '';
                $t5 = $tokens[$idx + 5] ?? '';
                $t6 = $tokens[$idx + 6] ?? '';
                $radiusX = $this->svgUnitToUnit($t0, $soid);
                $radiusY = $this->svgUnitToUnit($t1, $soid);
                $xAxisRot = \is_numeric($t2) ? (float) $t2 : 0.0;
                $largeArcFlag = \is_numeric($t3) && (float) $t3 >= 0.5;
                $sweepFlag = \is_numeric($t4) && (float) $t4 >= 0.5;
                $endX = $this->svgUnitToUnit($t5, $soid);
                $endY = $this->svgUnitToUnit($t6, $soid);
                if ($isRel) {
                    $endX += $curX;
                    $endY += $curY;
                }
                $samples = $this->sampleTextPathArc(
                    $curX,
                    $curY,
                    $radiusX,
                    $radiusY,
                    $xAxisRot,
                    $largeArcFlag,
                    $sweepFlag,
                    $endX,
                    $endY,
                );
                foreach ($samples as $point) {
                    $ptlist[] = $point;
                }
                $curX = $endX;
                $curY = $endY;
                if ($samples === []) {
                    $ptlist[] = [$curX, $curY];
                }
                $hasCurveCtrl = false;
                $hasQuadCtrl = false;
                $idx += 7;
                continue;
            }

            $hasCurveCtrl = false;
            $hasQuadCtrl = false;
            ++$idx;
        }

        return \count($ptlist) >= 2 ? $ptlist : null;
    }

    /**
     * Sample a quadratic bezier into a sequence of points.
     *
     * @return array<int, array{0: float, 1: float}>
     */
    protected function sampleTextPathQuadratic(
        float $startX,
        float $startY,
        float $ctrlX,
        float $ctrlY,
        float $endX,
        float $endY,
        int $steps,
    ): array {
        $points = [];
        $stepCount = \max(2, $steps);
        for ($step = 1; $step <= $stepCount; ++$step) {
            $param = (float) $step / (float) $stepCount;
            $inv = 1.0 - $param;
            $pointX = ($inv * $inv * $startX) + (2.0 * $inv * $param * $ctrlX) + ($param * $param * $endX);
            $pointY = ($inv * $inv * $startY) + (2.0 * $inv * $param * $ctrlY) + ($param * $param * $endY);
            $points[] = [$pointX, $pointY];
        }
        return $points;
    }

    /**
     * Sample a cubic bezier into a sequence of points.
     *
     * @return array<int, array{0: float, 1: float}>
     */
    protected function sampleTextPathCubic(
        float $startX,
        float $startY,
        float $ctrl1X,
        float $ctrl1Y,
        float $ctrl2X,
        float $ctrl2Y,
        float $endX,
        float $endY,
        int $steps,
    ): array {
        $points = [];
        $stepCount = \max(2, $steps);
        for ($step = 1; $step <= $stepCount; ++$step) {
            $param = (float) $step / (float) $stepCount;
            $inv = 1.0 - $param;
            $inv2 = $inv * $inv;
            $inv3 = $inv2 * $inv;
            $par2 = $param * $param;
            $par3 = $par2 * $param;
            $pointX =
                ($inv3 * $startX) + (3.0 * $inv2 * $param * $ctrl1X) + (3.0 * $inv * $par2 * $ctrl2X) + ($par3 * $endX);
            $pointY =
                ($inv3 * $startY) + (3.0 * $inv2 * $param * $ctrl1Y) + (3.0 * $inv * $par2 * $ctrl2Y) + ($par3 * $endY);
            $points[] = [$pointX, $pointY];
        }
        return $points;
    }

    /**
     * Sample an elliptical arc segment into a sequence of points.
     *
     * @return array<int, array{0: float, 1: float}>
     */
    protected function sampleTextPathArc(
        float $startX,
        float $startY,
        float $radiusX,
        float $radiusY,
        float $xAxisRotation,
        bool $largeArcFlag,
        bool $sweepFlag,
        float $endX,
        float $endY,
    ): array {
        if (\abs($startX - $endX) < self::SVGMINFLOATDIFF && \abs($startY - $endY) < self::SVGMINFLOATDIFF) {
            return [];
        }

        $radiusX = \abs($radiusX);
        $radiusY = \abs($radiusY);
        if ($radiusX <= self::SVGMINFLOATDIFF || $radiusY <= self::SVGMINFLOATDIFF) {
            return [[$endX, $endY]];
        }

        $phi = \deg2rad(\fmod($xAxisRotation, 360.0));
        $cosPhi = \cos($phi);
        $sinPhi = \sin($phi);

        $deltaX = ($startX - $endX) / 2.0;
        $deltaY = ($startY - $endY) / 2.0;
        $xPrime = ($cosPhi * $deltaX) + ($sinPhi * $deltaY);
        $yPrime = (-$sinPhi * $deltaX) + ($cosPhi * $deltaY);

        $rx2 = $radiusX * $radiusX;
        $ry2 = $radiusY * $radiusY;
        $xp2 = $xPrime * $xPrime;
        $yp2 = $yPrime * $yPrime;

        $lambda = ($xp2 / $rx2) + ($yp2 / $ry2);
        if ($lambda > 1.0) {
            $scale = \sqrt($lambda);
            $radiusX *= $scale;
            $radiusY *= $scale;
            $rx2 = $radiusX * $radiusX;
            $ry2 = $radiusY * $radiusY;
        }

        $sign = $largeArcFlag === $sweepFlag ? -1.0 : 1.0;
        $numerator = ($rx2 * $ry2) - ($rx2 * $yp2) - ($ry2 * $xp2);
        $denominator = ($rx2 * $yp2) + ($ry2 * $xp2);
        $coef = 0.0;
        if ($denominator > self::SVGMINFLOATDIFF) {
            $coef = $sign * \sqrt(\max(0.0, $numerator / $denominator));
        }

        $centerPrimeX = $coef * (($radiusX * $yPrime) / $radiusY);
        $centerPrimeY = $coef * -(($radiusY * $xPrime) / $radiusX);

        $centerX = ($cosPhi * $centerPrimeX) - ($sinPhi * $centerPrimeY) + (($startX + $endX) / 2.0);
        $centerY = ($sinPhi * $centerPrimeX) + ($cosPhi * $centerPrimeY) + (($startY + $endY) / 2.0);

        $unitStartX = ($xPrime - $centerPrimeX) / $radiusX;
        $unitStartY = ($yPrime - $centerPrimeY) / $radiusY;
        $unitEndX = (-$xPrime - $centerPrimeX) / $radiusX;
        $unitEndY = (-$yPrime - $centerPrimeY) / $radiusY;

        $thetaStart = $this->getArcVectorAngle(1.0, 0.0, $unitStartX, $unitStartY);
        $thetaDelta = $this->getArcVectorAngle($unitStartX, $unitStartY, $unitEndX, $unitEndY);

        if (!$sweepFlag && $thetaDelta > 0.0) {
            $thetaDelta -= 2.0 * \M_PI;
        } elseif ($sweepFlag && $thetaDelta < 0.0) {
            $thetaDelta += 2.0 * \M_PI;
        }

        $segmentCount = \max(4, (int) \ceil(\abs($thetaDelta) / (\M_PI / 12.0)));
        $points = [];
        for ($seg = 1; $seg <= $segmentCount; ++$seg) {
            $ratio = (float) $seg / (float) $segmentCount;
            $theta = $thetaStart + ($thetaDelta * $ratio);
            $cosTheta = \cos($theta);
            $sinTheta = \sin($theta);
            $pointX = $centerX + ($cosPhi * $radiusX * $cosTheta) - ($sinPhi * $radiusY * $sinTheta);
            $pointY = $centerY + ($sinPhi * $radiusX * $cosTheta) + ($cosPhi * $radiusY * $sinTheta);
            $points[] = [$pointX, $pointY];
        }

        return $points;
    }

    /**
     * Return signed angle between two vectors.
     */
    protected function getArcVectorAngle(
        float $vectorStartX,
        float $vectorStartY,
        float $vectorEndX,
        float $vectorEndY,
    ): float {
        $dot = ($vectorStartX * $vectorEndX) + ($vectorStartY * $vectorEndY);
        $det = ($vectorStartX * $vectorEndY) - ($vectorStartY * $vectorEndX);
        return \atan2($det, $dot);
    }

    /**
     * Get total polyline length from point list.
     *
     * @param array<int, array{0: float, 1: float}> $points
     */
    protected function getTextPathLength(array $points): float
    {
        $length = 0.0;
        for ($idx = 1; $idx < \count($points); ++$idx) {
            $pt0 = $points[$idx - 1] ?? [0.0, 0.0];
            $pt1 = $points[$idx] ?? [0.0, 0.0];
            $deltaX = ($pt1[0] ?? 0.0) - ($pt0[0] ?? 0.0);
            $deltaY = ($pt1[1] ?? 0.0) - ($pt0[1] ?? 0.0);
            $length += \sqrt(($deltaX * $deltaX) + ($deltaY * $deltaY));
        }
        return $length;
    }

    /**
     * Interpolate the point and angle at a given offset along the polyline.
     *
     * @param array<int, array{0: float, 1: float}> $points
     * @return array{0: float, 1: float, 2: float}|null (x, y, angleDeg)
     */
    protected function getTextPathPointAtOffset(array $points, float $offset): ?array
    {
        if (\count($points) < 2) {
            return null;
        }

        $total = $this->getTextPathLength($points);
        if ($total <= 0.0) {
            return null;
        }

        $remaining = \max(0.0, \min($offset, $total));
        for ($idx = 1; $idx < \count($points); ++$idx) {
            $pt0 = $points[$idx - 1] ?? [0.0, 0.0];
            $pt1 = $points[$idx] ?? [0.0, 0.0];
            $startX = $pt0[0] ?? 0.0;
            $startY = $pt0[1] ?? 0.0;
            $endX = $pt1[0] ?? 0.0;
            $endY = $pt1[1] ?? 0.0;
            $deltaX = $endX - $startX;
            $deltaY = $endY - $startY;
            $segLength = \sqrt(($deltaX * $deltaX) + ($deltaY * $deltaY));
            if ($segLength <= 0.0) {
                continue;
            }
            if ($remaining <= $segLength) {
                $ratio = $remaining / $segLength;
                $pointX = $startX + ($deltaX * $ratio);
                $pointY = $startY + ($deltaY * $ratio);
                $angle = \rad2deg(\atan2($deltaY, $deltaX));
                return [$pointX, $pointY, $angle];
            }
            $remaining -= $segLength;
        }

        $last = $points[\count($points) - 1] ?? [0.0, 0.0];
        $prev = $points[\count($points) - 2] ?? [0.0, 0.0];
        $angle = \rad2deg(\atan2(($last[1] ?? 0.0) - ($prev[1] ?? 0.0), ($last[0] ?? 0.0) - ($prev[0] ?? 0.0)));
        return [$last[0] ?? 0.0, $last[1] ?? 0.0, $angle];
    }

    /**
     * Parse the SVG Start tag 'textPath'.
     *
     * @param \XMLParser $parser The XML parser.
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     * @param TSVGStyle $svgstyle Current SVG style.
     * @param TSVGStyle $prev_svgstyle Previous SVG style.
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     */
    protected function parseSVGTagSTARTtextPath(
        \XMLParser $parser,
        int $soid,
        array $attr,
        array $svgstyle,
        array $prev_svgstyle,
    ): string {
        $textPathAttr = $attr;
        $href = $attr['xlink:href'] ?? $attr['href'] ?? '';
        $startOffsetRaw = $attr['startOffset'] ?? '0';

        $points = $this->getTextPathPoints($soid, $href);
        if ($points !== null) {
            $pathLength = $this->getTextPathLength($points);
            $startOffset = 0.0;

            if (\str_contains($startOffsetRaw, '%')) {
                $startOffset = ($pathLength * \floatval(\str_replace('%', '', $startOffsetRaw))) / 100.0;
            } elseif ($startOffsetRaw !== '') {
                $startOffset = $this->svgUnitToUnit($startOffsetRaw, $soid);
            }

            if ($pathLength > 0.0) {
                $pathPoint = $this->getTextPathPointAtOffset($points, $startOffset);
                if ($pathPoint !== null) {
                    $textPathAttr['x'] = (string) $pathPoint[0];
                    $textPathAttr['y'] = (string) $pathPoint[1];
                    if (!isset($textPathAttr['rotate']) || $textPathAttr['rotate'] === '') {
                        $textPathAttr['rotate'] = (string) $pathPoint[2];
                    }
                }
            }
        }

        if (isset($textPathAttr['xlink:href'])) {
            unset($textPathAttr['xlink:href']);
        }
        if (isset($textPathAttr['href'])) {
            unset($textPathAttr['href']);
        }

        $out = $this->parseSVGTagSTARTtext($parser, $soid, $textPathAttr, $svgstyle, $prev_svgstyle, true);

        if ($points !== null) {
            $pathLength = $this->getTextPathLength($points);
            $startOffset = 0.0;
            if ($pathLength > 0.0) {
                if (\str_contains($startOffsetRaw, '%')) {
                    $startOffset = ($pathLength * \floatval(\str_replace('%', '', $startOffsetRaw))) / 100.0;
                } elseif ($startOffsetRaw !== '') {
                    $startOffset = $this->svgUnitToUnit($startOffsetRaw, $soid);
                }
            }

            $this->svgobjs[$soid]['textmode']['textpathpoints'] = $points;

            $this->svgobjs[$soid]['textmode']['textpathoffset'] = $startOffset;

            $this->svgobjs[$soid]['textmode']['textpathmethod'] = $attr['method'] ?? 'align';

            $this->svgobjs[$soid]['textmode']['textpathspacing'] = $attr['spacing'] ?? 'exact';
        }

        return $out;
    }

    /**
     * Build per-glyph layout arrays for an active textPath run.
     *
     * `method="stretch"` scales glyph advances to fill available path length.
     * `spacing="auto"` adjusts inter-glyph gaps to consume the available path.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function applyTextPathGlyphLayout(int $soid): void
    {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return;
        }
        $svgobj = &$this->getSVGObjRef($soid);

        $text = $svgobj['text'];
        if ($text === '') {
            return;
        }

        $textPathPoints = $svgobj['textmode']['textpathpoints'] ?? [];
        if (\count($textPathPoints) < 2) {
            return;
        }

        $textPathLength = $this->getTextPathLength($textPathPoints);
        $startOffset = $svgobj['textmode']['textpathoffset'] ?? 0.0;
        $availableLength = \max(0.0, $textPathLength - $startOffset);
        $pathMethod = $svgobj['textmode']['textpathmethod'] ?? 'align';
        $pathSpacing = $svgobj['textmode']['textpathspacing'] ?? 'exact';
        $chars = \mb_str_split($text, 1, 'UTF-8');
        $charCount = \count($chars);
        if ($charCount === 0) {
            return;
        }

        $advances = [];
        $baseAdvance = 0.0;
        foreach ($chars as $charGlyph) {
            $charAdvance = $this->getStringWidth($charGlyph);
            $advances[] = $charAdvance;
            $baseAdvance += $charAdvance;
        }

        if ($pathMethod === 'stretch' && $baseAdvance > 0.0 && $availableLength > 0.0) {
            $stretchScale = $availableLength / $baseAdvance;
            foreach ($advances as $key => $charAdvance) {
                $advances[$key] = $charAdvance * $stretchScale;
            }
            $baseAdvance = $availableLength;
        }

        $gapAdjust = 0.0;
        if ($pathSpacing === 'auto' && $charCount > 1 && $availableLength > 0.0) {
            $remainingGap = $availableLength - $baseAdvance;
            if ($remainingGap > 0.0) {
                $gapAdjust = $remainingGap / (float) ($charCount - 1);
            }
        }

        $xcoords = [];
        $ycoords = [];
        $angles = [];
        $pathOffset = $startOffset;

        foreach ($chars as $charIndex => $charGlyph) {
            $pathPoint = $this->getTextPathPointAtOffset($textPathPoints, $pathOffset);
            if ($pathPoint === []) {
                break;
            }
            $xcoords[] = $pathPoint[0] ?? $pathOffset;
            $ycoords[] = $pathPoint[1] ?? 0.0;
            $angles[] = $pathPoint[2] ?? 0.0;
            $pathOffset += $advances[$charIndex] ?? $this->getStringWidth($charGlyph);
            if ($charIndex < ($charCount - 1)) {
                $pathOffset += $gapAdjust;
            }
        }

        if ($xcoords !== [] && $ycoords !== []) {
            $svgobj['textmode']['xlist'] = $xcoords;

            $svgobj['textmode']['ylist'] = $ycoords;

            $svgobj['textmode']['rotlist'] = $angles;

            $svgobj['x'] = $xcoords[0] ?? $svgobj['x'];

            $svgobj['y'] = $ycoords[0];
            if ((!isset($svgobj['textmode']['rotate']) || $svgobj['textmode']['rotate'] === 0.0) && isset($angles[0])) {
                $svgobj['textmode']['rotate'] = $angles[0];
            }
        }
    }

    /**
     * Parse the SVG Start tag 'symbol'.
     *
     * A <symbol> is captured into defs and expanded only when referenced by
     * <use>. The defsmode flag is set here so child elements are stored in the
     * defs array instead of being rendered.
     *
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     *
     * @return string
     */
    protected function parseSVGTagSTARTsymbol(int $soid, array $attr): string
    {
        return $this->registerSVGDefsContainer($soid, 'symbol', $attr);
    }

    /**
     * Parse the SVG End tag 'symbol'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     */
    protected function parseSVGTagENDsymbol(int $soid): string
    {
        return $this->setSVGDefsMode($soid, false);
    }

    /**
     * Parse the SVG Start tag 'marker'.
     *
     * Markers are reusable defs objects referenced by marker-start/mid/end.
     * This handler captures marker metadata and enables defs-child buffering.
     *
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     *
     * @return string
     */
    protected function parseSVGTagSTARTmarker(int $soid, array $attr): string
    {
        return $this->registerSVGDefsContainer($soid, 'marker', $attr);
    }

    /**
     * Parse the SVG End tag 'marker'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     */
    protected function parseSVGTagENDmarker(int $soid): string
    {
        return $this->setSVGDefsMode($soid, false);
    }

    /**
     * Parse the SVG Start tag 'pattern'.
     *
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     *
     * @return string
     */
    protected function parseSVGTagSTARTpattern(int $soid, array $attr): string
    {
        return $this->registerSVGDefsContainer($soid, 'pattern', $attr);
    }

    /**
     * Parse the SVG End tag 'pattern'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     */
    protected function parseSVGTagENDpattern(int $soid): string
    {
        return $this->setSVGDefsMode($soid, false);
    }

    /**
     * Parse the SVG Start tag 'mask'.
     *
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     *
     * @return string
     */
    protected function parseSVGTagSTARTmask(int $soid, array $attr): string
    {
        return $this->registerSVGDefsContainer($soid, 'mask', $attr);
    }

    /**
     * Parse the SVG End tag 'mask'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     */
    protected function parseSVGTagENDmask(int $soid): string
    {
        return $this->setSVGDefsMode($soid, false);
    }

    /**
     * Parse the SVG Start tag 'filter'.
     *
     * Filters are not rendered. A filter="url(#f)" reference on a shape is
     * ignored and the shape is drawn without the filter effect.
     *
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     *
     * @return string Empty string.
     *
     * @SuppressWarnings("PHPMD.UnusedFormalParameter")
     */
    protected function parseSVGTagSTARTfilter(int $soid, array $attr): string
    {
        // Enter defs-mode so the fe* children captured by SVGCHARDATASKIPTAGS are
        // discarded, and register a placeholder in defs for filter="url(#id)".
        return $this->registerSVGDefsContainer($soid, 'filter', $attr);
    }

    /**
     * Parse the SVG End tag 'filter'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string Empty string.
     */
    protected function parseSVGTagENDfilter(int $soid): string
    {
        return $this->setSVGDefsMode($soid, false);
    }

    /**
     * Parse the SVG Start tag 'a' (hyperlink).
     *
     * Records the link URL and bounding origin.  Child elements are rendered
     * normally; the annotation is emitted on the matching </a> end tag.
     * The bounding box of arbitrary child shapes is not tracked: the URI
     * annotation is emitted at the current cursor position with a placeholder
     * size on </a>.
     *
     * @param int $soid ID of the current SVG object.
     * @param TSVGAttributes $attr SVG attributes.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     *
     * @SuppressWarnings("PHPMD.UnusedFormalParameter")
     */
    protected function parseSVGTagSTARTa(int $soid, array $attr): string
    {
        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        $href = $attr['xlink:href'] ?? $attr['href'] ?? '';
        if ($href === '') {
            return '';
        }

        $this->svgobjs[$soid]['textmode']['linkhref'] = $href;

        $this->svgobjs[$soid]['textmode']['linkx'] = $this->svgobjs[$soid]['x'];

        $this->svgobjs[$soid]['textmode']['linky'] = $this->svgobjs[$soid]['y'];
        return '';
    }

    /**
     * Parse the SVG End tag 'a'.
     *
     * @param int $soid ID of the current SVG object.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     *
     * @SuppressWarnings("PHPMD.UnusedFormalParameter")
     */
    protected function parseSVGTagENDa(int $soid): string
    {
        $svgobjs = &$this->svgobjs;
        if (!isset($svgobjs[$soid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);

        $href = $svgobj['textmode']['linkhref'] ?? '';
        if ($href === '') {
            return '';
        }

        $startX = $svgobj['textmode']['linkx'] ?? $svgobj['x'];
        $startY = $svgobj['textmode']['linky'] ?? $svgobj['y'];
        $endX = $svgobj['x'];
        $endY = $svgobj['y'];

        $deltaX = \abs($endX - $startX);
        $deltaY = \abs($endY - $startY);
        $font = $this->font->getCurrentFont();
        $fontHeight = $this->toUnit($font['size']);
        if ($fontHeight <= 0.0) {
            $fontHeight = 1.0;
        }

        if ($svgobj['textmode']['vertical'] ?? false) {
            $width = \max($deltaX, $this->getStringWidth('M'));
            $height = \max($deltaY, $fontHeight);
        } else {
            $width = \max($deltaX, $this->getStringWidth(' '));
            $height = \max($deltaY, $fontHeight);
        }

        $posx = \min($startX, $endX);
        $posy = \min($startY, $endY);
        /** @var int|string|null $lnkid */
        $lnkid = $this->setLink($posx, $posy, $width, $height, $href);
        if (\is_int($lnkid)) {
            $this->page->addAnnotRef($lnkid, $this->page->getPageID());
        }

        $this->svgobjs[$soid]['textmode']['linkhref'] = '';

        $this->svgobjs[$soid]['textmode']['linkx'] = $svgobj['x'];

        $this->svgobjs[$soid]['textmode']['linky'] = $svgobj['y'];
        return '';
    }

    /**
     * Parse the SVG Start tag 'switch'.
     *
     * In a static PDF context we treat all feature requirements as satisfied
     * and simply render the first child (by letting the parser continue normally).
     *
     * @param int $_soid ID of the current SVG object.
     *
     * @return string
     *
     * @SuppressWarnings("PHPMD.UnusedFormalParameter")
     * @phpstan-param TSVGAttributes $_attr
     */
    protected function parseSVGTagSTARTswitch(int $_soid, array $_attr = []): string
    {
        return '';
    }

    /**
     * Parse the SVG End tag 'switch'.
     *
     * @param int $_soid ID of the current SVG object.
     *
     * @return string
     *
     * @SuppressWarnings("PHPMD.UnusedFormalParameter")
     */
    protected function parseSVGTagENDswitch(int $_soid): string
    {
        return '';
    }

    /**
     * Normalize a potentially malformed SVG node name to a safe string.
     */
    protected function normalizeSVGNodeName(mixed $name): string
    {
        return \is_string($name) ? $name : '';
    }

    /**
     * Parse the SVG Start tag 'use'.
     *
     * @param \XMLParser $parser The XML parser calling the handler.
     * @param int $soid ID of the current SVG object.
     * @phpstan-param TSVGAttributes $attr
     *
     * @return string
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function parseSVGTagSTARTuse(\XMLParser $parser, int $soid, array $attr): string
    {
        // SVG 2 uses plain 'href'; fall back from xlink:href for compatibility.
        $href = $attr['xlink:href'] ?? $attr['href'] ?? '';
        if ($href === '') {
            return '';
        }

        if (!isset($this->svgobjs[$soid])) {
            return '';
        }

        $svgdefid = \substr($href, 1);
        if (!isset($this->svgobjs[$soid]['defs'][$svgdefid])) {
            return '';
        }
        $svgobj = &$this->getSVGObjRef($soid);
        $defs = $svgobj['defs'];
        $use = $defs[$svgdefid] ?? null;
        if (!\is_array($use)) {
            return '';
        }

        // Cycle and depth guard: a target already being expanded, or a chain
        // deeper than SVGMAXUSEDEPTH, is refused.
        if (isset($svgobj['usechain'][$svgdefid]) || \count($svgobj['usechain']) >= self::SVGMAXUSEDEPTH) {
            return '';
        }
        $this->svgobjs[$soid]['usechain'][$svgdefid] = true;

        try {
            return $this->expandSVGUse($parser, $soid, $attr, $use);
        } finally {
            unset($this->svgobjs[$soid]['usechain'][$svgdefid]);
        }
    }

    /**
     * Expand a resolved <use> target into PDF output.
     *
     * @param \XMLParser $parser The XML parser calling the handler.
     * @param int $soid ID of the current SVG object.
     * @phpstan-param TSVGAttributes $attr Use-element attributes (href/id stripped upstream).
     * @phpstan-param TSVGAttribs $use Resolved target definition from the defs table.
     *
     * @return string
     *
     * @throws \Com\Tecnick\Color\Exception
     * @throws \Com\Tecnick\File\Exception
     * @throws \Com\Tecnick\Pdf\Exception
     * @throws \Com\Tecnick\Pdf\Font\Exception
     * @throws \Com\Tecnick\Pdf\Graph\Exception
     * @throws \Com\Tecnick\Pdf\Image\Exception
     * @throws \Com\Tecnick\Pdf\Page\Exception
     * @throws \Com\Tecnick\Unicode\Exception
     */
    protected function expandSVGUse(\XMLParser $parser, int $soid, array $attr, array $use): string
    {
        $svgobj = &$this->getSVGObjRef($soid);

        if (isset($attr['xlink:href'])) {
            unset($attr['xlink:href']);
        }
        if (isset($attr['href'])) {
            unset($attr['href']);
        }
        if (isset($attr['id'])) {
            unset($attr['id']);
        }

        // When the target is a <symbol>, expand it like an inner <svg>:
        // apply the <use> x/y offset, optional width/height, and the symbol's viewBox.
        if ($use['name'] === 'symbol') {
            $symAttr = $use['attr'];
            $useX = isset($attr['x']) ? $this->svgUnitToUnit($attr['x'], $soid) : 0.0;
            $useY = isset($attr['y']) ? $this->svgUnitToUnit($attr['y'], $soid) : 0.0;
            if (isset($attr['width'])) {
                $useW = $this->svgUnitToUnit($attr['width'], $soid);
            } elseif (isset($symAttr['width'])) {
                $useW = $this->svgUnitToUnit($symAttr['width'], $soid);
            } else {
                $useW = 0.0;
            }
            if (isset($attr['height'])) {
                $useH = $this->svgUnitToUnit($attr['height'], $soid);
            } elseif (isset($symAttr['height'])) {
                $useH = $this->svgUnitToUnit($symAttr['height'], $soid);
            } else {
                $useH = 0.0;
            }

            // If the use width/height are omitted, fall back to the viewBox size.
            if (($useW <= 0.0 || $useH <= 0.0) && isset($symAttr['viewBox']) && $symAttr['viewBox'] !== '') {
                $viewBoxVals = \preg_split('/[\s,]+/', \trim($symAttr['viewBox']), -1, \PREG_SPLIT_NO_EMPTY);
                if (\is_array($viewBoxVals) && isset($viewBoxVals[2], $viewBoxVals[3])) {
                    $vbw = \abs($this->svgUnitToUnit($viewBoxVals[2], $soid));
                    $vbh = \abs($this->svgUnitToUnit($viewBoxVals[3], $soid));
                    if ($useW <= 0.0 && $vbw > 0.0) {
                        $useW = $vbw;
                    }
                    if ($useH <= 0.0 && $vbh > 0.0) {
                        $useH = $vbh;
                    }
                }
            }

            // Build an attr array shaped like an inner <svg>, keeping the
            // use-level style, transform and inherited attributes.
            /** @var TSVGAttributes $svglikeAttr */
            $svglikeAttr = $attr;
            $svglikeAttr['x'] = (string) $useX;
            $svglikeAttr['y'] = (string) $useY;
            $svglikeAttr['width'] = (string) $useW;
            $svglikeAttr['height'] = (string) $useH;
            if (!isset($svglikeAttr['viewBox']) && isset($symAttr['viewBox']) && $symAttr['viewBox'] !== '') {
                $svglikeAttr['viewBox'] = $symAttr['viewBox'];
            }
            if (
                !isset($svglikeAttr['preserveAspectRatio'])
                && isset($symAttr['preserveAspectRatio'])
                && $symAttr['preserveAspectRatio'] !== ''
            ) {
                $svglikeAttr['preserveAspectRatio'] = $symAttr['preserveAspectRatio'];
            }

            // Temporarily bump tagdepth so parseSVGTagSTARTsvg treats this as an inner SVG.

            $this->svgobjs[$soid]['tagdepth'] = (int) ($this->svgobjs[$soid]['tagdepth'] ?? 0) + 1;
            $styles = $svgobj['styles'];
            $defStyleRaw = $styles[\array_key_last($styles) ?? 0] ?? null;
            $defStyle = \is_array($defStyleRaw) ? $defStyleRaw : self::DEFSVGSTYLE;

            $out = '';
            $useStyle = $defStyle;

            // Symbol-level presentation/style attributes are applied first,
            // then the use-level ones override them.
            $symbolStyleTag = '';
            if (isset($symAttr['style']) && $symAttr['style'] !== '') {
                $symbolStyleTag = $symAttr['style'][0] === ';' ? $symAttr['style'] : ';' . $symAttr['style'];
            }
            foreach (self::SVGINHPROP as $styleKey) {
                if (isset($symAttr[$styleKey]) && \is_string($symAttr[$styleKey]) && $symAttr[$styleKey] !== '') {
                    $useStyle[$styleKey] = $symAttr[$styleKey];
                } elseif ($symbolStyleTag !== '') {
                    $styleDefault = $useStyle[$styleKey] ?? '';
                    $useStyle[$styleKey] = $this->parseCSSAttrib(
                        $symbolStyleTag,
                        $styleKey,
                        \is_scalar($styleDefault) ? (string) $styleDefault : '',
                    );
                }
            }

            if (isset($symAttr['transform']) && $symAttr['transform'] !== '') {
                $useTransform = \is_array($useStyle['transfmatrix']) ? $useStyle['transfmatrix'] : self::TMXID;
                $useStyle['transfmatrix'] = $this->graph->getCtmProduct(
                    $useTransform,
                    $this->getSVGTransformMatrix($symAttr['transform']),
                );
            }

            // Preserve use-level presentation/style attributes for symbol expansion.
            $styleTag = '';
            if (isset($svglikeAttr['style']) && $svglikeAttr['style'] !== '') {
                $styleTag = $svglikeAttr['style'][0] === ';' ? $svglikeAttr['style'] : ';' . $svglikeAttr['style'];
            }
            foreach (self::SVGINHPROP as $styleKey) {
                if (
                    isset($svglikeAttr[$styleKey])
                    && \is_string($svglikeAttr[$styleKey])
                    && $svglikeAttr[$styleKey] !== ''
                ) {
                    $useStyle[$styleKey] = $svglikeAttr[$styleKey];
                } elseif ($styleTag !== '') {
                    $styleDefault = $useStyle[$styleKey] ?? '';
                    $useStyle[$styleKey] = $this->parseCSSAttrib(
                        $styleTag,
                        $styleKey,
                        \is_scalar($styleDefault) ? (string) $styleDefault : '',
                    );
                }
            }

            if (isset($svglikeAttr['transform']) && $svglikeAttr['transform'] !== '') {
                $useTransform = \is_array($useStyle['transfmatrix']) ? $useStyle['transfmatrix'] : self::TMXID;
                $useStyle['transfmatrix'] = $this->graph->getCtmProduct(
                    $useTransform,
                    $this->getSVGTransformMatrix($svglikeAttr['transform']),
                );
            }

            if (!\is_array($useStyle['transfmatrix'])) {
                $useStyle['transfmatrix'] = self::TMXID;
            }

            /** @var TSVGStyle $useStyleTyped */
            $useStyleTyped = $useStyle;

            $out .= $this->parseSVGTagSTARTsvg($parser, $soid, $svglikeAttr, $useStyleTyped, $defStyle);

            // Replay each child element stored under the symbol def.
            if (isset($use['child']) && $use['child'] !== []) {
                foreach ($use['child'] as $child) {
                    $childName = $this->normalizeSVGNodeName($child['name']);
                    if ($childName === '') {
                        continue;
                    }

                    if (isset($child['attr']['closing_tag'])) {
                        // closing-tag sentinel: emit the matching end handler
                        $this->handleSVGTagEnd($parser, $childName);
                    } else {
                        $childAttr = $child['attr'];
                        $this->handleSVGTagStart($parser, $childName, $childAttr, $soid);
                    }
                }
            }

            $out .= $this->parseSVGTagENDsvg($soid);
            // parseSVGTagENDsvg calls parseSVGTagENDg which decrements tagdepth via its
            // own stack pop; we also decremented it above, so restore the balance.
            if (isset($this->svgobjs[$soid]['tagdepth'])) {
                $this->svgobjs[$soid]['tagdepth'] -= 1;
            }
            return $out;
        }

        if (isset($use['attr']['x'], $attr['x'])) {
            $attr['x'] = \strval(\floatval($attr['x']) + \floatval($use['attr']['x']));
        }
        if (isset($use['attr']['y'], $attr['y'])) {
            $attr['y'] = \strval(\floatval($attr['y']) + \floatval($use['attr']['y']));
        }
        if (!isset($attr['style']) || $attr['style'] === '') {
            $attr['style'] = '';
        }
        if (isset($use['attr']['style']) && $use['attr']['style'] !== '') {
            // merge styles
            $attr['style'] = \str_replace(';;', ';', ';' . $use['attr']['style'] . $attr['style']);
        }
        /** @var TSVGAttributes $attr */
        $attr = \array_merge($use['attr'], $attr);
        $useName = $this->normalizeSVGNodeName($use['name']);
        if ($useName === '') {
            return '';
        }

        $this->handleSVGTagStart($parser, $useName, $attr, $soid);
        return '';
    }

    /**
     * Get the SVG data from a file or data string.
     *
     * @param string $img
     *
     * @return string
     *
     * @throws \Com\Tecnick\File\Exception
     */
    protected function getRawSVGData(string $img, bool $useMarkupResources = false): string
    {
        if ($img === '' || $img[0] === '@' && \strlen($img) === 1) {
            return '';
        }
        if ($img[0] === '@') { // image from string
            return \substr($img, 1);
        }

        $imgpath = $img;
        if (!\str_contains($img, '://')) {
            $realPath = \realpath($img);
            if (\is_string($realPath) && $realPath !== '') {
                $imgpath = $realPath;
            }
        }

        $fileHelper = $useMarkupResources ? $this->markupFile : $this->file;
        $data = $fileHelper->getFileData($imgpath);

        return \is_string($data) ? $data : '';
    }

    /**
     * Get the SVG size from the SVG data.
     *
     * @param string $data The string containing the SVG image data.
     *
     * @return TSVGSize Associative array with dimensions.
     *
     * @throws \Com\Tecnick\Pdf\Exception
     */
    protected function getSVGSize(string $data): array
    {
        $out = [
            'x' => 0.0,
            'y' => 0.0,
            'width' => 0.0,
            'height' => 0.0,
            'viewBox' => [0.0, 0.0, 0.0, 0.0],
            'ar_align' => 'xMidYMid',
            'ar_ms' => 'meet',
        ];

        $regs = [];
        \preg_match('/<svg([^\>]*)>/si', $data, $regs);
        if (!isset($regs[1]) || $regs[1] === '') {
            return $out;
        }

        $tmp = [];
        if (\preg_match('/[\s]+x[\s]*=[\s]*"([^"]*)"/si', $regs[1], $tmp) && isset($tmp[1])) {
            $out['x'] = $this->svgUnitToUnit($tmp[1]);
        }
        $tmp = [];
        if (\preg_match('/[\s]+y[\s]*=[\s]*"([^"]*)"/si', $regs[1], $tmp) && isset($tmp[1])) {
            $out['y'] = $this->svgUnitToUnit($tmp[1]);
        }
        $tmp = [];
        if (\preg_match('/[\s]+width[\s]*=[\s]*"([^"]*)"/si', $regs[1], $tmp) && isset($tmp[1])) {
            $out['width'] = $this->svgUnitToUnit($tmp[1]);
        }
        $tmp = [];
        if (\preg_match('/[\s]+height[\s]*=[\s]*"([^"]*)"/si', $regs[1], $tmp) && isset($tmp[1])) {
            $out['height'] = $this->svgUnitToUnit($tmp[1]);
        }

        $tmp = [];
        if (!\preg_match(
            '/[\s]+viewBox[\s]*=[\s]*"[\s]*([0-9\.\-]+)[\s]+([0-9\.\-]+)[\s]+([0-9\.]+)[\s]+([0-9\.]+)[\s]*"/si',
            $regs[1],
            $tmp,
        )) {
            return $out;
        }

        if (isset($tmp[1], $tmp[2], $tmp[3], $tmp[4])) {
            $vb0 = $this->svgUnitToUnit($tmp[1]);
            $vb1 = $this->svgUnitToUnit($tmp[2]);
            $vb2 = $this->svgUnitToUnit($tmp[3]);
            $vb3 = $this->svgUnitToUnit($tmp[4]);
            $out['viewBox'] = [
                0 => $vb0,
                1 => $vb1,
                2 => $vb2,
                3 => $vb3,
            ];
        }

        // get aspect ratio
        $tmp = [];
        if (!\preg_match('/[\s]+preserveAspectRatio[\s]*=[\s]*"([^"]*)"/si', $regs[1], $tmp)) {
            return $out;
        }

        if (!isset($tmp[1])) {
            return $out;
        }

        $asr = \preg_split('/[\s]+/si', $tmp[1]);
        if (!\is_array($asr) || \count($asr) < 1) {
            return $out;
        }
        switch (\count($asr)) {
            case 3:
                if (isset($asr[1], $asr[2])) {
                    $out['ar_align'] = $asr[1];
                    $out['ar_ms'] = $asr[2];
                }
                break;
            case 2:
                if (isset($asr[0], $asr[1])) {
                    $out['ar_align'] = $asr[0];
                    $out['ar_ms'] = $asr[1];
                }
                break;
            case 1:
                $out['ar_align'] = $asr[0];
                $out['ar_ms'] = 'meet';
                break;
        }

        return $out;
    }

    /**
     * Pre-scan SVG data to collect gradient definitions before the main parse.
     *
     * SVG allows forward references to gradients defined later in <defs>, while
     * the main parser generates PDF commands in a single pass. This scan extracts
     * <linearGradient>, <radialGradient> and <stop> elements and feeds them
     * through the tag handlers to populate the gradient arrays first.
     *
     * @param string $data Raw SVG XML string.
     * @param int    $soid SVG object ID.
     */
    protected function prescanSVGGradients(string $data, int $soid): void
    {
        $gradientDepth = 0;
        $startHandler = function (\XMLParser $xmlParser, string $name, array $attr) use ($soid, &$gradientDepth): void {
            unset($xmlParser);
            $attr = $this->getSVGPrescanAttributes($attr);
            $name = $this->removeTagNamespace($name);
            try {
                switch ($name) {
                    case 'linearGradient':
                        $this->parseSVGTagSTARTlinearGradient($soid, $attr);
                        $gradientDepth = 1;
                        break;
                    case 'radialGradient':
                        $this->parseSVGTagSTARTradialGradient($soid, $attr);
                        $gradientDepth = 1;
                        break;
                    case 'stop':
                        if ($gradientDepth === 0) {
                            break;
                        }
                        $svgstyle = $this->getSVGPrescanStopStyle($attr);
                        $this->parseSVGTagSTARTstop($soid, $attr, $svgstyle);
                        break;
                }
            } catch (\Com\Tecnick\Color\Exception|PdfException $exc) {
                // Prescan is only an optimization for forward gradient references.
                // If it fails, keep parsing the SVG with the regular parser.
                unset($exc);
            }
        };
        $endHandler = function (\XMLParser $xmlParser, string $name) use (&$gradientDepth): void {
            unset($xmlParser);
            $name = $this->removeTagNamespace($name);
            if ($name === 'linearGradient' || $name === 'radialGradient') {
                $gradientDepth = 0;
            }
        };

        $scanner = \xml_parser_create('UTF-8');
        \xml_parser_set_option($scanner, XML_OPTION_CASE_FOLDING, 0);
        \xml_set_element_handler($scanner, $startHandler, $endHandler);
        \xml_parse($scanner, $data);
        unset($scanner);
    }

    /**
     * Normalize XML parser callback attributes for gradient prescan handlers.
     *
     * @param array<int|string, mixed> $xmlAttr Raw XML callback attributes.
     *
     * @return TSVGAttributes
     */
    protected function getSVGPrescanAttributes(array $xmlAttr): array
    {
        /** @var TSVGAttributes $attr */
        $attr = [];

        foreach (self::SVGGRADIENTATTRIB as $key) {
            if (!isset($xmlAttr[$key])) {
                continue;
            }

            if (!\is_scalar($xmlAttr[$key])) {
                continue;
            }

            $value = (string) $xmlAttr[$key];
            switch ($key) {
                case 'id':
                case 'x1':
                case 'y1':
                case 'x2':
                case 'y2':
                case 'cx':
                case 'cy':
                case 'fx':
                case 'fy':
                case 'r':
                case 'offset':
                case 'gradientUnits':
                case 'gradientTransform':
                case 'xlink:href':
                case 'href':
                case 'stop-color':
                case 'stop-opacity':
                case 'style':
                    $attr[$key] = $value;
                    break;
            }
        }

        return $attr;
    }

    /**
     * Build the minimal typed style array needed to parse a gradient stop.
     *
     * @param TSVGAttributes $attr Prescanned stop tag attributes.
     *
     * @return TSVGStyle
     */
    protected function getSVGPrescanStopStyle(array $attr): array
    {
        $svgstyle = self::DEFSVGSTYLE;

        if (isset($attr['stop-color'])) {
            $svgstyle['stop-color'] = $attr['stop-color'];
        }
        if (isset($attr['stop-opacity'])) {
            $svgstyle['stop-opacity'] = $this->normalizeSVGAlphaValue($attr['stop-opacity']);
        }

        // Check inline style attribute for stop-color / stop-opacity.
        if (isset($attr['style'])) {
            $matches = [];
            if (\preg_match('/stop-color\s*:\s*([^;]+)/i', $attr['style'], $matches)) {
                if (isset($matches[1])) {
                    $svgstyle['stop-color'] = \trim($matches[1]);
                }
            }
            if (\preg_match('/stop-opacity\s*:\s*([^;]+)/i', $attr['style'], $matches)) {
                if (isset($matches[1])) {
                    $svgstyle['stop-opacity'] = $this->normalizeSVGAlphaValue($matches[1]);
                }
            }
        }

        return $svgstyle;
    }

    /**
     * Add a new SVG image and return its object ID.
     *
     * @param string $img The path to the SVG file, or the SVG image data prefixed with '@'.
     * @param float $posx X position in user units.
     * @param float $posy Y position in user units.
     * @param float $width Width in user units.
     * @param float $height Height in user units.
     * @param float $pageheight Page height in user units.
     *
     * @return int The SVG object ID.
     *
     * @throws PdfException If SVG data is invalid or parsing fails.
     * @throws \Com\Tecnick\File\Exception If the SVG file cannot be read.
     * @throws \Com\Tecnick\Pdf\Page\Exception If page dimensions cannot be read.
     */
    public function addSVG(
        string $img,
        float $posx = 0.0,
        float $posy = 0.0,
        float $width = 0.0,
        float $height = 0.0,
        float $pageheight = 0.0,
        bool $useMarkupResources = false,
    ): int {
        if ($pageheight <= 0.0) {
            $pageheight = $this->page->getPage()['height'];
        }
        $prevPageHeight = $this->graph->setPageHeight($pageheight);

        $imgdir = \dirname($img);
        if ($imgdir === '.') {
            $imgdir = '';
        }

        $data = $this->getRawSVGData($img, $useMarkupResources);
        if ($data === '') {
            throw new PdfException('Invalid SVG');
        }

        $size = $this->getSVGSize($data);
        if ($size['width'] <= 0.0 || $size['height'] <= 0.0) {
            throw new PdfException('Invalid SVG size');
        }

        if ($size['width'] <= 0.0) {
            $size['width'] = 1.0;
        }
        if ($size['height'] <= 0.0) {
            $size['height'] = 1.0;
        }

        // calculate image width && height on document
        if ($width <= 0.0 && $height <= 0.0) {
            // convert image size to document unit
            $width = $size['width'];
            $height = $size['height'];
        } elseif ($width <= 0.0) {
            $width = ($height * $size['width']) / $size['height'];
        } elseif ($height <= 0.0) {
            $height = ($width * $size['height']) / $size['width'];
        }

        if ($size['viewBox'][2] > 0.0 && $size['viewBox'][3] > 0.0) {
            $size['width'] = $size['viewBox'][2];
            $size['height'] = $size['viewBox'][3];
        } else {
            if ($size['width'] <= 0) {
                $size['width'] = $width;
            }
            if ($size['height'] <= 0) {
                $size['height'] = $height;
            }
        }

        // SVG position && scale factors
        $sizeX = $size['x'];
        $sizeY = $size['y'];
        $sizeWidth = $size['width'];
        $sizeHeight = $size['height'];
        $svgoffset_x = $this->toPoints($posx - $sizeX);
        $svgoffset_y = $this->toPoints($sizeY - $posy);
        $svgscale_x = $width / $sizeWidth;
        $svgscale_y = $height / $sizeHeight;

        // scaling && alignment
        if ($size['ar_align'] !== 'none') {
            // force uniform scaling
            if ($size['ar_ms'] === 'slice') {
                // the entire viewport is covered by the viewBox
                if ($svgscale_x > $svgscale_y) {
                    $svgscale_y = $svgscale_x;
                } elseif ($svgscale_x < $svgscale_y) {
                    $svgscale_x = $svgscale_y;
                }
            } else { // meet
                // the entire viewBox is visible within the viewport
                if ($svgscale_x < $svgscale_y) {
                    $svgscale_y = $svgscale_x;
                } elseif ($svgscale_x > $svgscale_y) {
                    $svgscale_x = $svgscale_y;
                }
            }
            // correct X alignment
            switch (\substr($size['ar_align'], 1, 3)) {
                case 'Min':
                    // do nothing
                    break;
                case 'Max':
                    $svgoffset_x += $this->toPoints($width - ($size['width'] * $svgscale_x));
                    break;
                default:
                case 'Mid':
                    $svgoffset_x += $this->toPoints(($width - ($size['width'] * $svgscale_x)) / 2);
                    break;
            }
            // correct Y alignment
            switch (\substr($size['ar_align'], 5)) {
                case 'Min':
                    // do nothing
                    break;
                case 'Max':
                    $svgoffset_y -= $this->toPoints($height - ($size['height'] * $svgscale_y));
                    break;
                default:
                case 'Mid':
                    $svgoffset_y -= $this->toPoints(($height - ($size['height'] * $svgscale_y)) / 2);
                    break;
            }
        }

        $soid = (int) \array_key_last($this->svgobjs);
        $soid++;

        $this->svgobjs[$soid] = self::SVGDEFOBJ;
        $this->svgobjs[$soid]['dir'] = $imgdir;
        $this->svgobjs[$soid]['markupresources'] = $useMarkupResources;
        $this->svgobjs[$soid]['refunitval']['page']['height'] = $this->toPoints($pageheight);

        $out = '';
        $out .= $this->graph->getStartTransform();
        $out .= $this->graph->getRawRect($posx, $posy, $width, $height, 'CNZ');

        // scale && translate
        $esx = $this->toPoints($sizeX * (1 - $svgscale_x));
        $fsy = $this->toPoints(($pageheight - $sizeY) * (1 - $svgscale_y));

        $ctm = [
            0 => $svgscale_x,
            1 => 0.0,
            2 => 0.0,
            3 => $svgscale_y,
            4 => $esx + $svgoffset_x,
            5 => $fsy + $svgoffset_y,
        ];

        $out .= $this->graph->getTransformation($ctm);

        $this->svgobjs[$soid]['out'] .= $out;

        // Pre-scan SVG to collect gradient definitions so that forward
        // references (e.g. <defs> at the end of the file) are available
        // when elements that use them are processed during the main parse.
        $this->prescanSVGGradients($data, $soid);

        // creates a new XML parser to be used by the other XML functions
        $parser = \xml_parser_create('UTF-8');
        // disable case-folding for this XML parser
        \xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
        // sets the element handler functions for the XML parser
        \xml_set_element_handler($parser, [$this, 'handleSVGTagStart'], [$this, 'handleSVGTagEnd']);
        // sets the character data handler function for the XML parser
        \xml_set_character_data_handler($parser, [$this, 'handlerSVGCharacter']);

        // start parsing an XML document
        if (!\xml_parse($parser, $data)) {
            throw new PdfException(\sprintf(
                'SVG Error: %s at line %d',
                \xml_error_string(\xml_get_error_code($parser)),
                \xml_get_current_line_number($parser),
            ));
        }

        // >= PHP 7.0.0 "explicitly unset the reference to parser to avoid memory leaks"
        unset($parser);

        if (!isset($this->svgobjs[$soid]['out'])) {
            $this->svgobjs[$soid]['out'] = '';
        }
        $this->svgobjs[$soid]['out'] .= $this->graph->getStopTransform();
        $this->graph->setPageHeight($prevPageHeight);

        return $soid;
    }

    /**
     * Get the PDF output string to print the specified SVG object.
     *
     * @param int   $soid       SVG Object ID (as returned by addSVG).
     *
     * @return string Image PDF page content.
     *
     * @throws PdfException If the SVG object ID is unknown.
     */
    public function getSetSVG(int $soid): string
    {
        if (!isset($this->svgobjs[$soid])) {
            throw new PdfException('Unknownn SVG ID: ' . $soid);
        }

        $out = $this->svgobjs[$soid]['out'] ?? '';
        $children = $this->svgobjs[$soid]['child'] ?? [];

        foreach ($children as $chid) {
            $out .= $this->getSetSVG($chid);
        }

        return $out;
    }
}
