Skip to content

Give SVG Form XObjects an explicit /Resources dictionary (mirrors mpdf/mpdf#2196) - #18

Closed
jakejackson1 wants to merge 1 commit into
gravitypdffrom
mirror/2196-form-xobject-resources
Closed

Give SVG Form XObjects an explicit /Resources dictionary (mirrors mpdf/mpdf#2196)#18
jakejackson1 wants to merge 1 commit into
gravitypdffrom
mirror/2196-form-xobject-resources

Conversation

@jakejackson1

Copy link
Copy Markdown
Member

Summary

This mirrors mpdf#2196, so the fork carries the fix while the upstream PR sits open.

mPDF draws an embedded SVG into a Form XObject and writes no /Resources for it, leaving it to inherit the page's. PDF 1.4 allowed that; PDF/A does not, so a validator rejects any PDF/A document containing an SVG. The fix declares what the form actually paints with — the graphics states, shadings and fonts an SVG flags on its way through Svg.php — which is the same set _putpatterns() already collects for these very objects.

Collecting them needs the shadings resolved first, and they were not. _putshaders() is what fills in a gradient's object number, and _putresources() ran it after _putformobjects(). So this branch also moves it earlier; _putpatterns() still sees the shadings resolved, as its comment has always required.

Try it

$mpdf = new \Mpdf\Mpdf();
$mpdf->PDFA = true;
$mpdf->PDFAauto = true;
$mpdf->PDFAversion = '3-B';
$mpdf->WriteHTML('<img src="tests/data/img/form-xobject-resources.svg" style="width:40mm">');
$mpdf->Output('svg.pdf', \Mpdf\Output\Destination::FILE);

On gravitypdf the Form XObject is

<</Type /XObject /Subtype /Form /Group 18 0 R /BBox [0 0 75 -45] /Length 410>>

and veraPDF reports the missing resource dictionary. On this branch it is

<</Type /XObject /Subtype /Form /Group 22 0 R /BBox [0 0 75 -45]
/Resources <</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
/ExtGState <</GS1 5 0 R /GS2 6 0 R /GS3 7 0 R /GS4 8 0 R /GS5 9 0 R>>
/Shading <</Sh1 19 0 R>> /Font <</F2 10 0 R>>>>
/Length 410>>

— every name the stream goes on to use.

Test plan

  • composer test1050 tests, 2542 assertions, green (gravitypdf is at 1048).
  • composer cs — clean.
  • PHPStan — same 33 findings as gravitypdf, none new.
  • Snapshots — the reordering renumbers objects in any document with a gradient or a pattern, so all 17 snapshots in tests/data/snapshots were regenerated on this branch and compared against the committed files: 0 differing pixels on every one. Imagick is not built for PHP 8.5 locally, so this used the ImageMagick CLI at the same 120 dpi on white that Snapshots\Snapshot uses. CI runs the real group.
  • tests/Mpdf/FormObjectResourcesTest.php fails on gravitypdf (no /Resources at all) and, importantly, also fails with the upstream patch applied without the reordering — on /Sh1.
More info

Where it differs from upstream

Upstream Here
Collects resources inside the per-object loop Collected once, before the loop; the set is the same for every form object either way, as _putpatterns() already notes
_putshaders() still runs after _putformobjects(), so $gradient['id'] is unset and the isset() guard silently drops /Shading _putshaders() moved ahead of _putformobjects() in _putresources(), so gradients resolve. The isset() guard is kept, for gradients _putshaders() does not write at all — a soft mask, say — since emitting /Sh1 0 R for one would be malformed
Reads $font['used'] before checking $font['fo'] Checks fo first. Core fonts have no used key, so the upstream order raises Undefined array key "used" on any document with a form object — tests/Issues/Issue539Test and Issue990Test both trip on it
A PDF/A test asserting /ExtGState is present A test that sweeps the form object's own content stream for gs, sh and Tf operands and asserts each one is declared with a real object number, plus a PDF/A case

Why the missing /Shading mattered

Without the reordering, a gradient SVG ends up with a /Resources dictionary that omits the shading its stream paints with. That is worse than having no dictionary: inheritance from the page had been covering it. So the ordering is not a tidy-up, it is what makes the change safe for gradient SVGs.

The fixture

tests/data/img/form-xobject-resources.svg is eight lines and deliberately paints with one of each: a linear gradient, a half-transparent rectangle, and a line of text. That gives a single form object naming /GS1/GS5, /Sh1 and /F2, so the test has something from all three categories to check.

Not covered

/XObject is not in the dictionary. Svg::svgImage() can write /I<n> Do for a raster image inside an SVG, but nothing marks those images the way gradients, graphics states and fonts are marked, and _putpatterns() has the same gap. In practice svgImage() does not currently get as far as emitting that operator, so there is nothing to declare — worth knowing about, but adding a category with no producer would be speculative.

A Form XObject without /Resources inherits the page's, which PDF 1.4
tolerated and PDF/A does not, so validators reject any document with an
embedded SVG. Declare what the form actually paints with: the graphics
states, shadings and fonts an SVG flags as it is drawn, which is the
same set _putpatterns already collects for these objects.

Collecting them needs the shadings resolved. _putshaders is what fills
in a gradient's object number, and it ran after _putformobjects, so a
gradient SVG would either name a shading as "0 R" or, with the isset()
guard, quietly leave /Shading out of a dictionary its own stream paints
with — worse than no dictionary at all, since inheritance had covered
it. Shaders are now written before form objects; _putpatterns still
sees them resolved, as its comment requires.

Every snapshot in tests/data/snapshots renders unchanged.

Mirrors mpdf#2196.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jakejackson1

Copy link
Copy Markdown
Member Author

Closing as this is resolved in jakejackson1#7 and jakejackson1#6

@jakejackson1 jakejackson1 added bug Something isn't working duplicate This issue or pull request already exists labels Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working duplicate This issue or pull request already exists

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant