Skip to content

Commit 39c8327

Browse files
committed
Avoid quadratic placeholder replacement in PDFObject::formatContent()
formatContent() restores every extracted string/dict placeholder with a full-content str_replace() in a loop, and does the extraction with a preg_quote()+preg_replace() per operand; both scan the whole content stream once per operand, so parsing is quadratic in the number of operands. Restore the placeholders in a single strtr() pass and use strpos()+substr_replace() for the initial extraction
1 parent c6288bc commit 39c8327

1 file changed

Lines changed: 26 additions & 12 deletions

File tree

src/Smalot/PdfParser/PDFObject.php

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -320,12 +320,18 @@ private function formatContent(?string $content): string
320320
// Replace the string with a unique placeholder
321321
$id = uniqid('STRING_', true);
322322
$pdfstrings[$id] = $text[0];
323-
$content = preg_replace(
324-
'/'.preg_quote($text[0], '/').'/',
325-
'@@@'.$id.'@@@',
326-
$content,
327-
1
328-
);
323+
// Replace the first literal occurrence without compiling a regex
324+
// per string operand (preg_quote + preg_replace is quadratic for
325+
// kerned TJ arrays with many operands).
326+
$stringPos = strpos($content, $text[0]);
327+
if (false !== $stringPos) {
328+
$content = substr_replace(
329+
$content,
330+
'@@@'.$id.'@@@',
331+
$stringPos,
332+
\strlen($text[0])
333+
);
334+
}
329335

330336
// Reset to search for the next string
331337
$attempt = '(';
@@ -381,24 +387,32 @@ private function formatContent(?string $content): string
381387

382388
// Restore the original content of the dictionary << >> commands
383389
$dictstore = array_reverse($dictstore, true);
384-
foreach ($dictstore as $id => $dict) {
385-
$content = str_replace('###'.$id.'###', $dict, $content);
390+
if ([] !== $dictstore) {
391+
$dictMap = [];
392+
foreach ($dictstore as $id => $dict) {
393+
$dictMap['###'.$id.'###'] = $dict;
394+
}
395+
$content = strtr($content, $dictMap);
386396
}
387397

388-
// Restore the original string content
398+
// Restore the original string content in a single pass (strtr) instead
399+
// of one full-content str_replace() per placeholder, which is quadratic
400+
// for kerned TJ arrays with many string operands.
389401
$pdfstrings = array_reverse($pdfstrings, true);
402+
$stringMap = [];
390403
foreach ($pdfstrings as $id => $text) {
391404
// Strings may contain escaped newlines, or literal newlines
392405
// and we should clean these up before replacing the string
393406
// back into the content stream; this ensures no strings are
394407
// split between two lines (every command must be on one line)
395-
$text = str_replace(
408+
$stringMap['@@@'.$id.'@@@'] = str_replace(
396409
["\\\r\n", "\\\r", "\\\n", "\r", "\n"],
397410
['', '', '', '\r', '\n'],
398411
$text
399412
);
400-
401-
$content = str_replace('@@@'.$id.'@@@', $text, $content);
413+
}
414+
if ([] !== $stringMap) {
415+
$content = strtr($content, $stringMap);
402416
}
403417

404418
// Restore the original content of any inline images

0 commit comments

Comments
 (0)