diff --git a/src/Writer/FormWriter.php b/src/Writer/FormWriter.php
index 88c6f356c..9f215645c 100644
--- a/src/Writer/FormWriter.php
+++ b/src/Writer/FormWriter.php
@@ -28,6 +28,12 @@ public function __construct(Mpdf $mpdf, BaseWriter $writer)
public function writeFormObjects() // _putformobjects
{
+ if (!$this->mpdf->formobjects) {
+ return;
+ }
+
+ $resources = $this->getResources();
+
foreach ($this->mpdf->formobjects as $file => $info) {
$this->writer->object();
@@ -38,6 +44,7 @@ public function writeFormObjects() // _putformobjects
$this->writer->write('/Subtype /Form');
$this->writer->write('/Group ' . ($this->mpdf->n + 1) . ' 0 R');
$this->writer->write('/BBox [' . $info['x'] . ' ' . $info['y'] . ' ' . ($info['w'] + $info['x']) . ' ' . ($info['h'] + $info['y']) . ']');
+ $this->writer->write('/Resources <<' . $resources . '>>');
if ($this->mpdf->compress) {
$this->writer->write('/Filter /FlateDecode');
@@ -59,4 +66,82 @@ public function writeFormObjects() // _putformobjects
$this->writer->write('endobj');
}
}
+
+ /**
+ * A Form XObject with no /Resources of its own inherits the page's, which PDF 1.4 allowed
+ * but PDF/A does not. The entries an SVG can reference are already flagged as it is drawn.
+ *
+ * As in _putpatterns, this hands every Form XObject the resources of every Form XObject.
+ *
+ * @return string
+ */
+ private function getResources()
+ {
+ $resources = ['/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'];
+
+ $extgstates = [];
+ foreach ($this->mpdf->extgstates as $k => $extgstate) {
+ if (!isset($extgstate['fo']) || !$extgstate['fo']) {
+ continue;
+ }
+
+ if (isset($extgstate['trans'])) {
+ $extgstates[] = '/' . $extgstate['trans'] . ' ' . $extgstate['n'] . ' 0 R';
+ } else {
+ $extgstates[] = '/GS' . $k . ' ' . $extgstate['n'] . ' 0 R';
+ }
+ }
+
+ if (count($extgstates)) {
+ $resources[] = '/ExtGState <<' . implode(' ', $extgstates) . '>>';
+ }
+
+ /* -- BACKGROUNDS -- */
+ $shadings = [];
+ if (isset($this->mpdf->gradients) && count($this->mpdf->gradients) > 0) {
+ foreach ($this->mpdf->gradients as $id => $grad) {
+ // 'id' is the shading's object number, filled in by _putshaders; a gradient it
+ // does not write (a soft mask, say) is not nameable here
+ if (!isset($grad['fo']) || !$grad['fo'] || !isset($grad['id'])) {
+ continue;
+ }
+
+ $shadings[] = '/Sh' . $id . ' ' . $grad['id'] . ' 0 R';
+ }
+ }
+
+ if (count($shadings)) {
+ $resources[] = '/Shading <<' . implode(' ', $shadings) . '>>';
+ }
+ /* -- END BACKGROUNDS -- */
+
+ $fonts = [];
+ foreach ($this->mpdf->fonts as $font) {
+ if (!isset($font['fo']) || !$font['fo']) {
+ continue;
+ }
+
+ if (isset($font['type']) && $font['type'] === 'TTF') {
+ if (!$font['used']) {
+ continue;
+ }
+
+ if ($font['sip'] || $font['smp']) {
+ foreach ($font['n'] as $k => $fid) {
+ $fonts[] = '/F' . $font['subsetfontids'][$k] . ' ' . $fid . ' 0 R';
+ }
+
+ continue;
+ }
+ }
+
+ $fonts[] = '/F' . $font['i'] . ' ' . $font['n'] . ' 0 R';
+ }
+
+ if (count($fonts)) {
+ $resources[] = '/Font <<' . implode(' ', $fonts) . '>>';
+ }
+
+ return implode(' ', $resources);
+ }
}
diff --git a/src/Writer/ResourceWriter.php b/src/Writer/ResourceWriter.php
index e0d18c8db..8c86b7c65 100644
--- a/src/Writer/ResourceWriter.php
+++ b/src/Writer/ResourceWriter.php
@@ -113,11 +113,14 @@ public function writeResources() // _putresources
$this->imageWriter->writeImages();
+ // Shaders are written first so that a Form XObject can name the gradients it paints
+ // with; _putpatterns still needs them resolved before it runs, as it always has
+ $this->backgroundWriter->writeShaders();
+
$this->formWriter->writeFormObjects();
$this->mpdf->writeImportedPagesAndResolvedObjects();
- $this->backgroundWriter->writeShaders();
$this->backgroundWriter->writePatterns();
// Resource dictionary
diff --git a/tests/Mpdf/FormObjectResourcesTest.php b/tests/Mpdf/FormObjectResourcesTest.php
new file mode 100644
index 000000000..95baa12a0
--- /dev/null
+++ b/tests/Mpdf/FormObjectResourcesTest.php
@@ -0,0 +1,89 @@
+renderFormObjects(false);
+ $this->assertCount(1, $objects);
+
+ list($header, $stream) = $objects[0];
+
+ $names = $this->namesUsedBy($stream);
+ $this->assertGreaterThanOrEqual(3, count($names), 'Expected the fixture to use several resources');
+
+ foreach ($names as $name) {
+ $this->assertMatchesRegularExpression(
+ '#/' . preg_quote($name, '#') . ' [1-9]\d* 0 R#',
+ $header,
+ sprintf('/%s is painted with but not declared in the Form XObject /Resources', $name)
+ );
+ }
+ }
+
+ public function testResourcesAreWrittenForPdfA()
+ {
+ $objects = $this->renderFormObjects(true);
+ $this->assertCount(1, $objects);
+
+ list($header) = $objects[0];
+
+ $this->assertStringContainsString('/Resources <<', $header);
+ }
+
+ /**
+ * @param string $stream
+ * @return string[]
+ */
+ private function namesUsedBy($stream)
+ {
+ preg_match_all('#/([A-Za-z0-9]+)\s+(?:gs|sh)[^A-Za-z0-9]#', $stream, $states);
+ preg_match_all('#/([A-Za-z0-9]+)\s+[\d.]+\s+Tf#', $stream, $fonts);
+
+ return array_values(array_unique(array_merge($states[1], $fonts[1])));
+ }
+
+ /**
+ * @param bool $pdfa
+ * @return array[] One [header, stream] pair per Form XObject mPDF wrote for the image
+ */
+ private function renderFormObjects($pdfa)
+ {
+ $mpdf = new Mpdf();
+ $mpdf->compress = false;
+
+ if ($pdfa) {
+ $mpdf->PDFA = true;
+ $mpdf->PDFAauto = true;
+ $mpdf->PDFAversion = '3-B';
+ }
+
+ $mpdf->WriteHTML('
');
+
+ $output = $mpdf->OutputBinaryData();
+ $mpdf->cleanup();
+
+ preg_match_all('/\d+ 0 obj\s*(.*?)\bstream\r?\n(.*?)\r?\nendstream/s', $output, $matches, PREG_SET_ORDER);
+
+ $objects = [];
+ foreach ($matches as $match) {
+ // The transparency group is written as a separate object, which is what tells a
+ // Form XObject mPDF wrote here apart from the inline groups a soft mask uses
+ if (strpos($match[1], '/Subtype /Form') !== false && preg_match('#/Group \d+ 0 R#', $match[1])) {
+ $objects[] = [$match[1], $match[2]];
+ }
+ }
+
+ return $objects;
+ }
+
+}
diff --git a/tests/data/img/form-xobject-resources.svg b/tests/data/img/form-xobject-resources.svg
new file mode 100644
index 000000000..4fa78f621
--- /dev/null
+++ b/tests/data/img/form-xobject-resources.svg
@@ -0,0 +1,8 @@
+