Skip to content

Commit d4fc3d6

Browse files
committed
draft svg add
1 parent a782e84 commit d4fc3d6

File tree

1 file changed

+316
-6
lines changed

1 file changed

+316
-6
lines changed

src/SVG.php

Lines changed: 316 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@
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,
@@ -387,6 +397,7 @@
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

Comments
 (0)