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
21 changes: 21 additions & 0 deletions doc/CustomConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The `Config` class has the following options:
| `setPdfWhitespaces` | String | `\0\t\n\f\r ` | |
| `setPdfWhitespacesRegex` | String | `[\0\t\n\f\r ]` | |
| `setRetainImageContent` | Boolean | `true` | If parsing fails due to memory exhaustion, you can set the value to `false`. This will reduce memory usage, although it will no longer retain image content. |
| `setContentSpooling` | Boolean | `false` | If parsing large documents fails due to memory exhaustion, set this to `true` to spool decoded stream content to a temporary file instead of keeping it in memory. Lowers peak memory usage at the cost of some extra disk I/O. |


## option setDecodeMemoryLimit + setRetainImageContent (manage memory usage)
Expand All @@ -41,6 +42,26 @@ $config->setDecodeMemoryLimit(1000000);
$parser = new \Smalot\PdfParser\Parser([], $config);
```

## option setContentSpooling (manage memory usage)

When parsing large documents, the bulk of the memory a parsed document keeps
alive is the decoded content of its stream objects. Enabling content spooling
writes that content to a single temporary file as objects are parsed and reads
it back on demand, so the full set of decoded streams no longer has to reside in
memory at once. This noticeably lowers peak memory usage for large files in
exchange for a small amount of disk I/O. The extracted text and document details
are identical with the option on or off.

```php
$config = new \Smalot\PdfParser\Config();
// Spool decoded stream content to a temporary file instead of memory
$config->setContentSpooling(true);
$parser = new \Smalot\PdfParser\Parser([], $config);
```

The temporary file is created in the system temp directory and removed
automatically once the parsed document is no longer referenced.

## option setHorizontalOffset

When words are broken up or when the structure of a table is not preserved, you can use `setHorizontalOffset`.
Expand Down
19 changes: 19 additions & 0 deletions src/Smalot/PdfParser/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ class Config
*/
private $ignoreEncryption = false;

/**
* Whether decoded object stream content is spooled to a temporary file
* instead of being kept in memory. Trades disk I/O for a lower peak memory
* footprint when parsing large documents.
*
* @var bool
*/
private $contentSpooling = false;

public function getFontSpaceLimit()
{
return $this->fontSpaceLimit;
Expand Down Expand Up @@ -172,4 +181,14 @@ public function setIgnoreEncryption(bool $ignoreEncryption): void
{
$this->ignoreEncryption = $ignoreEncryption;
}

public function getContentSpooling(): bool
{
return $this->contentSpooling;
}

public function setContentSpooling(bool $contentSpooling): void
{
$this->contentSpooling = $contentSpooling;
}
}
153 changes: 153 additions & 0 deletions src/Smalot/PdfParser/ContentSpool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

/**
* @file
* This file is part of the PdfParser library.
*
* @author Andreas Gohr <gohr@cosmocode.de>
*
* @date 2026-06-18
*
* @license LGPLv3
*
* @url <https://github.com/smalot/pdfparser>
*
* PdfParser is a pdf library written in PHP, extraction oriented.
* Copyright (C) 2017 - Sébastien MALOT <sebastien@malot.fr>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program.
* If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
*/

namespace Smalot\PdfParser;

/**
* Temporary on-disk store for decoded stream content.
*
* When content spooling is enabled (see Config::setContentSpooling()), the
* decoded content of each parsed object is appended to a single temporary file
* instead of being held in memory, and read back on demand. This trades a small
* amount of disk I/O for a markedly lower peak memory footprint, since the full
* set of decoded streams - usually the largest thing a parsed Document keeps
* alive - no longer has to reside in RAM all at once.
*
* The backing temporary file is created lazily on first use and removed
* automatically once the spool (and the Document owning it) is destroyed.
*
* @internal
*/
class ContentSpool
{
/**
* @var resource|null
*/
private $handle;

/**
* Current size of the spool file, i.e. the offset at which the next chunk
* of content will be written.
*
* @var int
*/
private $size = 0;

/**
* Append a chunk of content to the spool.
*
* @return array{0: int, 1: int}|null [offset, length] locating the stored
* content, or null if it could not be
* stored (the caller should then keep
* the content in memory)
*/
public function store(string $content): ?array
{
$length = \strlen($content);
if (0 === $length) {
return null;
}

$handle = $this->handle();
if (null === $handle) {
return null;
}

$offset = $this->size;
if (0 === fseek($handle, $offset) && fwrite($handle, $content) === $length) {
$this->size += $length;

return [$offset, $length];
}

return null;
}

/**
* Read back content previously stored via store().
*
* @param int $offset offset returned by store()
* @param int $length length returned by store()
*/
public function fetch(int $offset, int $length): string
{
if (!\is_resource($this->handle) || $length <= 0) {
return '';
}

// Seeking before reading also flushes any pending write buffer, which
// is required when reading data that was just written to the handle.
if (0 !== fseek($this->handle, $offset)) {
return '';
}

$content = '';
$remaining = $length;
while ($remaining > 0 && !feof($this->handle)) {
$chunk = fread($this->handle, $remaining);
if (false === $chunk || '' === $chunk) {
break;
}
$content .= $chunk;
$remaining -= \strlen($chunk);
}

return $content;
}

/**
* Lazily open the backing temporary file.
*
* @return resource|null
*/
private function handle()
{
if (null === $this->handle) {
// tmpfile() opens a binary-safe read-write handle and removes the
// underlying file automatically when the handle is closed.
$handle = tmpfile();
$this->handle = false !== $handle ? $handle : null;
}

return $this->handle;
}

/**
* Ensure the backing file is closed and removed when the spool is destroyed.
*/
public function __destruct()
{
if (\is_resource($this->handle)) {
fclose($this->handle);
}
}
}
18 changes: 18 additions & 0 deletions src/Smalot/PdfParser/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,29 @@ class Document
*/
protected $details;

/**
* Optional on-disk store for decoded object stream content, shared by all
* objects of this document. Only set when content spooling is enabled.
*
* @var ContentSpool|null
*/
protected $contentSpool;

public function __construct()
{
$this->trailer = new Header([], $this);
}

public function getContentSpool(): ?ContentSpool
{
return $this->contentSpool;
}

public function setContentSpool(?ContentSpool $contentSpool): void
{
$this->contentSpool = $contentSpool;
}

public function init()
{
$this->buildDictionary();
Expand Down
Loading
Loading