Skip to content
Open
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
28 changes: 20 additions & 8 deletions src/Smalot/PdfParser/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,24 +99,36 @@ public function parseFile(string $filename): Document
*/
public function parseContent(string $content): Document
{
// Create structure from raw data.
list($xref, $data) = $this->rawDataParser->parseData($content);
// Normalize the raw data and decode the cross-reference/trailer table.
list($xref, $pdfData) = $this->rawDataParser->parseHeaderAndXref($content);

// The original (possibly un-trimmed) input is no longer needed; drop it
// so it can be freed once the normalized $pdfData copy is also released.
unset($content);

if (isset($xref['trailer']['encrypt']) && false === $this->config->getIgnoreEncryption()) {
throw new \Exception('Secured pdf file are currently not supported.');
}

if (empty($data)) {
throw new \Exception('Object list not found. Possible secured file.');
}

// Create destination object.
$document = new Document();
$this->objects = [];

foreach ($data as $id => $structure) {
// Stream the raw objects one at a time instead of building the whole
// raw object graph up front. Each structure is turned into a PDFObject
// and then goes out of scope before the next is parsed, so the largest
// transient structure - the full raw object array - is never held,
// which markedly lowers peak memory on large documents.
foreach ($this->rawDataParser->getObjectsStream($pdfData, $xref) as $id => $structure) {
$this->parseObject($id, $structure, $document);
unset($data[$id]);
}

// Object parsing is done; the raw PDF string is no longer needed and
// can be released before text extraction is performed by the caller.
unset($pdfData);

if (empty($this->objects)) {
throw new \Exception('Object list not found. Possible secured file.');
}

$document->setTrailer($this->parseTrailer($xref['trailer'], $document));
Expand Down
41 changes: 31 additions & 10 deletions src/Smalot/PdfParser/RawData/RawDataParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -945,16 +945,21 @@ protected function getXrefData(string $pdfData, int $offset = 0, array $xref = [
}

/**
* Parses PDF data and returns extracted data as array.
* Normalize the raw PDF data and decode the cross-reference/trailer data.
*
* Returns the xref/trailer data together with the normalized PDF data so
* callers (e.g. Parser) can inspect the trailer (for instance to detect
* encryption) and then stream the objects one at a time via
* getObjectsStream() instead of materializing them all at once.
*
* @param string $data PDF data to parse
*
* @return array array of parsed PDF document objects
* @return array{0: array, 1: string} [$xref, $pdfData]
*
* @throws EmptyPdfException if empty PDF data given
* @throws MissingPdfHeaderException if PDF data missing `%PDF-` header
*/
public function parseData(string $data): array
public function parseHeaderAndXref(string $data): array
{
if (empty($data)) {
throw new EmptyPdfException('Empty PDF data given.');
Expand All @@ -976,15 +981,31 @@ public function parseData(string $data): array
$xref = $this->getXrefData($pdfData);
}

// parse all document objects
$objects = [];
return [$xref, $pdfData];
}

/**
* Yield each indirect object's raw structure one at a time.
*
* Yielding (rather than returning a fully built array) lets the consumer
* build its own representation of an object and discard the raw structure
* before the next one is parsed, so the complete raw object graph - by far
* the largest transient structure when parsing a document - never has to be
* held in memory at once.
*
* @param string $pdfData normalized PDF data, as returned by parseHeaderAndXref()
* @param array $xref xref/trailer data, as returned by parseHeaderAndXref()
*
* @return \Generator<string, array> raw object structure keyed by object reference
*/
public function getObjectsStream(string $pdfData, array $xref): \Generator
{
foreach ($xref['xref'] as $obj => $offset) {
if (!isset($objects[$obj]) && ($offset > 0)) {
// decode objects with positive offset
$objects[$obj] = $this->getIndirectObject($pdfData, $xref, $obj, $offset, true);
// decode objects with positive offset; xref is keyed by object
// reference so every $obj is unique and decoded exactly once
if ($offset > 0) {
yield $obj => $this->getIndirectObject($pdfData, $xref, $obj, $offset, true);
}
}

return [$xref, $objects];
}
}