3434 * @phpstan-import-type TTMatrix from \Com\Tecnick\Pdf\Graph\Base
3535 * @phpstan-import-type TRefUnitValues from \Com\Tecnick\Pdf\Base
3636 *
37+ * @phpstan-type TSVGSize array{
38+ * 'x': float,
39+ * 'y': float,
40+ * 'width': float,
41+ * 'height': float,
42+ * 'viewBox': array{float, float, float, float},
43+ * 'ar_align': string,
44+ * 'ar_ms': string,
45+ * }
46+ *
3747 * @phpstan-type TSCGCoord array{
3848 * 'x': float,
3949 * 'y': float,
387397 * 'defs': array<string, TSVGDefs>,
388398 * 'cliptm': TTMatrix,
389399 * 'styles': array<int, TSVGStyle>,
400+ * 'child': array<int>,
390401 * 'textmode': TSVGTextMode,
391402 * 'text': string,
392403 * 'out': string,
@@ -606,6 +617,7 @@ abstract class SVG extends \Com\Tecnick\Pdf\Text
606617 'cliptm ' => [1.0 ,0.0 ,0.0 ,1.0 ,0.0 ,0.0 ],
607618 'defs ' => [],
608619 'styles ' => [0 => self ::DEFSVGSTYLE ],
620+ 'child ' => [],
609621 'textmode ' => [
610622 'rtl ' => false ,
611623 'invisible ' => false ,
@@ -2088,7 +2100,13 @@ protected function parseSVGStyleClipPath(
20882100 array $ clippaths = [],
20892101 ): void {
20902102 foreach ($ clippaths as $ cp ) {
2091- $ this ->handleSVGTagStart ('clip-path ' , $ cp ['name ' ], $ soid , $ cp ['attr ' ], $ cp ['tm ' ]);
2103+ $ this ->handleSVGTagStart (
2104+ 'clip-path ' ,
2105+ $ cp ['name ' ],
2106+ $ cp ['attr ' ],
2107+ $ soid ,
2108+ $ cp ['tm ' ],
2109+ );
20922110 }
20932111 }
20942112
@@ -2365,8 +2383,8 @@ protected function parseSVGTagENDtext(int $soid): void
23652383 *
23662384 * @param string $parser The XML parser calling the handler.
23672385 * @param string $name Name of the element for which this handler is called.
2368- * @param int $soid ID of the current SVG object.
23692386 * @param TSVGAttribs $attribs Associative array with the element's attributes.
2387+ * @param int $soid ID of the current SVG object.
23702388 * @param TTMatrix $ctm Current transformation matrix (optional).
23712389 *
23722390 * @return void
@@ -2376,8 +2394,8 @@ protected function parseSVGTagENDtext(int $soid): void
23762394 protected function handleSVGTagStart (
23772395 string $ parser ,
23782396 string $ name ,
2379- int $ soid ,
23802397 array $ attribs ,
2398+ int $ soid = -1 ,
23812399 array $ ctm = [1.0 ,0.0 ,0.0 ,1.0 ,0.0 ,0.0 ], // identity matrix
23822400 ): void {
23832401 if (empty ($ this ->svgobjs [$ soid ])) {
@@ -2386,6 +2404,10 @@ protected function handleSVGTagStart(
23862404
23872405 $ name = $ this ->removeTagNamespace ($ name );
23882406
2407+ if ($ soid < 0 ) {
2408+ $ soid = (int )array_key_last ($ this ->svgobjs );
2409+ }
2410+
23892411 if ($ this ->svgobjs [$ soid ]['clipmode ' ]) {
23902412 $ this ->svgobjs [$ soid ]['clippaths ' ][] = [
23912413 'name ' => $ name ,
@@ -2544,8 +2566,8 @@ protected function handleSVGTagStart(
25442566 $ this ->handleSVGTagStart (
25452567 'child-tag ' ,
25462568 $ child ['name ' ],
2547- $ soid ,
25482569 $ child ['attr ' ], // @phpstan-ignore argument.type
2570+ $ soid ,
25492571 );
25502572 continue ;
25512573 }
@@ -3364,7 +3386,12 @@ protected function parseSVGTagSTARTimage(int $soid, array $attr, array $svgstyle
33643386 )
33653387 )
33663388 ) {
3367- // @TODO $this->ImageSVG($img, $posx, $posy, $width, $height);
3389+ try {
3390+ $ child = $ this ->addSVG ($ img , $ posx , $ posy , $ width , $ height );
3391+ } catch (Exception $ e ) {
3392+ return ;
3393+ }
3394+ $ this ->svgobjs [$ soid ]['child ' ][] = $ child ;
33683395 return ;
33693396 }
33703397 if (preg_match ('/^data:image\/[^;]+;base64,/ ' , $ img , $ match ) > 0 ) {
@@ -3513,6 +3540,289 @@ protected function parseSVGTagSTARTuse(int $soid, array $attribs, string $parser
35133540 $ attribs ['attr ' ] = array_merge ($ use ['attr ' ]['attr ' ], $ attr );
35143541 /** @var TSVGAttribs $attribs */
35153542 $ attribs = (array ) $ attribs ;
3516- $ this ->handleSVGTagStart ($ parser , $ use ['name ' ], $ soid , $ attribs );
3543+ $ this ->handleSVGTagStart (
3544+ $ parser ,
3545+ $ use ['name ' ],
3546+ $ attribs ,
3547+ $ soid ,
3548+ );
3549+ }
3550+
3551+ /**
3552+ * Get the SVG data from a file or data string.
3553+ *
3554+ * @param string $img
3555+ *
3556+ * @return string
3557+ */
3558+ protected function getRawSVGData (string $ img ): string
3559+ {
3560+ if (empty ($ img ) || (($ img [0 ] === '@ ' ) && (strlen ($ img ) === 1 ))) {
3561+ return '' ;
3562+ }
3563+ if ($ img [0 ] === '@ ' ) { // image from string
3564+ return substr ($ img , 1 );
3565+ }
3566+ $ data = $ this ->file ->getFileData ($ img );
3567+ if (empty ($ data )) {
3568+ return '' ;
3569+ }
3570+ return $ data ;
3571+ }
3572+
3573+ /**
3574+ * Get the SVG size from the SVG data.
3575+ *
3576+ * @param string $data The string containing the SVG image data.
3577+ *
3578+ * @return TSVGSize Associative array with dimensions.
3579+ */
3580+ protected function getSVGSize (string $ data ): array
3581+ {
3582+ $ out = [
3583+ 'x ' => 0.0 ,
3584+ 'y ' => 0.0 ,
3585+ 'width ' => 0.0 ,
3586+ 'height ' => 0.0 ,
3587+ 'viewBox ' => [0.0 ,0.0 ,0.0 ,0.0 ],
3588+ 'ar_align ' => 'xMidYMid ' ,
3589+ 'ar_ms ' => 'meet ' ,
3590+ ];
3591+
3592+ preg_match ('/<svg([^\>]*)>/si ' , $ data , $ regs );
3593+ if (!isset ($ regs [1 ]) || empty ($ regs [1 ])) {
3594+ return $ out ;
3595+ }
3596+
3597+ $ tmp = [];
3598+ if (preg_match ('/[\s]+x[\s]*=[\s]*"([^"]*)"/si ' , $ regs [1 ], $ tmp )) {
3599+ $ out ['x ' ] = $ this ->toUnit ($ this ->getUnitValuePoints ($ tmp [1 ], self ::REFUNITVAL , self ::SVGUNIT ));
3600+ }
3601+ $ tmp = array ();
3602+ if (preg_match ('/[\s]+y[\s]*=[\s]*"([^"]*)"/si ' , $ regs [1 ], $ tmp )) {
3603+ $ out ['y ' ] = $ this ->toUnit ($ this ->getUnitValuePoints ($ tmp [1 ], self ::REFUNITVAL , self ::SVGUNIT ));
3604+ }
3605+ $ tmp = array ();
3606+ if (preg_match ('/[\s]+width[\s]*=[\s]*"([^"]*)"/si ' , $ regs [1 ], $ tmp )) {
3607+ $ out ['width ' ] = $ this ->toUnit ($ this ->getUnitValuePoints ($ tmp [1 ], self ::REFUNITVAL , self ::SVGUNIT ));
3608+ }
3609+ $ tmp = array ();
3610+ if (preg_match ('/[\s]+height[\s]*=[\s]*"([^"]*)"/si ' , $ regs [1 ], $ tmp )) {
3611+ $ out ['height ' ] = $ this ->toUnit ($ this ->getUnitValuePoints ($ tmp [1 ], self ::REFUNITVAL , self ::SVGUNIT ));
3612+ }
3613+
3614+ $ tmp = [];
3615+ if (
3616+ !preg_match (
3617+ '/[\s]+viewBox[\s]*=[\s]*"[\s]*([0-9\.\-]+)[\s]+([0-9\.\-]+)[\s]+([0-9\.]+)[\s]+([0-9\.]+)[\s]*"/si ' ,
3618+ $ regs [1 ],
3619+ $ tmp ,
3620+ )
3621+ ) {
3622+ return $ out ;
3623+ }
3624+
3625+ if (count ($ tmp ) == 5 ) {
3626+ array_shift ($ tmp );
3627+ foreach ($ tmp as $ key => $ val ) {
3628+ $ out ['viewBox ' ][$ key ] = $ this ->toUnit ($ this ->getUnitValuePoints ($ val , self ::REFUNITVAL , self ::SVGUNIT ));
3629+ }
3630+ }
3631+
3632+ // get aspect ratio
3633+ $ tmp = [];
3634+ if (!preg_match ('/[\s]+preserveAspectRatio[\s]*=[\s]*"([^"]*)"/si ' , $ regs [1 ], $ tmp )) {
3635+ return $ out ;
3636+ }
3637+
3638+ $ asr = preg_split ('/[\s]+/si ' , $ tmp [1 ]);
3639+ if (!is_array ($ asr ) || count ($ asr ) < 1 ) {
3640+ return $ out ;
3641+ }
3642+ switch (count ($ asr )) {
3643+ case 3 :
3644+ $ out ['ar_align ' ] = $ asr [1 ];
3645+ $ out ['ar_ms ' ] = $ asr [2 ];
3646+ break ;
3647+ case 2 :
3648+ $ out ['ar_align ' ] = $ asr [0 ];
3649+ $ out ['ar_ms ' ] = $ asr [1 ];
3650+ break ;
3651+ case 1 :
3652+ $ out ['ar_align ' ] = $ asr [0 ];
3653+ $ out ['ar_ms ' ] = 'meet ' ;
3654+ break ;
3655+ }
3656+
3657+ return $ out ;
3658+ }
3659+
3660+ /**
3661+ * Add a new SVG image and return its object ID.
3662+ *
3663+ * @param string $img The string containing the SVG image data or the path to the SVG file.
3664+ * @param float $posx X position in user units.
3665+ * @param float $posy Y position in user units.
3666+ * @param float $width Width in user units.
3667+ * @param float $height Height in user units.
3668+ *
3669+ * @return int The SVG object ID.
3670+ */
3671+ public function addSVG (
3672+ string $ img ,
3673+ float $ posx = 0.0 ,
3674+ float $ posy = 0.0 ,
3675+ float $ width = 0.0 ,
3676+ float $ height = 0.0 ,
3677+ ): int {
3678+ $ data = $ this ->getRawSVGData ($ img );
3679+ if (empty ($ data )) {
3680+ throw new PdfException ('Invalid SVG ' );
3681+ }
3682+
3683+ $ size = $ this ->getSVGSize ($ data );
3684+ if ($ size ['width ' ] <= 0.0 || $ size ['height ' ] <= 0.0 ) {
3685+ throw new PdfException ('Invalid SVG size ' );
3686+ }
3687+
3688+ if ($ size ['width ' ] <= 0.0 ) {
3689+ $ size ['width ' ] = 1.0 ;
3690+ }
3691+ if ($ size ['height ' ] <= 0.0 ) {
3692+ $ size ['height ' ] = 1.0 ;
3693+ }
3694+
3695+ // calculate image width && height on document
3696+ if (($ width <= 0.0 ) && ($ height <= 0.0 )) {
3697+ // convert image size to document unit
3698+ $ width = $ size ['width ' ];
3699+ $ height = $ size ['height ' ];
3700+ } elseif ($ width <= 0.0 ) {
3701+ $ width = $ height * $ size ['width ' ] / $ size ['height ' ];
3702+ } elseif ($ height <= 0.0 ) {
3703+ $ height = $ width * $ size ['height ' ] / $ size ['width ' ];
3704+ }
3705+
3706+ if (!empty ($ size ['viewBox ' ][2 ]) && !empty ($ size ['viewBox ' ][3 ])) {
3707+ $ size ['width ' ] = $ size ['viewBox ' ][2 ];
3708+ $ size ['height ' ] = $ size ['viewBox ' ][3 ];
3709+ } else {
3710+ if ($ size ['width ' ] <= 0 ) {
3711+ $ size ['width ' ] = $ width ;
3712+ }
3713+ if ($ size ['height ' ] <= 0 ) {
3714+ $ size ['height ' ] = $ height ;
3715+ }
3716+ }
3717+
3718+ // SVG position && scale factors
3719+ $ svgoffset_x = $ this ->toUnit ($ posx - $ size ['x ' ]);
3720+ $ svgoffset_y = $ this ->toUnit ($ size ['y ' ] - $ posy );
3721+ $ svgscale_x = $ width / $ size ['width ' ];
3722+ $ svgscale_y = $ height / $ size ['height ' ];
3723+
3724+ // scaling && alignment
3725+ if ($ size ['ar_align ' ] != 'none ' ) {
3726+ // force uniform scaling
3727+ if ($ size ['ar_ms ' ] == 'slice ' ) {
3728+ // the entire viewport is covered by the viewBox
3729+ if ($ svgscale_x > $ svgscale_y ) {
3730+ $ svgscale_y = $ svgscale_x ;
3731+ } elseif ($ svgscale_x < $ svgscale_y ) {
3732+ $ svgscale_x = $ svgscale_y ;
3733+ }
3734+ } else { // meet
3735+ // the entire viewBox is visible within the viewport
3736+ if ($ svgscale_x < $ svgscale_y ) {
3737+ $ svgscale_y = $ svgscale_x ;
3738+ } elseif ($ svgscale_x > $ svgscale_y ) {
3739+ $ svgscale_x = $ svgscale_y ;
3740+ }
3741+ }
3742+ // correct X alignment
3743+ switch (substr ($ size ['ar_align ' ], 1 , 3 )) {
3744+ case 'Min ' :
3745+ // do nothing
3746+ break ;
3747+ case 'Max ' :
3748+ $ svgoffset_x += $ this ->toUnit ($ width - ($ size ['width ' ] * $ svgscale_x ));
3749+ break ;
3750+ default :
3751+ case 'Mid ' :
3752+ $ svgoffset_x += $ this ->toUnit (($ width - ($ size ['width ' ] * $ svgscale_x )) / 2 );
3753+ break ;
3754+ }
3755+ // correct Y alignment
3756+ switch (substr ($ size ['ar_align ' ], 5 )) {
3757+ case 'Min ' :
3758+ // do nothing
3759+ break ;
3760+ case 'Max ' :
3761+ $ svgoffset_y -= $ this ->toUnit ($ height - ($ size ['height ' ] * $ svgscale_y ));
3762+ break ;
3763+ default :
3764+ case 'Mid ' :
3765+ $ svgoffset_y -= $ this ->toUnit (($ height - ($ size ['height ' ] * $ svgscale_y )) / 2 );
3766+ break ;
3767+ }
3768+ }
3769+
3770+ $ soid = (int )array_key_last ($ this ->svgobjs );
3771+ $ soid ++;
3772+
3773+ $ this ->svgobjs [$ soid ] = self ::SVGDEFOBJ ; // @phpstan-ignore-line assign.propertyType
3774+
3775+ $ this ->svgobjs [$ soid ]['out ' ] .= $ this ->graph ->getStartTransform ();
3776+ $ this ->svgobjs [$ soid ]['out ' ] .= $ this ->graph ->getRawRect (
3777+ $ posx ,
3778+ $ posy ,
3779+ $ width ,
3780+ $ height ,
3781+ 'CNZ ' ,
3782+ );
3783+
3784+ // scale && translate
3785+ $ esx = $ this ->toUnit ($ size ['x ' ]) * (1 - $ svgscale_x );
3786+ $ fsy = $ this ->toYUnit ($ size ['y ' ]) * (1 - $ svgscale_y );
3787+ $ ctm = [
3788+ 0 => $ svgscale_x ,
3789+ 1 => 0.0 ,
3790+ 2 => 0.0 ,
3791+ 3 => $ svgscale_y ,
3792+ 4 => $ esx + $ svgoffset_x ,
3793+ 5 => $ fsy + $ svgoffset_y ,
3794+ ];
3795+ $ this ->svgobjs [$ soid ]['out ' ] .= $ this ->graph ->getTransformation ($ ctm );
3796+
3797+ // creates a new XML parser to be used by the other XML functions
3798+ $ parser = xml_parser_create ('UTF-8 ' );
3799+ // the following function allows to use parser inside object
3800+ xml_set_object ($ parser , $ this );
3801+ // disable case-folding for this XML parser
3802+ xml_parser_set_option ($ parser , XML_OPTION_CASE_FOLDING , 0 );
3803+ // sets the element handler functions for the XML parser
3804+ xml_set_element_handler ($ parser , [$ this , 'handleSVGTagStart ' ], [$ this , 'handleSVGTagEnd ' ]);
3805+ // sets the character data handler function for the XML parser
3806+ xml_set_character_data_handler ($ parser , [$ this , 'handlerSVGCharacter ' ]);
3807+
3808+ // start parsing an XML document
3809+ if (!xml_parse ($ parser , $ data )) {
3810+ throw new PdfException (sprintf (
3811+ 'SVG Error: %s at line %d ' ,
3812+ xml_error_string (
3813+ xml_get_error_code ($ parser )
3814+ ),
3815+ xml_get_current_line_number ($ parser ),
3816+ ),);
3817+ }
3818+
3819+ // free this XML parser
3820+ xml_parser_free ($ parser );
3821+ // >= PHP 7.0.0 "explicitly unset the reference to parser to avoid memory leaks"
3822+ unset($ parser );
3823+
3824+ $ this ->svgobjs [$ soid ]['out ' ] .= $ this ->graph ->getStopTransform ();
3825+
3826+ return $ soid ;
35173827 }
35183828}
0 commit comments