Skip to content
Closed
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
85 changes: 85 additions & 0 deletions src/Writer/FormWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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');
Expand All @@ -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);
}
}
5 changes: 4 additions & 1 deletion src/Writer/ResourceWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 89 additions & 0 deletions tests/Mpdf/FormObjectResourcesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace Mpdf;

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

const SVG = __DIR__ . '/../data/img/form-xobject-resources.svg';

/**
* The fixture paints with a gradient, a half-transparent rectangle and a line of text, so
* the Form XObject names an entry from all three resource categories.
*/
public function testEveryNameTheStreamUsesIsDeclared()
{
$objects = $this->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('<img src="' . self::SVG . '" style="width:40mm">');

$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;
}

}
8 changes: 8 additions & 0 deletions tests/data/img/form-xobject-resources.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading