Report an SVG the XML parser cannot read instead of dividing by zero - #44
Merged
Conversation
An attribute value with no quotes round it is well formed in HTML but not in XML, so a picture written as <svg class=narrow ...> reaches an XML parser that gives up on the <svg> element itself. That element is what sets the picture's dimensions, so nothing set them: ImageSVG() went on to read svg_info['x'], ['y'], ['w'] and ['h'] anyway, and handed a width of zero back to Tag\Img, which raised four "Undefined array key" warnings and then a DivisionByZeroError that took the whole document with it. The class was reachable in real markup - anything that pastes an HTML fragment holding an inline SVG into WriteHTML() can hit it. ImageSVG() already has a way to say a picture could not be read: return false, which ImageProcessor turns into "Error parsing SVG file", a broken-image placeholder and a logged warning, or an MpdfImageException where showImageErrors is on. Nothing ever took it, because svg_error was never set. Set it when the parse leaves no dimensions behind. The parser is still fed the document in one piece without being told it is the last, so a document with an element left open is not newly refused - it draws as it always did, and the tests hold that. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A follow-up to #41, though it stands on its own and targets
gravitypdf. Not a mirror of anything upstream.An attribute value with no quotes round it is well formed in HTML and not in XML.
AdjustHTML()lifts an inline<svg>…</svg>out of the HTML, writes it to a temporary file and points an<img>at it, andImage\Svg::ImageSVG()then hands that file to an expat parser — which gives up on the<svg>element itself:svgOffset()is what setssvg_info['x'],['y'],['w']and['h'], and it is the<svg>element's own start handler, so nothing set them.xml_parse()'s return value is discarded, and the method read all four anyway:Four
Undefined array keywarnings, a width of zero handed back toTag\Img, and thenDivision by zeroatImg.php:372— which is fatal, so the whole document is lost over one picture.svg_errorwas the way out all along and nothing ever set it.ImageProcessor::processSvg()already turns afalseintoimageError($file, $firstTime, 'Error parsing SVG file'): a broken-image placeholder and a logged warning normally, anMpdfImageExceptionwhereshowImageErrorsordebugis on. So set the flag when the parse leaves no dimensions behind, and an unreadable SVG is treated like any other picture that cannot be read.Try it
Before: four
Warning: Undefined array keyinSvg.php, thenPHP Fatal error: Uncaught DivisionByZeroError: Division by zero in src/Tag/Img.php:372. No file is written.After: a PDF with both paragraphs and a broken-image placeholder where the circle would have been. With
$mpdf->showImageErrors = true;it raisesMpdf\MpdfImageException: Error parsing SVG fileinstead, naming the file.Test plan
tests/Mpdf/Image/SvgErrorTest.php, eight cases. Three fail ongravitypdf, five are controls that pass on both.testAnSvgTheParserCannotReadDoesNotBringTheDocumentDown— the document is written and both paragraphs are in it.DivisionByZeroErrorongravitypdf.testItRaisesNothingOfItsOwn— installs aset_error_handlerand asserts it collected nothing. Four warnings ongravitypdf.testItIsReportedAsAnImageErrorLikeAnyOtherPictureThatCannotBeRead— withshowImageErrorson,MpdfImageExceptioncarryingError parsing SVG file. Ongravitypdfthe warning fires first.testAWellFormedSvgIsStillDrawn,testAnSvgWithNoWidthOrHeightIsStillDrawn,testAnSvgWhoseViewBoxIsAllZeroesIsStillDrawn,testAnSvgWithNothingToDrawInItIsStillNotAnError— controls. Each asserts a form XObject reaches the page, which is how an SVG is drawn.testAnSvgWithAnElementLeftOpenIsStillDrawn— the control that matters most; see below.All twenty-two stored snapshot documents render pixel-identical at the harness's 120 dpi, so nothing that draws today stops drawing. No new snapshot: the only new output is mPDF's existing broken-image placeholder.
composer test— 1130 tests, 2653 assertions, up from 1122/2643.composer csclean.phpstanoutput identical togravitypdf.More info — why not just check what
xml_parse()returnsxml_parse($parser, $data)is called once, without$is_final, so expat is still waiting for more input when the call comes back and never gets round to complaining about anything left open at the end. Documents that rely on that render today. Refusing on the return value alone would be tempting, and passingtrueand refusing would be stricter still, but both would newly reject SVGs that currently draw — browsers are lenient about this too, and mPDF's own regex passes over the markup beforehand (stripping<pattern>and<marker>blocks, rewriting<use>into<g>) can leave things unbalanced on their own.Testing for the dimensions instead is the narrower question and the one that matches the symptom: it is true exactly when the parser never reached the
<svg>element, which is the only case that produced a zero-sized image.testAnSvgWithAnElementLeftOpenIsStillDrawnpins that a document with an element left open is not newly refused.A
viewBoxof all zeroes and awidth/heightof zero both already fall throughsvgOffset()to the default sizing and were never part of this; two controls hold them there.