diff --git a/src/Mpdf.php b/src/Mpdf.php index 4ab0d0f7d..4641a9fec 100644 --- a/src/Mpdf.php +++ b/src/Mpdf.php @@ -27195,7 +27195,8 @@ function AdjustHTML($html, $tabSpaces = 8) if (count($svgi[0])) { for ($i = 0; $i < count($svgi[0]); $i++) { $file = $this->cache->write('/_tempSVG' . uniqid(random_int(1, 100000), true) . '_' . $i . '.svg', $svgi[0][$i]); - $html = str_replace($svgi[0][$i], '', $html); + $class = $this->svgClassAttribute($svgi[0][$i]); + $html = str_replace($svgi[0][$i], '', $html); } } @@ -27341,6 +27342,28 @@ function AdjustHTML($html, $tabSpaces = 8) return $html; } + /** + * The class an embedded SVG was given, so the img element it becomes can still be reached by the + * selectors written for it. The value may be double quoted, single quoted or unquoted, as any + * attribute value may be; a double quote is dropped because no class name can hold one. + * + * @param string $svg + * + * @return string + */ + private function svgClassAttribute($svg) + { + $matches = []; + if (!preg_match('/^]*?\sclass\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>"\']+))/si', $svg, $matches)) { + return ''; + } + + // PCRE drops the groups after the one that took part + $class = array_pop($matches); + + return str_replace('"', '', $class); + } + // mPDF 5.7+ function tabs2spaces_callback($matches) { diff --git a/tests/Mpdf/SvgClassTest.php b/tests/Mpdf/SvgClassTest.php new file mode 100644 index 000000000..8aad1232a --- /dev/null +++ b/tests/Mpdf/SvgClassTest.php @@ -0,0 +1,103 @@ +mpdf = new Mpdf(['mode' => 'c']); + } + + protected function tear_down() + { + parent::tear_down(); + + $this->mpdf->cleanup(); + } + + private function svg($attributes) + { + return '' + . '' + . ''; + } + + /** + * The class the img element AdjustHTML() replaced the SVG with was given, or '' for none + */ + private function convertedClass($svg) + { + $matches = []; + if (!preg_match('//', $this->mpdf->AdjustHTML($svg), $matches)) { + return null; + } + + return isset($matches[1]) ? $matches[1] : ''; + } + + /** + * An embedded SVG is written out to a file and replaced by an img element pointing at it, which + * used to drop everything the svg tag was styled by. See mpdf/mpdf#1404. + */ + public function quotingProvider() + { + return [ + 'double quoted' => ['class="framed wide"', 'framed wide'], + 'single quoted' => ["class='framed wide'", 'framed wide'], + 'unquoted' => ['class=framed', 'framed'], + 'spaced out' => ['class = "framed"', 'framed'], + ]; + } + + /** + * @dataProvider quotingProvider + */ + public function testTheClassIsCarriedOverToTheImg($attribute, $expected) + { + $this->assertSame($expected, $this->convertedClass($this->svg($attribute))); + } + + public function testAnSvgWithNoClassBecomesAnImgWithNone() + { + $this->assertSame('', $this->convertedClass($this->svg('id="plain"'))); + } + + /** + * Only the svg tag's own class is wanted - the shapes inside it are drawn by the SVG reader and + * have nothing to do with the img element + */ + public function testAClassOnSomethingInsideTheSvgIsNotTaken() + { + $svg = ''; + + $this->assertSame('', $this->convertedClass($svg)); + } + + public function testTheImgIsStillWrittenWhenTheClassIsEmpty() + { + $this->assertSame('', $this->convertedClass($this->svg('class=""'))); + } + + /** + * The point of carrying it over: a selector written for the svg reaches the img + */ + public function testAStyleRuleWrittenForTheClassReachesTheImage() + { + $this->mpdf->compress = false; + $this->mpdf->WriteHTML( + '

' . $this->svg('class="framed"') . '

' + ); + + $this->assertStringContainsString('1.000 0.000 0.000 RG', $this->mpdf->Output('', 'S')); + } + +} diff --git a/tests/Snapshots/SvgClassSnapshotTest.php b/tests/Snapshots/SvgClassSnapshotTest.php new file mode 100644 index 000000000..4b8ba27a3 --- /dev/null +++ b/tests/Snapshots/SvgClassSnapshotTest.php @@ -0,0 +1,59 @@ +'; + } + + public function generatePdf() + { + ob_start(); + ?> + + +

mPDF

+

class on an embedded SVG

+ +

An embedded SVG is written out to a file and drawn as an image. The class it + was given comes with it, so these rules reach the picture.

+ +

No class - drawn at the size the SVG asks for

+

circle() ?>

+ +

class="framed" - a border and padding around it

+

circle() ?>

+ +

class="wide" - a width

+

circle() ?>

+ +

class='narrow' - a width, in a single-quoted attribute

+

circle() ?>

+ +

class="narrow spaced framed" - three of them at once

+

circle() ?>

+ mpdf = new \Mpdf\Mpdf(); + $this->mpdf->WriteHTML($html); + } +} diff --git a/tests/data/snapshots/svg-class.pdf b/tests/data/snapshots/svg-class.pdf new file mode 100644 index 000000000..a50041151 Binary files /dev/null and b/tests/data/snapshots/svg-class.pdf differ