Skip to content

Commit 29929ad

Browse files
committed
test: cover single page pdf exporter
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 01a5220 commit 29929ad

1 file changed

Lines changed: 289 additions & 0 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Tests\Unit\Pdf;
9+
10+
use LibreSign\XObjectTemplate\Dto\CompileResult;
11+
use LibreSign\XObjectTemplate\Pdf\EmbeddedPdfImage;
12+
use LibreSign\XObjectTemplate\Pdf\PdfImageEmbedderInterface;
13+
use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter;
14+
use PHPUnit\Framework\Attributes\DataProvider;
15+
use PHPUnit\Framework\TestCase;
16+
17+
final class SinglePagePdfExporterTest extends TestCase
18+
{
19+
public function testExportWrapsCompileResultInSinglePagePdfSizedFromBoundingBox(): void
20+
{
21+
$exporter = new SinglePagePdfExporter(new class () implements PdfImageEmbedderInterface
22+
{
23+
public function embed(string $source): EmbeddedPdfImage
24+
{
25+
throw new \LogicException(sprintf('Image embedding should not be called for %s.', $source));
26+
}
27+
});
28+
29+
$pdf = $exporter->export(new CompileResult(
30+
contentStream: "q\nBT\n/F1 10 Tf\n0 0 0 rg\n8 72 Td\n(Rendered for Alice) Tj\nET\nQ",
31+
resources: [
32+
'Font' => [
33+
'F1' => [
34+
'Type' => '/Font',
35+
'Subtype' => '/Type1',
36+
'BaseFont' => '/Helvetica',
37+
],
38+
],
39+
],
40+
bbox: [12.5, 4.0, 252.5, 88.0],
41+
));
42+
43+
self::assertStringStartsWith('%PDF-1.4', $pdf);
44+
self::assertStringContainsString('/Type /Catalog', $pdf);
45+
self::assertStringContainsString('/Type /Page', $pdf);
46+
self::assertStringContainsString('/MediaBox [0 0 240 84]', $pdf);
47+
self::assertStringContainsString('/Subtype /Form', $pdf);
48+
self::assertStringContainsString('/BBox [12.5 4 252.5 88]', $pdf);
49+
self::assertStringContainsString('q 1 0 0 1 -12.5 -4 cm /Fm0 Do Q', $pdf);
50+
self::assertStringContainsString('/BaseFont /Helvetica', $pdf);
51+
self::assertStringContainsString('(Rendered for Alice) Tj', $pdf);
52+
}
53+
54+
public function testExportUsesInjectedImageEmbedderForImageResources(): void
55+
{
56+
$embedder = new class () implements PdfImageEmbedderInterface
57+
{
58+
/** @var list<string> */
59+
public array $sources = [];
60+
61+
public function embed(string $source): EmbeddedPdfImage
62+
{
63+
$this->sources[] = $source;
64+
65+
return new EmbeddedPdfImage(
66+
dictionary: [
67+
'Type' => '/XObject',
68+
'Subtype' => '/Image',
69+
'Width' => 1,
70+
'Height' => 1,
71+
'ColorSpace' => '/DeviceRGB',
72+
'BitsPerComponent' => 8,
73+
'Filter' => '/FlateDecode',
74+
'DecodeParms' => [
75+
'Predictor' => 15,
76+
'Colors' => 3,
77+
'BitsPerComponent' => 8,
78+
'Columns' => 1,
79+
],
80+
],
81+
stream: gzcompress("\x00\xff\x00\x00"),
82+
);
83+
}
84+
};
85+
86+
$exporter = new SinglePagePdfExporter($embedder);
87+
88+
$pdf = $exporter->export(new CompileResult(
89+
contentStream: 'q 18 0 0 18 0 45 cm /Im0 Do Q',
90+
resources: [
91+
'Font' => [],
92+
'XObject' => [
93+
'Im0' => [
94+
'Type' => '/XObject',
95+
'Subtype' => '/Image',
96+
'Source' => '/tmp/example-image.png',
97+
'Width' => 18.0,
98+
'Height' => 18.0,
99+
],
100+
],
101+
],
102+
bbox: [0.0, 0.0, 240.0, 84.0],
103+
));
104+
105+
self::assertSame(['/tmp/example-image.png'], $embedder->sources);
106+
self::assertStringContainsString('/Subtype /Image', $pdf);
107+
self::assertStringContainsString('/ColorSpace /DeviceRGB', $pdf);
108+
self::assertStringContainsString(
109+
'/DecodeParms << /Predictor 15 /Colors 3 /BitsPerComponent 8 /Columns 1 >>',
110+
$pdf,
111+
);
112+
self::assertStringContainsString('/Im0', $pdf);
113+
self::assertStringContainsString('/Im0 Do', $pdf);
114+
}
115+
116+
public function testExportSerializesSoftMasksLiteralStringsAndBooleans(): void
117+
{
118+
$exporter = new SinglePagePdfExporter(new class () implements PdfImageEmbedderInterface
119+
{
120+
public function embed(string $source): EmbeddedPdfImage
121+
{
122+
return new EmbeddedPdfImage(
123+
dictionary: [
124+
'Type' => '/XObject',
125+
'Subtype' => '/Image',
126+
'Width' => 1,
127+
'Height' => 1,
128+
'ColorSpace' => '/DeviceRGB',
129+
'BitsPerComponent' => 8,
130+
'Filter' => '/FlateDecode',
131+
'Note' => 'Preview (QA)',
132+
'Interpolate' => true,
133+
],
134+
stream: gzcompress("\x00\xff\x00\x00"),
135+
softMask: new EmbeddedPdfImage(
136+
dictionary: [
137+
'Type' => '/XObject',
138+
'Subtype' => '/Image',
139+
'Width' => 1,
140+
'Height' => 1,
141+
'ColorSpace' => '/DeviceGray',
142+
'BitsPerComponent' => 8,
143+
'Filter' => '/FlateDecode',
144+
],
145+
stream: gzcompress("\x00\x80"),
146+
),
147+
);
148+
}
149+
});
150+
151+
$pdf = $exporter->export(new CompileResult(
152+
contentStream: 'q 10 0 0 10 0 0 cm /Im0 Do Q',
153+
resources: [
154+
'Font' => [],
155+
'XObject' => [
156+
'Im0' => [
157+
'Type' => '/XObject',
158+
'Subtype' => '/Image',
159+
'Source' => '/tmp/mask-preview.png',
160+
'Width' => 10.0,
161+
'Height' => 10.0,
162+
],
163+
],
164+
],
165+
bbox: [0.0, 0.0, 40.0, 40.0],
166+
));
167+
168+
self::assertStringContainsString('/SMask', $pdf);
169+
self::assertStringContainsString('/Note (Preview \(QA\))', $pdf);
170+
self::assertStringContainsString('/Interpolate true', $pdf);
171+
}
172+
173+
#[DataProvider('invalidCompileResultProvider')]
174+
public function testExportRejectsInvalidCompileResults(CompileResult $result, string $expectedMessage): void
175+
{
176+
$exporter = new SinglePagePdfExporter(new class () implements PdfImageEmbedderInterface
177+
{
178+
public function embed(string $source): EmbeddedPdfImage
179+
{
180+
return new EmbeddedPdfImage(
181+
dictionary: [
182+
'Type' => '/XObject',
183+
'Subtype' => '/Image',
184+
'Width' => 1,
185+
'Height' => 1,
186+
'ColorSpace' => '/DeviceRGB',
187+
'BitsPerComponent' => 8,
188+
'Filter' => '/FlateDecode',
189+
],
190+
stream: gzcompress("\x00\xff\x00\x00"),
191+
);
192+
}
193+
});
194+
195+
$this->expectException(\InvalidArgumentException::class);
196+
$this->expectExceptionMessage($expectedMessage);
197+
198+
$exporter->export($result);
199+
}
200+
201+
/**
202+
* @return iterable<string, array{result: CompileResult, expectedMessage: string}>
203+
*/
204+
public static function invalidCompileResultProvider(): iterable
205+
{
206+
yield 'bbox without positive area' => [
207+
'result' => new CompileResult(
208+
contentStream: 'BT ET',
209+
resources: ['Font' => []],
210+
bbox: [0.0, 0.0, 0.0, 40.0],
211+
),
212+
'expectedMessage' => 'CompileResult bbox must describe a positive area.',
213+
];
214+
215+
yield 'font resource must be an array' => [
216+
'result' => new CompileResult(
217+
contentStream: 'BT ET',
218+
resources: ['Font' => ['F1' => '/Helvetica']],
219+
bbox: [0.0, 0.0, 40.0, 40.0],
220+
),
221+
'expectedMessage' => 'Font resource "F1" must be an array.',
222+
];
223+
224+
yield 'unsupported dictionary value type' => [
225+
'result' => new CompileResult(
226+
contentStream: 'BT ET',
227+
resources: [
228+
'Font' => [
229+
'F1' => [
230+
'Type' => '/Font',
231+
'Subtype' => '/Type1',
232+
'Meta' => new \stdClass(),
233+
],
234+
],
235+
],
236+
bbox: [0.0, 0.0, 40.0, 40.0],
237+
),
238+
'expectedMessage' => 'Unsupported PDF value type "stdClass".',
239+
];
240+
241+
yield 'xobject resource must be an array' => [
242+
'result' => new CompileResult(
243+
contentStream: 'BT ET',
244+
resources: [
245+
'Font' => [],
246+
'XObject' => ['Im0' => '/Image'],
247+
],
248+
bbox: [0.0, 0.0, 40.0, 40.0],
249+
),
250+
'expectedMessage' => 'XObject resource "Im0" must be an array.',
251+
];
252+
253+
yield 'unsupported xobject subtype' => [
254+
'result' => new CompileResult(
255+
contentStream: 'BT ET',
256+
resources: [
257+
'Font' => [],
258+
'XObject' => [
259+
'Im0' => [
260+
'Type' => '/XObject',
261+
'Subtype' => '/Form',
262+
'Source' => '/tmp/form.xobject',
263+
],
264+
],
265+
],
266+
bbox: [0.0, 0.0, 40.0, 40.0],
267+
),
268+
'expectedMessage' => 'Unsupported XObject subtype for "Im0".',
269+
];
270+
271+
yield 'missing image source' => [
272+
'result' => new CompileResult(
273+
contentStream: 'BT ET',
274+
resources: [
275+
'Font' => [],
276+
'XObject' => [
277+
'Im0' => [
278+
'Type' => '/XObject',
279+
'Subtype' => '/Image',
280+
'Source' => '',
281+
],
282+
],
283+
],
284+
bbox: [0.0, 0.0, 40.0, 40.0],
285+
),
286+
'expectedMessage' => 'Image resource "Im0" must expose a non-empty Source.',
287+
];
288+
}
289+
}

0 commit comments

Comments
 (0)