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 @@ + + + + + + + Hi +