<?php

/**
 * SVGTest.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 Test;

class SVGTest extends TestUtil
{
    public static function setUpBeforeClass(): void
    {
        self::setUpFontsPath();
    }

    /** @throws \Throwable */
    protected function getTestObject(): \Com\Tecnick\Pdf\Tcpdf
    {
        return new \Com\Tecnick\Pdf\Tcpdf();
    }

    /** @throws \Throwable */
    protected function getInternalTestObject(): TestableSVG
    {
        return new TestableSVG();
    }

    /** @throws \Throwable */
    public function testAddSVGStoresInlineSvgObject(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50">'
            . '<rect x="0" y="0" width="100" height="50" fill="#ff0000"/>'
            . '</svg>';

        $soid = $obj->addSVG($svg, 10, 20, 40, 20, $page['height']);

        $this->assertSame(1, $soid);
        /** @var array<int, array<string, mixed>> $svgobjs */
        $svgobjs = $this->getObjectProperty($obj, 'svgobjs');
        $this->assertArrayHasKey($soid, $svgobjs);
        $svgObj = $svgobjs[$soid] ?? null;
        $this->assertIsArray($svgObj);
        $this->assertNotSame('', $svgObj['out'] ?? '');
    }

    /** @throws \Throwable */
    public function testAddSVGRejectsInvalidSvg(): void
    {
        $obj = $this->getTestObject();
        $this->initFontAndPage($obj);
        $this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);

        $obj->addSVG('@<svg xmlns="http://www.w3.org/2000/svg"></svg>');
    }

    /** @throws \Throwable */
    public function testGetSetSVGReturnsRenderableOutput(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20">'
            . '<circle cx="10" cy="10" r="8" fill="#00ff00"/>'
            . '</svg>';

        $soid = $obj->addSVG($svg, 5, 6, 12, 12, $page['height']);
        $out = $obj->getSetSVG($soid);

        $this->assertNotSame('', $out);
        $this->assertStringStartsWith("q\n", $out);
        $this->assertStringContainsString(" cm\n", $out);
        $this->assertStringEndsWith("Q\nQ\n", $out);
    }

    /** @throws \Throwable */
    public function testSvgPatternFillEmitsPdfPatternResources(): void
    {
        $obj = new \Com\Tecnick\Pdf\Tcpdf('mm', true, false, false);
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20">'
            . '<defs>'
            . '<pattern id="p1" patternUnits="userSpaceOnUse" x="0" y="0" width="4" height="4">'
            . '<rect x="0" y="0" width="4" height="4" fill="none" stroke="#000000" stroke-width="1"/>'
            . '</pattern>'
            . '</defs>'
            . '<rect x="0" y="0" width="20" height="20" fill="url(#p1)"/>'
            . '</svg>';

        $soid = $obj->addSVG($svg, 5, 6, 12, 12, $page['height']);
        $svgOut = $obj->getSetSVG($soid);
        $pdf = $obj->getOutPDFString();

        $this->assertStringContainsString('/Pattern cs /PTN_', $svgOut);
        $this->assertStringContainsString(' scn', $svgOut);
        $this->assertStringContainsString('/Type /Pattern', $pdf);
        $this->assertStringContainsString('/Pattern <<', $pdf);
        $this->assertStringContainsString('/Type /Pattern /PatternType 1', $pdf);
        $this->assertStringContainsString('/Resources << /ProcSet [/PDF /Text /ImageB /ImageC /ImageI]', $pdf);
        $this->assertMatchesRegularExpression('/\/PTN_[A-F0-9]{16}\s+\d+\s+0\s+R/', $pdf);
    }

    /** @throws \Throwable */
    public function testGetSetSVGThrowsForUnknownId(): void
    {
        $obj = $this->getTestObject();
        $this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);

        $obj->getSetSVG(999);
    }

    /** @throws \Throwable */
    public function testParseSVGTMtranslateWithTwoValues(): void
    {
        $obj = $this->getInternalTestObject();

        $result = $obj->exposeParseSVGTMtranslate('10.5, 20.3');

        $this->assertSame([1.0, 0.0, 0.0, 1.0, 10.5, 20.3], $result);
    }

    /** @throws \Throwable */
    public function testParseSVGTMtranslateWithOneValue(): void
    {
        $obj = $this->getInternalTestObject();

        $result = $obj->exposeParseSVGTMtranslate('5.5');

        $this->assertSame([1.0, 0.0, 0.0, 1.0, 5.5, 0.0], $result);
    }

    /** @throws \Throwable */
    public function testParseSVGTMscaleWithTwoValues(): void
    {
        $obj = $this->getInternalTestObject();

        $result = $obj->exposeParseSVGTMscale('2.0, 3.0');

        $this->assertSame([2.0, 0.0, 0.0, 3.0, 0.0, 0.0], $result);
    }

    /** @throws \Throwable */
    public function testParseSVGTMscaleWithOneValue(): void
    {
        $obj = $this->getInternalTestObject();

        $result = $obj->exposeParseSVGTMscale('1.5');

        $this->assertSame([1.5, 0.0, 0.0, 1.5, 0.0, 0.0], $result);
    }

    /** @throws \Throwable */
    public function testParseSVGTMrotateWithDegrees(): void
    {
        $obj = $this->getInternalTestObject();

        $result = $obj->exposeParseSVGTMrotate('45');

        $this->assertCount(6, $result);
        // Rotation matrix has cos and sin components that can be floating point
        $this->assertNotSame([1.0, 0.0, 0.0, 1.0, 0.0, 0.0], $result);
    }

    /** @throws \Throwable */
    public function testParseSVGTMskewXWithDegrees(): void
    {
        $obj = $this->getInternalTestObject();

        $result = $obj->exposeParseSVGTMskewX('30');

        $this->assertCount(6, $result);
        $this->assertNotSame([1.0, 0.0, 0.0, 1.0, 0.0, 0.0], $result);
    }

    /** @throws \Throwable */
    public function testParseSVGTMskewYWithDegrees(): void
    {
        $obj = $this->getInternalTestObject();

        $result = $obj->exposeParseSVGTMskewY('30');

        $this->assertCount(6, $result);
        $this->assertNotSame([1.0, 0.0, 0.0, 1.0, 0.0, 0.0], $result);
    }

    /** @throws \Throwable */
    public function testParseSVGTMmatrixWithSixValues(): void
    {
        $obj = $this->getInternalTestObject();

        $result = $obj->exposeParseSVGTMmatrix('1 2 3 4 5 6');

        $this->assertSame([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], $result);
    }

    /** @throws \Throwable */
    public function testParseSVGTMmatrixWithCSVFormat(): void
    {
        $obj = $this->getInternalTestObject();

        $result = $obj->exposeParseSVGTMmatrix('1.5, 2.5, 3.5, 4.5, 5.5, 6.5');

        $this->assertSame([1.5, 2.5, 3.5, 4.5, 5.5, 6.5], $result);
    }

    /** @throws \Throwable */
    public function testSvgUnitConvertersAndTransformHelpers(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->setSvgRefUnit(1);

        $this->assertSame(72.0, $obj->exposeSvgUnitToPoints('1in', 1));
        $this->assertEqualsWithDelta(25.4, $obj->exposeSvgUnitToUnit('25.4mm', 1), 0.0001);

        $transformMatrix = $obj->exposeGetSVGTransformMatrix('translate(10,20) scale(2) rotate(0)');
        $this->assertCount(6, $transformMatrix);

        $converted = $obj->exposeConvertSVGMatrix([1, 0, 0, 1, 10, 20], 1);
        $this->assertCount(6, $converted);
        $this->assertSame(7.5, $converted[4]);
        $this->assertSame(-15.0, $converted[5]);

        $out = $obj->exposeGetOutSVGTransformation([1, 0, 0, 1, 10, 20], 1);
        $this->assertStringContainsString(' cm', $out);
    }

    /** @throws \Throwable */
    public function testSvgTagPathAndCssAttributeHelpers(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->setSvgRefUnit(2);

        $this->assertSame('rect', $obj->exposeRemoveTagNamespace('svg:rect'));
        $this->assertSame('', $obj->exposeGetSVGPath(2, 'M 0 0 L 10 10 Z', ''));

        $path = $obj->exposeGetSVGPath(2, 'M 0 0 L 10 10 H 5 V 3 Z', 'S');
        $this->assertStringContainsString('m', $path);
        $this->assertStringContainsString('l', $path);
        $this->assertStringContainsString("h\n", $path);

        $tag = 'font-size: 12px; font-weight: bold;';
        $this->assertSame('12px', $obj->exposeParseCSSAttrib($tag, 'font-size', '10px'));
        $this->assertSame('fallback', $obj->exposeParseCSSAttrib($tag, 'line-height', 'fallback'));
    }

    /** @throws \Throwable */
    public function testSvgHelperNormalizationAndClipDispatchBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->setSvgRefUnit(4);

        $this->assertSame(4.0, $obj->exposeNormalizeSVGStyleFloatValue(4, 1.5));
        $this->assertSame(2.5, $obj->exposeNormalizeSVGStyleFloatValue(2.5, 1.5));
        $this->assertSame(3.75, $obj->exposeNormalizeSVGStyleFloatValue(' 3.75 ', 1.5));
        $this->assertSame(1.5, $obj->exposeNormalizeSVGStyleFloatValue('not-a-number', 1.5));

        $this->assertTrue($obj->exposeNormalizeSVGBoolLike(true));
        $this->assertFalse($obj->exposeNormalizeSVGBoolLike(false));
        $this->assertTrue($obj->exposeNormalizeSVGBoolLike(1));
        $this->assertFalse($obj->exposeNormalizeSVGBoolLike(0.0));
        $this->assertTrue($obj->exposeNormalizeSVGBoolLike(' yes '));
        $this->assertFalse($obj->exposeNormalizeSVGBoolLike('off'));

        $this->assertSame('', $obj->exposeGetSVGMarkerId(''));
        $this->assertSame('', $obj->exposeGetSVGMarkerId('none'));
        $this->assertSame('marker1', $obj->exposeGetSVGMarkerId('url(#marker1)'));
        $this->assertSame('', $obj->exposeGetSVGMarkerId('invalid-marker-value'));

        $this->assertSame('', $obj->exposeApplySVGClipFunction('unknownClip', []));
        $this->assertStringContainsString('m', $obj->exposeApplySVGClipFunction('getSVGPath', [
            4,
            'M 0 0 L 10 10 Z',
            'S',
        ]));
        $this->assertNotSame('', $obj->exposeApplySVGClipFunction('getRoundedRect', [
            5.0,
            5.0,
            10.0,
            8.0,
            2.0,
            2.0,
            '1111',
            'DF',
        ]));
        $this->assertNotSame('', $obj->exposeApplySVGClipFunction('getCircle', [10.0, 10.0, 5.0, 0.0, 360.0, 'S']));
        $this->assertNotSame('', $obj->exposeApplySVGClipFunction('getEllipse', [
            10.0,
            10.0,
            6.0,
            3.0,
            0.0,
            0.0,
            360.0,
            'S',
        ]));
        $this->assertNotSame('', $obj->exposeApplySVGClipFunction('getLine', [0.0, 0.0, 10.0, 10.0]));
        $this->assertNotSame('', $obj->exposeApplySVGClipFunction('getPolygon', [
            [0.0, 0.0, 10.0, 0.0, 10.0, 10.0],
            'S',
        ]));
        $this->assertNotSame('', $obj->exposeApplySVGClipFunction('getClippingRect', [0.0, 0.0, 10.0, 10.0, true]));

        $this->assertSame(0.0, $obj->exposeResolveSVGMarkerRefCoordinate('', 10.0, 20.0, 4));
        $this->assertEqualsWithDelta(20.0, $obj->exposeResolveSVGMarkerRefCoordinate('50%', 10.0, 20.0, 4), 0.001);
        $this->assertEqualsWithDelta(12.5, $obj->exposeResolveSVGMarkerRefCoordinate('12.5mm', 10.0, 20.0, 4), 0.001);

        $this->assertEqualsWithDelta(100.0, $obj->exposeResolveSVGPatternLength('50%', 200.0, 4), 0.001);
        $this->assertEqualsWithDelta(25.4, $obj->exposeResolveSVGPatternLength('25.4mm', 200.0, 4), 0.001);
        $this->assertEqualsWithDelta(0.0, $obj->exposeResolveSVGPatternLength('', 200.0, 4), 0.001);
    }

    /** @throws \Throwable */
    public function testSvgTextAttributeAndBlendAlphaHelpers(): void
    {
        $obj = $this->getInternalTestObject();

        $this->assertSame(0.0, $obj->exposeGetTALetterSpacing('normal'));
        $this->assertSame(1.25, $obj->exposeGetTALetterSpacing('inherit', 1.25));
        $this->assertGreaterThan(0, $obj->exposeGetTALetterSpacing('2px'));

        $this->assertSame(70.0, $obj->exposeGetTAFontStretching('condensed'));
        $this->assertSame(120.0, $obj->exposeGetTAFontStretching('wider', 110));
        $this->assertSame(95.0, $obj->exposeGetTAFontStretching('narrower', 105));
        $this->assertSame(80.0, $obj->exposeGetTAFontStretching('80%'));

        $this->assertSame('B', $obj->exposeGetTAFontWeight('bold'));
        $this->assertSame('', $obj->exposeGetTAFontWeight('normal'));
        $this->assertSame('I', $obj->exposeGetTAFontStyle('italic'));
        $this->assertSame('', $obj->exposeGetTAFontStyle('normal'));
        $this->assertSame('U', $obj->exposeGetTAFontDecoration('underline'));
        $this->assertSame('O', $obj->exposeGetTAFontDecoration('overline'));
        $this->assertSame('D', $obj->exposeGetTAFontDecoration('line-through'));
        $this->assertSame('', $obj->exposeGetTAFontDecoration('none'));

        $this->assertSame('Multiply', $obj->exposeNormalizeSVGBlendMode('multiply'));
        $this->assertSame('Normal', $obj->exposeNormalizeSVGBlendMode('no-such-mode'));
        $this->assertSame(1.0, $obj->exposeNormalizeSVGAlphaValue(1.5));
        $this->assertSame(0.0, $obj->exposeNormalizeSVGAlphaValue(-0.4));
        $this->assertSame(0.5, $obj->exposeNormalizeSVGAlphaValue('0.5'));

        $this->assertSame('', $obj->exposeGetSVGExtGState(null, null, ''));
        $gstate = $obj->exposeGetSVGExtGState(0.8, 0.5, 'Multiply');
        $this->assertStringContainsString('gs', $gstate);

        $pdfx3 = new TestableSVG('mm', true, false, true, 'pdfx3');
        $this->assertSame('', $pdfx3->exposeGetSVGExtGState(0.8, 0.5, 'Multiply'));

        $pdfx4 = new TestableSVG('mm', true, false, true, 'pdfx4');
        $this->assertStringContainsString('gs', $pdfx4->exposeGetSVGExtGState(0.8, 0.5, 'Multiply'));
    }

    /** @throws \Throwable */
    public function testSvgPathCommandHelpersProducePathOperations(): void
    {
        $obj = $this->getInternalTestObject();

        $coord = $obj->getPathCoordDefaults();
        [$mout, $coord] = $obj->exposeSvgPathCmdM([1, 2, 3, 4], $coord);
        $this->assertStringContainsString('m', $mout);

        [$lout, $coord] = $obj->exposeSvgPathCmdL([5, 6, 7, 8], $coord);
        $this->assertStringContainsString('l', $lout);

        [$hout, $coord] = $obj->exposeSvgPathCmdH([9, 10], $coord);
        $this->assertStringContainsString('l', $hout);

        [$vout, $coord] = $obj->exposeSvgPathCmdV([11, 12], $coord);
        $this->assertStringContainsString('l', $vout);

        [$cout, $coord] = $obj->exposeSvgPathCmdC([13, 14, 15, 16, 17, 18], $coord);
        $this->assertStringContainsString('c', $cout);

        [$qout, $coord] = $obj->exposeSvgPathCmdQ([19, 20, 21, 22], $coord);
        $this->assertStringContainsString('c', $qout);

        [$sout, $coord] = $obj->exposeSvgPathCmdS([23, 24, 25, 26], $coord);
        $this->assertStringContainsString('c', $sout);

        [$tout, $coord] = $obj->exposeSvgPathCmdT([27, 28], $coord);
        $this->assertStringContainsString('c', $tout);

        $arcPaths = [['0', 'A'], ['1', 'z']];
        [$aout, $coord] = $obj->exposeSvgPathCmdA([5, 5, 0, 0, 1, 30, 35], $coord, $arcPaths, 0, [
            '5',
            '5',
            '0',
            '0',
            '1',
            '30',
            '35',
        ]);
        $this->assertNotSame('', $aout);

        [$zout, $coord] = $obj->exposeSvgPathCmdZ($coord);
        $this->assertSame("h\n", $zout);
        $this->assertSame($coord['xinit'], $coord['x']);
        $this->assertSame($coord['yinit'], $coord['y']);
    }

    /** @throws \Throwable */
    public function testSvgStyleHelperMethodsCoverCorePaths(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->setSvgObjMeta(3);

        $base = $obj->exposeDefaultSVGStyle();

        [$fontOut, $fontStyle] = $obj->exposeParseSVGStyleFont($base, $base);
        $this->assertNotSame('', $fontOut);
        $this->assertArrayHasKey('font-size-val', $fontStyle);

        $strokeStyle = $base;
        $strokeStyle['stroke'] = '#112233';
        $strokeStyle['stroke-width'] = 1.0;
        $strokeStyle['stroke-linecap'] = 'butt';
        $strokeStyle['stroke-linejoin'] = 'miter';
        $strokeStyle['stroke-opacity'] = 1.0;
        $strokeStyle['opacity'] = 1.0;
        $strokeStyle['mix-blend-mode'] = 'normal';
        $strokeStyle['objstyle'] = '';
        [$strokeOut, $strokeStyle] = $obj->exposeParseSVGStyleStroke(3, $strokeStyle);
        $this->assertNotSame('', $strokeOut);
        $this->assertStringContainsString('D', $strokeStyle['objstyle']);

        $colorStyle = $base;
        $colorStyle['opacity'] = 0.5;
        $colorStyle['mix-blend-mode'] = 'multiply';
        $colorStyle['color'] = '#ff0000';
        $colorStyle['text-color'] = '#00ff00';
        [$colorOut] = $obj->exposeParseSVGStyleColor($colorStyle);
        $this->assertNotSame('', $colorOut);

        $pdfx3 = new TestableSVG('mm', true, false, true, 'pdfx3');
        [$pdfx3ColorOut] = $pdfx3->exposeParseSVGStyleColor($colorStyle);
        $this->assertStringNotContainsString('/GS', $pdfx3ColorOut);

        $clipStyle = $base;
        $clipStyle['clip'] = 'rect(1 2 3 4)';
        $clipStyle['clip-rule'] = 'evenodd';
        [$clipOut] = $obj->exposeParseSVGStyleClip($clipStyle, 10, 20, 100, 50);
        $this->assertNotSame('', $clipOut);

        $this->assertSame('', $obj->exposeParseSVGStyleGradient(3, [], 'missing', 0, 0, 10, 10));

        $fillNone = $base;
        $fillNone['fill'] = 'none';
        [$fillNoneOut] = $obj->exposeParseSVGStyleFill(3, $fillNone, [], 0, 0, 10, 10);
        $this->assertSame('', $fillNoneOut);

        $fillUrl = $base;
        $fillUrl['fill'] = 'url(#missing)';
        $fillUrl['opacity'] = 1.0;
        $fillUrl['fill-opacity'] = 1.0;
        [$fillUrlOut] = $obj->exposeParseSVGStyleFill(3, $fillUrl, [], 0, 0, 10, 10);
        $this->assertSame('', $fillUrlOut);

        $obj->patchSvgObj(3, [
            'defs' => [
                'patEmpty' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patEmpty',
                        'patternUnits' => 'userSpaceOnUse',
                        'x' => '0',
                        'y' => '0',
                        'width' => '3',
                        'height' => '3',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '0',
                                'y' => '0',
                                'width' => '3',
                                'height' => '3',
                                'fill' => 'none',
                                'stroke' => '#000000',
                                'stroke-width' => '1',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'rect',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);
        $fillPattern = $base;
        $fillPattern['fill'] = 'url(#patEmpty)';
        $fillPattern['opacity'] = 1.0;
        $fillPattern['fill-opacity'] = 1.0;
        $obj->patchSvgObj(3, ['out' => 'BASE']);
        [$fillPatternOut] = $obj->exposeParseSVGStyleFill(3, $fillPattern, [], 0, 0, 10, 10, 'getClippingRect', [
            0.0,
            0.0,
            10.0,
            10.0,
            0.0,
        ]);
        $this->assertSame('BASE', $obj->getSvgObj(3)['out']);
        $this->assertIsString($fillPatternOut);

        $parser = \xml_parser_create('UTF-8');
        $obj->exposeParseSVGStyleClipPath($parser, 3, []);

        $earlyStyle = $base;
        $earlyStyle['opacity'] = 0;
        [$styleOut, $objstyle] = $obj->exposeParseSVGStyle($parser, 3, $earlyStyle, $base);
        $this->assertSame('', $styleOut);
        $this->assertSame('', $objstyle);
    }

    /** @throws \Throwable */
    public function testSvgRawSizeAndPrescanHelpers(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(40);

        $inline = '<svg width="10" height="20"></svg>';
        $this->assertSame($inline, $obj->exposeGetRawSVGData('@' . $inline));
        $this->assertSame('', $obj->exposeGetRawSVGData('@'));

        $tmp = \tempnam(\sys_get_temp_dir(), 'tc-svg-');
        $this->assertNotFalse($tmp);
        \file_put_contents($tmp, $inline);
        try {
            $this->assertSame($inline, $obj->exposeGetRawSVGData($tmp));
        } finally {
            if (\file_exists($tmp)) {
                \unlink($tmp);
            }
        }

        $size = $obj->exposeGetSVGSize(
            '<svg x="1" y="2" width="100" height="50" viewBox="0 0 100 50" preserveAspectRatio="xMinYMin meet"></svg>',
        );
        $this->assertGreaterThan(0, $size['width']);
        $this->assertGreaterThan(0, $size['height']);
        $this->assertSame('xMinYMin', $size['ar_align']);

        $getPrescanAttributes = new \ReflectionMethod($obj, 'exposeGetSVGPrescanAttributes');
        /** @var array<string, string> $attr */
        $attr = $getPrescanAttributes->invokeArgs($obj, [['id' => 'g1', 'stop-color' => '#fff', 'junk' => 'x']]);
        $this->assertArrayHasKey('id', $attr);
        $this->assertArrayHasKey('stop-color', $attr);
        $this->assertArrayNotHasKey('junk', $attr);

        $stopStyle = $obj->exposeGetSVGPrescanStopStyle(['style' => 'stop-color: #123456; stop-opacity: 0.25']);
        $this->assertSame('#123456', $stopStyle['stop-color']);
        $this->assertSame(0.25, $stopStyle['stop-opacity']);

        $obj->exposePrescanSVGGradients(
            '<svg><defs><linearGradient id="lg">'
            . '<stop offset="0%" stop-color="#000"/>'
            . '<stop offset="100%" style="stop-color:#fff;stop-opacity:0.4"/>'
            . '</linearGradient></defs></svg>',
            40,
        );
        $svgobj = $obj->getSvgObj(40);
        $this->assertArrayHasKey('lg', $svgobj['gradients']);
        $lg = $svgobj['gradients']['lg'] ?? null;
        $this->assertIsArray($lg);
        $stops = $lg['stops'];
        $this->assertIsArray($stops);
        $this->assertGreaterThanOrEqual(1, \count($stops));
    }

    /** @throws \Throwable */
    public function testSvgGetSVGSizeCoversMissingViewboxAndThreeTokenAspectRatio(): void
    {
        $obj = $this->getInternalTestObject();

        $noViewBox = $obj->exposeGetSVGSize('<svg x="3" y="4" width="60" height="30"></svg>');
        $this->assertGreaterThan(0.0, $noViewBox['x']);
        $this->assertGreaterThan(0.0, $noViewBox['y']);
        $this->assertGreaterThan(0.0, $noViewBox['width']);
        $this->assertGreaterThan(0.0, $noViewBox['height']);
        $this->assertSame([0.0, 0.0, 0.0, 0.0], $noViewBox['viewBox']);

        $threeTokens = $obj->exposeGetSVGSize(
            '<svg width="10" height="10" viewBox="0 0 10 10" preserveAspectRatio="defer xMaxYMax slice"></svg>',
        );
        $this->assertSame('xMaxYMax', $threeTokens['ar_align']);
        $this->assertSame('slice', $threeTokens['ar_ms']);
    }

    /** @throws \Throwable */
    public function testSvgPrescanRadialGradientWithDirectStopAttributes(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(44);

        $obj->exposePrescanSVGGradients(
            '<svg><defs><radialGradient id="rg">'
            . '<stop offset="0%" stop-color="#101010" stop-opacity="0.2"/>'
            . '</radialGradient></defs></svg>',
            44,
        );

        $svgobj = $obj->getSvgObj(44);
        $this->assertArrayHasKey('rg', $svgobj['gradients']);
        $rg = $svgobj['gradients']['rg'] ?? null;
        $this->assertIsArray($rg);
        $stops = $rg['stops'];
        $this->assertIsArray($stops);
        $this->assertGreaterThanOrEqual(1, \count($stops));
    }

    /** @throws \Throwable */
    public function testSvgTagHandlersAndEndStartHelpers(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(41);

        $parser = \xml_parser_create('UTF-8');
        $obj->exposeHandlerSVGCharacter($parser, 'Hello');
        $svgobj = $obj->getSvgObj(41);
        $this->assertSame('Hello', $svgobj['text']);

        $obj->exposeParseSVGTagSTARTdefs(41);
        $this->assertTrue($obj->getSvgObj(41)['defsmode']);
        $obj->exposeParseSVGTagENDdefs(41);
        $this->assertFalse($obj->getSvgObj(41)['defsmode']);

        $obj->exposeParseSVGTagSTARTclipPath(41, [1, 0, 0, 1, 0, 0]);
        $this->assertTrue($obj->getSvgObj(41)['clipmode']);
        $obj->exposeParseSVGTagENDclipPath(41);
        $this->assertFalse($obj->getSvgObj(41)['clipmode']);

        $obj->patchSvgObj(41, ['styles' => [$obj->exposeDefaultSVGStyle(), $obj->exposeDefaultSVGStyle()]]);
        $this->assertSame('', $obj->exposeParseSVGTagENDg(41));
        $this->assertCount(1, $obj->getSvgObj(41)['styles']);

        $obj->patchSvgObj(41, ['textmode' => ['invisible' => true]]);
        $this->assertSame('', $obj->exposeParseSVGTagENDtext(41));
        $this->assertSame('', $obj->exposeParseSVGTagENDtspan(41));

        $obj->patchSvgObj(41, ['tagdepth' => 1]);
        $this->assertSame('', $obj->exposeParseSVGTagENDsvg(41));

        $obj->patchSvgObj(41, ['clipmode' => true, 'cliptm' => [1, 0, 0, 1, 0, 0]]);
        $obj->exposeHandleSVGTagStart($parser, 'rect', ['x' => '1', 'y' => '2'], 41, false, [1, 0, 0, 1, 0, 0]);
        $this->assertNotEmpty($obj->getSvgObj(41)['clippaths']);

        $obj->patchSvgObj(41, ['defsmode' => false]);
        $obj->exposeHandleSVGTagEnd($parser, 'defs');
        $this->assertFalse($obj->getSvgObj(41)['defsmode']);
    }

    /** @throws \Throwable */
    public function testSvgRemainingStartTagMethodsCoveredViaGuardBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(42);
        $base = $obj->exposeDefaultSVGStyle();

        $parser = \xml_parser_create('UTF-8');
        // Root svg branch exits immediately.
        $obj->patchSvgObj(42, ['tagdepth' => 0]);
        $this->assertSame('', $obj->exposeParseSVGTagSTARTsvg($parser, 42, [], $base, $base));

        // Group start path should return a transform block.
        $gOut = $obj->exposeParseSVGTagSTARTg($parser, 42, [], $base, $base);
        $this->assertNotSame('', $gOut);

        // Gradient starts and stop insertion.
        $this->assertSame('', $obj->exposeParseSVGTagSTARTlinearGradient(42, ['id' => 'lg2']));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTradialGradient(42, ['id' => 'rg2']));
        $obj->patchSvgObj(42, ['gradientid' => 'lg2']);
        $this->assertSame('', $obj->exposeParseSVGTagSTARTstop(42, ['offset' => '50%'], $base));
        $svg42 = $obj->getSvgObj(42);
        $this->assertNotEmpty($svg42['gradients']['lg2']['stops'] ?? []);

        // Invisibility/clip guards for shape and text tags.
        $obj->patchSvgObj(42, ['textmode' => ['invisible' => true], 'clipmode' => false]);
        $this->assertSame('', $obj->exposeParseSVGTagSTARTpath($parser, 42, ['d' => 'M 0 0 L 1 1'], $base, $base));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTrect(
            $parser,
            42,
            ['width' => '1', 'height' => '1'],
            $base,
            $base,
        ));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTcircle($parser, 42, ['r' => '1'], $base, $base));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTellipse(
            $parser,
            42,
            ['rx' => '1', 'ry' => '1'],
            $base,
            $base,
        ));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTline(
            $parser,
            42,
            ['x1' => '0', 'y1' => '0', 'x2' => '1', 'y2' => '1'],
            $base,
            $base,
        ));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTpolygon($parser, 42, ['points' => '0,0 1,1'], $base, $base));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTimage($parser, 42, ['xlink:href' => 'x.png'], $base, $base));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTtext($parser, 42, ['x' => '0', 'y' => '0'], $base, $base));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTtspan($parser, 42, ['dx' => '1'], $base, $base));

        // use tag guards when href is missing or unknown.
        $this->assertSame('', $obj->exposeParseSVGTagSTARTuse($parser, 42, []));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTuse($parser, 42, ['xlink:href' => '#missing']));
    }

    /** @throws \Throwable */
    public function testSvgUseTagResolvesDefinitionAndAppendsOutput(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(45);

        $obj->patchSvgObj(45, [
            'defs' => [
                'shape1' => [
                    'name' => 'rect',
                    'attr' => [
                        'x' => '2',
                        'y' => '3',
                        'width' => '5',
                        'height' => '4',
                        'style' => 'fill:#000000;stroke:none;',
                    ],
                ],
            ],
            'out' => '',
        ]);

        $parser = \xml_parser_create('UTF-8');
        $this->assertSame('', $obj->exposeParseSVGTagSTARTuse($parser, 45, [
            'xlink:href' => '#shape1',
            'x' => '5',
            'y' => '7',
        ]));

        $svgobj = $obj->getSvgObj(45);
        $this->assertNotSame('', $svgobj['out']);
    }

    /**
     * A <use> whose target is itself a <use> pointing back at the same id must
     * terminate instead of recursing until memory is exhausted (CWE-400).
     *
     * @throws \Throwable
     */
    public function testSvgUseSelfReferenceDoesNotRecurInfinitely(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(46);

        $obj->patchSvgObj(46, [
            'defs' => [
                'a' => [
                    'name' => 'use',
                    'attr' => ['id' => 'a', 'href' => '#a'],
                ],
            ],
            'out' => '',
        ]);

        $parser = \xml_parser_create('UTF-8');
        $result = $obj->exposeParseSVGTagSTARTuse($parser, 46, ['href' => '#a']);

        $this->assertSame('', $result);
        $this->assertSame('', $obj->getSvgObj(46)['out']);
    }

    /**
     * Two <use> elements in a mutual cycle (#a -> #b -> #a) must terminate.
     *
     * @throws \Throwable
     */
    public function testSvgUseMutualCycleTerminates(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(47);

        $obj->patchSvgObj(47, [
            'defs' => [
                'a' => ['name' => 'use', 'attr' => ['id' => 'a', 'href' => '#b']],
                'b' => ['name' => 'use', 'attr' => ['id' => 'b', 'href' => '#a']],
            ],
            'out' => '',
        ]);

        $parser = \xml_parser_create('UTF-8');
        $result = $obj->exposeParseSVGTagSTARTuse($parser, 47, ['href' => '#a']);

        $this->assertSame('', $result);
        $this->assertSame('', $obj->getSvgObj(47)['out']);
    }

    /**
     * The cycle guard refuses to expand a target already on the active chain,
     * so seeding its id into usechain suppresses rendering of an otherwise
     * valid shape.
     *
     * @throws \Throwable
     */
    public function testSvgUseCycleGuardRefusesTargetAlreadyBeingExpanded(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(48);

        $obj->patchSvgObj(48, [
            'defs' => [
                'shape1' => [
                    'name' => 'rect',
                    'attr' => [
                        'x' => '2',
                        'y' => '3',
                        'width' => '5',
                        'height' => '4',
                        'style' => 'fill:#000000;stroke:none;',
                    ],
                ],
            ],
            'usechain' => ['shape1' => true],
            'out' => '',
        ]);

        $parser = \xml_parser_create('UTF-8');
        $result = $obj->exposeParseSVGTagSTARTuse($parser, 48, ['href' => '#shape1']);

        $this->assertSame('', $result);
        $this->assertSame('', $obj->getSvgObj(48)['out']);
    }

    /**
     * The depth ceiling refuses expansion once the active chain reaches
     * SVGMAXUSEDEPTH (64), bounding non-cyclic but pathologically deep graphs.
     *
     * @throws \Throwable
     */
    public function testSvgUseDepthGuardRefusesBeyondMaxDepth(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(49);

        $deepChain = [];
        for ($i = 0; $i < 64; $i++) {
            $deepChain['d' . $i] = true;
        }

        $obj->patchSvgObj(49, [
            'defs' => [
                'shape1' => [
                    'name' => 'rect',
                    'attr' => [
                        'x' => '2',
                        'y' => '3',
                        'width' => '5',
                        'height' => '4',
                        'style' => 'fill:#000000;stroke:none;',
                    ],
                ],
            ],
            'usechain' => $deepChain,
            'out' => '',
        ]);

        $parser = \xml_parser_create('UTF-8');
        $result = $obj->exposeParseSVGTagSTARTuse($parser, 49, ['href' => '#shape1']);

        $this->assertSame('', $result);
        $this->assertSame('', $obj->getSvgObj(49)['out']);
    }

    /**
     * The guard is a stack-scoped active chain, not a permanent seen-set: two
     * separate <use> references to the same shape must both render.
     *
     * @throws \Throwable
     */
    public function testSvgUseRepeatedReferenceToSameShapeStillRenders(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(50);

        $obj->patchSvgObj(50, [
            'defs' => [
                'shape1' => [
                    'name' => 'rect',
                    'attr' => [
                        'x' => '2',
                        'y' => '3',
                        'width' => '5',
                        'height' => '4',
                        'style' => 'fill:#000000;stroke:none;',
                    ],
                ],
            ],
            'out' => '',
        ]);

        $parser = \xml_parser_create('UTF-8');
        $obj->exposeParseSVGTagSTARTuse($parser, 50, ['href' => '#shape1', 'x' => '5', 'y' => '7']);
        $firstOut = $obj->getSvgObj(50)['out'];
        $this->assertNotSame('', $firstOut);

        $obj->exposeParseSVGTagSTARTuse($parser, 50, ['href' => '#shape1', 'x' => '9', 'y' => '1']);
        $secondOut = $obj->getSvgObj(50)['out'];
        $this->assertNotSame($firstOut, $secondOut);
    }

    /**
     * End-to-end: the ~90-byte advisory PoC (a self-referencing <use> inside
     * <defs>) must render without a fatal memory-exhaustion crash.
     *
     * @throws \Throwable
     */
    public function testAddSVGSelfReferencingUseDoesNotExhaustMemory(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">'
            . '<defs><use id="a" href="#a"/></defs>'
            . '<use href="#a"/>'
            . '</svg>';

        $soid = $obj->addSVG($svg, 10, 10, 50, 50, $page['height']);

        $this->assertSame(1, $soid);
        /** @var array<int, array<string, mixed>> $svgobjs */
        $svgobjs = $this->getObjectProperty($obj, 'svgobjs');
        $this->assertArrayHasKey($soid, $svgobjs);
    }

    /** @throws \Throwable */
    public function testSvgTransformRotateCenterAndEmptyTransformMatrix(): void
    {
        $obj = $this->getInternalTestObject();

        $rot = $obj->exposeParseSVGTMrotate('45 10 20');
        $this->assertCount(6, $rot);
        assert(isset($rot[4]), "\$rot[4] must be set");
        $this->assertNotSame(0.0, $rot[4]);
        assert(isset($rot[5]), "\$rot[5] must be set");
        $this->assertNotSame(0.0, $rot[5]);

        $tmx = $obj->exposeGetSVGTransformMatrix('');
        $this->assertSame([1.0, 0.0, 0.0, 1.0, 0.0, 0.0], $tmx);
    }

    /** @throws \Throwable */
    public function testSvgPathCmdAHandlesIdenticalEndpointsAndSweepAdjustments(): void
    {
        $obj = $this->getInternalTestObject();
        $coord = $obj->getPathCoordDefaults();
        $coord['x'] = 10.0;
        $coord['y'] = 10.0;

        [$sameOut, $sameCoord] = $obj->exposeSvgPathCmdA(
            [5.0, 5.0, 0.0, 0.0, 1.0, 10.0, 10.0],
            $coord,
            [['0', 'A']],
            0,
            ['5', '5', '0', '0', '1', '10', '10'],
        );

        $this->assertSame('', $sameOut);
        $this->assertLessThanOrEqual(10.0, $sameCoord['xmin']);
        $this->assertLessThanOrEqual(10.0, $sameCoord['ymin']);
        $this->assertGreaterThanOrEqual(10.0, $sameCoord['xmax']);
        $this->assertGreaterThanOrEqual(10.0, $sameCoord['ymax']);

        [$arcOut] = $obj->exposeSvgPathCmdA(
            [4.0, 2.0, 0.0, 1.0, 1.0, 15.0, 18.0],
            $coord,
            [['0', 'A'], ['1', 'z']],
            0,
            ['4', '2', '0', '1', '1', '15', '18'],
        );

        $this->assertNotSame('', $arcOut);
    }

    /** @throws \Throwable */
    public function testSvgParseStyleGradientHandlesXrefMeasureAndPercentageModes(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(46);

        $gradients = [
            'base' => [
                'xref' => '',
                'type' => 2,
                'gradientUnits' => 'objectBoundingBox',
                'mode' => 'measure',
                'coords' => [0.0, 0.0, 10.0, 0.0, 3.0],
                'stops' => [
                    ['color' => '#000000', 'opacity' => 1.0, 'offset' => 0.0],
                    ['color' => '#ffffff', 'opacity' => 1.0, 'offset' => 1.0],
                ],
                'gradientTransform' => [1.0, 0.0, 0.0, 1.0, 2.0, 3.0],
            ],
            'ref' => [
                'xref' => 'base',
                'type' => 2,
                'gradientUnits' => 'objectBoundingBox',
                'mode' => 'measure',
                'coords' => [1.0, 2.0, 3.0, 4.0, 2.0],
                'stops' => [],
                'gradientTransform' => [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
            ],
            'pct' => [
                'xref' => '',
                'type' => 3,
                'gradientUnits' => 'objectBoundingBox',
                'mode' => 'percentage',
                'coords' => [-10.0, 150.0, 50.0, 50.0, 20.0],
                'stops' => [
                    ['color' => '#111111', 'opacity' => 1.0, 'offset' => 0.0],
                    ['color' => '#eeeeee', 'opacity' => 1.0, 'offset' => 1.0],
                ],
                'gradientTransform' => [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
            ],
        ];

        $refOut = $obj->exposeParseSVGStyleGradient(46, $gradients, 'ref', 5.0, 6.0, 20.0, 10.0, 'getClippingRect', [
            5.0,
            6.0,
            20.0,
            10.0,
            0.0,
        ]);
        $this->assertNotSame('', $refOut);

        $pctOut = $obj->exposeParseSVGStyleGradient(46, $gradients, 'pct', 0.0, 0.0, 10.0, 10.0);
        $this->assertNotSame('', $pctOut);
    }

    /** @throws \Throwable */
    public function testSvgDefsModeStartEndAndInnerSvgStartBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(47);
        $base = $obj->exposeDefaultSVGStyle();
        $parser = \xml_parser_create('UTF-8');

        $obj->patchSvgObj(47, [
            'defsmode' => true,
            'text' => 'txt',
            'defs' => [
                'def1' => [
                    'name' => 'g',
                    'attr' => ['id' => 'def1'],
                    'child' => [
                        'child1' => [
                            'name' => 'path',
                            'attr' => ['id' => 'child1'],
                        ],
                    ],
                ],
            ],
        ]);

        $obj->exposeHandleSVGTagEnd($parser, 'path');
        $defs = $obj->getSvgObj(47)['defs'];
        /** @var array<string, mixed> $def1 */
        $def1 = $defs['def1'] ?? [];
        /** @var array<string, mixed> $child */
        $child = isset($def1['child']) && \is_array($def1['child']) ? $def1['child'] : [];
        $this->assertArrayHasKey('child1_CLOSE', $child);

        $obj->exposeHandleSVGTagStart($parser, 'line', [], 47);
        $defs = $obj->getSvgObj(47)['defs'];
        /** @var array<string, mixed> $def1 */
        $def1 = $defs['def1'] ?? [];
        /** @var array<string, mixed> $child */
        $child = isset($def1['child']) && \is_array($def1['child']) ? $def1['child'] : [];
        $this->assertArrayHasKey('DF_3', $child);

        $obj->exposeHandleSVGTagStart($parser, 'circle', ['id' => 'newdef'], 47);
        $this->assertArrayHasKey('newdef', $obj->getSvgObj(47)['defs']);

        $obj->patchSvgObj(47, ['defsmode' => false, 'tagdepth' => 1]);
        $innerOut = $obj->exposeParseSVGTagSTARTsvg(
            $parser,
            47,
            ['x' => '2', 'y' => '3', 'width' => '12', 'height' => '8'],
            $base,
            $base,
        );

        $this->assertNotSame('', $innerOut);
        $this->assertGreaterThan(1, \count($obj->getSvgObj(47)['styles']));
    }

    /** @throws \Throwable */
    public function testSvgPathCommandsCoverRelativeAndFallbackBranches(): void
    {
        $obj = $this->getInternalTestObject();

        $coord = $obj->getPathCoordDefaults();
        $coord['relcoord'] = true;
        $coord['x'] = 2.0;
        $coord['y'] = 3.0;
        $coord['x0'] = 2.0;
        $coord['y0'] = 3.0;
        $coord['xoffset'] = 2.0;
        $coord['yoffset'] = 3.0;

        [, $coord] = $obj->exposeSvgPathCmdC([1, 1, 2, 2, 3, 3], $coord);
        $this->assertGreaterThan(2.0, $coord['xoffset']);
        $this->assertGreaterThan(3.0, $coord['yoffset']);

        [, $coord] = $obj->exposeSvgPathCmdH([4], $coord);
        $this->assertGreaterThan(0.0, $coord['xoffset']);

        [, $coord] = $obj->exposeSvgPathCmdL([5, 6], $coord);
        $this->assertGreaterThan(0.0, $coord['xoffset']);
        $this->assertGreaterThan(0.0, $coord['yoffset']);

        [, $coord] = $obj->exposeSvgPathCmdM([7, 8], $coord);
        $this->assertGreaterThan(0.0, $coord['xoffset']);
        $this->assertGreaterThan(0.0, $coord['yoffset']);

        [, $coord] = $obj->exposeSvgPathCmdQ([1, 2, 3, 4], $coord);
        $this->assertGreaterThan(0.0, $coord['xoffset']);
        $this->assertGreaterThan(0.0, $coord['yoffset']);

        [, $coord] = $obj->exposeSvgPathCmdS([2, 3, 4, 5], $coord);
        $this->assertGreaterThan(0.0, $coord['xoffset']);
        $this->assertGreaterThan(0.0, $coord['yoffset']);

        [, $coord] = $obj->exposeSvgPathCmdT([6, 7], $coord);
        $this->assertGreaterThan(0.0, $coord['xoffset']);
        $this->assertGreaterThan(0.0, $coord['yoffset']);

        [, $coord] = $obj->exposeSvgPathCmdV([9], $coord);
        $this->assertGreaterThan(0.0, $coord['yoffset']);
    }

    /** @throws \Throwable */
    public function testSvgStyleFontAndStrokeExtraBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->setSvgObjMeta(48);

        $base = $obj->exposeDefaultSVGStyle();
        $fontStyle = $base;
        $fontStyle['font'] = 'italic small-caps bold 14px/16px helvetica';
        $fontStyle['font-family'] = '';
        [$fontOut, $fontStyle] = $obj->exposeParseSVGStyleFont($fontStyle, $base);
        $this->assertNotSame('', $fontOut);
        $this->assertNotSame('', $fontStyle['font-family']);

        $strokeStyle = $base;
        $strokeStyle['stroke'] = 'rgba(10,20,30,0.5)';
        $strokeStyle['stroke-width'] = 1.2;
        $strokeStyle['stroke-linecap'] = 'round';
        $strokeStyle['stroke-linejoin'] = 'bevel';
        $strokeStyle['stroke-opacity'] = 0.8;
        $strokeStyle['opacity'] = 0.7;
        $strokeStyle['mix-blend-mode'] = 'screen';
        [$strokeOut] = $obj->exposeParseSVGStyleStroke(48, $strokeStyle);
        $this->assertNotSame('', $strokeOut);
    }

    /** @throws \Throwable */
    public function testSvgHandleStartAndEndAdditionalBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');

        $obj->exposeHandleSVGTagStart($parser, 'g', [], 999);

        $obj->initSvgObjForHandlers(49);
        $base = $obj->exposeDefaultSVGStyle();

        $obj->patchSvgObj(49, [
            'clipmode' => true,
            'styles' => [$base],
            'textmode' => ['invisible' => false, 'stroke' => 1, 'rtl' => false, 'text-anchor' => 'middle'],
            'x' => 11.0,
            'y' => 12.0,
            'text' => 'abc',
            'defsmode' => false,
            'tagdepth' => 2,
        ]);

        $obj->exposeHandleSVGTagStart($parser, 'path', ['d' => 'M0 0 L1 1'], 49, true, [1, 0, 0, 1, 0, 0]);
        $this->assertNotEmpty($obj->getSvgObj(49)['clippaths']);

        $textOut = $obj->exposeParseSVGTagENDtext(49);
        $this->assertSame('', $obj->getSvgObj(49)['text']);
        $this->assertNotSame('', $textOut);

        $obj->exposeParseSVGTagENDsvg(49);
        $this->assertNotSame('', $textOut);
    }

    /** @throws \Throwable */
    public function testSvgStartSvgViewboxFallbackAndAspectBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(50);
        $base = $obj->exposeDefaultSVGStyle();
        $parser = \xml_parser_create('UTF-8');

        $obj->patchSvgObj(50, ['tagdepth' => 1]);
        $badViewBox = $obj->exposeParseSVGTagSTARTsvg(
            $parser,
            50,
            ['x' => '1', 'y' => '2', 'width' => '12', 'height' => '8', 'viewBox' => '0 0 10'],
            $base,
            $base,
        );
        $this->assertNotSame('', $badViewBox);

        $obj->patchSvgObj(50, ['tagdepth' => 1]);
        $aspectOut = $obj->exposeParseSVGTagSTARTsvg(
            $parser,
            50,
            [
                'x' => '0',
                'y' => '0',
                'width' => '40',
                'height' => '20',
                'viewBox' => '0 0 100 50',
                'preserveAspectRatio' => 'xMaxYMax slice',
            ],
            $base,
            $base,
        );
        $this->assertNotSame('', $aspectOut);
    }

    /** @throws \Throwable */
    public function testSvgVisibleStartTagHandlersProduceOutput(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(51);
        $base = $obj->exposeDefaultSVGStyle();
        $base['fill'] = '#336699';
        $base['stroke'] = '#112233';
        $base['stroke-width'] = 0.4;
        $base['opacity'] = 1.0;
        $base['fill-opacity'] = 1.0;
        $base['stroke-opacity'] = 1.0;
        $base['text-anchor'] = 'middle';
        $base['direction'] = 'rtl';
        $obj->patchSvgObj(51, [
            'clipmode' => false,
            'textmode' => ['invisible' => false, 'stroke' => 0, 'rtl' => false, 'text-anchor' => 'start'],
            'styles' => [$base],
            'dir' => '/tmp',
        ]);

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

        $pathOut = $obj->exposeParseSVGTagSTARTpath(
            $parser,
            51,
            ['d' => 'M 1 1 L 5 5 H 6 V 7 C 8 8 9 9 10 10 Q 11 11 12 12 S 13 13 14 14 T 15 15 A 2 2 0 1 1 16 16 z'],
            $base,
            $base,
        );
        $this->assertNotSame('', $pathOut);

        $rectOut = $obj->exposeParseSVGTagSTARTrect(
            $parser,
            51,
            ['x' => '2', 'y' => '3', 'width' => '8', 'height' => '4', 'rx' => '1', 'ry' => '1'],
            $base,
            $base,
        );
        $this->assertNotSame('', $rectOut);

        $circleOut = $obj->exposeParseSVGTagSTARTcircle(
            $parser,
            51,
            ['cx' => '10', 'cy' => '10', 'r' => '4'],
            $base,
            $base,
        );
        $this->assertNotSame('', $circleOut);

        $ellipseOut = $obj->exposeParseSVGTagSTARTellipse(
            $parser,
            51,
            ['cx' => '12', 'cy' => '9', 'rx' => '5', 'ry' => '2'],
            $base,
            $base,
        );
        $this->assertNotSame('', $ellipseOut);

        $lineOut = $obj->exposeParseSVGTagSTARTline(
            $parser,
            51,
            ['x1' => '1', 'y1' => '2', 'x2' => '9', 'y2' => '4'],
            $base,
            $base,
        );
        $this->assertNotSame('', $lineOut);

        $polyOut = $obj->exposeParseSVGTagSTARTpolygon($parser, 51, ['points' => '1,1 5,1 5,5 1,5'], $base, $base);
        $this->assertNotSame('', $polyOut);

        $imgPath = (string) \realpath(__DIR__ . '/../examples/images/tcpdf_signature.png');
        $imgOut = $obj->exposeParseSVGTagSTARTimage(
            $parser,
            51,
            ['xlink:href' => $imgPath, 'x' => '1', 'y' => '1', 'width' => '3', 'height' => '3'],
            $base,
            $base,
        );
        $this->assertNotSame('', $imgOut);

        $textOut = $obj->exposeParseSVGTagSTARTtext(
            $parser,
            51,
            ['x' => '4', 'y' => '6', 'dx' => '1', 'dy' => '1'],
            $base,
            $base,
        );
        $this->assertNotSame('', $textOut);

        $tspanOut = $obj->exposeParseSVGTagSTARTtspan($parser, 51, ['dx' => '1', 'dy' => '1'], $base, $base);
        $this->assertNotSame('', $tspanOut);
    }

    /** @throws \Throwable */
    public function testSvgHandleEndDefsAddsParentCloseWhenNameMatches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(52);
        $obj->patchSvgObj(52, [
            'defsmode' => true,
            'text' => 'close-parent',
            'defs' => [
                'grp1' => [
                    'name' => 'g',
                    'attr' => ['id' => 'grp1'],
                    'child' => [
                        'other' => [
                            'name' => 'path',
                            'attr' => ['id' => 'other'],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $obj->exposeHandleSVGTagEnd($parser, 'g');

        $defs = $obj->getSvgObj(52)['defs'];
        /** @var array<string, mixed> $grp */
        $grp = $defs['grp1'] ?? [];
        /** @var array<string, mixed> $child */
        $child = isset($grp['child']) && \is_array($grp['child']) ? $grp['child'] : [];
        $this->assertArrayHasKey('grp1_CLOSE', $child);
    }

    /** @throws \Throwable */
    public function testSvgMarkerDefsCaptureAndLifecycle(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(521);

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

        $obj->exposeHandleSVGTagStart($parser, 'defs', [], 521);
        $obj->exposeHandleSVGTagStart(
            $parser,
            'marker',
            [
                'id' => 'mk1',
                'viewBox' => '0 0 10 10',
                'refX' => '5',
                'refY' => '5',
                'markerWidth' => '5',
                'markerHeight' => '5',
                'orient' => 'auto',
            ],
            521,
        );

        $obj->exposeHandleSVGTagStart($parser, 'path', ['d' => 'M 0 0 L 10 5 L 0 10 Z'], 521);
        $obj->exposeHandleSVGTagEnd($parser, 'path');
        $obj->exposeHandleSVGTagEnd($parser, 'marker');

        $svgobj = $obj->getSvgObj(521);
        $this->assertFalse($svgobj['defsmode']);
        $this->assertArrayHasKey('mk1', $svgobj['defs']);

        /** @var array<string, mixed> $marker */
        $marker = $svgobj['defs']['mk1'] ?? [];
        $this->assertSame('marker', $marker['name'] ?? null);

        /** @var array<string, mixed> $child */
        $child = isset($marker['child']) && \is_array($marker['child']) ? $marker['child'] : [];
        $this->assertArrayHasKey('DF_1', $child);
        $this->assertArrayHasKey('DF_1_CLOSE', $child);
    }

    /** @throws \Throwable */
    public function testSvgMarkerStartWithoutIdOnlyEnablesDefsMode(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(522);

        $this->assertSame('', $obj->exposeParseSVGTagSTARTmarker(522, []));
        $svgobj = $obj->getSvgObj(522);
        $this->assertTrue($svgobj['defsmode']);
        $this->assertSame([], $svgobj['defs']);

        $this->assertSame('', $obj->exposeParseSVGTagENDmarker(522));
        $this->assertFalse($obj->getSvgObj(522)['defsmode']);
    }

    /** @throws \Throwable */
    public function testSvgPatternDefsCaptureAndLifecycle(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(538);

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

        $obj->exposeHandleSVGTagStart($parser, 'defs', [], 538);
        $obj->exposeHandleSVGTagStart(
            $parser,
            'pattern',
            [
                'id' => 'pat1',
                'patternUnits' => 'userSpaceOnUse',
                'x' => '0',
                'y' => '0',
                'width' => '10',
                'height' => '10',
            ],
            538,
        );
        $obj->exposeHandleSVGTagStart(
            $parser,
            'rect',
            ['x' => '0', 'y' => '0', 'width' => '10', 'height' => '10', 'fill' => '#ff0000'],
            538,
        );
        $obj->exposeHandleSVGTagEnd($parser, 'rect');
        $obj->exposeHandleSVGTagEnd($parser, 'pattern');

        $svgobj = $obj->getSvgObj(538);
        $this->assertFalse($svgobj['defsmode']);
        $this->assertArrayHasKey('pat1', $svgobj['defs']);

        /** @var array<string, mixed> $pattern */
        $pattern = $svgobj['defs']['pat1'] ?? [];
        $this->assertSame('pattern', $pattern['name'] ?? null);

        /** @var array<string, mixed> $child */
        $child = isset($pattern['child']) && \is_array($pattern['child']) ? $pattern['child'] : [];
        $this->assertArrayHasKey('DF_1', $child);
        $this->assertArrayHasKey('DF_1_CLOSE', $child);
    }

    /** @throws \Throwable */
    public function testSvgPatternStartWithoutIdOnlyEnablesDefsMode(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(539);

        $this->assertSame('', $obj->exposeParseSVGTagSTARTpattern(539, []));
        $svgobj = $obj->getSvgObj(539);
        $this->assertTrue($svgobj['defsmode']);
        $this->assertSame([], $svgobj['defs']);

        $this->assertSame('', $obj->exposeParseSVGTagENDpattern(539));
        $this->assertFalse($obj->getSvgObj(539)['defsmode']);
    }

    /** @throws \Throwable */
    public function testSvgMaskDefsCaptureAndLifecycle(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(549);

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

        $obj->exposeHandleSVGTagStart($parser, 'defs', [], 549);
        $obj->exposeHandleSVGTagStart(
            $parser,
            'mask',
            [
                'id' => 'msk1',
                'x' => '0',
                'y' => '0',
                'width' => '10',
                'height' => '10',
            ],
            549,
        );
        $obj->exposeHandleSVGTagStart(
            $parser,
            'rect',
            ['x' => '0', 'y' => '0', 'width' => '10', 'height' => '10', 'fill' => '#ffffff'],
            549,
        );
        $obj->exposeHandleSVGTagEnd($parser, 'rect');
        $obj->exposeHandleSVGTagEnd($parser, 'mask');

        $svgobj = $obj->getSvgObj(549);
        $this->assertFalse($svgobj['defsmode']);
        $this->assertArrayHasKey('msk1', $svgobj['defs']);

        /** @var array<string, mixed> $mask */
        $mask = $svgobj['defs']['msk1'] ?? [];
        $this->assertSame('mask', $mask['name'] ?? null);

        /** @var array<string, mixed> $child */
        $child = isset($mask['child']) && \is_array($mask['child']) ? $mask['child'] : [];
        $this->assertArrayHasKey('DF_1', $child);
        $this->assertArrayHasKey('DF_1_CLOSE', $child);
    }

    /** @throws \Throwable */
    public function testSvgMaskStartWithoutIdOnlyEnablesDefsMode(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(550);

        $this->assertSame('', $obj->exposeParseSVGTagSTARTmask(550, []));
        $svgobj = $obj->getSvgObj(550);
        $this->assertTrue($svgobj['defsmode']);
        $this->assertSame([], $svgobj['defs']);

        $this->assertSame('', $obj->exposeParseSVGTagENDmask(550));
        $this->assertFalse($obj->getSvgObj(550)['defsmode']);
    }

    /** @throws \Throwable */
    public function testSvgPatternHrefInheritanceIsResolvedByFill(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(540);

        $base = $obj->exposeDefaultSVGStyle();
        $base['fill'] = 'url(#patRef)';
        $base['opacity'] = 1.0;
        $base['fill-opacity'] = 1.0;

        $obj->patchSvgObj(540, [
            'defs' => [
                'patBase' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patBase',
                        'patternUnits' => 'userSpaceOnUse',
                        'x' => '0',
                        'y' => '0',
                        'width' => '4',
                        'height' => '4',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '0',
                                'y' => '0',
                                'width' => '4',
                                'height' => '4',
                                'fill' => 'none',
                                'stroke' => '#111111',
                                'stroke-width' => '1',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'rect',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
                'patRef' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patRef',
                        'xlink:href' => '#patBase',
                    ],
                    'child' => [],
                ],
            ],
            'out' => 'BASE',
        ]);

        [$out] = $obj->exposeParseSVGStyleFill(540, $base, [], 0, 0, 10, 10, 'getClippingRect', [
            0.0,
            0.0,
            10.0,
            10.0,
            0.0,
        ]);

        $this->assertNotSame('', $out);
        $this->assertSame('BASE', $obj->getSvgObj(540)['out']);
        $this->assertSame(0, $obj->getSvgObj(540)['patternmode']);
    }

    /** @throws \Throwable */
    public function testSvgPatternHrefInheritanceUsesChildAttrAndParentFallback(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(544);

        $obj->patchSvgObj(544, [
            'defs' => [
                'patBase' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patBase',
                        'patternUnits' => 'userSpaceOnUse',
                        'x' => '1',
                        'y' => '2',
                        'width' => '4',
                        'height' => '6',
                    ],
                    'child' => [
                        'PARENT_RECT' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '0',
                                'y' => '0',
                                'width' => '4',
                                'height' => '6',
                                'fill' => 'none',
                                'stroke' => '#111111',
                            ],
                        ],
                    ],
                ],
                'patRef' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patRef',
                        'href' => '#patBase',
                        'width' => '9',
                    ],
                    'child' => [],
                ],
            ],
        ]);

        $resolved = $obj->exposeResolveSVGPatternDef(544, 'patRef');

        $this->assertNotNull($resolved);
        $resolvedAttr = $resolved['attr'];
        $resolvedChild = isset($resolved['child']) ? $resolved['child'] : [];

        $this->assertSame('9', $resolvedAttr['width'] ?? '');
        $this->assertSame('6', $resolvedAttr['height'] ?? '');
        $this->assertSame('1', $resolvedAttr['x'] ?? '');
        $this->assertSame('2', $resolvedAttr['y'] ?? '');
        $this->assertSame('userSpaceOnUse', $resolvedAttr['patternUnits'] ?? '');
        $this->assertArrayHasKey('PARENT_RECT', $resolvedChild);
    }

    /** @throws \Throwable */
    public function testSvgPatternHrefInheritanceKeepsChildContentWhenPresent(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(545);

        $obj->patchSvgObj(545, [
            'defs' => [
                'patBase' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patBase',
                        'patternUnits' => 'userSpaceOnUse',
                        'x' => '0',
                        'y' => '0',
                        'width' => '4',
                        'height' => '4',
                    ],
                    'child' => [
                        'PARENT_RECT' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '0',
                                'y' => '0',
                                'width' => '4',
                                'height' => '4',
                                'fill' => '#aaaaaa',
                            ],
                        ],
                    ],
                ],
                'patRef' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patRef',
                        'xlink:href' => '#patBase',
                    ],
                    'child' => [
                        'CHILD_RECT' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '1',
                                'y' => '1',
                                'width' => '2',
                                'height' => '2',
                                'fill' => '#222222',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $resolved = $obj->exposeResolveSVGPatternDef(545, 'patRef');

        $this->assertNotNull($resolved);
        $resolvedChild = isset($resolved['child']) ? $resolved['child'] : [];
        $this->assertArrayHasKey('CHILD_RECT', $resolvedChild);
        $this->assertArrayNotHasKey('PARENT_RECT', $resolvedChild);
    }

    /** @throws \Throwable */
    public function testSvgPatternHrefMissingParentFallsBackToChildDefinition(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(546);

        $obj->patchSvgObj(546, [
            'defs' => [
                'patRef' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patRef',
                        'href' => '#missingPattern',
                        'x' => '0',
                        'y' => '0',
                        'width' => '5',
                        'height' => '5',
                    ],
                    'child' => [
                        'CHILD_RECT' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '0',
                                'y' => '0',
                                'width' => '5',
                                'height' => '5',
                                'fill' => 'none',
                                'stroke' => '#000000',
                                'stroke-width' => '1',
                            ],
                        ],
                        'CHILD_RECT_CLOSE' => [
                            'name' => 'rect',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $resolved = $obj->exposeResolveSVGPatternDef(546, 'patRef');

        $this->assertNotNull($resolved);
        $resolvedAttr = $resolved['attr'];
        $resolvedChild = isset($resolved['child']) ? $resolved['child'] : [];
        $this->assertSame('5', $resolvedAttr['width'] ?? '');
        $this->assertSame('5', $resolvedAttr['height'] ?? '');
        $this->assertArrayNotHasKey('patternUnits', $resolvedAttr);
        $this->assertArrayHasKey('CHILD_RECT', $resolvedChild);

        $style = $obj->exposeDefaultSVGStyle();
        $style['fill'] = 'url(#patRef)';
        $style['opacity'] = 1.0;
        $style['fill-opacity'] = 1.0;

        [$out] = $obj->exposeParseSVGStyleFill(546, $style, [], 0, 0, 10, 10, 'getClippingRect', [
            0.0,
            0.0,
            10.0,
            10.0,
            0.0,
        ]);

        $this->assertNotSame('', $out);
    }

    /** @throws \Throwable */
    public function testSvgPatternHrefNonFragmentKeepsLocalPatternDefinition(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(547);

        $obj->patchSvgObj(547, [
            'defs' => [
                'patBase' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patBase',
                        'patternUnits' => 'userSpaceOnUse',
                        'x' => '0',
                        'y' => '0',
                        'width' => '4',
                        'height' => '4',
                    ],
                    'child' => [
                        'PARENT_RECT' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '0',
                                'y' => '0',
                                'width' => '4',
                                'height' => '4',
                                'fill' => '#00ff00',
                            ],
                        ],
                    ],
                ],
                'patRef' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patRef',
                        'href' => 'https://example.com/patterns.svg#patBase',
                        'x' => '1',
                        'y' => '1',
                        'width' => '5',
                        'height' => '5',
                    ],
                    'child' => [
                        'CHILD_RECT' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '1',
                                'y' => '1',
                                'width' => '3',
                                'height' => '3',
                                'fill' => 'none',
                                'stroke' => '#000000',
                                'stroke-width' => '1',
                            ],
                        ],
                        'CHILD_RECT_CLOSE' => [
                            'name' => 'rect',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $resolved = $obj->exposeResolveSVGPatternDef(547, 'patRef');

        $this->assertNotNull($resolved);
        $resolvedAttr = $resolved['attr'];
        $resolvedChild = isset($resolved['child']) ? $resolved['child'] : [];
        $this->assertSame('5', $resolvedAttr['width'] ?? '');
        $this->assertSame('5', $resolvedAttr['height'] ?? '');
        $this->assertArrayNotHasKey('patternUnits', $resolvedAttr);
        $this->assertArrayHasKey('CHILD_RECT', $resolvedChild);
        $this->assertArrayNotHasKey('PARENT_RECT', $resolvedChild);

        $style = $obj->exposeDefaultSVGStyle();
        $style['fill'] = 'url(#patRef)';
        $style['opacity'] = 1.0;
        $style['fill-opacity'] = 1.0;

        [$out] = $obj->exposeParseSVGStyleFill(547, $style, [], 0, 0, 10, 10, 'getClippingRect', [
            0.0,
            0.0,
            10.0,
            10.0,
            0.0,
        ]);

        $this->assertNotSame('', $out);
    }

    /** @throws \Throwable */
    public function testSvgPatternHrefInheritsViewBoxAndPatternTransformInteraction(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(548);

        $defsPatternChild = [
            'DF_1' => [
                'name' => 'rect',
                'attr' => [
                    'x' => '0',
                    'y' => '0',
                    'width' => '10',
                    'height' => '20',
                    'fill' => 'none',
                    'stroke' => '#000000',
                    'stroke-width' => '1',
                ],
            ],
            'DF_1_CLOSE' => [
                'name' => 'rect',
                'attr' => [
                    'closing_tag' => true,
                    'content' => '',
                ],
            ],
        ];

        $obj->patchSvgObj(548, [
            'defs' => [
                'patBase' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patBase',
                        'patternUnits' => 'userSpaceOnUse',
                        'x' => '0',
                        'y' => '0',
                        'width' => '6',
                        'height' => '6',
                        'viewBox' => '0 0 10 20',
                        'preserveAspectRatio' => 'xMidYMid meet',
                        'patternTransform' => 'translate(1,2) scale(0.5)',
                    ],
                    'child' => $defsPatternChild,
                ],
                'patRefInherit' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patRefInherit',
                        'href' => '#patBase',
                    ],
                    'child' => [],
                ],
                'patRefOverride' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patRefOverride',
                        'xlink:href' => '#patBase',
                        'patternTransform' => 'translate(0,0) scale(1)',
                    ],
                    'child' => [],
                ],
            ],
        ]);

        $resolvedInherit = $obj->exposeResolveSVGPatternDef(548, 'patRefInherit');
        $resolvedOverride = $obj->exposeResolveSVGPatternDef(548, 'patRefOverride');
        $this->assertNotNull($resolvedInherit);
        $this->assertNotNull($resolvedOverride);

        $attrInherit = $resolvedInherit['attr'];
        $attrOverride = $resolvedOverride['attr'];
        $this->assertSame('0 0 10 20', $attrInherit['viewBox'] ?? '');
        $this->assertSame('translate(1,2) scale(0.5)', $attrInherit['patternTransform'] ?? '');
        $this->assertSame('0 0 10 20', $attrOverride['viewBox'] ?? '');
        $this->assertSame('translate(0,0) scale(1)', $attrOverride['patternTransform'] ?? '');

        $style = $obj->exposeDefaultSVGStyle();
        $style['opacity'] = 1.0;
        $style['fill-opacity'] = 1.0;

        $style['fill'] = 'url(#patBase)';
        [$outBase] = $obj->exposeParseSVGStyleFill(548, $style, [], 0, 0, 6, 6, 'getClippingRect', [
            0.0,
            0.0,
            6.0,
            6.0,
            0.0,
        ]);

        $style['fill'] = 'url(#patRefInherit)';
        [$outInherit] = $obj->exposeParseSVGStyleFill(548, $style, [], 0, 0, 6, 6, 'getClippingRect', [
            0.0,
            0.0,
            6.0,
            6.0,
            0.0,
        ]);

        $style['fill'] = 'url(#patRefOverride)';
        [$outOverride] = $obj->exposeParseSVGStyleFill(548, $style, [], 0, 0, 6, 6, 'getClippingRect', [
            0.0,
            0.0,
            6.0,
            6.0,
            0.0,
        ]);

        $this->assertNotSame('', $outBase);
        $this->assertNotSame('', $outInherit);
        $this->assertNotSame('', $outOverride);
    }

    /** @throws \Throwable */
    public function testSvgPatternPreserveAspectRatioChangesViewBoxTransform(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(541);

        $style = $obj->exposeDefaultSVGStyle();
        $style['opacity'] = 1.0;
        $style['fill-opacity'] = 1.0;

        $obj->patchSvgObj(541, [
            'defs' => [
                'patNone' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patNone',
                        'patternUnits' => 'userSpaceOnUse',
                        'patternContentUnits' => 'userSpaceOnUse',
                        'x' => '0',
                        'y' => '0',
                        'width' => '6',
                        'height' => '6',
                        'viewBox' => '0 0 10 20',
                        'preserveAspectRatio' => 'none',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '0',
                                'y' => '0',
                                'width' => '10',
                                'height' => '20',
                                'fill' => 'none',
                                'stroke' => '#000000',
                                'stroke-width' => '1',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'rect',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
                'patMeet' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patMeet',
                        'patternUnits' => 'userSpaceOnUse',
                        'patternContentUnits' => 'userSpaceOnUse',
                        'x' => '0',
                        'y' => '0',
                        'width' => '6',
                        'height' => '6',
                        'viewBox' => '0 0 10 20',
                        'preserveAspectRatio' => 'xMidYMid meet',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '0',
                                'y' => '0',
                                'width' => '10',
                                'height' => '20',
                                'fill' => 'none',
                                'stroke' => '#000000',
                                'stroke-width' => '1',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'rect',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $style['fill'] = 'url(#patNone)';
        [$outNone] = $obj->exposeParseSVGStyleFill(541, $style, [], 0, 0, 6, 6, 'getClippingRect', [
            0.0,
            0.0,
            6.0,
            6.0,
            0.0,
        ]);

        $style['fill'] = 'url(#patMeet)';
        [$outMeet] = $obj->exposeParseSVGStyleFill(541, $style, [], 0, 0, 6, 6, 'getClippingRect', [
            0.0,
            0.0,
            6.0,
            6.0,
            0.0,
        ]);

        $this->assertNotSame('', $outNone);
        $this->assertNotSame('', $outMeet);
        $this->assertNotSame($outNone, $outMeet);
    }

    /** @throws \Throwable */
    public function testSvgPatternHrefCycleIsHandledSafely(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(542);

        $style = $obj->exposeDefaultSVGStyle();
        $style['fill'] = 'url(#patCycleA)';
        $style['opacity'] = 1.0;
        $style['fill-opacity'] = 1.0;

        $obj->patchSvgObj(542, [
            'defs' => [
                'patCycleA' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patCycleA',
                        'xlink:href' => '#patCycleB',
                        'patternUnits' => 'userSpaceOnUse',
                        'x' => '0',
                        'y' => '0',
                        'width' => '4',
                        'height' => '4',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'rect',
                            'attr' => [
                                'x' => '0',
                                'y' => '0',
                                'width' => '4',
                                'height' => '4',
                                'fill' => 'none',
                                'stroke' => '#000000',
                                'stroke-width' => '1',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'rect',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
                'patCycleB' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patCycleB',
                        'xlink:href' => '#patCycleA',
                    ],
                    'child' => [],
                ],
            ],
            'out' => 'BASE',
        ]);

        [$out] = $obj->exposeParseSVGStyleFill(542, $style, [], 0, 0, 10, 10, 'getClippingRect', [
            0.0,
            0.0,
            10.0,
            10.0,
            0.0,
        ]);

        $this->assertNotSame('', $out);
        $this->assertSame('BASE', $obj->getSvgObj(542)['out']);
        $this->assertSame(0, $obj->getSvgObj(542)['patternmode']);
    }

    /** @throws \Throwable */
    public function testSvgPatternViewBoxIgnoresPatternContentUnits(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(543);

        $style = $obj->exposeDefaultSVGStyle();
        $style['opacity'] = 1.0;
        $style['fill-opacity'] = 1.0;

        $defsPatternChild = [
            'DF_1' => [
                'name' => 'rect',
                'attr' => [
                    'x' => '0',
                    'y' => '0',
                    'width' => '10',
                    'height' => '20',
                    'fill' => 'none',
                    'stroke' => '#000000',
                    'stroke-width' => '1',
                ],
            ],
            'DF_1_CLOSE' => [
                'name' => 'rect',
                'attr' => [
                    'closing_tag' => true,
                    'content' => '',
                ],
            ],
        ];

        $obj->patchSvgObj(543, [
            'defs' => [
                'patUser' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patUser',
                        'patternUnits' => 'userSpaceOnUse',
                        'patternContentUnits' => 'userSpaceOnUse',
                        'x' => '0',
                        'y' => '0',
                        'width' => '6',
                        'height' => '6',
                        'viewBox' => '0 0 10 20',
                        'preserveAspectRatio' => 'xMidYMid meet',
                    ],
                    'child' => $defsPatternChild,
                ],
                'patObj' => [
                    'name' => 'pattern',
                    'attr' => [
                        'id' => 'patObj',
                        'patternUnits' => 'userSpaceOnUse',
                        'patternContentUnits' => 'objectBoundingBox',
                        'x' => '0',
                        'y' => '0',
                        'width' => '6',
                        'height' => '6',
                        'viewBox' => '0 0 10 20',
                        'preserveAspectRatio' => 'xMidYMid meet',
                    ],
                    'child' => $defsPatternChild,
                ],
            ],
        ]);

        $style['fill'] = 'url(#patUser)';
        [$outUser] = $obj->exposeParseSVGStyleFill(543, $style, [], 0, 0, 6, 6, 'getClippingRect', [
            0.0,
            0.0,
            6.0,
            6.0,
            0.0,
        ]);

        $style['fill'] = 'url(#patObj)';
        [$outObj] = $obj->exposeParseSVGStyleFill(543, $style, [], 0, 0, 6, 6, 'getClippingRect', [
            0.0,
            0.0,
            6.0,
            6.0,
            0.0,
        ]);

        $this->assertNotSame('', $outUser);
        $this->assertNotSame('', $outObj);
        $normUser = (string) \preg_replace('/PTN_[A-F0-9]{16}/', 'PTN_ID', $outUser);
        $normObj = (string) \preg_replace('/PTN_[A-F0-9]{16}/', 'PTN_ID', $outObj);
        $this->assertSame($normUser, $normObj);
    }

    /** @throws \Throwable */
    public function testSvgLineRendersStartEndMarkersWhenDefined(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(523);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker-start'] = 'url(#mkline)';
        $base['marker-end'] = 'url(#mkline)';

        $obj->patchSvgObj(523, [
            'styles' => [$base],
            'defs' => [
                'mkline' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkline',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => 'auto',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $out = $obj->exposeParseSVGTagSTARTline(
            $parser,
            523,
            ['x1' => '1', 'y1' => '1', 'x2' => '9', 'y2' => '1'],
            $base,
            $base,
        );

        $this->assertNotSame('', $out);
        $this->assertGreaterThanOrEqual(3, \substr_count($out, "q\n"));
    }

    /** @throws \Throwable */
    public function testSvgLineIgnoresMissingMarkerRefsGracefully(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(524);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker-start'] = 'url(#missing)';
        $base['marker-end'] = 'none';

        $parser = \xml_parser_create('UTF-8');
        $out = $obj->exposeParseSVGTagSTARTline(
            $parser,
            524,
            ['x1' => '0', 'y1' => '0', 'x2' => '5', 'y2' => '0'],
            $base,
            $base,
        );

        $this->assertNotSame('', $out);
        $this->assertSame(1, \substr_count($out, "q\n"));
    }

    /** @throws \Throwable */
    public function testSvgPathRendersStartMidEndMarkersWhenDefined(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(525);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.6;
        $base['marker-start'] = 'url(#mkpath)';
        $base['marker-mid'] = 'url(#mkpath)';
        $base['marker-end'] = 'url(#mkpath)';

        $obj->patchSvgObj(525, [
            'styles' => [$base],
            'defs' => [
                'mkpath' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkpath',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => 'auto',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $out = $obj->exposeParseSVGTagSTARTpath($parser, 525, ['d' => 'M 1 1 L 9 1 L 9 7'], $base, $base);

        $this->assertNotSame('', $out);
        $this->assertGreaterThanOrEqual(4, \substr_count($out, "q\n"));
    }

    /** @throws \Throwable */
    public function testSvgPolylineRendersMidMarkerWhenDefined(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(526);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker-start'] = 'url(#mkpoly)';
        $base['marker-mid'] = 'url(#mkpoly)';
        $base['marker-end'] = 'url(#mkpoly)';

        $obj->patchSvgObj(526, [
            'styles' => [$base],
            'defs' => [
                'mkpoly' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkpoly',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => 'auto',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $out = $obj->exposeParseSVGTagSTARTpolygon($parser, 526, ['points' => '1,1 8,1 8,6'], $base, $base, true);

        $this->assertNotSame('', $out);
        $this->assertGreaterThanOrEqual(4, \substr_count($out, "q\n"));
    }

    /** @throws \Throwable */
    public function testSvgCurvedPathRendersMarkersWithoutErrors(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(527);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker-start'] = 'url(#mkcurve)';
        $base['marker-mid'] = 'url(#mkcurve)';
        $base['marker-end'] = 'url(#mkcurve)';

        $obj->patchSvgObj(527, [
            'styles' => [$base],
            'defs' => [
                'mkcurve' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkcurve',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => 'auto',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $out = $obj->exposeParseSVGTagSTARTpath($parser, 527, ['d' => 'M 1 1 C 3 9 7 -1 9 7 L 12 9'], $base, $base);

        $this->assertNotSame('', $out);
        $this->assertGreaterThanOrEqual(4, \substr_count($out, "q\n"));
    }

    /** @throws \Throwable */
    public function testSvgClosedPathAddsMidMarkerAtClosureJoin(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(528);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker-start'] = 'none';
        $base['marker-mid'] = 'url(#mkclose)';
        $base['marker-end'] = 'none';

        $obj->patchSvgObj(528, [
            'styles' => [$base],
            'defs' => [
                'mkclose' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkclose',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => 'auto',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $out = $obj->exposeParseSVGTagSTARTpath($parser, 528, ['d' => 'M 1 1 L 8 1 L 8 6 Z'], $base, $base);

        $this->assertNotSame('', $out);
        // Closed triangle has 3 vertices, each should receive marker-mid.
        $this->assertGreaterThanOrEqual(4, \substr_count($out, "q\n"));
    }

    /** @throws \Throwable */
    public function testSvgMarkerShorthandAppliesToLineAnchors(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(529);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker'] = 'url(#mkall)';
        $base['marker-start'] = 'none';
        $base['marker-mid'] = 'none';
        $base['marker-end'] = 'none';

        $obj->patchSvgObj(529, [
            'styles' => [$base],
            'defs' => [
                'mkall' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkall',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => 'auto',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $out = $obj->exposeParseSVGTagSTARTline(
            $parser,
            529,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $this->assertNotSame('', $out);
        $this->assertGreaterThanOrEqual(3, \substr_count($out, "q\n"));
    }

    /** @throws \Throwable */
    public function testSvgMarkerOrientDegSuffixIsAccepted(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(530);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker-start'] = 'url(#mkdeg)';
        $base['marker-end'] = 'url(#mkdeg)';

        $obj->patchSvgObj(530, [
            'styles' => [$base],
            'defs' => [
                'mkdeg' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkdeg',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => '45deg',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $out = $obj->exposeParseSVGTagSTARTline(
            $parser,
            530,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $this->assertNotSame('', $out);
        $this->assertGreaterThanOrEqual(3, \substr_count($out, "q\n"));
    }

    /** @throws \Throwable */
    public function testSvgMarkerPercentRefMatchesAbsoluteRefInViewBox(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(531);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker-start'] = 'url(#mkpct)';
        $base['marker-end'] = 'none';

        $obj->patchSvgObj(531, [
            'styles' => [$base],
            'defs' => [
                'mkpct' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkpct',
                        'viewBox' => '0 0 10 10',
                        'refX' => '50%',
                        'refY' => '50%',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => '0',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
                'mkabs' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkabs',
                        'viewBox' => '0 0 10 10',
                        'refX' => '5',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => '0',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $outPct = $obj->exposeParseSVGTagSTARTline(
            $parser,
            531,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $base['marker-start'] = 'url(#mkabs)';
        $outAbs = $obj->exposeParseSVGTagSTARTline(
            $parser,
            531,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $this->assertNotSame('', $outPct);
        $this->assertSame($outAbs, $outPct);
    }

    /** @throws \Throwable */
    public function testSvgMarkerPreserveAspectRatioChangesOutputTransform(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(532);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker-start'] = 'url(#mknone)';
        $base['marker-end'] = 'none';

        $obj->patchSvgObj(532, [
            'styles' => [$base],
            'defs' => [
                'mknone' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mknone',
                        'viewBox' => '0 0 10 20',
                        'refX' => '0',
                        'refY' => '0',
                        'markerWidth' => '6',
                        'markerHeight' => '6',
                        'preserveAspectRatio' => 'none',
                        'orient' => '0',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
                'mkmeet' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkmeet',
                        'viewBox' => '0 0 10 20',
                        'refX' => '0',
                        'refY' => '0',
                        'markerWidth' => '6',
                        'markerHeight' => '6',
                        'preserveAspectRatio' => 'xMidYMid meet',
                        'orient' => '0',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $outNone = $obj->exposeParseSVGTagSTARTline(
            $parser,
            532,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $base['marker-start'] = 'url(#mkmeet)';
        $outMeet = $obj->exposeParseSVGTagSTARTline(
            $parser,
            532,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $this->assertNotSame('', $outNone);
        $this->assertNotSame('', $outMeet);
        $this->assertNotSame($outNone, $outMeet);
    }

    /** @throws \Throwable */
    public function testSvgMarkerSpecificAnchorsOverrideShorthand(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(533);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker'] = 'url(#mkall2)';
        $base['marker-start'] = 'none';
        $base['marker-mid'] = 'url(#mkall2)';
        $base['marker-end'] = 'url(#mkall2)';

        $obj->patchSvgObj(533, [
            'styles' => [$base],
            'defs' => [
                'mkall2' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkall2',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => 'auto',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $out = $obj->exposeParseSVGTagSTARTline(
            $parser,
            533,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $this->assertNotSame('', $out);
        // Base line transform, then one end marker only (no start marker) with
        // its own transform and the transform of the path it contains.
        $this->assertSame(3, \substr_count($out, "q\n"));
    }

    /** @throws \Throwable */
    public function testSvgMarkerPreserveAspectRatioDeferMatchesEquivalentValue(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(534);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = '#000000';
        $base['stroke-width'] = 0.5;
        $base['marker-start'] = 'url(#mkdefer)';
        $base['marker-end'] = 'none';

        $obj->patchSvgObj(534, [
            'styles' => [$base],
            'defs' => [
                'mkdefer' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkdefer',
                        'viewBox' => '0 0 10 20',
                        'refX' => '0',
                        'refY' => '0',
                        'markerWidth' => '6',
                        'markerHeight' => '6',
                        'preserveAspectRatio' => 'defer xMaxYMax slice',
                        'orient' => '0',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
                'mkeq' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkeq',
                        'viewBox' => '0 0 10 20',
                        'refX' => '0',
                        'refY' => '0',
                        'markerWidth' => '6',
                        'markerHeight' => '6',
                        'preserveAspectRatio' => 'xMaxYMax slice',
                        'orient' => '0',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $outDefer = $obj->exposeParseSVGTagSTARTline(
            $parser,
            534,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $base['marker-start'] = 'url(#mkeq)';
        $outEq = $obj->exposeParseSVGTagSTARTline(
            $parser,
            534,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $this->assertNotSame('', $outDefer);
        $this->assertSame($outEq, $outDefer);
    }

    /** @throws \Throwable */
    public function testSvgMarkerUnitsUserSpaceOnUseIgnoresStrokeWidthScale(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(535);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = 'none';
        $base['marker-start'] = 'url(#mkus)';
        $base['marker-end'] = 'none';

        $obj->patchSvgObj(535, [
            'styles' => [$base],
            'defs' => [
                'mkus' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkus',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'markerUnits' => 'userSpaceOnUse',
                        'orient' => '0',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $base['stroke-width'] = 0.5;
        $outThin = $obj->exposeParseSVGTagSTARTline(
            $parser,
            535,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $base['stroke-width'] = 3.0;
        $outThick = $obj->exposeParseSVGTagSTARTline(
            $parser,
            535,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $this->assertNotSame('', $outThin);
        $this->assertSame($outThin, $outThick);
    }

    /** @throws \Throwable */
    public function testSvgMarkerUnitsStrokeWidthScalesWithStrokeWidth(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(536);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = 'none';
        $base['marker-start'] = 'url(#mksw)';
        $base['marker-end'] = 'none';

        $obj->patchSvgObj(536, [
            'styles' => [$base],
            'defs' => [
                'mksw' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mksw',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'markerUnits' => 'strokeWidth',
                        'orient' => '0',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $base['stroke-width'] = 0.5;
        $outThin = $obj->exposeParseSVGTagSTARTline(
            $parser,
            536,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $base['stroke-width'] = 3.0;
        $outThick = $obj->exposeParseSVGTagSTARTline(
            $parser,
            536,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $this->assertNotSame('', $outThin);
        $this->assertNotSame($outThin, $outThick);
    }

    /** @throws \Throwable */
    public function testSvgMarkerAutoStartReverseDiffersFromAutoAtStart(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(537);

        $base = $obj->exposeDefaultSVGStyle();
        $base['stroke'] = 'none';
        $base['stroke-width'] = 1.0;
        $base['marker-start'] = 'url(#mkauto)';
        $base['marker-end'] = 'none';

        $obj->patchSvgObj(537, [
            'styles' => [$base],
            'defs' => [
                'mkauto' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkauto',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => 'auto',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
                'mkrev' => [
                    'name' => 'marker',
                    'attr' => [
                        'id' => 'mkrev',
                        'viewBox' => '0 0 10 10',
                        'refX' => '0',
                        'refY' => '5',
                        'markerWidth' => '4',
                        'markerHeight' => '4',
                        'orient' => 'auto-start-reverse',
                    ],
                    'child' => [
                        'DF_1' => [
                            'name' => 'path',
                            'attr' => [
                                'id' => 'DF_1',
                                'd' => 'M 0 0 L 10 5 L 0 10 Z',
                                'fill' => '#000000',
                                'stroke' => 'none',
                            ],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'path',
                            'attr' => [
                                'closing_tag' => true,
                                'content' => '',
                            ],
                        ],
                    ],
                ],
            ],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $outAuto = $obj->exposeParseSVGTagSTARTline(
            $parser,
            537,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $base['marker-start'] = 'url(#mkrev)';
        $outRev = $obj->exposeParseSVGTagSTARTline(
            $parser,
            537,
            ['x1' => '1', 'y1' => '1', 'x2' => '8', 'y2' => '1'],
            $base,
            $base,
        );

        $this->assertNotSame('', $outAuto);
        $this->assertNotSame('', $outRev);
        $this->assertNotSame($outAuto, $outRev);
    }

    /** @throws \Throwable */
    public function testSvgHandleStartInheritAndUnknownTagBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(53);
        $base = $obj->exposeDefaultSVGStyle();
        $obj->patchSvgObj(53, [
            'styles' => [$base],
            'clipmode' => false,
            'textmode' => ['invisible' => false, 'stroke' => 0, 'rtl' => false, 'text-anchor' => 'start'],
        ]);

        $parser = \xml_parser_create('UTF-8');
        $obj->exposeHandleSVGTagStart(
            $parser,
            'unknownTag',
            [
                'style' => 'color:inherit;display:none;',
                'transform' => 'translate(1,2) rotate(5)',
            ],
            53,
            false,
            [1, 0, 0, 1, 0, 0],
        );

        $svgobj = $obj->getSvgObj(53);
        $this->assertArrayHasKey('out', $svgobj);
        $this->assertTrue($svgobj['textmode']['invisible']);
    }

    /** @throws \Throwable */
    public function testSvgClipPathInvisibleAndShapeImageGuardBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(54);
        $base = $obj->exposeDefaultSVGStyle();
        $parser = \xml_parser_create('UTF-8');

        $obj->patchSvgObj(54, ['textmode' => ['invisible' => true]]);
        $this->assertSame('', $obj->exposeParseSVGTagSTARTclipPath(54, [1, 0, 0, 1, 0, 0]));

        $obj->patchSvgObj(54, ['textmode' => ['invisible' => false], 'clipmode' => true]);
        $ellipseClip = $obj->exposeParseSVGTagSTARTellipse(
            $parser,
            54,
            ['cx' => '5', 'cy' => '6', 'rx' => '3', 'ry' => '2'],
            $base,
            $base,
        );
        $this->assertNotSame('', $ellipseClip);

        $lineClip = $obj->exposeParseSVGTagSTARTline(
            $parser,
            54,
            ['x1' => '0', 'y1' => '0', 'x2' => '5', 'y2' => '5'],
            $base,
            $base,
        );
        $this->assertSame('', $lineClip);

        $imgNoHref = $obj->exposeParseSVGTagSTARTimage($parser, 54, [], $base, $base);
        $this->assertSame('', $imgNoHref);

        $imgClip = $obj->exposeParseSVGTagSTARTimage(
            $parser,
            54,
            ['xlink:href' => 'image.png', 'x' => '1', 'y' => '1', 'width' => '2', 'height' => '2'],
            $base,
            $base,
        );
        $this->assertSame('', $imgClip);
    }

    /** @throws \Throwable */
    public function testSvgAddSVGAlignmentAndScalingBranches(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);

        $svgSlice =
            '@<svg xmlns="http://www.w3.org/2000/svg"'
            . ' width="20" height="10" viewBox="0 0 20 10" preserveAspectRatio="xMaxYMax slice">'
            . '<rect x="0" y="0" width="20" height="10" fill="#ffcc00"/>'
            . '</svg>';
        $idSlice = $obj->addSVG($svgSlice, 10, 12, 30, 10, $page['height']);
        $this->assertGreaterThan(0, $idSlice);

        $svgMeet =
            '@<svg xmlns="http://www.w3.org/2000/svg"'
            . ' width="10" height="20" viewBox="0 0 10 20" preserveAspectRatio="xMidYMid meet">'
            . '<circle cx="5" cy="10" r="4" fill="#00ccff"/>'
            . '</svg>';
        $idMeet = $obj->addSVG($svgMeet, 15, 18, 0, 25, $page['height']);
        $this->assertGreaterThan($idSlice, $idMeet);

        $svgNone =
            '@<svg xmlns="http://www.w3.org/2000/svg"'
            . ' width="12" height="12" viewBox="0 0 12 12" preserveAspectRatio="none">'
            . '<line x1="0" y1="0" x2="12" y2="12" stroke="#000"/>'
            . '</svg>';
        $idNone = $obj->addSVG($svgNone, 3, 4, 0, 0, $page['height']);
        $this->assertGreaterThan($idMeet, $idNone);
    }

    /** @throws \Throwable */
    public function testSvgRemainingHelperGradientAndFillBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(55);
        $base = $obj->exposeDefaultSVGStyle();

        $this->assertSame(40.0, $obj->exposeGetTAFontStretching('ultra-condensed'));
        $this->assertSame(55.0, $obj->exposeGetTAFontStretching('extra-condensed'));
        $this->assertSame(85.0, $obj->exposeGetTAFontStretching('semi-condensed'));
        $this->assertSame(100.0, $obj->exposeGetTAFontStretching('normal'));
        $this->assertSame(115.0, $obj->exposeGetTAFontStretching('semi-expanded'));
        $this->assertSame(130.0, $obj->exposeGetTAFontStretching('expanded'));
        $this->assertSame(145.0, $obj->exposeGetTAFontStretching('extra-expanded'));
        $this->assertSame(160.0, $obj->exposeGetTAFontStretching('ultra-expanded'));

        $fill = $base;
        $fill['fill'] = 'rgba(255,0,0,0.5)';
        $fill['opacity'] = 0.6;
        $fill['fill-opacity'] = 0.5;
        $fill['mix-blend-mode'] = 'multiply';
        [$fillOut, $fill] = $obj->exposeParseSVGStyleFill(55, $fill, [], 0, 0, 5, 5);
        $this->assertNotSame('', $fillOut);
        $this->assertStringContainsString('F', $fill['objstyle']);

        $fillBad = $base;
        $fillBad['fill'] = 'transparent';
        [$badOut] = $obj->exposeParseSVGStyleFill(55, $fillBad, [], 0, 0, 5, 5);
        $this->assertSame('', $badOut);

        $parser = \xml_parser_create('UTF-8');
        $obj->exposeParseSVGStyleClipPath($parser, 55, [
            'cp1' => [
                'name' => 'rect',
                'attr' => ['x' => '1', 'y' => '1', 'width' => '2', 'height' => '2'],
                'tm' => [1, 0, 0, 1, 0, 0],
            ],
        ]);
        $this->assertNotSame('', $obj->getSvgObj(55)['out']);

        $this->assertSame('', $obj->exposeParseSVGTagSTARTlinearGradient(55, [
            'gradientTransform' => 'matrix(1 0 0 1 1 2)',
        ]));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTradialGradient(55, ['r' => '0.5']));
        $svgobj = $obj->getSvgObj(55);
        $this->assertNotSame('', $svgobj['gradientid']);
        $this->assertNotEmpty($svgobj['gradients']);
    }

    /** @throws \Throwable */
    public function testSvgAdditionalEdgeBranchesForCoverage(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(56);
        $base = $obj->exposeDefaultSVGStyle();
        $parser = \xml_parser_create('UTF-8');

        // cover defs-mode early-return branches
        $obj->patchSvgObj(56, ['defsmode' => true, 'defs' => [], 'text' => 't']);
        $obj->exposeHandleSVGTagEnd($parser, 'unknown');
        $obj->exposeHandleSVGTagStart($parser, 'unknown', ['id' => 'd1'], 56);

        // style inherit + dispatch branches including polyline/image/use/tspan
        $obj->patchSvgObj(56, [
            'defsmode' => false,
            'clipmode' => false,
            'styles' => [$base],
            'textmode' => ['invisible' => false, 'stroke' => 0, 'rtl' => false, 'text-anchor' => 'start'],
        ]);
        $obj->exposeHandleSVGTagStart($parser, 'polyline', ['points' => '0,0 1,1 2,2', 'style' => 'color:inherit'], 56);
        $obj->exposeHandleSVGTagStart($parser, 'defs', [], 56);
        $obj->patchSvgObj(56, ['clipmode' => false]);
        $obj->exposeHandleSVGTagStart($parser, 'line', ['x1' => '0', 'y1' => '0', 'x2' => '1', 'y2' => '1'], 56);

        // path clipmode and empty-d guard
        $obj->patchSvgObj(56, ['clipmode' => true]);
        $this->assertSame('', $obj->exposeParseSVGTagSTARTpath($parser, 56, ['d' => ''], $base, $base));
        $clipPathOut = $obj->exposeParseSVGTagSTARTpath($parser, 56, ['d' => 'M 0 0 L 2 2'], $base, $base);
        $this->assertNotSame('', $clipPathOut);

        // circle/ellipse fallback x/y and clip branches
        $circleClip = $obj->exposeParseSVGTagSTARTcircle(
            $parser,
            56,
            ['x' => '4', 'y' => '5', 'r' => '2'],
            $base,
            $base,
        );
        $this->assertNotSame('', $circleClip);
        $ellipseClip = $obj->exposeParseSVGTagSTARTellipse(
            $parser,
            56,
            ['x' => '4', 'y' => '5', 'rx' => '2', 'ry' => '1'],
            $base,
            $base,
        );
        $this->assertNotSame('', $ellipseClip);

        // polygon invalid and clip branches
        $obj->patchSvgObj(56, ['clipmode' => false]);
        $this->assertSame('', $obj->exposeParseSVGTagSTARTpolygon($parser, 56, ['points' => '0,0 1'], $base, $base));
        $obj->patchSvgObj(56, ['clipmode' => true]);
        $polyClip = $obj->exposeParseSVGTagSTARTpolygon($parser, 56, ['points' => '0,0 2,0 2,2 0,2'], $base, $base);
        $this->assertNotSame('', $polyClip);

        // image guards and svg child path
        $obj->patchSvgObj(56, ['clipmode' => false, 'dir' => __DIR__]);
        $this->assertSame('', $obj->exposeParseSVGTagSTARTimage($parser, 56, [], $base, $base));
        $svgTmp = \tempnam(\sys_get_temp_dir(), 'tc-svg-child-');
        $this->assertNotFalse($svgTmp);
        $svgPath = $svgTmp . '.svg';
        \rename($svgTmp, $svgPath);
        \file_put_contents(
            $svgPath,
            '<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2">'
            . '<rect x="0" y="0" width="2" height="2" fill="#000"/></svg>',
        );
        try {
            $svgImgOut = $obj->exposeParseSVGTagSTARTimage(
                $parser,
                56,
                ['xlink:href' => $svgPath, 'x' => '1', 'y' => '1', 'width' => '2', 'height' => '2'],
                $base,
                $base,
            );
            $this->assertNotSame('', $svgImgOut);
        } finally {
            if (\file_exists($svgPath)) {
                \unlink($svgPath);
            }
        }

        // text defaults without explicit anchor/direction/stroke
        $textBase = $base;
        $textBase['text-anchor'] = '';
        $textBase['direction'] = '';
        $textBase['stroke'] = 'none';
        $textOut = $obj->exposeParseSVGTagSTARTtext($parser, 56, ['x' => '1', 'y' => '1'], $textBase, $base);
        $this->assertNotSame('', $textOut);

        // use: unset id + non-string name guard
        $obj->patchSvgObj(56, [
            'defs' => [
                'u1' => [
                    'name' => [],
                    'attr' => ['id' => 'u1', 'x' => '1', 'y' => '2', 'style' => 'fill:#000;'],
                ],
            ],
        ]);
        $this->assertSame('', $obj->exposeParseSVGTagSTARTuse($parser, 56, [
            'xlink:href' => '#u1',
            'id' => 'tmp',
            'x' => '2',
            'y' => '3',
        ]));

        // raw svg + size fallback branches
        $emptyFile = \tempnam(\sys_get_temp_dir(), 'tc-svg-empty-');
        $this->assertNotFalse($emptyFile);
        try {
            \file_put_contents($emptyFile, '');
            $this->assertSame('', $obj->exposeGetRawSVGData($emptyFile));
        } finally {
            if (\file_exists($emptyFile)) {
                \unlink($emptyFile);
            }
        }
        $this->assertSame(0.0, $obj->exposeGetSVGSize('not svg data')['width']);
        $this->assertSame('xMidYMid', $obj->exposeGetSVGSize('<svg viewBox="0 0 10 10"></svg>')['ar_align']);

        // prescan stop outside gradient
        $obj->initSvgObjForHandlers(57);
        $obj->exposePrescanSVGGradients('<svg><stop offset="0%" stop-color="#000"/></svg>', 57);

        // gradient mode branches including pdfa guards
        $obj->initSvgObjForHandlers(58);
        $this->setObjectProperty($obj, 'pdfa', 1);
        $this->assertSame('', $obj->exposeParseSVGTagSTARTlinearGradient(58, []));
        $this->assertSame('', $obj->exposeParseSVGTagSTARTradialGradient(58, []));
        $this->setObjectProperty($obj, 'pdfa', 0);
        $obj->exposeParseSVGTagSTARTlinearGradient(58, [
            'x1' => '1',
            'y1' => '2',
            'x2' => '3',
            'y2' => '4',
            'xlink:href' => '#gref',
        ]);
        $obj->exposeParseSVGTagSTARTradialGradient(58, [
            'r' => '0.5',
            'gradientTransform' => 'matrix(1 0 0 1 1 1)',
            'xlink:href' => '#gref2',
        ]);

        // addSVG parser error + width/height branch variants
        $objMain = $this->getTestObject();
        $page = $this->initFontAndPage($objMain);
        $this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
        $objMain->addSVG('@<svg><g></svg>', 1, 1, 2, 2, $page['height']);
    }

    /** @throws \Throwable */
    public function testSvgHandlersCoverAdditionalDispatchAndEarlyReturns(): void
    {
        $parser = \xml_parser_create('UTF-8');

        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $base = $obj->exposeDefaultSVGStyle();

        $obj->initSvgObjForHandlers(59);
        $obj->patchSvgObj(59, [
            'styles' => [$base],
            'clipmode' => false,
            'defsmode' => false,
            'textmode' => ['invisible' => false, 'stroke' => 0, 'rtl' => false, 'text-anchor' => 'start'],
            'out' => '',
            'x' => 1.0,
            'y' => 1.0,
            'text' => 'txt',
            'child' => [],
        ]);

        $obj->exposeHandleSVGTagStart($parser, 'clipPath', ['id' => 'cp1'], 59);
        $obj->exposeHandleSVGTagStart(
            $parser,
            'linearGradient',
            ['id' => 'lg1', 'x1' => '0', 'y1' => '0', 'x2' => '100%', 'y2' => '0%'],
            59,
        );
        $obj->exposeHandleSVGTagStart(
            $parser,
            'radialGradient',
            ['id' => 'rg1', 'cx' => '50%', 'cy' => '50%', 'r' => '0.5'],
            59,
        );
        $obj->exposeHandleSVGTagStart($parser, 'stop', ['offset' => '0%'], 59);
        $obj->exposeHandleSVGTagStart($parser, 'polygon', ['points' => '0,0 1,0 1,1'], 59);
        $obj->exposeHandleSVGTagStart($parser, 'text', ['x' => '1', 'y' => '1'], 59);
        $obj->exposeHandleSVGTagStart($parser, 'tspan', ['x' => '1', 'y' => '1'], 59);

        $obj->initSvgObjForHandlers(61);
        $obj->patchSvgObj(61, ['styles' => [$base], 'text' => 'txt', 'x' => 1.0, 'y' => 1.0]);
        $obj->exposeHandleSVGTagEnd($parser, 'clipPath');
        $obj->patchSvgObj(61, ['styles' => [$base], 'text' => 'txt', 'x' => 1.0, 'y' => 1.0]);
        $obj->exposeHandleSVGTagEnd($parser, 'g');
        $obj->patchSvgObj(61, ['styles' => [$base], 'text' => 'txt', 'x' => 1.0, 'y' => 1.0]);
        $obj->exposeHandleSVGTagEnd($parser, 'text');
        $obj->patchSvgObj(61, ['styles' => [$base], 'text' => 'txt', 'x' => 1.0, 'y' => 1.0]);
        $obj->exposeHandleSVGTagEnd($parser, 'tspan');

        $this->assertArrayHasKey('out', $obj->getSvgObj(59));
    }

    /** @throws \Throwable */
    public function testSvgAddSvgCoversParseErrorAndInvalidInputBranches(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);

        $tmpRel = __DIR__ . '/../tmp-svg-rel.svg';
        \file_put_contents(
            $tmpRel,
            '<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2">'
            . '<rect x="0" y="0" width="2" height="2"/></svg>',
        );
        try {
            $svgId = $obj->addSVG('tmp-svg-rel.svg', 1, 1, 2, 2, $page['height']);
            $this->assertGreaterThan(0, $svgId);
        } finally {
            if (\file_exists($tmpRel)) {
                \unlink($tmpRel);
            }
        }

        $this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
        $obj->addSVG('/path/does-not-exist.svg', 1, 1, 2, 2, $page['height']);
    }

    /** @throws \Throwable */
    public function testSvgAddSvgThrowsOnXmlParseErrorAfterSizeParsing(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);

        $this->bcExpectException(\Com\Tecnick\Pdf\Exception::class);
        $obj->addSVG(
            '@<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><g></svg>',
            1,
            1,
            1,
            1,
            $page['height'],
        );
    }

    /** @throws \Throwable */
    public function testSvgImageTagCoversNestedSvgFailureAndBase64Path(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        $obj->initSvgObjForHandlers(60);
        $obj->patchSvgObj(60, [
            'clipmode' => false,
            'dir' => __DIR__,
            'styles' => [$base],
            'textmode' => ['invisible' => false],
        ]);

        $badSvg = __DIR__ . '/fixtures/invalid-child.svg';
        \file_put_contents($badSvg, '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><g></svg>');
        try {
            $this->assertSame('', $obj->exposeParseSVGTagSTARTimage(
                $parser,
                60,
                ['xlink:href' => $badSvg, 'x' => '0', 'y' => '0', 'width' => '1', 'height' => '1'],
                $base,
                $base,
            ));
        } finally {
            if (\file_exists($badSvg)) {
                \unlink($badSvg);
            }
        }

        $onePx = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7ZkQAAAABJRU5ErkJggg==';
        try {
            $imgOut = $obj->exposeParseSVGTagSTARTimage(
                $parser,
                60,
                [
                    'xlink:href' => 'data:image/png;base64,' . $onePx,
                    'x' => '0',
                    'y' => '0',
                    'width' => '1',
                    'height' => '1',
                ],
                $base,
                $base,
            );
            $this->assertNotSame('', $imgOut);
        } catch (\Throwable $e) {
            $this->assertNotSame('', $e->getMessage());
        }
    }

    /** @throws \Throwable */
    public function testSvgAdditionalTransformAndDispatchBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->setSvgRefUnit(62);
        $base = $obj->exposeDefaultSVGStyle();
        $parser = \xml_parser_create('UTF-8');

        $tmSkew = $obj->exposeGetSVGTransformMatrix('skewX(10) skewY(5)');
        $this->assertCount(6, $tmSkew);

        // Uppercase command forces the default match arm.
        $tmDefault = $obj->exposeGetSVGTransformMatrix('SKEWX(10)');
        $this->assertCount(6, $tmDefault);

        $this->assertSame('', $obj->exposeGetSVGPath(62, '', 'S'));
        $this->assertSame(90.0, $obj->exposeGetTAFontStretching('inherit', 90));

        $strokeStyle = $base;
        $strokeStyle['stroke'] = 'not-a-color';
        try {
            [$strokeOut] = $obj->exposeParseSVGStyleStroke(62, $strokeStyle);
            $this->assertSame('', $strokeOut);
        } catch (\Throwable $e) {
            $this->assertNotSame('', $e->getMessage());
        }

        $fresh = new TestableSVG();
        $fresh->exposeHandleSVGCharacter($parser, 'orphan');
        $this->assertSame('', $fresh->exposeGetSVGExtGState(null, null, ''));

        $obj->initSvgObjForHandlers(63);
        $obj->patchSvgObj(63, [
            'styles' => [$base],
            'textmode' => ['invisible' => false, 'stroke' => 0, 'rtl' => false, 'text-anchor' => 'end'],
            'x' => 2.0,
            'y' => 3.0,
            'text' => 'anchor-end',
        ]);
        $this->assertNotSame('', $obj->exposeParseSVGTagENDtext(63));

        $obj->initSvgObjForHandlers(64);
        $obj->patchSvgObj(64, [
            'styles' => [$base],
            'defsmode' => false,
            'clipmode' => false,
            'textmode' => ['invisible' => false, 'stroke' => 0, 'rtl' => false, 'text-anchor' => 'start'],
        ]);

        $obj->exposeHandleSVGTagStart($parser, 'g', ['fill' => 'inherit'], 64);
        $obj->exposeHandleSVGTagStart(
            $parser,
            'linearGradient',
            ['id' => 'lgx', 'gradientUnits' => 'userSpaceOnUse'],
            64,
        );
        $obj->exposeHandleSVGTagStart(
            $parser,
            'radialGradient',
            ['id' => 'rgx', 'gradientUnits' => 'userSpaceOnUse', 'r' => '0.5', 'cx' => '2', 'cy' => '3'],
            64,
        );
        $obj->exposeHandleSVGTagStart($parser, 'stop', ['offset' => '50%'], 64);
        $obj->exposeHandleSVGTagStart($parser, 'ellipse', ['cx' => '3', 'cy' => '4', 'rx' => '2', 'ry' => '1'], 64);
        $obj->exposeHandleSVGTagStart($parser, 'polygon', ['points' => '0,0 2,0 2,2'], 64);
        try {
            $obj->exposeHandleSVGTagStart($parser, 'image', ['xlink:href' => 'missing.png'], 64);
        } catch (\Throwable $e) {
            $this->assertNotSame('', $e->getMessage());
        }
        $obj->exposeHandleSVGTagStart($parser, 'text', ['x' => '1', 'y' => '1'], 64);
        $obj->exposeHandleSVGTagStart($parser, 'tspan', ['dx' => '1'], 64);
        $obj->exposeHandleSVGTagStart($parser, 'use', ['xlink:href' => '#missing'], 64);

        $svgOut = $obj->getSvgObj(64)['out'];
        $this->assertIsString($svgOut);
    }

    /** @throws \Throwable */
    public function testSvgGradientAndFillRemainingBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(65);
        $obj->setSvgObjMeta(65);
        $base = $obj->exposeDefaultSVGStyle();

        $gradients = [
            'eq' => [
                'xref' => '',
                'type' => 2,
                'gradientUnits' => 'objectBoundingBox',
                'mode' => 'measure',
                'coords' => [0.0, 0.0, 0.0, 0.0],
                'stops' => [
                    ['color' => '#000000', 'opacity' => 1.0, 'offset' => 0.0],
                    ['color' => '#ffffff', 'opacity' => 1.0, 'offset' => 1.0],
                ],
                'gradientTransform' => [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
            ],
        ];

        $grOut = $obj->exposeParseSVGStyleGradient(65, $gradients, 'eq', 0.0, 0.0, 0.0, 0.0);
        $this->assertNotSame('', $grOut);

        $fillStyle = $base;
        $fillStyle['fill'] = 'url(#eq)';
        $fillStyle['opacity'] = 0.5;
        $fillStyle['fill-opacity'] = 0.5;
        $fillStyle['mix-blend-mode'] = 'multiply';
        [$fillOut] = $obj->exposeParseSVGStyleFill(65, $fillStyle, $gradients, 0.0, 0.0, 5.0, 5.0);
        $this->assertNotSame('', $fillOut);
    }

    /** @throws \Throwable */
    public function testSvgAddSvgScalingAndRecursiveChildBranches(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);

        $svgSlice =
            '@<svg xmlns="http://www.w3.org/2000/svg"'
            . ' width="100" height="10" viewBox="0 0 100 10" preserveAspectRatio="xMinYMin slice">'
            . '<rect x="0" y="0" width="100" height="10" fill="#cccccc"/>'
            . '</svg>';
        $idSlice = $obj->addSVG($svgSlice, 2, 2, 100, 100, $page['height']);
        $this->assertGreaterThan(0, $idSlice);

        $svgMeet =
            '@<svg xmlns="http://www.w3.org/2000/svg"'
            . ' width="10" height="100" viewBox="0 0 10 100" preserveAspectRatio="xMinYMin meet">'
            . '<rect x="0" y="0" width="10" height="100" fill="#999999"/>'
            . '</svg>';
        $idMeet = $obj->addSVG($svgMeet, 3, 3, 100, 100, $page['height']);
        $this->assertGreaterThan($idSlice, $idMeet);

        $svgAutoHeight =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10">'
            . '<rect x="0" y="0" width="20" height="10"/></svg>';
        $idAutoHeight = $obj->addSVG($svgAutoHeight, 4, 4, 30, 0, $page['height']);
        $this->assertGreaterThan($idMeet, $idAutoHeight);

        $tmpDir = (string) \realpath(\sys_get_temp_dir());
        $childPath = $tmpDir . '/tc-svg-child-recursive.svg';
        $parentPath = $tmpDir . '/tc-svg-parent-recursive.svg';

        \file_put_contents(
            $childPath,
            '<svg xmlns="http://www.w3.org/2000/svg" width="3" height="3">'
            . '<rect x="0" y="0" width="3" height="3" fill="#000"/></svg>',
        );
        \file_put_contents(
            $parentPath,
            '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">'
            . '<image x="0" y="0" width="5" height="5" xlink:href="tc-svg-child-recursive.svg"/></svg>',
        );

        try {
            $parentId = $obj->addSVG($parentPath, 1, 1, 10, 10, $page['height']);
            $setOut = $obj->getSetSVG($parentId);
            $this->assertNotSame('', $setOut);
        } finally {
            if (\file_exists($childPath)) {
                \unlink($childPath);
            }
            if (\file_exists($parentPath)) {
                \unlink($parentPath);
            }
        }
    }

    /** @throws \Throwable */
    public function testAddSvgWithMarkupResourcesBlocksTempChildRasterByDefault(): void
    {
        $obj = $this->getInternalTestObject();
        $page = $this->initFontAndPage($obj);

        $tmpImage = (string) \tempnam(\sys_get_temp_dir(), 'tc-svg-markup-');
        \unlink($tmpImage);
        $tmpImage .= '.png';
        \copy(__DIR__ . '/../examples/images/tcpdf_cell.png', $tmpImage);

        try {
            $svg =
                '@<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">'
                . '<image x="0" y="0" width="5" height="5" xlink:href="file://'
                . $tmpImage
                . '"/></svg>';

            $id = $obj->addSVG($svg, 1, 1, 10, 10, $page['height'], true);
            $out = $obj->getSetSVG($id);

            $this->assertNotSame('', $out);
            /** @var \Com\Tecnick\Pdf\Image\Import $image */
            $image = $this->getObjectProperty($obj, 'image');
            /** @var array<int, mixed> $images */
            $images = $this->getObjectProperty($image, 'image');
            $this->assertSame([], $images);
        } finally {
            if (\file_exists($tmpImage)) {
                \unlink($tmpImage);
            }
        }
    }

    /** @throws \Throwable */
    public function testSvgFinalFeasibleBranchCoverageBatch(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        $coord = $obj->getPathCoordDefaults();
        $coord['relcoord'] = true;
        $coord['xoffset'] = 1.0;
        $coord['yoffset'] = 1.0;
        [, $coord] = $obj->exposeSvgPathCmdA([4.0, 3.0, 0.0, 0.0, 0.0, 8.0, 9.0], $coord, [['0', 'A'], ['1', 'z']], 0, [
            '4',
            '3',
            '0',
            '0',
            '0',
            '8',
            '9',
        ]);
        $this->assertGreaterThan(0.0, $coord['xoffset']);
        $this->assertGreaterThan(0.0, $coord['yoffset']);

        $obj->initSvgObjForHandlers(70);
        $obj->patchSvgObj(70, [
            'defsmode' => true,
            'defs' => [],
            'styles' => [$base],
            'textmode' => ['invisible' => false],
        ]);
        /** @var array<int, array<string, mixed>> $svgobjs */
        $svgobjs = $this->getObjectProperty($obj, 'svgobjs');
        unset($svgobjs[70]['clippaths']);
        $this->setObjectProperty($obj, 'svgobjs', $svgobjs);
        $obj->exposeHandleSVGTagStart($parser, 'line', [], 70);

        $obj->patchSvgObj(70, ['defsmode' => false, 'clipmode' => false, 'styles' => [$base]]);
        $obj->exposeHandleSVGTagStart($parser, 'path', ['d' => 'M 0 0 L 2 2'], 70);

        $obj->patchSvgObj(70, ['tagdepth' => 1]);
        $svgNone = $obj->exposeParseSVGTagSTARTsvg(
            $parser,
            70,
            [
                'x' => '0',
                'y' => '0',
                'width' => '40',
                'height' => '10',
                'viewBox' => '0 0 100 50',
                'preserveAspectRatio' => 'none',
            ],
            $base,
            $base,
        );
        $this->assertNotSame('', $svgNone);

        $obj->patchSvgObj(70, ['tagdepth' => 1]);
        $svgMeetXMax = $obj->exposeParseSVGTagSTARTsvg(
            $parser,
            70,
            [
                'x' => '0',
                'y' => '0',
                'width' => '40',
                'height' => '10',
                'viewBox' => '0 0 100 50',
                'preserveAspectRatio' => 'xMaxYMid meet',
            ],
            $base,
            $base,
        );
        $this->assertNotSame('', $svgMeetXMax);

        $obj->patchSvgObj(70, ['tagdepth' => 1]);
        $svgMeetYMax = $obj->exposeParseSVGTagSTARTsvg(
            $parser,
            70,
            [
                'x' => '0',
                'y' => '0',
                'width' => '10',
                'height' => '40',
                'viewBox' => '0 0 50 100',
                'preserveAspectRatio' => 'xMidYMax meet',
            ],
            $base,
            $base,
        );
        $this->assertNotSame('', $svgMeetYMax);

        $obj->initSvgObjForHandlers(71);
        $obj->exposeParseSVGTagSTARTradialGradient(71, [
            'id' => 'rgm',
            'gradientUnits' => 'userSpaceOnUse',
            'cx' => '2',
            'cy' => '3',
            'r' => '2',
        ]);
        $svgobj71 = $obj->getSvgObj(71);
        $this->assertSame('measure', $svgobj71['gradients']['rgm']['mode'] ?? null);

        $obj->initSvgObjForHandlers(72);
        $styleNoAnchor = $base;
        $styleNoAnchor['text-anchor'] = '';
        $styleNoAnchor['direction'] = '';
        $txtOut = $obj->exposeParseSVGTagSTARTtext($parser, 72, ['x' => '1', 'y' => '1'], $styleNoAnchor, $base);
        $this->assertNotSame('', $txtOut);

        $obj->initSvgObjForHandlers(73);
        $obj->initSvgObjForHandlers(74);
        $obj->patchSvgObj(73, ['out' => 'A', 'child' => [74]]);
        $obj->patchSvgObj(74, ['out' => 'B', 'child' => []]);
        $this->assertSame('AB', $obj->getSetSVG(73));
    }

    /** @throws \Throwable */
    public function testSvgRemainingFeasibleHotspotBranches(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $base = $obj->exposeDefaultSVGStyle();

        $coord = $obj->getPathCoordDefaults();
        $paths = [['0', 'A'], ['1', 'z']];
        $rawSets = [
            ['4', '2', '0',  '1', '0', '10', '20'],
            ['4', '2', '0',  '0', '1', '10', '20'],
            ['6', '3', '45', '1', '0', '14', '16'],
            ['6', '3', '45', '0', '1', '14', '16'],
        ];
        foreach ($rawSets as $raw) {
            $params = [
                (float) $raw[0],
                (float) $raw[1],
                (float) $raw[2],
                (float) $raw[3],
                (float) $raw[4],
                (float) $raw[5],
                (float) $raw[6],
            ];
            [$arcOut] = $obj->exposeSvgPathCmdA($params, $coord, $paths, 0, $raw);
            $this->assertIsString($arcOut);
        }

        $obj->setSvgObjMeta(75);
        $strokeStyle = $base;
        $strokeStyle['stroke'] = 'url(#not-a-color)';
        try {
            [$strokeOut] = $obj->exposeParseSVGStyleStroke(75, $strokeStyle);
            $this->assertSame('', $strokeOut);
        } catch (\Throwable $e) {
            $this->assertNotSame('', $e->getMessage());
        }

        $gradients = [
            'm' => [
                'xref' => '',
                'type' => 2,
                'gradientUnits' => 'objectBoundingBox',
                'mode' => 'measure',
                'coords' => [0.0, 0.0, 0.0, 0.0],
                'stops' => [
                    ['color' => '#000000', 'opacity' => 1.0, 'offset' => 0.0],
                    ['color' => '#ffffff', 'opacity' => 1.0, 'offset' => 1.0],
                ],
                'gradientTransform' => [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
            ],
        ];
        $gradOut = $obj->exposeParseSVGStyleGradient(75, $gradients, 'm', 0.0, 0.0, 0.0, 0.0);
        $this->assertNotSame('', $gradOut);

        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(76);
        $obj->patchSvgObj(76, ['tagdepth' => 1]);
        $xMidMeet = $obj->exposeParseSVGTagSTARTsvg(
            $parser,
            76,
            [
                'x' => '0',
                'y' => '0',
                'width' => '40',
                'height' => '10',
                'viewBox' => '0 0 100 50',
                'preserveAspectRatio' => 'xMidYMid meet',
            ],
            $base,
            $base,
        );
        $this->assertNotSame('', $xMidMeet);

        $obj->patchSvgObj(76, ['tagdepth' => 1]);
        $yMidMeet = $obj->exposeParseSVGTagSTARTsvg(
            $parser,
            76,
            [
                'x' => '0',
                'y' => '0',
                'width' => '10',
                'height' => '40',
                'viewBox' => '0 0 50 100',
                'preserveAspectRatio' => 'xMinYMid meet',
            ],
            $base,
            $base,
        );
        $this->assertNotSame('', $yMidMeet);
    }

    // -----------------------------------------------------------------------
    // P1 fixes: T-1, T-2, S-5, R-4
    // -----------------------------------------------------------------------

    /**
     * T-1: visibility:hidden text must still advance the layout cursor.
     * @throws \Throwable
     */
    public function testSvgInvisibleTextAdvancesCursor(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(80);
        $base = $obj->exposeDefaultSVGStyle();
        $obj->patchSvgObj(80, [
            'styles' => [$base],
            'defsmode' => false,
            'textmode' => [
                'invisible' => true,
                'stroke' => 0,
                'rtl' => false,
                'text-anchor' => 'start',
            ],
            'text' => 'Hello',
            'x' => 10.0,
            'y' => 20.0,
        ]);

        $xBefore = $obj->getSvgObj(80)['x'];
        $out = $obj->exposeParseSVGTagENDtext(80);

        // No drawing output for invisible text.
        $this->assertSame('', $out);
        // Text buffer must be cleared.
        $this->assertSame('', $obj->getSvgObj(80)['text']);
        // Cursor must have advanced (x increased).
        $this->assertGreaterThan($xBefore, $obj->getSvgObj(80)['x']);
    }

    /**
     * T-1: empty invisible text does not change the cursor.
     * @throws \Throwable
     */
    public function testSvgInvisibleEmptyTextDoesNotChangeCursor(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(81);
        $base = $obj->exposeDefaultSVGStyle();
        $obj->patchSvgObj(81, [
            'styles' => [$base],
            'defsmode' => false,
            'textmode' => [
                'invisible' => true,
                'stroke' => 0,
                'rtl' => false,
                'text-anchor' => 'start',
            ],
            'text' => '',
            'x' => 5.0,
            'y' => 5.0,
        ]);

        $xBefore = $obj->getSvgObj(81)['x'];
        $obj->exposeParseSVGTagENDtext(81);
        $this->assertSame($xBefore, $obj->getSvgObj(81)['x']);
    }

    /**
     * Metadata text like <desc> must not leak into renderable SVG text buffer.
     * @throws \Throwable
     */
    public function testSvgDescCharacterDataIsNotRenderedAsText(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(811);

        $obj->exposeHandleSVGTagStart($parser, 'desc', [], 811);
        $obj->exposeHandleSVGCharacter($parser, 'TCPDF SVG EXAMPLE');
        $obj->exposeHandleSVGTagEnd($parser, 'desc');

        $this->assertSame('', $obj->getSvgObj(811)['text']);
    }

    /**
     * T-2: starting a new text/tspan while buffered text exists flushes the run.
     * @throws \Throwable
     */
    public function testSvgStartTextFlushesBufferedRun(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(82);
        $base = $obj->exposeDefaultSVGStyle();

        // Simulate state after an outer <text> started and accumulated content.
        $obj->patchSvgObj(82, [
            'styles' => [$base, $base], // outer-text style already on stack
            'defsmode' => false,
            'textmode' => [
                'invisible' => false,
                'stroke' => 0,
                'rtl' => false,
                'text-anchor' => 'start',
            ],
            'text' => 'Outer',
            'x' => 5.0,
            'y' => 10.0,
        ]);

        // Starting a tspan should flush 'Outer' and produce non-empty output.
        $tspanOut = $obj->exposeParseSVGTagSTARTtspan($parser, 82, ['x' => '5', 'y' => '10'], $base, $base);
        // After flush the text buffer must be empty (new run started fresh).
        $this->assertSame('', $obj->getSvgObj(82)['text']);
        // Output contains both the flushed run and the new transform.
        $this->assertNotSame('', $tspanOut);
    }

    /**
     * S-5: parseSVGTagSTARTuse resolves an element via plain href (SVG 2).
     * @throws \Throwable
     */
    public function testSvgUseTagResolvesViaPlainHref(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(83);

        // Register a simple rect in defs under id 'shape83'.
        $obj->patchSvgObj(83, [
            'defs' => [
                'shape83' => [
                    'name' => 'rect',
                    'attr' => [
                        'width' => '10',
                        'height' => '5',
                        'style' => '',
                    ],
                ],
            ],
        ]);

        // Empty href → empty return.
        $this->assertSame('', $obj->exposeParseSVGTagSTARTuse($parser, 83, []));
        // Plain href (no xlink:href) must resolve.
        $obj->exposeParseSVGTagSTARTuse($parser, 83, ['href' => '#shape83']);
        // The out buffer should have been written to (rect was dispatched).
        $this->assertNotSame('', $obj->getSvgObj(83)['out']);
    }

    /**
     * S-5: parseSVGTagSTARTimage falls back to plain href when xlink:href absent.
     * @throws \Throwable
     */
    public function testSvgImageTagAcceptsPlainHref(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(84);
        $base = $obj->exposeDefaultSVGStyle();

        // Missing both href attributes → empty.
        $this->assertSame('', $obj->exposeParseSVGTagSTARTimage($parser, 84, [], $base, $base));

        // Plain href pointing to an image that will cause a load exception;
        // what matters is that the early-return guard no longer rejects the call
        // solely because xlink:href is absent — the code reaches the image loader.
        try {
            $out = $obj->exposeParseSVGTagSTARTimage(
                $parser,
                84,
                ['href' => 'nonexistent.png', 'x' => '0', 'y' => '0', 'width' => '10', 'height' => '10'],
                $base,
                $base,
            );
        } catch (\Throwable $e) {
            // An image-load exception confirms the code progressed past the href guard.
            $this->assertNotSame('', $e->getMessage());
            return;
        }
        $this->assertSame('', $out);
    }

    /**
     * S-5: linearGradient and radialGradient store xref from plain href.
     * @throws \Throwable
     */
    public function testSvgGradientXrefFromPlainHref(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(85);

        $obj->exposeParseSVGTagSTARTlinearGradient(85, ['id' => 'lg85', 'href' => '#lgBase']);
        $obj->exposeParseSVGTagSTARTradialGradient(85, ['id' => 'rg85', 'href' => '#rgBase']);

        $svgobj = $obj->getSvgObj(85);
        $this->assertSame('lgBase', $svgobj['gradients']['lg85']['xref'] ?? null);
        $this->assertSame('rgBase', $svgobj['gradients']['rg85']['xref'] ?? null);
    }

    /**
     * S-5: gradient prescan preserves plain href so xref inheritance works.
     * @throws \Throwable
     */
    public function testSvgPrescanGradientsKeepsPlainHrefXref(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(851);

        $obj->exposePrescanSVGGradients(
            '<svg><defs>'
            . '<linearGradient id="baseLg"><stop offset="0%" stop-color="#000"/></linearGradient>'
            . '<linearGradient id="refLg" href="#baseLg" />'
            . '<radialGradient id="baseRg"><stop offset="0%" stop-color="#000"/></radialGradient>'
            . '<radialGradient id="refRg" href="#baseRg" />'
            . '</defs></svg>',
            851,
        );

        $svgobj = $obj->getSvgObj(851);
        $this->assertSame('baseLg', $svgobj['gradients']['refLg']['xref'] ?? '');
        $this->assertSame('baseRg', $svgobj['gradients']['refRg']['xref'] ?? '');
    }

    /**
     * R-4: stroke-dasharray values with unit suffixes are normalised to user units.
     * @throws \Throwable
     */
    public function testSvgDasharrayWithUnitSuffixIsNormalised(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->setSvgObjMeta(86);
        $base = $obj->exposeDefaultSVGStyle();

        // Provide dash tokens with a unit suffix; these should survive normalisation
        // (not become zero) and produce a non-empty dashArray in the output.
        $style = $base;
        $style['stroke'] = '#000000';
        $style['stroke-dasharray'] = '5 3';

        [$strokeOut] = $obj->exposeParseSVGStyleStroke(86, $style);
        $this->assertNotSame('', $strokeOut);

        // Ensure a 'none' dasharray still produces empty dashArray.
        $styleNone = $base;
        $styleNone['stroke'] = '#000000';
        $styleNone['stroke-dasharray'] = 'none';
        [$strokeNone] = $obj->exposeParseSVGStyleStroke(86, $styleNone);
        // dashArray=[] means no 'w' dash command prefix — still has stroke output.
        $this->assertIsString($strokeNone);
    }

    /**
     * E-1: <symbol id="sym"> enters defs-capture mode and stores the id in defs.
     * @throws \Throwable
     */
    public function testSvgSymbolEnterDefsMode(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(90);

        $obj->exposeParseSVGTagSTARTsymbol(90, ['id' => 'mySymbol', 'viewBox' => '0 0 100 100']);

        $svgobj = $obj->getSvgObj(90);
        $this->assertTrue($svgobj['defsmode']);
        $this->assertArrayHasKey('mySymbol', $svgobj['defs']);
        $this->assertSame('symbol', $svgobj['defs']['mySymbol']['name'] ?? null);
    }

    /**
     * E-1: </symbol> exits defs-capture mode.
     * @throws \Throwable
     */
    public function testSvgSymbolEndExitsDefsMode(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(91);
        $obj->patchSvgObj(91, ['defsmode' => true]);

        $out = $obj->exposeParseSVGTagENDsymbol(91);

        $this->assertSame('', $out);
        $this->assertFalse($obj->getSvgObj(91)['defsmode']);
    }

    /**
     * E-1: symbol without id does not crash and defsmode is still entered.
     * @throws \Throwable
     */
    public function testSvgSymbolWithoutIdStillEntersDefsMode(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(92);

        $out = $obj->exposeParseSVGTagSTARTsymbol(92, []);

        $this->assertSame('', $out);
        $this->assertTrue($obj->getSvgObj(92)['defsmode']);
    }

    /**
     * E-1: first non-id child inside symbol is captured in defs child list.
     * @throws \Throwable
     */
    public function testSvgSymbolCapturesFirstAnonymousChild(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(92);

        $obj->exposeParseSVGTagSTARTsymbol(92, ['id' => 'symChild']);
        $obj->exposeHandleSVGTagStart($parser, 'rect', ['width' => '10', 'height' => '5'], 92);

        $svgobj = $obj->getSvgObj(92);
        $this->assertArrayHasKey('symChild', $svgobj['defs']);
        $symbolDef = $svgobj['defs']['symChild'] ?? [];
        $children = $symbolDef['child'] ?? [];
        $this->assertIsArray($children);
        $this->assertNotSame([], $children);

        $first = \reset($children);
        $this->assertIsArray($first);
        $this->assertSame('rect', $first['name']);
    }

    /**
     * E-1/R-3: <use> on symbol falls back to symbol viewBox size when width/height missing.
     * @throws \Throwable
     */
    public function testSvgUseSymbolFallsBackToViewBoxDimensions(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(921);
        $base = $obj->exposeDefaultSVGStyle();
        $obj->patchSvgObj(921, ['styles' => [$base]]);

        $obj->patchSvgObj(921, [
            'defs' => [
                'symvb' => [
                    'name' => 'symbol',
                    'attr' => [
                        'viewBox' => '0 0 40 20',
                    ],
                    'child' => [
                        'c1' => [
                            'name' => 'rect',
                            'attr' => ['x' => '0', 'y' => '0', 'width' => '40', 'height' => '20', 'fill' => '#000'],
                        ],
                    ],
                ],
            ],
        ]);

        $out = $obj->exposeParseSVGTagSTARTuse($parser, 921, ['href' => '#symvb', 'x' => '1', 'y' => '2']);
        $this->assertNotSame('', $out);
        $this->assertStringContainsString(' cm', $out);
    }

    /**
     * E-1/R-3: explicit width/height on use still override symbol defaults.
     * @throws \Throwable
     */
    public function testSvgUseSymbolRespectsExplicitUseDimensions(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(922);
        $base = $obj->exposeDefaultSVGStyle();
        $obj->patchSvgObj(922, ['styles' => [$base]]);

        $obj->patchSvgObj(922, [
            'defs' => [
                'symovr' => [
                    'name' => 'symbol',
                    'attr' => [
                        'viewBox' => '0 0 100 50',
                        'width' => '100',
                        'height' => '50',
                    ],
                    'child' => [
                        'c1' => [
                            'name' => 'rect',
                            'attr' => ['x' => '0', 'y' => '0', 'width' => '100', 'height' => '50', 'fill' => '#000'],
                        ],
                    ],
                ],
            ],
        ]);

        $out = $obj->exposeParseSVGTagSTARTuse($parser, 922, [
            'href' => '#symovr',
            'x' => '0',
            'y' => '0',
            'width' => '10',
            'height' => '5',
        ]);
        $this->assertNotSame('', $out);
        // Rendering with explicit dimensions should still produce content and transforms.
        $this->assertStringContainsString(' cm', $out);
    }

    /**
     * E-1: use-level transform is preserved when expanding <symbol>.
     * @throws \Throwable
     */
    public function testSvgUseSymbolPreservesUseLevelTransformAttribute(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        $defs = [
            'symstyle' => [
                'name' => 'symbol',
                'attr' => [
                    'viewBox' => '0 0 20 10',
                ],
                'child' => [
                    'c1' => [
                        'name' => 'rect',
                        'attr' => ['x' => '0', 'y' => '0', 'width' => '20', 'height' => '10'],
                    ],
                ],
            ],
        ];

        $obj->initSvgObjForHandlers(923);
        $obj->patchSvgObj(923, ['styles' => [$base], 'defs' => $defs]);
        $outDefault = $obj->exposeParseSVGTagSTARTuse($parser, 923, [
            'href' => '#symstyle',
            'x' => '0',
            'y' => '0',
            'width' => '20',
            'height' => '10',
        ]);

        $obj->initSvgObjForHandlers(924);
        $obj->patchSvgObj(924, ['styles' => [$base], 'defs' => $defs]);
        $outTransformed = $obj->exposeParseSVGTagSTARTuse($parser, 924, [
            'href' => '#symstyle',
            'x' => '0',
            'y' => '0',
            'width' => '20',
            'height' => '10',
            'transform' => 'translate(5,7)',
        ]);

        $this->assertNotSame('', $outDefault);
        $this->assertNotSame('', $outTransformed);
        $this->assertNotSame($outDefault, $outTransformed);
    }

    /**
     * E-1: use-level style is preserved when expanding <symbol>.
     * @throws \Throwable
     */
    public function testSvgUseSymbolPreservesUseLevelStyleAttribute(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        $defs = [
            'symstyle2' => [
                'name' => 'symbol',
                'attr' => [
                    'viewBox' => '0 0 20 10',
                ],
                'child' => [
                    'c1' => [
                        'name' => 'rect',
                        'attr' => ['x' => '0', 'y' => '0', 'width' => '20', 'height' => '10'],
                    ],
                ],
            ],
        ];

        $obj->initSvgObjForHandlers(925);
        $obj->patchSvgObj(925, ['styles' => [$base], 'defs' => $defs]);
        $outDefault = $obj->exposeParseSVGTagSTARTuse($parser, 925, [
            'href' => '#symstyle2',
            'x' => '0',
            'y' => '0',
            'width' => '20',
            'height' => '10',
        ]);

        $obj->initSvgObjForHandlers(926);
        $obj->patchSvgObj(926, ['styles' => [$base], 'defs' => $defs]);
        $outStyled = $obj->exposeParseSVGTagSTARTuse($parser, 926, [
            'href' => '#symstyle2',
            'x' => '0',
            'y' => '0',
            'width' => '20',
            'height' => '10',
            'style' => 'fill:#ff0000;stroke:none;',
        ]);

        $this->assertNotSame('', $outDefault);
        $this->assertNotSame('', $outStyled);
        $this->assertNotSame($outDefault, $outStyled);
    }

    /**
     * E-1: symbol-level style is preserved when expanding <symbol>.
     * @throws \Throwable
     */
    public function testSvgUseSymbolPreservesSymbolLevelStyleAttribute(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        $obj->initSvgObjForHandlers(927);
        $obj->patchSvgObj(927, [
            'styles' => [$base],
            'defs' => [
                'symstyle3' => [
                    'name' => 'symbol',
                    'attr' => [
                        'viewBox' => '0 0 20 10',
                    ],
                    'child' => [
                        'c1' => [
                            'name' => 'rect',
                            'attr' => ['x' => '0', 'y' => '0', 'width' => '20', 'height' => '10'],
                        ],
                    ],
                ],
            ],
        ]);
        $outDefault = $obj->exposeParseSVGTagSTARTuse($parser, 927, [
            'href' => '#symstyle3',
            'x' => '0',
            'y' => '0',
            'width' => '20',
            'height' => '10',
        ]);

        $obj->initSvgObjForHandlers(928);
        $obj->patchSvgObj(928, [
            'styles' => [$base],
            'defs' => [
                'symstyle3' => [
                    'name' => 'symbol',
                    'attr' => [
                        'viewBox' => '0 0 20 10',
                        'style' => 'fill:#ff0000;stroke:none;',
                    ],
                    'child' => [
                        'c1' => [
                            'name' => 'rect',
                            'attr' => ['x' => '0', 'y' => '0', 'width' => '20', 'height' => '10'],
                        ],
                    ],
                ],
            ],
        ]);
        $outStyled = $obj->exposeParseSVGTagSTARTuse($parser, 928, [
            'href' => '#symstyle3',
            'x' => '0',
            'y' => '0',
            'width' => '20',
            'height' => '10',
        ]);

        $this->assertNotSame('', $outDefault);
        $this->assertNotSame('', $outStyled);
        $this->assertNotSame($outDefault, $outStyled);
    }

    /**
     * E-1: symbol-level transform is preserved when expanding <symbol>.
     * @throws \Throwable
     */
    public function testSvgUseSymbolPreservesSymbolLevelTransformAttribute(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        $obj->initSvgObjForHandlers(929);
        $obj->patchSvgObj(929, [
            'styles' => [$base],
            'defs' => [
                'symstyle4' => [
                    'name' => 'symbol',
                    'attr' => [
                        'viewBox' => '0 0 20 10',
                    ],
                    'child' => [
                        'c1' => [
                            'name' => 'rect',
                            'attr' => ['x' => '0', 'y' => '0', 'width' => '20', 'height' => '10'],
                        ],
                    ],
                ],
            ],
        ]);
        $outDefault = $obj->exposeParseSVGTagSTARTuse($parser, 929, [
            'href' => '#symstyle4',
            'x' => '0',
            'y' => '0',
            'width' => '20',
            'height' => '10',
        ]);

        $obj->initSvgObjForHandlers(930);
        $obj->patchSvgObj(930, [
            'styles' => [$base],
            'defs' => [
                'symstyle4' => [
                    'name' => 'symbol',
                    'attr' => [
                        'viewBox' => '0 0 20 10',
                        'transform' => 'translate(3,4)',
                    ],
                    'child' => [
                        'c1' => [
                            'name' => 'rect',
                            'attr' => ['x' => '0', 'y' => '0', 'width' => '20', 'height' => '10'],
                        ],
                    ],
                ],
            ],
        ]);
        $outTransformed = $obj->exposeParseSVGTagSTARTuse($parser, 930, [
            'href' => '#symstyle4',
            'x' => '0',
            'y' => '0',
            'width' => '20',
            'height' => '10',
        ]);

        $this->assertNotSame('', $outDefault);
        $this->assertNotSame('', $outTransformed);
        $this->assertNotSame($outDefault, $outTransformed);
    }

    /**
     * E-7: <a> start stores link metadata and </a> emits an annotation ref.
     * @throws \Throwable
     */
    public function testSvgATagCreatesAnnotationReference(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(93);

        $pageBefore = $obj->exposeGetCurrentPageData();
        $annotRefsBefore = [];
        if (isset($pageBefore['annotrefs']) && \is_array($pageBefore['annotrefs'])) {
            $annotRefsBefore = $pageBefore['annotrefs'];
        }
        $this->assertIsArray($annotRefsBefore);
        $countBefore = \count($annotRefsBefore);

        $start = $obj->exposeParseSVGTagSTARTa(93, ['href' => 'https://example.com']);
        $this->assertSame('', $start);

        // Simulate that wrapped content advanced text position.
        $obj->patchSvgObj(93, ['x' => 40.0, 'y' => 20.0]);

        $end = $obj->exposeParseSVGTagENDa(93);
        $this->assertSame('', $end);

        $pageAfter = $obj->exposeGetCurrentPageData();
        $annotRefsAfter = [];
        if (isset($pageAfter['annotrefs']) && \is_array($pageAfter['annotrefs'])) {
            $annotRefsAfter = $pageAfter['annotrefs'];
        }
        $this->assertIsArray($annotRefsAfter);
        $countAfter = \count($annotRefsAfter);
        $this->assertSame($countBefore + 1, $countAfter);
        $this->assertSame('', $obj->getSvgObj(93)['textmode']['linkhref'] ?? '');
    }

    /**
     * E-7: missing href on <a> should degrade gracefully with no annotation.
     * @throws \Throwable
     */
    public function testSvgATagWithoutHrefProducesNoAnnotation(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(94);

        $pageBefore = $obj->exposeGetCurrentPageData();
        $annotRefsBefore = [];
        if (isset($pageBefore['annotrefs']) && \is_array($pageBefore['annotrefs'])) {
            $annotRefsBefore = $pageBefore['annotrefs'];
        }
        $this->assertIsArray($annotRefsBefore);
        $countBefore = \count($annotRefsBefore);

        $start = $obj->exposeParseSVGTagSTARTa(94, []);
        $this->assertSame('', $start);
        $end = $obj->exposeParseSVGTagENDa(94);
        $this->assertSame('', $end);

        $pageAfter = $obj->exposeGetCurrentPageData();
        $annotRefsAfter = [];
        if (isset($pageAfter['annotrefs']) && \is_array($pageAfter['annotrefs'])) {
            $annotRefsAfter = $pageAfter['annotrefs'];
        }
        $this->assertIsArray($annotRefsAfter);
        $countAfter = \count($annotRefsAfter);
        $this->assertSame($countBefore, $countAfter);
    }

    /**
     * E-8 stub: <switch> start returns empty string.
     * @throws \Throwable
     */
    public function testSvgSwitchTagStartReturnsEmpty(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(95);

        $out = $obj->exposeParseSVGTagSTARTswitch(95, []);
        $this->assertSame('', $out);
    }

    /**
     * E-8 stub: </switch> end returns empty string.
     * @throws \Throwable
     */
    public function testSvgSwitchTagEndReturnsEmpty(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(95);

        $out = $obj->exposeParseSVGTagENDswitch(95);
        $this->assertSame('', $out);
    }

    /**
     * E-8: only first direct child of switch is rendered.
     * @throws \Throwable
     */
    public function testSvgSwitchRendersOnlyFirstChild(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(960);

        $obj->exposeHandleSVGTagStart($parser, 'switch', [], 960);

        $obj->exposeHandleSVGTagStart(
            $parser,
            'rect',
            ['x' => '1', 'y' => '1', 'width' => '10', 'height' => '5', 'fill' => '#000000'],
            960,
        );
        $obj->exposeHandleSVGTagEnd($parser, 'rect');
        $outAfterFirst = $obj->getSvgObj(960)['out'];
        $this->assertNotSame('', $outAfterFirst);

        // Second sibling should be skipped by switch selection logic.
        $obj->exposeHandleSVGTagStart(
            $parser,
            'circle',
            ['cx' => '5', 'cy' => '5', 'r' => '2', 'fill' => '#ff0000'],
            960,
        );
        $obj->exposeHandleSVGTagEnd($parser, 'circle');
        $outAfterSecond = $obj->getSvgObj(960)['out'];
        $this->assertSame($outAfterFirst, $outAfterSecond);

        $obj->exposeHandleSVGTagEnd($parser, 'switch');
        $this->assertEmpty($obj->getSvgObj(960)['switchstack'] ?? []);
    }

    /**
     * S-1: dominant-baseline='hanging' shifts renderY by -ascent.
     * We verify that the output changes when baseline keyword differs.
     * @throws \Throwable
     */
    public function testSvgDominantBaselineHangingChangesOutput(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $base = $obj->exposeDefaultSVGStyle();

        // Render with default (auto) baseline.
        $obj->initSvgObjForHandlers(95);
        $obj->patchSvgObj(95, [
            'styles' => [$base, $base],
            'defsmode' => false,
            'textmode' => [
                'rtl' => false,
                'invisible' => false,
                'stroke' => 0,
                'text-anchor' => 'start',
                'baseline' => 'auto',
                'rotate' => 0.0,
                'textlength' => 0.0,
                'lengthadjust' => 'spacing',
                'xlist' => [],
                'ylist' => [],
            ],
            'text' => 'Hi',
            'x' => 10.0,
            'y' => 20.0,
        ]);
        $outAuto = $obj->exposeParseSVGTagENDtext(95);

        // Render with hanging baseline.
        $obj->initSvgObjForHandlers(96);
        $obj->patchSvgObj(96, [
            'styles' => [$base, $base],
            'defsmode' => false,
            'textmode' => [
                'rtl' => false,
                'invisible' => false,
                'stroke' => 0,
                'text-anchor' => 'start',
                'baseline' => 'hanging',
                'rotate' => 0.0,
                'textlength' => 0.0,
                'lengthadjust' => 'spacing',
                'xlist' => [],
                'ylist' => [],
            ],
            'text' => 'Hi',
            'x' => 10.0,
            'y' => 20.0,
        ]);
        $outHanging = $obj->exposeParseSVGTagENDtext(96);

        // Both must produce non-empty drawing output.
        $this->assertNotSame('', $outAuto);
        $this->assertNotSame('', $outHanging);
        // The Y coordinate embedded in the output should differ between the two.
        $this->assertNotSame($outAuto, $outHanging);
    }

    /**
     * S-3: textLength with spacingAndGlyphs wraps output in an extra transform.
     * @throws \Throwable
     */
    public function testSvgTextLengthGlyphsAddsScaleTransform(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $base = $obj->exposeDefaultSVGStyle();

        // Without textLength.
        $obj->initSvgObjForHandlers(97);
        $obj->patchSvgObj(97, [
            'styles' => [$base, $base],
            'defsmode' => false,
            'textmode' => [
                'rtl' => false,
                'invisible' => false,
                'stroke' => 0,
                'text-anchor' => 'start',
                'baseline' => 'auto',
                'rotate' => 0.0,
                'textlength' => 0.0,
                'lengthadjust' => 'spacingAndGlyphs',
                'xlist' => [],
                'ylist' => [],
            ],
            'text' => 'Test',
            'x' => 0.0,
            'y' => 20.0,
        ]);
        $outNoScale = $obj->exposeParseSVGTagENDtext(97);

        // With textLength=200 (much wider than natural).
        $obj->initSvgObjForHandlers(98);
        $obj->patchSvgObj(98, [
            'styles' => [$base, $base],
            'defsmode' => false,
            'textmode' => [
                'rtl' => false,
                'invisible' => false,
                'stroke' => 0,
                'text-anchor' => 'start',
                'baseline' => 'auto',
                'rotate' => 0.0,
                'textlength' => 200.0,
                'lengthadjust' => 'spacingAndGlyphs',
                'xlist' => [],
                'ylist' => [],
            ],
            'text' => 'Test',
            'x' => 0.0,
            'y' => 20.0,
        ]);
        $outWithScale = $obj->exposeParseSVGTagENDtext(98);

        $this->assertNotSame('', $outNoScale);
        $this->assertNotSame('', $outWithScale);
        // The scaled variant must be longer (extra transform operators).
        $this->assertGreaterThan(\strlen($outNoScale), \strlen($outWithScale));
    }

    /**
     * S-4: rotate adds extra transform operators around the text output.
     * @throws \Throwable
     */
    public function testSvgTextRotateAddsTransformOperators(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $base = $obj->exposeDefaultSVGStyle();

        // Without rotation.
        $obj->initSvgObjForHandlers(99);
        $obj->patchSvgObj(99, [
            'styles' => [$base, $base],
            'defsmode' => false,
            'textmode' => [
                'rtl' => false,
                'invisible' => false,
                'stroke' => 0,
                'text-anchor' => 'start',
                'baseline' => 'auto',
                'rotate' => 0.0,
                'textlength' => 0.0,
                'lengthadjust' => 'spacing',
                'xlist' => [],
                'ylist' => [],
            ],
            'text' => 'Abc',
            'x' => 5.0,
            'y' => 10.0,
        ]);
        $outNoRotate = $obj->exposeParseSVGTagENDtext(99);

        // With 45-degree rotation.
        $obj->initSvgObjForHandlers(100);
        $obj->patchSvgObj(100, [
            'styles' => [$base, $base],
            'defsmode' => false,
            'textmode' => [
                'rtl' => false,
                'invisible' => false,
                'stroke' => 0,
                'text-anchor' => 'start',
                'baseline' => 'auto',
                'rotate' => 45.0,
                'textlength' => 0.0,
                'lengthadjust' => 'spacing',
                'xlist' => [],
                'ylist' => [],
            ],
            'text' => 'Abc',
            'x' => 5.0,
            'y' => 10.0,
        ]);
        $outRotated = $obj->exposeParseSVGTagENDtext(100);

        $this->assertNotSame('', $outNoRotate);
        $this->assertNotSame('', $outRotated);
        // Rotated output should be longer due to extra transform save/restore.
        $this->assertGreaterThan(\strlen($outNoRotate), \strlen($outRotated));
    }

    /**
     * S-4: multi-value rotate is parsed and applied per glyph.
     * @throws \Throwable
     */
    public function testSvgTextRotateListAppliesPerGlyph(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        // First run: single zero rotation should not add per-glyph transforms.
        $obj->initSvgObjForHandlers(1001);
        $obj->patchSvgObj(1001, [
            'styles' => [$base],
        ]);
        $obj->exposeParseSVGTagSTARTtext($parser, 1001, ['x' => '5', 'y' => '10', 'rotate' => '0'], $base, $base);
        $obj->exposeHandleSVGCharacter($parser, 'AB');
        $outSingle = $obj->exposeParseSVGTagENDtext(1001);

        // Second run: second glyph rotates, forcing per-glyph transforms.
        $obj->initSvgObjForHandlers(1002);
        $obj->patchSvgObj(1002, [
            'styles' => [$base],
        ]);
        $obj->exposeParseSVGTagSTARTtext($parser, 1002, ['x' => '5', 'y' => '10', 'rotate' => '0 45'], $base, $base);
        $obj->exposeHandleSVGCharacter($parser, 'AB');
        $outList = $obj->exposeParseSVGTagENDtext(1002);
        $svgobj = $obj->getSvgObj(1002);
        $rotlist = $svgobj['textmode']['rotlist'] ?? [];

        $this->assertGreaterThanOrEqual(2, \count($rotlist));
        assert(isset($rotlist[0]), "\$rotlist[0] must be set");
        $this->assertEqualsWithDelta(0.0, $rotlist[0], 0.001);
        assert(isset($rotlist[1]), "\$rotlist[1] must be set");
        $this->assertEqualsWithDelta(45.0, $rotlist[1], 0.001);
        $this->assertGreaterThan(\strlen($outSingle), \strlen($outList));
    }

    /**
     * R-1: multi-value x list triggers per-character rendering (more operators).
     * @throws \Throwable
     */
    public function testSvgMultiValueXListRendersPerCharacter(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $base = $obj->exposeDefaultSVGStyle();

        // Single-position (normal) rendering.
        $obj->initSvgObjForHandlers(101);
        $obj->patchSvgObj(101, [
            'styles' => [$base, $base],
            'defsmode' => false,
            'textmode' => [
                'rtl' => false,
                'invisible' => false,
                'stroke' => 0,
                'text-anchor' => 'start',
                'baseline' => 'auto',
                'rotate' => 0.0,
                'textlength' => 0.0,
                'lengthadjust' => 'spacing',
                'xlist' => [],
                'ylist' => [],
            ],
            'text' => 'AB',
            'x' => 5.0,
            'y' => 10.0,
        ]);
        $outNormal = $obj->exposeParseSVGTagENDtext(101);

        // Multi-value x list (two characters, two explicit x positions).
        $obj->initSvgObjForHandlers(102);
        $obj->patchSvgObj(102, [
            'styles' => [$base, $base],
            'defsmode' => false,
            'textmode' => [
                'rtl' => false,
                'invisible' => false,
                'stroke' => 0,
                'text-anchor' => 'start',
                'baseline' => 'auto',
                'rotate' => 0.0,
                'textlength' => 0.0,
                'lengthadjust' => 'spacing',
                'xlist' => [5.0, 15.0],
                'ylist' => [],
            ],
            'text' => 'AB',
            'x' => 5.0,
            'y' => 10.0,
        ]);
        $outMultiX = $obj->exposeParseSVGTagENDtext(102);

        $this->assertNotSame('', $outNormal);
        $this->assertNotSame('', $outMultiX);
        // Per-character rendering emits multiple text operators so output is longer.
        $this->assertGreaterThan(\strlen($outNormal), \strlen($outMultiX));
    }

    /**
     * E-6: textPath startOffset maps text origin along referenced path.
     * @throws \Throwable
     */
    public function testSvgTextPathStartOffsetUpdatesPosition(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(103);
        $base = $obj->exposeDefaultSVGStyle();

        $obj->patchSvgObj(103, [
            'styles' => [$base],
            'defs' => [
                'tp_line' => [
                    'name' => 'line',
                    'attr' => [
                        'x1' => '0',
                        'y1' => '0',
                        'x2' => '200',
                        'y2' => '0',
                    ],
                ],
            ],
        ]);

        $out = $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            103,
            ['href' => '#tp_line', 'startOffset' => '25%'],
            $base,
            $base,
        );

        $this->assertNotSame('', $out);
        $svgobj = $obj->getSvgObj(103);
        $pos25 = $svgobj['x'];

        $obj->initSvgObjForHandlers(106);
        $obj->patchSvgObj(106, [
            'styles' => [$base],
            'defs' => [
                'tp_line' => [
                    'name' => 'line',
                    'attr' => [
                        'x1' => '0',
                        'y1' => '0',
                        'x2' => '200',
                        'y2' => '0',
                    ],
                ],
            ],
        ]);

        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            106,
            ['href' => '#tp_line', 'startOffset' => '50%'],
            $base,
            $base,
        );
        $pos50 = $obj->getSvgObj(106)['x'];

        $this->assertGreaterThan(0.0, $pos25);
        $this->assertGreaterThan($pos25, $pos50);
        $this->assertEqualsWithDelta(0.0, $svgobj['y'], 0.001);
    }

    /**
     * E-6: textPath on vertical segment sets rotate to path angle.
     * @throws \Throwable
     */
    public function testSvgTextPathVerticalSegmentSetsRotate(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(104);
        $base = $obj->exposeDefaultSVGStyle();

        $obj->patchSvgObj(104, [
            'styles' => [$base],
            'defs' => [
                'tp_vertical' => [
                    'name' => 'line',
                    'attr' => [
                        'x1' => '10',
                        'y1' => '10',
                        'x2' => '10',
                        'y2' => '110',
                    ],
                ],
            ],
        ]);

        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            104,
            ['href' => '#tp_vertical', 'startOffset' => '10'],
            $base,
            $base,
        );

        $svgobj = $obj->getSvgObj(104);
        $this->assertEqualsWithDelta(90.0, $svgobj['textmode']['rotate'] ?? 0.0, 0.001);
    }

    /**
     * E-6: offset on bent path uses cumulative segment length and local tangent.
     * @throws \Throwable
     */
    public function testSvgTextPathOffsetUsesBendSegmentTangent(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(1041);
        $base = $obj->exposeDefaultSVGStyle();

        // Polyline path: (0,0)->(10,0)->(10,10), total length 20.
        // At 75% offset (=15), point is on second segment at (10,5), tangent 90°.
        $obj->patchSvgObj(1041, [
            'styles' => [$base],
            'defs' => [
                'tp_bend' => [
                    'name' => 'polyline',
                    'attr' => [
                        'points' => '0,0 10,0 10,10',
                    ],
                ],
            ],
        ]);

        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1041,
            ['href' => '#tp_bend', 'startOffset' => '75%'],
            $base,
            $base,
        );

        $svgobj = $obj->getSvgObj(1041);
        $textX = $svgobj['x'];
        $textY = $svgobj['y'];
        $this->assertGreaterThan(0.0, $textY);
        $this->assertGreaterThan($textY, $textX);
        $this->assertNotEqualsWithDelta($textX, $textY, 0.001);
        $this->assertEqualsWithDelta(90.0, $svgobj['textmode']['rotate'] ?? 0.0, 0.001);
    }

    /**
     * E-6: per-glyph textPath layout updates rotation as glyphs pass a bend.
     * @throws \Throwable
     */
    public function testSvgTextPathPerGlyphRotationFollowsBend(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(1042);
        $base = $obj->exposeDefaultSVGStyle();

        // First segment is intentionally tiny so the second glyph lands on
        // the vertical segment and picks a near-90deg local tangent.
        $obj->patchSvgObj(1042, [
            'styles' => [$base],
            'defs' => [
                'tp_glyph_bend' => [
                    'name' => 'polyline',
                    'attr' => [
                        'points' => '0,0 0.1,0 0.1,100',
                    ],
                ],
            ],
        ]);

        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1042,
            ['href' => '#tp_glyph_bend', 'startOffset' => '0'],
            $base,
            $base,
        );

        $obj->exposeHandleSVGCharacter($parser, 'ab');
        $obj->exposeParseSVGTagENDtextPath(1042);

        $svgobj = $obj->getSvgObj(1042);
        $rotlist = $svgobj['textmode']['rotlist'] ?? [];
        $this->assertGreaterThanOrEqual(2, \count($rotlist));
        assert(isset($rotlist[0]), "\$rotlist[0] must be set");
        $this->assertLessThan(45.0, $rotlist[0]);
        assert(isset($rotlist[1]), "\$rotlist[1] must be set");
        $this->assertGreaterThan(45.0, $rotlist[1]);
    }

    /**
     * E-6: path-command decomposition handles H/V bends for tangent angle.
     * @throws \Throwable
     */
    public function testSvgTextPathPathHVCommandsUseLocalVerticalTangent(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(1047);
        $base = $obj->exposeDefaultSVGStyle();

        // Path: M0,0 H100 V100 has total length 200.
        // At 75% offset (=150), point is on the vertical segment.
        $obj->patchSvgObj(1047, [
            'styles' => [$base],
            'defs' => [
                'tp_hv' => [
                    'name' => 'path',
                    'attr' => [
                        'd' => 'M0 0 H100 V100',
                    ],
                ],
            ],
        ]);

        $obj->exposeParseSVGTagSTARTtextPath($parser, 1047, ['href' => '#tp_hv', 'startOffset' => '75%'], $base, $base);

        $svgobj = $obj->getSvgObj(1047);
        $this->assertEqualsWithDelta(90.0, $svgobj['textmode']['rotate'] ?? 0.0, 0.001);
        $this->assertGreaterThan(0.0, $svgobj['y']);
    }

    /**
     * E-6: cubic path sampling uses local start tangent instead of endpoint chord.
     * @throws \Throwable
     */
    public function testSvgTextPathCubicUsesLocalStartTangentAtSmallOffset(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(1048);
        $base = $obj->exposeDefaultSVGStyle();

        // C endpoint chord is vertical (0,0)->(0,100), but the curve starts
        // with an almost horizontal tangent due to the first control point.
        $obj->patchSvgObj(1048, [
            'styles' => [$base],
            'defs' => [
                'tp_cubic' => [
                    'name' => 'path',
                    'attr' => [
                        'd' => 'M0 0 C100 0 100 100 0 100',
                    ],
                ],
            ],
        ]);

        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1048,
            ['href' => '#tp_cubic', 'startOffset' => '5%'],
            $base,
            $base,
        );

        $svgobj = $obj->getSvgObj(1048);
        $angle = $svgobj['textmode']['rotate'] ?? 0.0;
        $this->assertGreaterThan(-45.0, $angle);
        $this->assertLessThan(45.0, $angle);
    }

    /**
     * E-6: arc path sampling uses local arc tangent at small offsets.
     * @throws \Throwable
     */
    public function testSvgTextPathArcUsesLocalStartTangentAtSmallOffset(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(1049);
        $base = $obj->exposeDefaultSVGStyle();

        // A semicircular arc from left to right: endpoint chord is horizontal,
        // while local tangent near the start is near vertical.
        $obj->patchSvgObj(1049, [
            'styles' => [$base],
            'defs' => [
                'tp_arc' => [
                    'name' => 'path',
                    'attr' => [
                        'd' => 'M0 0 A50 50 0 0 1 100 0',
                    ],
                ],
            ],
        ]);

        $obj->exposeParseSVGTagSTARTtextPath($parser, 1049, ['href' => '#tp_arc', 'startOffset' => '5%'], $base, $base);

        $svgobj = $obj->getSvgObj(1049);
        $angle = $svgobj['textmode']['rotate'] ?? 0.0;
        $this->assertGreaterThan(45.0, \abs($angle));
    }

    /**
     * E-6: arc sweep flag influences initial tangent direction.
     * @throws \Throwable
     */
    public function testSvgTextPathArcSweepDirectionAffectsInitialTangentSign(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        // sweep=1 should start with upward tangent for this semicircle.
        $obj->initSvgObjForHandlers(1050);
        $obj->patchSvgObj(1050, [
            'styles' => [$base],
            'defs' => [
                'tp_arc_sweep1' => [
                    'name' => 'path',
                    'attr' => [
                        'd' => 'M0 0 A50 50 0 0 1 100 0',
                    ],
                ],
            ],
        ]);
        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1050,
            ['href' => '#tp_arc_sweep1', 'startOffset' => '5%'],
            $base,
            $base,
        );
        $angleUp = $obj->getSvgObj(1050)['textmode']['rotate'] ?? 0.0;

        // sweep=0 should mirror direction, yielding a negative initial tangent.
        $obj->initSvgObjForHandlers(1051);
        $obj->patchSvgObj(1051, [
            'styles' => [$base],
            'defs' => [
                'tp_arc_sweep0' => [
                    'name' => 'path',
                    'attr' => [
                        'd' => 'M0 0 A50 50 0 0 0 100 0',
                    ],
                ],
            ],
        ]);
        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1051,
            ['href' => '#tp_arc_sweep0', 'startOffset' => '5%'],
            $base,
            $base,
        );
        $angleDown = $obj->getSvgObj(1051)['textmode']['rotate'] ?? 0.0;

        $this->assertGreaterThan(45.0, \abs($angleUp));
        $this->assertGreaterThan(45.0, \abs($angleDown));
        $this->assertLessThan(0.0, $angleUp * $angleDown);
    }

    /**
     * E-6: zero-radius arc falls back to a straight segment endpoint behavior.
     * @throws \Throwable
     */
    public function testSvgTextPathZeroRadiusArcFallsBackToLine(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(1052);
        $base = $obj->exposeDefaultSVGStyle();

        $obj->patchSvgObj(1052, [
            'styles' => [$base],
            'defs' => [
                'tp_arc_zero' => [
                    'name' => 'path',
                    'attr' => [
                        'd' => 'M0 0 A0 0 0 0 1 100 0',
                    ],
                ],
            ],
        ]);

        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1052,
            ['href' => '#tp_arc_zero', 'startOffset' => '50%'],
            $base,
            $base,
        );

        $svgobj = $obj->getSvgObj(1052);
        $this->assertGreaterThan(0.0, $svgobj['x']);
        $this->assertEqualsWithDelta(0.0, $svgobj['y'], 0.001);
        $this->assertEqualsWithDelta(0.0, $svgobj['textmode']['rotate'] ?? 0.0, 0.001);
    }

    /**
     * E-6: spacing="auto" expands inter-glyph distance to fill path.
     * @throws \Throwable
     */
    public function testSvgTextPathSpacingAutoExpandsGlyphGap(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        $defs = [
            'tp_space' => [
                'name' => 'line',
                'attr' => [
                    'x1' => '0',
                    'y1' => '0',
                    'x2' => '300',
                    'y2' => '0',
                ],
            ],
        ];

        $obj->initSvgObjForHandlers(1043);
        $obj->patchSvgObj(1043, ['styles' => [$base], 'defs' => $defs]);
        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1043,
            ['href' => '#tp_space', 'spacing' => 'exact'],
            $base,
            $base,
        );
        $obj->exposeHandleSVGCharacter($parser, 'ab');
        $obj->exposeParseSVGTagENDtextPath(1043);
        $exact = $obj->getSvgObj(1043);
        $xExact = $exact['textmode']['xlist'] ?? [];
        $this->assertGreaterThanOrEqual(2, \count($xExact));

        $obj->initSvgObjForHandlers(1044);
        $obj->patchSvgObj(1044, ['styles' => [$base], 'defs' => $defs]);
        $obj->exposeParseSVGTagSTARTtextPath($parser, 1044, ['href' => '#tp_space', 'spacing' => 'auto'], $base, $base);
        $obj->exposeHandleSVGCharacter($parser, 'ab');
        $obj->exposeParseSVGTagENDtextPath(1044);
        $auto = $obj->getSvgObj(1044);
        $xAuto = $auto['textmode']['xlist'] ?? [];
        $this->assertGreaterThanOrEqual(2, \count($xAuto));

        assert(isset($xExact[1]), "\$xExact[1] must be set");
        assert(isset($xExact[0]), "\$xExact[0] must be set");
        $gapExact = $xExact[1] - $xExact[0];
        assert(isset($xAuto[1]), "\$xAuto[1] must be set");
        assert(isset($xAuto[0]), "\$xAuto[0] must be set");
        $gapAuto = $xAuto[1] - $xAuto[0];
        $this->assertGreaterThan($gapExact, $gapAuto);
    }

    /**
     * E-6: method="stretch" scales glyph advances to consume path length.
     * @throws \Throwable
     */
    public function testSvgTextPathMethodStretchExpandsGlyphGap(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        $defs = [
            'tp_stretch' => [
                'name' => 'line',
                'attr' => [
                    'x1' => '0',
                    'y1' => '0',
                    'x2' => '250',
                    'y2' => '0',
                ],
            ],
        ];

        $obj->initSvgObjForHandlers(1045);
        $obj->patchSvgObj(1045, ['styles' => [$base], 'defs' => $defs]);
        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1045,
            ['href' => '#tp_stretch', 'method' => 'align'],
            $base,
            $base,
        );
        $obj->exposeHandleSVGCharacter($parser, 'ab');
        $obj->exposeParseSVGTagENDtextPath(1045);
        $align = $obj->getSvgObj(1045);
        $xAlign = $align['textmode']['xlist'] ?? [];
        $this->assertGreaterThanOrEqual(2, \count($xAlign));

        $obj->initSvgObjForHandlers(1046);
        $obj->patchSvgObj(1046, ['styles' => [$base], 'defs' => $defs]);
        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1046,
            ['href' => '#tp_stretch', 'method' => 'stretch'],
            $base,
            $base,
        );
        $obj->exposeHandleSVGCharacter($parser, 'ab');
        $obj->exposeParseSVGTagENDtextPath(1046);
        $stretch = $obj->getSvgObj(1046);
        $xStretch = $stretch['textmode']['xlist'] ?? [];
        $this->assertGreaterThanOrEqual(2, \count($xStretch));

        assert(isset($xAlign[1]), "\$xAlign[1] must be set");
        assert(isset($xAlign[0]), "\$xAlign[0] must be set");
        $gapAlign = $xAlign[1] - $xAlign[0];
        assert(isset($xStretch[1]), "\$xStretch[1] must be set");
        assert(isset($xStretch[0]), "\$xStretch[0] must be set");
        $gapStretch = $xStretch[1] - $xStretch[0];
        $this->assertGreaterThan($gapAlign, $gapStretch);
    }

    /**
     * E-6: spacing="auto" distributes extra length as equal inter-glyph gaps.
     * @throws \Throwable
     */
    public function testSvgTextPathSpacingAutoPreservesMixedWidthGapDelta(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $base = $obj->exposeDefaultSVGStyle();

        $defs = [
            'tp_mixed_gap' => [
                'name' => 'line',
                'attr' => [
                    'x1' => '0',
                    'y1' => '0',
                    'x2' => '600',
                    'y2' => '0',
                ],
            ],
        ];

        $obj->initSvgObjForHandlers(1053);
        $obj->patchSvgObj(1053, ['styles' => [$base], 'defs' => $defs]);
        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1053,
            ['href' => '#tp_mixed_gap', 'spacing' => 'exact'],
            $base,
            $base,
        );
        $obj->exposeHandleSVGCharacter($parser, 'WiW');
        $obj->exposeParseSVGTagENDtextPath(1053);
        $exact = $obj->getSvgObj(1053);
        $xExact = $exact['textmode']['xlist'] ?? [];
        $this->assertGreaterThanOrEqual(3, \count($xExact));
        assert(isset($xExact[1]), "\$xExact[1] must be set");
        assert(isset($xExact[0]), "\$xExact[0] must be set");
        $gapExactOne = $xExact[1] - $xExact[0];
        assert(isset($xExact[2]), "\$xExact[2] must be set");
        $gapExactTwo = $xExact[2] - $xExact[1];

        $obj->initSvgObjForHandlers(1054);
        $obj->patchSvgObj(1054, ['styles' => [$base], 'defs' => $defs]);
        $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            1054,
            ['href' => '#tp_mixed_gap', 'spacing' => 'auto'],
            $base,
            $base,
        );
        $obj->exposeHandleSVGCharacter($parser, 'WiW');
        $obj->exposeParseSVGTagENDtextPath(1054);
        $auto = $obj->getSvgObj(1054);
        $xAuto = $auto['textmode']['xlist'] ?? [];
        $this->assertGreaterThanOrEqual(3, \count($xAuto));
        assert(isset($xAuto[1]), "\$xAuto[1] must be set");
        assert(isset($xAuto[0]), "\$xAuto[0] must be set");
        $gapAutoOne = $xAuto[1] - $xAuto[0];
        assert(isset($xAuto[2]), "\$xAuto[2] must be set");
        $gapAutoTwo = $xAuto[2] - $xAuto[1];

        $this->assertGreaterThan($gapExactOne, $gapAutoOne);
        $this->assertGreaterThan($gapExactTwo, $gapAutoTwo);
        $this->assertEqualsWithDelta($gapExactOne - $gapExactTwo, $gapAutoOne - $gapAutoTwo, 0.5);
    }

    /**
     * E-6: textPath with unresolved href still behaves like a nested text run.
     * @throws \Throwable
     */
    public function testSvgTextPathMissingReferenceStillRendersText(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(105);
        $base = $obj->exposeDefaultSVGStyle();
        $obj->patchSvgObj(105, ['styles' => [$base]]);

        $startOut = $obj->exposeParseSVGTagSTARTtextPath(
            $parser,
            105,
            ['href' => '#missing_path', 'x' => '12', 'y' => '34'],
            $base,
            $base,
        );
        $this->assertNotSame('', $startOut);

        $obj->exposeHandleSVGCharacter($parser, 'abc');
        $endOut = $obj->exposeParseSVGTagENDtextPath(105);
        $this->assertNotSame('', $endOut);
        $this->assertSame('', $obj->getSvgObj(105)['text']);
    }

    /**
     * S-2: writing-mode vertical is stored in textmode by STARTtext.
     * @throws \Throwable
     */
    public function testSvgWritingModeVerticalSetsTextModeFlag(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(107);
        $base = $obj->exposeDefaultSVGStyle();
        $vertical = $base;
        $vertical['writing-mode'] = 'vertical-rl';

        $out = $obj->exposeParseSVGTagSTARTtext($parser, 107, ['x' => '10', 'y' => '20'], $vertical, $base);

        $this->assertNotSame('', $out);
        $this->assertTrue($obj->getSvgObj(107)['textmode']['vertical'] ?? false);
        $this->assertEqualsWithDelta(90.0, $obj->getSvgObj(107)['textmode']['rotate'] ?? 0.0, 0.001);
    }

    /**
     * S-2: glyph-orientation-vertical overrides default 90deg vertical rotation.
     * @throws \Throwable
     */
    public function testSvgWritingModeVerticalGlyphOrientationOverride(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(109);
        $base = $obj->exposeDefaultSVGStyle();
        $vertical = $base;
        $vertical['writing-mode'] = 'tb-rl';
        $vertical['glyph-orientation-vertical'] = '0deg';

        $out = $obj->exposeParseSVGTagSTARTtext($parser, 109, ['x' => '10', 'y' => '20'], $vertical, $base);

        $this->assertNotSame('', $out);
        $this->assertTrue($obj->getSvgObj(109)['textmode']['vertical'] ?? false);
        $this->assertEqualsWithDelta(0.0, $obj->getSvgObj(109)['textmode']['rotate'] ?? 0.0, 0.001);
    }

    /**
     * S-2: glyph-orientation-horizontal is applied in horizontal writing mode.
     * @throws \Throwable
     */
    public function testSvgHorizontalGlyphOrientationSetsRotation(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $parser = \xml_parser_create('UTF-8');
        $obj->initSvgObjForHandlers(110);
        $base = $obj->exposeDefaultSVGStyle();
        $hstyle = $base;
        $hstyle['writing-mode'] = 'lr-tb';
        $hstyle['glyph-orientation-horizontal'] = '180deg';

        $out = $obj->exposeParseSVGTagSTARTtext($parser, 110, ['x' => '10', 'y' => '20'], $hstyle, $base);

        $this->assertNotSame('', $out);
        $this->assertFalse($obj->getSvgObj(110)['textmode']['vertical'] ?? false);
        $this->assertEqualsWithDelta(180.0, $obj->getSvgObj(110)['textmode']['rotate'] ?? 0.0, 0.001);
    }

    /**
     * S-2: invisible vertical text advances Y instead of X.
     * @throws \Throwable
     */
    public function testSvgInvisibleVerticalTextAdvancesYCursor(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(108);
        $base = $obj->exposeDefaultSVGStyle();
        $obj->patchSvgObj(108, [
            'styles' => [$base],
            'defsmode' => false,
            'textmode' => [
                'invisible' => true,
                'vertical' => true,
                'stroke' => 0,
                'rtl' => false,
                'text-anchor' => 'start',
            ],
            'text' => 'VV',
            'x' => 7.0,
            'y' => 9.0,
        ]);

        $xBefore = $obj->getSvgObj(108)['x'];
        $yBefore = $obj->getSvgObj(108)['y'];
        $out = $obj->exposeParseSVGTagENDtext(108);

        $this->assertSame('', $out);
        $this->assertSame($xBefore, $obj->getSvgObj(108)['x']);
        $this->assertGreaterThan($yBefore, $obj->getSvgObj(108)['y']);
    }

    // -------------------------------------------------------------------------
    // E-4 mask application tests
    // -------------------------------------------------------------------------

    /**
     * parseSVGStyleMask with a valid url(#id) registers the mask stream and
     * returns a PDF ExtGState activation command containing the mask key.
     * @throws \Throwable
     */
    public function testSvgMaskStyleResolvesUrlAndRegistersMaskStream(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(560);

        // Build defs: one mask with a white rect child (captured structure).
        $obj->patchSvgObj(560, [
            'defs' => [
                'myMask' => [
                    'name' => 'mask',
                    'attr' => ['id' => 'myMask'],
                    'child' => [
                        'DF_1' => [
                            'name' => 'rect',
                            'attr' => ['x' => '0', 'y' => '0', 'width' => '10', 'height' => '10', 'fill' => '#ffffff'],
                        ],
                        'DF_1_CLOSE' => [
                            'name' => 'rect',
                            'attr' => ['closing_tag' => true, 'content' => ''],
                        ],
                    ],
                ],
            ],
        ]);

        $style = $obj->exposeDefaultSVGStyle();
        $style['mask'] = 'url(#myMask)';

        $out = $obj->exposeParseSVGStyleMask(560, $style);

        // Must return a GS activation command.
        $this->assertStringContainsString(' gs', $out);

        // The mask key used in the command must match what's registered.
        $masks = $obj->getSvgMasks();
        $this->assertNotEmpty($masks, 'svgmasks must contain the registered mask');

        $maskKey = \array_key_first($masks);
        if (!\is_string($maskKey)) {
            $this->fail('Expected non-empty svgmasks key to be a string');
        }

        $this->assertStringContainsString($maskKey, $out);
        $this->assertStringStartsWith('MSK_', $maskKey);

        $maskData = [];
        if (isset($masks[$maskKey]) && \is_array($masks[$maskKey])) {
            $maskData = $masks[$maskKey];
        }
        $this->assertIsArray($maskData);
        $this->assertArrayHasKey('stream', $maskData);
        $this->assertArrayHasKey('bbox', $maskData);
        $this->assertSame(0, $maskData['gs_n'] ?? null, 'gs_n is 0 until PDF output phase');
    }

    /**
     * Calling parseSVGStyleMask with mask="none" returns empty string and does
     * not register anything.
     * @throws \Throwable
     */
    public function testSvgMaskStyleNoneIsNoOp(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(561);

        $style = $obj->exposeDefaultSVGStyle();
        $style['mask'] = 'none';

        $out = $obj->exposeParseSVGStyleMask(561, $style);

        $this->assertSame('', $out);
        $this->assertEmpty($obj->getSvgMasks());
    }

    /**
     * A mask url that references a missing def returns empty string.
     * @throws \Throwable
     */
    public function testSvgMaskStyleMissingDefIsNoOp(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(562);

        $style = $obj->exposeDefaultSVGStyle();
        $style['mask'] = 'url(#nonExistent)';

        $out = $obj->exposeParseSVGStyleMask(562, $style);

        $this->assertSame('', $out);
        $this->assertEmpty($obj->getSvgMasks());
    }

    /**
     * Calling parseSVGStyleMask while patternmode > 0 returns empty string
     * (guard against recursion during mask/pattern child replay).
     * @throws \Throwable
     */
    public function testSvgMaskStyleSkippedInsidePatternMode(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(563);

        $obj->patchSvgObj(563, [
            'patternmode' => 1,
            'defs' => [
                'mskX' => [
                    'name' => 'mask',
                    'attr' => ['id' => 'mskX'],
                    'child' => [],
                ],
            ],
        ]);

        $style = $obj->exposeDefaultSVGStyle();
        $style['mask'] = 'url(#mskX)';

        $out = $obj->exposeParseSVGStyleMask(563, $style);

        $this->assertSame('', $out);
        $this->assertEmpty($obj->getSvgMasks());
    }

    /**
     * Calling parseSVGStyleMask twice with the same mask id re-uses the
     * already-registered mask entry without duplicating it.
     * @throws \Throwable
     */
    public function testSvgMaskStyleSameMaskIsRegisteredOnce(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(564);

        $obj->patchSvgObj(564, [
            'defs' => [
                'sharedMask' => [
                    'name' => 'mask',
                    'attr' => ['id' => 'sharedMask'],
                    'child' => [],
                ],
            ],
        ]);

        $style = $obj->exposeDefaultSVGStyle();
        $style['mask'] = 'url(#sharedMask)';

        $out1 = $obj->exposeParseSVGStyleMask(564, $style);
        $out2 = $obj->exposeParseSVGStyleMask(564, $style);

        $this->assertSame($out1, $out2, 'Both calls return the same GS command');
        $this->assertCount(1, $obj->getSvgMasks(), 'Only one mask entry registered');
    }

    // -------------------------------------------------------------------------
    // E-5 filter unsupported / no-op tests
    // -------------------------------------------------------------------------

    /**
     * parseSVGTagSTARTfilter with an id registers a placeholder in defs and
     * enables defsmode so fe* children are swallowed.
     * @throws \Throwable
     */
    public function testSvgFilterStartRegistersPlaceholderAndEnablesDefsMode(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(570);

        $this->assertSame('', $obj->exposeParseSVGTagSTARTfilter(570, ['id' => 'f1', 'x' => '0', 'y' => '0']));

        $svgobj = $obj->getSvgObj(570);
        $this->assertTrue($svgobj['defsmode']);
        $this->assertArrayHasKey('f1', $svgobj['defs']);
        $this->assertSame('filter', $svgobj['defs']['f1']['name'] ?? null);
    }

    /**
     * parseSVGTagSTARTfilter without an id still enables defsmode but does not
     * add a defs entry.
     * @throws \Throwable
     */
    public function testSvgFilterStartWithoutIdOnlyEnablesDefsMode(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(571);

        $this->assertSame('', $obj->exposeParseSVGTagSTARTfilter(571, []));

        $svgobj = $obj->getSvgObj(571);
        $this->assertTrue($svgobj['defsmode']);
        $this->assertSame([], $svgobj['defs']);
    }

    /**
     * parseSVGTagENDfilter clears defsmode.
     * @throws \Throwable
     */
    public function testSvgFilterEndClearsDefsMode(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(572);

        $obj->exposeParseSVGTagSTARTfilter(572, ['id' => 'f2']);
        $this->assertTrue($obj->getSvgObj(572)['defsmode']);

        $this->assertSame('', $obj->exposeParseSVGTagENDfilter(572));
        $this->assertFalse($obj->getSvgObj(572)['defsmode']);
    }

    // -------------------------------------------------------------------------
    // S-6 rendering-hint tests
    // -------------------------------------------------------------------------

    /**
     * 'auto' hints for all properties emit no ri operator (use PDF default).
     * @throws \Throwable
     */
    public function testSvgRenderingHintsAllAutoEmitsNothing(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);

        $style = $obj->exposeDefaultSVGStyle();
        // defaults are already 'auto'
        $this->assertSame('', $obj->exposeParseSVGStyleRenderingHints($style));
    }

    /**
     * 'optimizeQuality' on any property emits /RelativeColorimetric ri.
     * @throws \Throwable
     */
    public function testSvgRenderingHintsOptimizeQualityEmitsRelativeColorimetric(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);

        $style = $obj->exposeDefaultSVGStyle();
        $style['image-rendering'] = 'optimizeQuality';

        $out = $obj->exposeParseSVGStyleRenderingHints($style);
        $this->assertSame("/RelativeColorimetric ri\n", $out);
    }

    /**
     * 'optimizeSpeed' on any property emits /AbsoluteColorimetric ri.
     * @throws \Throwable
     */
    public function testSvgRenderingHintsOptimizeSpeedEmitsAbsoluteColorimetric(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);

        $style = $obj->exposeDefaultSVGStyle();
        $style['color-rendering'] = 'optimizeSpeed';

        $out = $obj->exposeParseSVGStyleRenderingHints($style);
        $this->assertSame("/AbsoluteColorimetric ri\n", $out);
    }

    /**
     * 'crispEdges' and 'geometricPrecision' map to RelativeColorimetric.
     * @throws \Throwable
     */
    public function testSvgRenderingHintsCrispEdgesAndGeometricPrecision(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);

        $style = $obj->exposeDefaultSVGStyle();
        $style['shape-rendering'] = 'crispEdges';
        $this->assertSame("/RelativeColorimetric ri\n", $obj->exposeParseSVGStyleRenderingHints($style));

        $style['shape-rendering'] = 'geometricPrecision';
        $this->assertSame("/RelativeColorimetric ri\n", $obj->exposeParseSVGStyleRenderingHints($style));
    }

    /**
     * When both optimizeQuality and optimizeSpeed hints are present,
     * optimizeSpeed wins (last in priority order).
     * @throws \Throwable
     */
    public function testSvgRenderingHintsSpeedBeatsQualityWhenBothPresent(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);

        $style = $obj->exposeDefaultSVGStyle();
        $style['image-rendering'] = 'optimizeQuality';
        $style['color-rendering'] = 'optimizeSpeed';

        $out = $obj->exposeParseSVGStyleRenderingHints($style);
        $this->assertSame("/AbsoluteColorimetric ri\n", $out);
    }

    /**
     * <filter> is in SVGCHARDATASKIPTAGS so the entire filter+fe* subtree is
     * silently skipped via the charskip counter: charskip is balanced (back to
     * zero) after the subtree, defsmode is correctly restored by </defs>, and
     * no output is written.
     * @throws \Throwable
     */
    public function testSvgFilterRoundTripViaHandlersProducesNoOutput(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(573);

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

        // Enter defs context.
        $obj->exposeHandleSVGTagStart($parser, 'defs', [], 573);

        // <filter> and all fe* children are in SVGCHARDATASKIPTAGS so the start
        // handler just does charskip++ and returns immediately (no dispatch).
        $obj->exposeHandleSVGTagStart($parser, 'filter', ['id' => 'blur1'], 573);
        $obj->exposeHandleSVGTagStart($parser, 'feGaussianBlur', [], 573);
        $obj->exposeHandleSVGTagEnd($parser, 'feGaussianBlur');
        $obj->exposeHandleSVGTagEnd($parser, 'filter');

        // Close defs.
        $obj->exposeHandleSVGTagEnd($parser, 'defs');

        unset($parser);

        $svgobj = $obj->getSvgObj(573);

        // charskip counter must be balanced back to zero after the subtree.
        $this->assertSame(0, (int) $svgobj['charskip'], 'charskip balanced');

        // defsmode off after </defs>
        $this->assertFalse($svgobj['defsmode'], 'defsmode false after </defs>');

        // No stray rendered output.
        $this->assertSame('', $svgobj['out'], 'no output from filter subtree');
    }

    /**
     * S-3: getSVGPath returns empty when paint operator is empty (line 1111).
     * @throws \Throwable
     */
    public function testGetSVGPathReturnEmptyOnEmptyPaintOp(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(574);
        // mode 'X' produces empty paint operator
        $out = $obj->exposeGetSVGPath(574, 'M 10 10 L 20 20', 'X');
        $this->assertSame('', $out);
    }

    /**
     * S-3: getSVGPath covers default branch in match (unknown cmd → line 1184).
     * @throws \Throwable
     */
    public function testGetSVGPathUnknownCommandProducesEmptyOutput(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(575);
        // 'D' is not a valid SVG path command, hitting the default '' branch
        $out = $obj->exposeGetSVGPath(575, 'D 10 10', 'DF');
        $this->assertSame('', $out);
    }

    /**
     * S-3: parseSVGGlyphOrientationAngle with rad unit (lines 2156-2160).
     * @throws \Throwable
     */
    public function testParseSVGGlyphOrientationAngleRadians(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeParseSVGGlyphOrientationAngle('1.5708rad', 0.0);
        $this->assertEqualsWithDelta(90.0, $result, 0.01);
    }

    /**
     * S-3: parseSVGGlyphOrientationAngle with grad unit (line 2167-2168).
     * @throws \Throwable
     */
    public function testParseSVGGlyphOrientationAngleGradians(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeParseSVGGlyphOrientationAngle('100grad', 0.0);
        $this->assertEqualsWithDelta(90.0, $result, 0.001);
    }

    /**
     * S-3: parseSVGGlyphOrientationAngle with empty string returns default (line 2156).
     * @throws \Throwable
     */
    public function testParseSVGGlyphOrientationAngleEmptyStringReturnsDefault(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeParseSVGGlyphOrientationAngle('', 45.0);
        $this->assertEqualsWithDelta(45.0, $result, 0.001);
    }

    /**
     * S-3: parseSVGGlyphOrientationAngle non-matching string returns default (line 2160).
     * @throws \Throwable
     */
    public function testParseSVGGlyphOrientationAngleInvalidReturnsDefault(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeParseSVGGlyphOrientationAngle('auto', 30.0);
        $this->assertEqualsWithDelta(30.0, $result, 0.001);
    }

    /**
     * S-3: getSVGGlyphOrientationRotation horizontal 'auto' returns 0.0 (line 2197).
     * @throws \Throwable
     */
    public function testGetSVGGlyphOrientationRotationHorizontalAutoReturnsZero(): void
    {
        $obj = $this->getInternalTestObject();
        $base = $obj->exposeDefaultSVGStyle();
        $style = $base;
        $style['glyph-orientation-horizontal'] = 'auto';
        $result = $obj->exposeGetSVGGlyphOrientationRotation($style, false);
        $this->assertEqualsWithDelta(0.0, $result, 0.001);
    }

    /**
     * S-3: resolveSVGPatternLength with percentage value (lines 2525, 2529-2530).
     * @throws \Throwable
     */
    public function testResolveSVGPatternLengthPercentage(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(576);
        $result = $obj->exposeResolveSVGPatternLength('50%', 200.0, 576);
        $this->assertEqualsWithDelta(100.0, $result, 0.001);
    }

    /**
     * S-3: resolveSVGPatternLength empty string returns 0 (line 2525).
     * @throws \Throwable
     */
    public function testResolveSVGPatternLengthEmptyReturnsZero(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(577);
        $result = $obj->exposeResolveSVGPatternLength('', 100.0, 577);
        $this->assertEqualsWithDelta(0.0, $result, 0.001);
    }

    /**
     * S-3: getTextPathPointsFromPathData with horizontal H command (lines 6136-6187 range).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataWithHCommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(578);
        $points = $obj->exposeGetTextPathPointsFromPathData(578, 'M 0 0 H 100');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-3: getTextPathPointsFromPathData with V command (lines 6235-6240).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataWithVCommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(579);
        $points = $obj->exposeGetTextPathPointsFromPathData(579, 'M 0 0 V 50');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-3: getTextPathPointsFromPathData with cubic bezier C command (lines 6267-6298).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataWithCCommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(580);
        $points = $obj->exposeGetTextPathPointsFromPathData(580, 'M 0 0 C 10 20 30 40 50 0');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-3: getTextPathPointsFromPathData with smooth S command (lines 6300-6334).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataWithSCommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(581);
        $points = $obj->exposeGetTextPathPointsFromPathData(581, 'M 0 0 C 10 20 30 40 50 0 S 70 -20 100 0');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-3: getTextPathPointsFromPathData with quadratic Q command (lines 6311-6322).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataWithQCommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(582);
        $points = $obj->exposeGetTextPathPointsFromPathData(582, 'M 0 0 Q 50 50 100 0');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-3: getTextPathPointsFromPathData with T (smooth quadratic) command (lines 6324-6343).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataWithTCommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(583);
        $points = $obj->exposeGetTextPathPointsFromPathData(583, 'M 0 0 Q 25 50 50 0 T 100 0');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-3: getTextPathPointsFromPathData with arc A command (lines 6348-6388).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataWithACommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(584);
        $points = $obj->exposeGetTextPathPointsFromPathData(584, 'M 0 0 A 50 50 0 0 1 100 0');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-3: getTextPathPointsFromPathData returns null for empty path (line 6112).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataEmptyReturnsNull(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(585);
        $points = $obj->exposeGetTextPathPointsFromPathData(585, '');
        $this->assertNull($points);
    }

    /**
     * S-3: getTextPathPointsFromDef with 'line' defName (lines 6077-6087).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromDefLine(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(586);
        $points = $obj->exposeGetTextPathPointsFromDef(586, 'line', [
            'x1' => '0',
            'y1' => '0',
            'x2' => '100',
            'y2' => '0',
        ]);
        $this->assertIsArray($points);
        $this->assertCount(2, $points);
    }

    /**
     * S-3: getTextPathPointsFromDef with 'polyline' defName (lines 6077-6087).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromDefPolyline(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(587);
        $points = $obj->exposeGetTextPathPointsFromDef(587, 'polyline', ['points' => '0,0 50,50 100,0']);
        $this->assertIsArray($points);
        $this->assertCount(3, $points);
    }

    /**
     * S-3: getTextPathPointsFromDef with 'polygon' closes the path (line 6096).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromDefPolygonClosesPath(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(588);
        $points = $obj->exposeGetTextPathPointsFromDef(588, 'polygon', ['points' => '0,0 50,50 100,0']);
        $this->assertIsArray($points);
        // polygon adds closing point so 4 points total
        $this->assertCount(4, $points);
    }

    /**
     * S-3: getTextPathPointsFromDef with unknown defName returns null (line 6112).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromDefUnknownReturnsNull(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(589);
        $points = $obj->exposeGetTextPathPointsFromDef(589, 'rect', ['width' => '100', 'height' => '50']);
        $this->assertNull($points);
    }

    /**
     * S-3: getTextPathPointsFromPathData with Z close command (covers Z → ptlist append).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataWithZCommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(590);
        $points = $obj->exposeGetTextPathPointsFromPathData(590, 'M 0 0 L 50 0 L 50 50 Z');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-3: getTextPathPointsFromPathData with relative commands (lowercase m l h v).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataRelativeCommands(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(591);
        $points = $obj->exposeGetTextPathPointsFromPathData(591, 'M 10 10 l 40 0 h 20 v 30');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    // ── sampleTextPathArc ────────────────────────────────────────────────────

    /**
     * S-4: sampleTextPathArc returns [] when start == end (same point).
     * @throws \Throwable
     */
    public function testSampleTextPathArcSameStartEndReturnsEmpty(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeSampleTextPathArc(10.0, 20.0, 50.0, 50.0, 0.0, false, true, 10.0, 20.0);
        $this->assertSame([], $result);
    }

    /**
     * S-4: sampleTextPathArc scales radii (lambda > 1) and returns sampled points.
     * @throws \Throwable
     */
    public function testSampleTextPathArcScaledRadiusReturnsPoints(): void
    {
        $obj = $this->getInternalTestObject();
        // radiusX=1.0 is far too small for a 100-unit span → lambda > 1 → scaling branch fires
        $result = $obj->exposeSampleTextPathArc(0.0, 0.0, 1.0, 1.0, 0.0, false, true, 100.0, 0.0);
        $this->assertGreaterThanOrEqual(2, \count($result));
    }

    /**
     * S-4: sampleTextPathArc with a normal semicircle returns multiple points.
     * @throws \Throwable
     */
    public function testSampleTextPathArcNormalArcReturnsPoints(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeSampleTextPathArc(0.0, 0.0, 50.0, 50.0, 0.0, false, true, 100.0, 0.0);
        $this->assertGreaterThanOrEqual(2, \count($result));
        // last point should approach the end coordinates
        $last = \end($result);
        $this->assertNotFalse($last);
        $this->assertEqualsWithDelta(100.0, $last[0], 1.0);
        $this->assertEqualsWithDelta(0.0, $last[1], 1.0);
    }

    /**
     * S-4: sampleTextPathArc with large-arc flag covers alternate path.
     * @throws \Throwable
     */
    public function testSampleTextPathArcLargeArcFlagReturnsPoints(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeSampleTextPathArc(0.0, 0.0, 50.0, 50.0, 0.0, true, false, 100.0, 0.0);
        $this->assertGreaterThanOrEqual(2, \count($result));
    }

    // ── getArcVectorAngle ────────────────────────────────────────────────────

    /**
     * S-4: getArcVectorAngle returns 90° between (1,0) and (0,1).
     * @throws \Throwable
     */
    public function testGetArcVectorAngleReturnsExpectedAngle(): void
    {
        $obj = $this->getInternalTestObject();
        $angle = $obj->exposeGetArcVectorAngle(1.0, 0.0, 0.0, 1.0);
        $this->assertEqualsWithDelta(\M_PI / 2.0, $angle, 0.001);
    }

    /**
     * S-4: getArcVectorAngle returns 0 for parallel same-direction vectors.
     * @throws \Throwable
     */
    public function testGetArcVectorAngleParallelVectorsReturnsZero(): void
    {
        $obj = $this->getInternalTestObject();
        $angle = $obj->exposeGetArcVectorAngle(1.0, 0.0, 1.0, 0.0);
        $this->assertEqualsWithDelta(0.0, $angle, 0.001);
    }

    // ── getTextPathLength ────────────────────────────────────────────────────

    /**
     * S-4: getTextPathLength of a horizontal segment returns correct length.
     * @throws \Throwable
     */
    public function testGetTextPathLengthHorizontalSegment(): void
    {
        $obj = $this->getInternalTestObject();
        $length = $obj->exposeGetTextPathLength([[0.0, 0.0], [100.0, 0.0]]);
        $this->assertEqualsWithDelta(100.0, $length, 0.001);
    }

    /**
     * S-4: getTextPathLength of empty / single-point list returns 0.
     * @throws \Throwable
     */
    public function testGetTextPathLengthEmptyReturnsZero(): void
    {
        $obj = $this->getInternalTestObject();
        $this->assertEqualsWithDelta(0.0, $obj->exposeGetTextPathLength([]), 0.001);
        $this->assertEqualsWithDelta(0.0, $obj->exposeGetTextPathLength([[5.0, 5.0]]), 0.001);
    }

    // ── getTextPathPointAtOffset ─────────────────────────────────────────────

    /**
     * S-4: getTextPathPointAtOffset returns null for fewer than 2 points.
     * @throws \Throwable
     */
    public function testGetTextPathPointAtOffsetTooFewPointsReturnsNull(): void
    {
        $obj = $this->getInternalTestObject();
        $this->assertNull($obj->exposeGetTextPathPointAtOffset([], 0.0));
        $this->assertNull($obj->exposeGetTextPathPointAtOffset([[0.0, 0.0]], 0.0));
    }

    /**
     * S-4: getTextPathPointAtOffset returns null for zero-length path.
     * @throws \Throwable
     */
    public function testGetTextPathPointAtOffsetZeroLengthReturnsNull(): void
    {
        $obj = $this->getInternalTestObject();
        // duplicate point → zero-length path
        $result = $obj->exposeGetTextPathPointAtOffset([[5.0, 5.0], [5.0, 5.0]], 0.0);
        $this->assertNull($result);
    }

    /**
     * S-4: getTextPathPointAtOffset skips zero-length (duplicate) segment via continue.
     * @throws \Throwable
     */
    public function testGetTextPathPointAtOffsetZeroLengthSegmentIsContinued(): void
    {
        $obj = $this->getInternalTestObject();
        // First segment has zero length (duplicate point); offset is in the second segment.
        $points = [[0.0, 0.0], [0.0, 0.0], [100.0, 0.0]];
        $result = $obj->exposeGetTextPathPointAtOffset($points, 50.0);
        $this->assertIsArray($result);
        $this->assertEqualsWithDelta(50.0, $result[0], 0.001);
        $this->assertEqualsWithDelta(0.0, $result[1], 0.001);
    }

    /**
     * S-4: getTextPathPointAtOffset skips past a segment (covers remaining -= segLength branch).
     * @throws \Throwable
     */
    public function testGetTextPathPointAtOffsetMultiSegmentSkipsFirst(): void
    {
        $obj = $this->getInternalTestObject();
        // Three-point path; offset=75 is past the first segment (length 50).
        $points = [[0.0, 0.0], [50.0, 0.0], [100.0, 0.0]];
        $result = $obj->exposeGetTextPathPointAtOffset($points, 75.0);
        $this->assertIsArray($result);
        $this->assertEqualsWithDelta(75.0, $result[0], 0.001);
        $this->assertEqualsWithDelta(0.0, $result[1], 0.001);
    }

    /**
     * S-4: getTextPathPointAtOffset at offset 0 returns near the start point.
     * @throws \Throwable
     */
    public function testGetTextPathPointAtOffsetZeroReturnsStartPoint(): void
    {
        $obj = $this->getInternalTestObject();
        $points = [[0.0, 0.0], [100.0, 0.0]];
        $result = $obj->exposeGetTextPathPointAtOffset($points, 0.0);
        $this->assertIsArray($result);
        $this->assertEqualsWithDelta(0.0, $result[0], 0.001);
        $this->assertEqualsWithDelta(0.0, $result[1], 0.001);
        $this->assertEqualsWithDelta(0.0, $result[2], 0.001); // angle
    }

    /**
     * S-4: getTextPathPointAtOffset past the end returns the last point.
     * @throws \Throwable
     */
    public function testGetTextPathPointAtOffsetPastEndReturnsLastPoint(): void
    {
        $obj = $this->getInternalTestObject();
        $points = [[0.0, 0.0], [100.0, 0.0]];
        $result = $obj->exposeGetTextPathPointAtOffset($points, 999.0);
        $this->assertIsArray($result);
        $this->assertEqualsWithDelta(100.0, $result[0], 0.001);
        $this->assertEqualsWithDelta(0.0, $result[1], 0.001);
    }

    /**
     * S-4: getTextPathPointAtOffset at midpoint returns interpolated point.
     * @throws \Throwable
     */
    public function testGetTextPathPointAtOffsetMidpointInterpolates(): void
    {
        $obj = $this->getInternalTestObject();
        $points = [[0.0, 0.0], [100.0, 0.0]];
        $result = $obj->exposeGetTextPathPointAtOffset($points, 50.0);
        $this->assertIsArray($result);
        $this->assertEqualsWithDelta(50.0, $result[0], 0.001);
        $this->assertEqualsWithDelta(0.0, $result[1], 0.001);
    }

    // ── getSVGResolvedMarker ─────────────────────────────────────────────────

    /**
     * S-4: getSVGResolvedMarker returns specific when it is set and not 'none'.
     * @throws \Throwable
     */
    public function testGetSVGResolvedMarkerSpecificReturned(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeGetSVGResolvedMarker('url(#arrow)', 'url(#other)');
        $this->assertSame('url(#arrow)', $result);
    }

    /**
     * S-4: getSVGResolvedMarker returns 'none' when specific='none' and fallback=false.
     * @throws \Throwable
     */
    public function testGetSVGResolvedMarkerSpecificNoneNoFallbackReturnsNone(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeGetSVGResolvedMarker('none', 'url(#arrow)', false);
        $this->assertSame('none', $result);
    }

    /**
     * S-4: getSVGResolvedMarker falls back to markerAll when specific='none' and markerAll valid.
     * @throws \Throwable
     */
    public function testGetSVGResolvedMarkerFallsBackToMarkerAll(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeGetSVGResolvedMarker('none', 'url(#arrow)', true);
        $this->assertSame('url(#arrow)', $result);
    }

    /**
     * S-4: getSVGResolvedMarker returns specific when specific='none', fallback=true but markerAll empty.
     * @throws \Throwable
     */
    public function testGetSVGResolvedMarkerNoneWithEmptyMarkerAllReturnsSpecific(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeGetSVGResolvedMarker('none', '', true);
        $this->assertSame('none', $result);
    }

    /**
     * S-4: getSVGResolvedMarker returns empty when both specific and markerAll are empty.
     * @throws \Throwable
     */
    public function testGetSVGResolvedMarkerBothEmptyReturnsEmpty(): void
    {
        $obj = $this->getInternalTestObject();
        $result = $obj->exposeGetSVGResolvedMarker('', '');
        $this->assertSame('', $result);
    }

    // ── parseSVGTagENDa ──────────────────────────────────────────────────────

    /**
     * S-4: parseSVGTagENDa returns '' and resets linkhref when href is set (horizontal).
     * @throws \Throwable
     */
    public function testParseSVGTagENDaWithLinkHrefReturnsEmpty(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(592);
        $obj->patchSvgObj(592, [
            'x' => 20.0,
            'y' => 10.0,
            'textmode' => [
                'linkhref' => 'https://example.com',
                'linkx' => 5.0,
                'linky' => 5.0,
                'vertical' => false,
                'invisible' => false,
                'stroke' => 0,
                'rtl' => false,
                'text-anchor' => 'start',
            ],
        ]);
        $out = $obj->exposeParseSVGTagENDa(592);
        // linkhref is cleared, returns ''
        $this->assertSame('', $out);
        $svgobj = $obj->getSvgObj(592);
        $this->assertSame('', $svgobj['textmode']['linkhref'] ?? '');
    }

    /**
     * S-4: parseSVGTagENDa with vertical text mode covers the vertical width/height branch.
     * @throws \Throwable
     */
    public function testParseSVGTagENDaVerticalModeCoversVerticalBranch(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(593);
        $obj->patchSvgObj(593, [
            'x' => 20.0,
            'y' => 50.0,
            'textmode' => [
                'linkhref' => 'https://example.com/vertical',
                'linkx' => 5.0,
                'linky' => 5.0,
                'vertical' => true,
                'invisible' => false,
                'stroke' => 0,
                'rtl' => false,
                'text-anchor' => 'start',
            ],
        ]);
        $out = $obj->exposeParseSVGTagENDa(593);
        $this->assertSame('', $out);
        $svgobj = $obj->getSvgObj(593);
        $this->assertSame('', $svgobj['textmode']['linkhref'] ?? '');
    }

    // ── getSetSVG ────────────────────────────────────────────────────────────

    /**
     * S-4: getSetSVG throws PdfException for an unknown soid.
     * @throws \Throwable
     */
    public function testGetSetSVGInvalidSoidThrows(): void
    {
        $obj = $this->getTestObject();
        $this->expectException(\Com\Tecnick\Pdf\Exception::class);
        $obj->getSetSVG(99999);
    }

    // ── getTextPathPointsFromPathData – branch coverage ──────────────────────

    /**
     * S-5: path with no leading letter hits the empty-command handler (lines 6148-6149).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataNoLeadingLetterReturnsNull(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(599);
        $result = $obj->exposeGetTextPathPointsFromPathData(599, '100 200');
        $this->assertNull($result);
    }

    /**
     * S-5: 'M 0' has too few coords → break in m/l/t block (line 6157).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataTooFewMLTCoordsBreaks(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(600);
        $result = $obj->exposeGetTextPathPointsFromPathData(600, 'M 0');
        $this->assertNull($result);
    }

    /**
     * S-5: lowercase 'c' covers the relative C adjustment block (lines 6235-6240).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataRelativeCCommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(601);
        $points = $obj->exposeGetTextPathPointsFromPathData(601, 'M 0 0 c 10 20 30 40 50 0');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-5: incomplete 'C' with too few params hits break (line 6226).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataIncompleteCBreaks(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(602);
        $result = $obj->exposeGetTextPathPointsFromPathData(602, 'M 0 0 C 10 20 30');
        $this->assertNull($result);
    }

    /**
     * S-5: lowercase 's' covers the relative S adjustment block (lines 6281-6284).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataRelativeSCommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(603);
        $points = $obj->exposeGetTextPathPointsFromPathData(603, 'M 0 0 s 10 20 50 0');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-5: incomplete 'S' hits break (line 6268).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataIncompleteSBreaks(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(604);
        $result = $obj->exposeGetTextPathPointsFromPathData(604, 'M 0 0 S 10 20');
        $this->assertNull($result);
    }

    /**
     * S-5: lowercase 'q' covers the relative Q adjustment block (lines 6319-6322).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataRelativeQCommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(605);
        $points = $obj->exposeGetTextPathPointsFromPathData(605, 'M 0 0 q 25 50 50 0');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-5: incomplete 'Q' hits break (line 6312).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataIncompleteQBreaks(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(606);
        $result = $obj->exposeGetTextPathPointsFromPathData(606, 'M 0 0 Q 10 20');
        $this->assertNull($result);
    }

    /**
     * S-5: lowercase 'a' covers the relative A end-point adjustment (lines 6358-6359).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataRelativeACommand(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(607);
        $points = $obj->exposeGetTextPathPointsFromPathData(607, 'M 0 0 a 50 50 0 0 1 100 0');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    /**
     * S-5: incomplete 'A' hits break (line 6348).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataIncompleteABreaks(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(608);
        $result = $obj->exposeGetTextPathPointsFromPathData(608, 'M 0 0 A 50 50 0 0');
        $this->assertNull($result);
    }

    /**
     * S-5: arc with same start/end → sampleTextPathArc returns [] → fallback ptlist
     * entry is appended (line 6378).
     * @throws \Throwable
     */
    public function testGetTextPathPointsFromPathDataSameStartEndArcAddsPoint(): void
    {
        $obj = $this->getInternalTestObject();
        $obj->initSvgObjForHandlers(609);
        // Start at (50,0), arc back to same point → sampleTextPathArc returns [].
        $points = $obj->exposeGetTextPathPointsFromPathData(609, 'M 50 0 A 50 50 0 0 1 50 0');
        $this->assertIsArray($points);
        $this->assertGreaterThanOrEqual(2, \count($points));
    }

    // ── parseSVGStyleMask branch coverage ────────────────────────────────────

    /**
     * S-4: parseSVGStyleMask returns '' when transparency is not allowed (PDF/X-1a mode).
     * @throws \Throwable
     */
    public function testSvgMaskStyleNoTransparencyReturnsEmpty(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(594);
        // Disable transparency by setting PDF/X mode to pdfx1a.
        $this->setObjectProperty($obj, 'pdfx', true);
        $this->setObjectProperty($obj, 'pdfxMode', 'pdfx1a');

        $obj->patchSvgObj(594, [
            'defs' => [
                'myMask' => ['name' => 'mask', 'attr' => ['id' => 'myMask'], 'child' => []],
            ],
        ]);
        $style = $obj->exposeDefaultSVGStyle();
        $style['mask'] = 'url(#myMask)';

        $out = $obj->exposeParseSVGStyleMask(594, $style);
        $this->assertSame('', $out);
    }

    /**
     * S-4: parseSVGStyleMask with non-url mask value returns '' (regex mismatch branch).
     * @throws \Throwable
     */
    public function testSvgMaskStyleNonUrlValueReturnsEmpty(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(595);

        $style = $obj->exposeDefaultSVGStyle();
        // Not 'none', not '', but also not a url(...) value.
        $style['mask'] = 'invalidValue';

        $out = $obj->exposeParseSVGStyleMask(595, $style);
        $this->assertSame('', $out);
    }

    /**
     * S-4: parseSVGStyleMask where def exists but has wrong type returns ''.
     * @throws \Throwable
     */
    public function testSvgMaskStyleWrongDefTypeReturnsEmpty(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(596);

        $obj->patchSvgObj(596, [
            'defs' => [
                'badDef' => ['name' => 'linearGradient', 'attr' => ['id' => 'badDef']],
            ],
        ]);

        $style = $obj->exposeDefaultSVGStyle();
        $style['mask'] = 'url(#badDef)';

        $out = $obj->exposeParseSVGStyleMask(596, $style);
        $this->assertSame('', $out);
    }

    /**
     * S-4: parseSVGStyleMask skips non-array child elements (continue branch).
     * @throws \Throwable
     */
    public function testSvgMaskStyleNonArrayChildIsContinued(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(597);

        $obj->patchSvgObj(597, [
            'defs' => [
                'm1' => [
                    'name' => 'mask',
                    'attr' => ['id' => 'm1'],
                    // One scalar (non-array) child → triggers the continue branch.
                    'child' => ['not_an_array'],
                ],
            ],
        ]);

        $style = $obj->exposeDefaultSVGStyle();
        $style['mask'] = 'url(#m1)';

        $out = $obj->exposeParseSVGStyleMask(597, $style);
        $this->assertStringContainsString(' gs', $out);
    }

    /**
     * S-4: parseSVGStyleMask closing_tag child with content appends to text.
     * @throws \Throwable
     */
    public function testSvgMaskStyleClosingTagWithContentAppendsText(): void
    {
        $obj = $this->getInternalTestObject();
        $this->initFontAndPage($obj);
        $obj->initSvgObjForHandlers(598);

        $obj->patchSvgObj(598, [
            'defs' => [
                'm2' => [
                    'name' => 'mask',
                    'attr' => ['id' => 'm2'],
                    'child' => [
                        'open' => [
                            'name' => 'rect',
                            'attr' => ['x' => '0', 'y' => '0', 'width' => '10', 'height' => '10'],
                        ],
                        'close' => [
                            'name' => 'rect',
                            'attr' => ['closing_tag' => true, 'content' => 'hello'],
                        ],
                    ],
                ],
            ],
        ]);

        $style = $obj->exposeDefaultSVGStyle();
        $style['mask'] = 'url(#m2)';

        $out = $obj->exposeParseSVGStyleMask(598, $style);
        $this->assertStringContainsString(' gs', $out);
    }

    /**
     * A nested <svg> with a degenerate (zero-extent) viewBox must not crash
     * with a DivisionByZeroError; the viewBox is ignored as if absent.
     *
     * @throws \Throwable
     */
    public function testNestedSvgZeroAreaViewBoxDoesNotThrow(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40">'
            . '<svg x="0" y="0" width="20" height="20" viewBox="0 0 0 100">'
            . '<rect x="0" y="0" width="10" height="10" fill="#000000"/>'
            . '</svg>'
            . '</svg>';

        $soid = $obj->addSVG($svg, 5, 6, 12, 12, $page['height']);

        $this->assertNotSame('', $obj->getSetSVG($soid));
    }

    /**
     * The nested <svg> viewBox parser must handle decimal values (not split
     * "10.0" into two tokens) and preserve negative origins (not strip the
     * sign). Differential check: a decimal viewBox renders identically to its
     * integer equivalent, and a negative origin renders differently from the
     * positive one.
     *
     * @throws \Throwable
     */
    public function testNestedSvgViewBoxParsesDecimalsAndNegatives(): void
    {
        // Decimal extent must be parsed like the integer equivalent (was
        // previously dropped because "10.0" split into two regex tokens).
        $this->assertSame($this->renderNestedSvgViewBox('0 0 10 10'), $this->renderNestedSvgViewBox('0 0 10.0 10.0'));

        // Negative origin must keep its sign (a digits-only regex stripped it,
        // making these two render identically); they must now differ.
        $this->assertNotSame($this->renderNestedSvgViewBox('5 5 10 10'), $this->renderNestedSvgViewBox('-5 -5 10 10'));
    }

    /**
     * Render a nested <svg> carrying the given viewBox and return its output.
     *
     * @throws \Throwable
     */
    private function renderNestedSvgViewBox(string $viewBox): string
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40">'
            . '<svg x="0" y="0" width="20" height="20" viewBox="'
            . $viewBox
            . '">'
            . '<rect x="0" y="0" width="10" height="10" fill="#000000"/>'
            . '</svg>'
            . '</svg>';
        $soid = $obj->addSVG($svg, 5, 6, 12, 12, $page['height']);
        return $obj->getSetSVG($soid);
    }

    /**
     * A path number may omit the digit before the decimal point ('.5', '-.5')
     * and may use scientific notation. The compact forms must produce the same
     * geometry as the fully spelled out equivalent.
     *
     * @throws \Throwable
     */
    public function testSvgPathParsesCompactNumberForms(): void
    {
        // Leading-dot numbers, with and without a sign.
        $this->assertSame(
            $this->renderSvgPath('M0.5 0.5C0.5 1.5 -0.5 1.5 -0.5 0.5Z'),
            $this->renderSvgPath('M.5 .5C.5 1.5 -.5 1.5 -.5 .5Z'),
        );

        // Numbers packed without any separator between them.
        $this->assertSame($this->renderSvgPath('M1.5 0.5L2.5 0.5'), $this->renderSvgPath('M1.5.5L2.5.5'));

        // Scientific notation.
        $this->assertSame($this->renderSvgPath('M0 0L10 20'), $this->renderSvgPath('M0 0L1e1 2E1'));

        // A sign must not be dropped: mirrored coordinates must differ.
        $this->assertNotSame($this->renderSvgPath('M0 0L.5 .5'), $this->renderSvgPath('M0 0L-.5 -.5'));
    }

    /**
     * The two flags of an 'A' (elliptical arc) command are single digits that
     * may be packed with the values around them, as path optimizers do.
     *
     * @throws \Throwable
     */
    public function testSvgPathParsesPackedArcFlags(): void
    {
        $spaced = $this->renderSvgPath('M10 20a10 10 0 0 1 20 0');
        $this->assertStringContainsString(' c', $spaced);

        // Both flags packed with the following coordinate.
        $this->assertSame($spaced, $this->renderSvgPath('M10 20a10 10 0 0120 0'));

        // Packed flags followed by a negative coordinate, and two arcs in a row.
        $this->assertSame(
            $this->renderSvgPath('M10 20a10 10 0 0 1 20 0a10 10 0 0 1 -20 0'),
            $this->renderSvgPath('M10 20a10 10 0 0120 0a10 10 0 01-20 0'),
        );

        // The flags must still be read as two separate values.
        $this->assertNotSame($spaced, $this->renderSvgPath('M10 20a10 10 0 1120 0'));
    }

    /**
     * The smooth curve commands 'S' and 'T' mirror the control point of the
     * preceding curve about the current point, and fall back to the current
     * point when the preceding command is not a curve of the same degree.
     *
     * @throws \Throwable
     */
    public function testSvgPathSmoothCurvesMirrorThePreviousControlPoint(): void
    {
        // 'S' after 'C': the mirror of (40,60) about (50,100) is (60,140).
        $this->assertSame(
            $this->renderSvgPath('M10 100C20 60 40 60 50 100C60 140 80 140 90 100'),
            $this->renderSvgPath('M10 100C20 60 40 60 50 100S80 140 90 100'),
        );

        // 'T' after 'Q': the mirror of (30,120) about (50,160) is (70,200).
        $this->assertSame(
            $this->renderSvgPath('M10 160Q30 120 50 160Q70 200 90 160'),
            $this->renderSvgPath('M10 160Q30 120 50 160T90 160'),
        );

        // Chained smooth commands mirror the segment they follow.
        $this->assertSame(
            $this->renderSvgPath('M10 100C20 60 40 60 50 100C60 140 80 140 90 100C100 60 120 60 130 100'),
            $this->renderSvgPath('M10 100C20 60 40 60 50 100S80 140 90 100S120 60 130 100'),
        );

        // Without a preceding curve the control point is the current point.
        $this->assertSame($this->renderSvgPath('M10 40C10 40 30 0 50 40'), $this->renderSvgPath('M10 40S30 0 50 40'));
        $this->assertSame($this->renderSvgPath('M110 40Q110 40 150 40'), $this->renderSvgPath('M110 40T150 40'));

        // A lineto between two curves resets the mirroring.
        $this->assertSame(
            $this->renderSvgPath('M10 100C20 60 40 60 50 100L60 100C60 100 80 140 90 100'),
            $this->renderSvgPath('M10 100C20 60 40 60 50 100L60 100S80 140 90 100'),
        );
    }

    /**
     * Render a single <path> with the given 'd' attribute and return its output.
     *
     * @throws \Throwable
     */
    private function renderSvgPath(string $pathd): string
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -10 40 40">'
            . '<path fill="#0066cc" d="'
            . $pathd
            . '"/>'
            . '</svg>';
        $soid = $obj->addSVG($svg, 5, 6, 12, 12, $page['height']);
        return $obj->getSetSVG($soid);
    }

    /**
     * An SVG made only of geometry must not require a font to be inserted first.
     *
     * @throws \Throwable
     */
    public function testAddSVGWithoutTextDoesNotRequireAFont(): void
    {
        $obj = $this->getTestObject();
        $page = $obj->addPage();
        if (!isset($page['height']) || !\is_float($page['height'])) {
            $this->fail('Unexpected addPage() return shape.');
        }
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40">'
            . '<path fill="#0066cc" d="M 4 4 H 36 V 36 H 4 Z"/>'
            . '</svg>';

        $soid = $obj->addSVG($svg, 5, 6, 12, 12, $page['height']);

        $this->assertNotSame('', $obj->getSetSVG($soid));
    }

    /**
     * The text-anchor offset is a text width, so it must be converted from
     * points to user units before being applied to the position.
     *
     * @throws \Throwable
     */
    public function testSvgTextAnchorShiftMatchesTextWidth(): void
    {
        // Advance of the '0' glyph of the 10pt Helvetica test font, in points.
        $zeroWidth = 5.56;

        $start = $this->renderSvgTextAnchorPosX('start', '0');
        $end = $this->renderSvgTextAnchorPosX('end', '0');
        $middle = $this->renderSvgTextAnchorPosX('middle', '0');
        $endTwo = $this->renderSvgTextAnchorPosX('end', '00');

        $this->assertEqualsWithDelta($zeroWidth, $start - $end, 0.01);
        $this->assertEqualsWithDelta($zeroWidth / 2, $start - $middle, 0.01);
        $this->assertEqualsWithDelta(2 * $zeroWidth, $start - $endTwo, 0.01);
    }

    /**
     * Render a <text> with the given anchor and return the X coordinate of the
     * emitted Td operator.
     *
     * @throws \Throwable
     */
    private function renderSvgTextAnchorPosX(string $anchor, string $txt): float
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">'
            . '<text x="50" y="50" text-anchor="'
            . $anchor
            . '">'
            . $txt
            . '</text>'
            . '</svg>';
        $soid = $obj->addSVG($svg, 0, 0, 100, 100, $page['height']);
        $match = [];
        if (\preg_match('/([\-0-9\.]+) [\-0-9\.]+ Td/', $obj->getSetSVG($soid), $match) !== 1 || !isset($match[1])) {
            $this->fail('No Td operator found in the SVG text output.');
        }

        return \floatval($match[1]);
    }

    /**
     * Character data outside a text-content element is not rendered: the
     * white space between the tags of a pretty-printed SVG must not produce
     * any text operator.
     *
     * @throws \Throwable
     */
    public function testSvgIgnoresCharacterDataOutsideTextElements(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            "@<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\">\n"
            . "    <rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"#000000\"/>\n"
            . "    <text x=\"5\" y=\"30\">one</text>\n"
            . "    <text x=\"5\" y=\"60\">two</text>\n"
            . '</svg>';

        $soid = $obj->addSVG($svg, 0, 0, 100, 100, $page['height']);
        $out = $obj->getSetSVG($soid);

        // Only the two text elements are rendered, not the indentation around them.
        $this->assertSame(2, \substr_count($out, ' Tj'));
        $this->assertStringContainsString('(one) Tj', $out);
        $this->assertStringContainsString('(two) Tj', $out);
    }

    /**
     * The XML white space of the text content is collapsed unless
     * xml:space='preserve' is set.
     *
     * @throws \Throwable
     */
    public function testSvgTextContentWhiteSpaceHandling(): void
    {
        $this->assertStringContainsString('(Hello world) Tj', $this->renderSvgTextContent(
            "\n    Hello\n    world\n  ",
            '',
        ));

        $this->assertStringContainsString('( Hello  world ) Tj', $this->renderSvgTextContent(
            ' Hello  world ',
            ' xml:space="preserve"',
        ));
    }

    /**
     * Render a <text> with the given raw content and extra attributes.
     *
     * @throws \Throwable
     */
    private function renderSvgTextContent(string $txt, string $attr): string
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">'
            . '<text x="5" y="50"'
            . $attr
            . '>'
            . $txt
            . '</text>'
            . '</svg>';
        $soid = $obj->addSVG($svg, 0, 0, 100, 100, $page['height']);
        return $obj->getSetSVG($soid);
    }

    /**
     * Consecutive text chunks of the same text element must not be stacked on
     * the same position: each one starts where the previous one ends.
     *
     * @throws \Throwable
     */
    public function testSvgTspanChunksAdvanceTheTextCursor(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">'
            . '<text x="5" y="50">Hello <tspan fill="#ff0000">world</tspan></text>'
            . '</svg>';

        $soid = $obj->addSVG($svg, 0, 0, 200, 100, $page['height']);

        $match = [];
        $count = \preg_match_all('/([\-0-9\.]+) [\-0-9\.]+ Td/', $obj->getSetSVG($soid), $match);
        $this->assertSame(2, $count);
        $posx = $match[1] ?? [];
        $this->assertGreaterThan(\floatval($posx[0] ?? '0'), \floatval($posx[1] ?? '0'));
    }

    /**
     * The inheritable presentation attributes of the enclosing element apply to
     * its children: the style stack must be read at its top, not one level up.
     *
     * @throws \Throwable
     */
    public function testSvgChildrenInheritTheEnclosingElementStyle(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">'
            . '<g fill="#ff0000">'
            . '<rect x="0" y="0" width="10" height="10"/>'
            . '</g>'
            . '<text x="5" y="50" font-size="4.5">Hi <tspan fill="#0000ff">there</tspan></text>'
            . '</svg>';

        $soid = $obj->addSVG($svg, 0, 0, 100, 100, $page['height']);
        $out = $obj->getSetSVG($soid);

        // The rect takes the fill of the group.
        $this->assertSame(2, \substr_count($out, "1.000000 0.000000 0.000000 rg\n"));

        // The tspan keeps the font size of the text element.
        $this->assertSame(2, \substr_count($out, '/F1 4.500000 Tf'));
    }

    /**
     * Markers are placed on the path vertices: their content must sit inside
     * the marker transform, and that transform is expressed in SVG user units.
     *
     * @throws \Throwable
     */
    public function testSvgMarkersArePlacedOnThePathVertices(): void
    {
        $obj = $this->getTestObject();
        $page = $this->initFontAndPage($obj);
        $svg =
            '@<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">'
            . '<defs>'
            . '<marker id="sq" viewBox="0 0 4 4" refX="2" refY="2"'
            . ' markerWidth="4" markerHeight="4" orient="auto">'
            . '<rect x="0" y="0" width="4" height="4" fill="#ff0000"/>'
            . '</marker>'
            . '</defs>'
            . '<path fill="none" stroke="#000000" stroke-width="2"'
            . ' marker-start="url(#sq)" marker-end="url(#sq)" d="M10 50L90 50"/>'
            . '</svg>';

        $soid = $obj->addSVG($svg, 0, 0, 100, 100, $page['height']);
        $out = $obj->getSetSVG($soid);

        $match = [];
        $count = \preg_match_all(
            '/([\-0-9\.]+) [\-0-9\.]+ [\-0-9\.]+ [\-0-9\.]+ ([\-0-9\.]+) ([\-0-9\.]+) cm/',
            $out,
            $match,
        );
        $this->assertGreaterThanOrEqual(2, $count);

        // markerUnits defaults to strokeWidth, so the 4 unit wide marker box is
        // scaled by the stroke width of 2 and keeps the 1:1 viewBox ratio.
        $scales = \array_map('floatval', $match[1] ?? []);
        $this->assertContains(2.0, $scales);

        // Both markers are centred on the path ends, 80 units apart.
        $marks = [];
        foreach ($scales as $idx => $scale) {
            if ($scale !== 2.0) {
                continue;
            }

            $marks[] = \floatval($match[2][$idx] ?? '0');
        }
        $this->assertCount(2, $marks);
        $this->assertEqualsWithDelta(60.0, \abs(($marks[1] ?? 0.0) - ($marks[0] ?? 0.0)), 0.01);

        // The rect of the last marker is emitted after its transform.
        $lastMarker = \strrpos($out, '2.000000 0.000000 0.000000 2.000000 ');
        $this->assertIsInt($lastMarker);
        $this->assertGreaterThan($lastMarker, (int) \strrpos($out, ' re'));
    }
}
