Skip to content

Commit 289ed83

Browse files
committed
Add tests for formatContent() placeholder handling (#712)
Add a performance regression test in tests/Performance and PHPUnit tests covering the changed formatContent()
1 parent 39c8327 commit 289ed83

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

tests/PHPUnit/Unit/PDFObjectTest.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,81 @@ public function testTextArrayObjects(): void
9595
// array.
9696
self::assertSame([' '], $page4->getTextArray());
9797
}
98+
99+
/**
100+
* Kerned TJ arrays split a word into many small string operands. Make sure
101+
* they end up in the right order again (issue #712).
102+
*/
103+
public function testGetTextArrayReassemblesKernedTjArray(): void
104+
{
105+
$document = new Document();
106+
$document->init();
107+
108+
$content = 'BT /F1 12 Tf 10 10 Td '
109+
. '[(H)10(e)-5(l)3(l)20(o)-40( )30(W)5(o)-3(r)8(l)2(d)]TJ ET';
110+
111+
$form = new Form($document, null, $content, new Config());
112+
$header = new Header([
113+
'Resources' => new Header([
114+
'XObject' => new Header([
115+
'Fr0' => $form,
116+
])
117+
]),
118+
'Contents' => new ElementArray([new Element('/Fr0 Do', $document)], $document),
119+
]);
120+
$page = new Page($document, $header);
121+
122+
self::assertSame(['Hello World '], $page->getTextArray());
123+
}
124+
125+
/**
126+
* A << ... >> BDC dictionary around a text block must not swallow the
127+
* text that follows it (issue #712).
128+
*/
129+
public function testGetTextArrayRestoresMarkedContentDictionary(): void
130+
{
131+
$document = new Document();
132+
$document->init();
133+
134+
$content = '/OC << /MCID 0 /Foo (bar) >> BDC '
135+
. 'BT /F1 12 Tf 10 10 Td (Hello) Tj ET EMC';
136+
137+
$form = new Form($document, null, $content, new Config());
138+
$header = new Header([
139+
'Resources' => new Header([
140+
'XObject' => new Header([
141+
'Fr0' => $form,
142+
])
143+
]),
144+
'Contents' => new ElementArray([new Element('/Fr0 Do', $document)], $document),
145+
]);
146+
$page = new Page($document, $header);
147+
148+
self::assertSame(['Hello '], $page->getTextArray());
149+
}
150+
151+
/**
152+
* A string can hold balanced unescaped parentheses; check they survive
153+
* extraction (issue #712).
154+
*/
155+
public function testGetTextArrayKeepsBalancedParenthesesInsideString(): void
156+
{
157+
$document = new Document();
158+
$document->init();
159+
160+
$content = 'BT /F1 12 Tf 10 10 Td (a(b)c) Tj ET';
161+
162+
$form = new Form($document, null, $content, new Config());
163+
$header = new Header([
164+
'Resources' => new Header([
165+
'XObject' => new Header([
166+
'Fr0' => $form,
167+
])
168+
]),
169+
'Contents' => new ElementArray([new Element('/Fr0 Do', $document)], $document),
170+
]);
171+
$page = new Page($document, $header);
172+
173+
self::assertSame(['a(b)c '], $page->getTextArray());
174+
}
98175
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
/**
4+
* @file This file is part of the PdfParser library.
5+
*
6+
* @license LGPLv3
7+
*
8+
* @url <https://github.com/smalot/pdfparser>
9+
*/
10+
11+
namespace PerformanceTests\Test;
12+
13+
use PerformanceTests\AbstractPerformanceTest;
14+
use Smalot\PdfParser\Config;
15+
use Smalot\PdfParser\Document;
16+
use Smalot\PdfParser\Element;
17+
use Smalot\PdfParser\Element\ElementArray;
18+
use Smalot\PdfParser\Header;
19+
use Smalot\PdfParser\Page;
20+
use Smalot\PdfParser\XObject\Form;
21+
22+
/**
23+
* PDFs that emit text as kerned TJ arrays (for fine letter spacing) split a
24+
* single line into thousands of tiny string operands. formatContent() parks
25+
* each operand behind a unique placeholder and restores it afterward.
26+
* Restoring them with one str_replace() per placeholder scans the whole
27+
* content stream once per operand, i.e. O(operands * length) - quadratic in
28+
* the number of operands.
29+
*
30+
* This test builds a content stream with 20,000 such operands and extracts
31+
* its text. With the single-pass strtr() restoration this runs in ~1.5s here;
32+
* with the previous per-placeholder str_replace() loop it took ~10s. The time
33+
* budget below fails if the quadratic behaviour is reintroduced.
34+
*
35+
* @see https://github.com/smalot/pdfparser/issues/712
36+
*/
37+
class KernedTjArrayFormatContentTest extends AbstractPerformanceTest
38+
{
39+
/**
40+
* @var string
41+
*/
42+
protected $content;
43+
44+
public function init(): void
45+
{
46+
// Like a PDF that emits text letter-by-letter for fine kerning.
47+
$operands = '';
48+
for ($i = 0; $i < 20000; ++$i) {
49+
$operands .= '(a)'.(($i % 20) - 10).' ';
50+
}
51+
52+
$this->content = 'BT /F1 12 Tf 10 10 Td ['.$operands.']TJ ET';
53+
}
54+
55+
public function run(): void
56+
{
57+
$document = new Document();
58+
$document->init();
59+
60+
$form = new Form($document, null, $this->content, new Config());
61+
$header = new Header([
62+
'Resources' => new Header([
63+
'XObject' => new Header(['Fr0' => $form]),
64+
]),
65+
'Contents' => new ElementArray([new Element('/Fr0 Do', $document)], $document),
66+
]);
67+
68+
(new Page($document, $header))->getTextArray();
69+
}
70+
71+
public function getMaxEstimatedTime(): int
72+
{
73+
return 5;
74+
}
75+
}

0 commit comments

Comments
 (0)