Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/Mpdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -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], '<img src="' . $file . '" />', $html);
$class = $this->svgClassAttribute($svgi[0][$i]);
$html = str_replace($svgi[0][$i], '<img src="' . $file . '"' . ($class !== '' ? ' class="' . $class . '"' : '') . ' />', $html);
}
}

Expand Down Expand Up @@ -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('/^<svg\b[^>]*?\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)
{
Expand Down
103 changes: 103 additions & 0 deletions tests/Mpdf/SvgClassTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

namespace Mpdf;

class SvgClassTest extends \Yoast\PHPUnitPolyfills\TestCases\TestCase
{

/**
* @var \Mpdf\Mpdf
*/
private $mpdf;

protected function set_up()
{
parent::set_up();

$this->mpdf = new Mpdf(['mode' => 'c']);
}

protected function tear_down()
{
parent::tear_down();

$this->mpdf->cleanup();
}

private function svg($attributes)
{
return '<svg ' . $attributes . ' width="100" height="100">'
. '<circle cx="50" cy="50" r="40" fill="#ffff00" />'
. '</svg>';
}

/**
* The class the img element AdjustHTML() replaced the SVG with was given, or '' for none
*/
private function convertedClass($svg)
{
$matches = [];
if (!preg_match('/<img src="[^"]*"(?: class="([^"]*)")? \/>/', $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 = '<svg width="100" height="100"><circle class="inner" cx="50" cy="50" r="40" /></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(
'<style>.framed { border: 2px solid #ff0000 }</style><p>' . $this->svg('class="framed"') . '</p>'
);

$this->assertStringContainsString('1.000 0.000 0.000 RG', $this->mpdf->Output('', 'S'));
}

}
59 changes: 59 additions & 0 deletions tests/Snapshots/SvgClassSnapshotTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Snapshots;

/**
* @group snapshot
*/
class SvgClassSnapshotTest extends Snapshot
{
public function getId()
{
return 'svg-class';
}

private function circle()
{
return '<circle cx="50" cy="50" r="40" fill="#f0d878" stroke="#14386b" stroke-width="4" />';
}

public function generatePdf()
{
ob_start();
?>
<style>
.framed { border: 1mm solid #d02020; padding: 2mm; }
.wide { width: 38mm; }
.narrow { width: 20mm; }
.spaced { margin: 0 0 0 40mm; }

p.caption { margin: 5mm 0 2mm 0; color: #606060; }
</style>

<h1>mPDF</h1>
<h2>class on an embedded SVG</h2>

<p class="caption">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.</p>

<p class="caption">No class - drawn at the size the SVG asks for</p>
<p><svg width="100" height="100" viewBox="0 0 100 100"><?= $this->circle() ?></svg></p>

<p class="caption">class="framed" - a border and padding around it</p>
<p><svg class="framed" width="100" height="100" viewBox="0 0 100 100"><?= $this->circle() ?></svg></p>

<p class="caption">class="wide" - a width</p>
<p><svg class="wide" width="100" height="100" viewBox="0 0 100 100"><?= $this->circle() ?></svg></p>

<p class="caption">class='narrow' - a width, in a single-quoted attribute</p>
<p><svg class='narrow' width="100" height="100" viewBox="0 0 100 100"><?= $this->circle() ?></svg></p>

<p class="caption">class="narrow spaced framed" - three of them at once</p>
<p><svg class="narrow spaced framed" width="100" height="100" viewBox="0 0 100 100"><?= $this->circle() ?></svg></p>
<?php
$html = ob_get_clean();

$this->mpdf = new \Mpdf\Mpdf();
$this->mpdf->WriteHTML($html);
}
}
Binary file added tests/data/snapshots/svg-class.pdf
Binary file not shown.
Loading