daniel-jorg-schuppelius / php-pdf-toolkit
PHP 8.2+ library for PDF text extraction with automatic reader selection. Supports embedded text and scanned documents via OCR.
Package info
github.com/Daniel-Jorg-Schuppelius/php-pdf-toolkit
pkg:composer/daniel-jorg-schuppelius/php-pdf-toolkit
Requires
- php: >=8.2 <8.6
- ext-gd: *
- dompdf/dompdf: ^3.1
- dschuppelius/php-common-toolkit: ^2.9
- setasign/fpdf: ^1.8
- setasign/fpdi: ^2.3
- tecnickcom/tcpdf: ^6.6 || ^7.0
Requires (Dev)
- laravel/pint: ^1.27
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.33.0
- v0.32.3
- v0.32.2
- v0.32.1
- v0.32.0
- v0.31.1
- v0.31.0
- v0.30.0
- v0.29.1
- v0.29.0
- v0.28.0
- v0.27.2
- v0.27.1
- v0.27.0
- v0.26.3
- v0.26.2
- v0.26.1
- v0.26.0
- v0.25.4
- v0.25.3
- v0.25.2
- v0.25.1
- v0.25.0
- v0.24.1
- v0.24.0
- v0.23.0
- v0.22.1
- v0.22.0
- v0.21.2
- v0.21.1
- v0.21.0
- v0.20.2
- v0.20.1
- v0.20.0
- v0.19.4
- v0.19.3
- v0.19.2
- v0.19.1
- v0.19.0
- v0.18.0
- v0.17.3
- v0.17.2
- v0.17.1
- v0.16.2
- v0.16.1
- v0.16
- v0.15.7
- v0.15.6
- v0.15.5
- v0.15.4
- v0.15.3
- v0.15.2
- v0.15.1
- v0.15
- v0.14.2.1
- v0.14.2
- v0.14.1
- v0.14
- v0.13.4
- v0.13.3
- v0.13.2
- v0.13.1
- v0.13
- v0.12.3
- v0.12.2
- v0.12.1
- v0.12
- v0.11.1
- v0.11
- v0.10.1.1
- v0.10.1
- v0.10
- v0.9
- v0.8.2
- v0.8.1
- v0.8.0.1
- v0.8
- v0.7.1
- v0.7
- v0.6
- v0.5.2
- v0.5.1
- v0.5
- v0.4.12
- v0.4.11
- v0.4.10
- v0.4.9
- v0.4.8
- v0.4.7
- v0.4.6
- v0.4.5
- v0.4.4
- v0.4.3
- v0.4.2
- v0.4.1
- v0.4
- v0.3.1
- v0.3
- v0.2.2
- v0.2.1
- v0.2
- v0.1
This package is auto-updated.
Last update: 2026-11-14 11:10:25 UTC
README
A PHP 8.2+ library for extracting text from PDF documents and creating PDFs with intelligent reader/writer selection.
Features
PDF Text Extraction (Readers)
-
Multiple PDF Readers with automatic fallback:
pdftotext(poppler-utils) - Fast extraction for text-based PDFsPDFBox(Apache, Java) - Better handling of complex layoutsTesseract- OCR for scanned documentsOCRmyPDF- High-quality OCR with preprocessing
-
Automatic Reader Selection - Tries text extraction first, falls back to OCR if needed
-
Caching - Extracted text is cached to avoid redundant processing
-
Language Support - Configurable OCR languages (German + English by default)
PDF Creation (Writers)
-
Multiple PDF Writers with automatic fallback:
Dompdf- HTML to PDF conversion (pure PHP, LGPL)TCPDF- Programmatic PDF creation (pure PHP, LGPL)wkhtmltopdf- High-quality HTML rendering via WebKit (external tool)
-
Automatic Writer Selection - Uses the first available writer by priority
-
Multiple Input Formats - HTML, plain text, or HTML files
-
Metadata Support - Title, author, subject for generated PDFs
-
Stationery Overlay -
StationeryOverlayWriterplaces a letterhead PDF as a layer under imported content pages, with separate assets for the first and following pages -
ZUGFeRD / Factur-X -
ZugferdWriterwrites PDF/A-3 with the invoice XML (factur-x.xml, AFRelationshipAlternative). The optionattachmentsembeds further files next to it, such as supporting documents of an e-invoice: each entry hasname,contentand optionallymime,descriptionandrelationship(defaultSupplement, seeZugferdWriter::ATTACHMENT_RELATIONSHIPS). Every file is listed in the catalog's/AFarray. Names that repeat, ignoring case, and empty files are skipped with a warning. With TCPDF 7 the invoice is also declared in the XMP metadata (Factur-X/ZUGFeRD extension schema:DocumentFileName,Version,ConformanceLevelfromzugferd_profile).embedIntoPdf($sourcePdfPath, $invoiceXml, $options)takes the pages of an existing PDF unchanged (as form XObjects) and writes them with a new invoice XML and theattachmentsas PDF/A-3, e.g. to add supporting documents to an invoice issued elsewhere; document-level content of the source (embedded files including the old invoice, bookmarks, links, form fields, tags, metadata, signatures) is not taken over, and a parser failure is retried once afterPDFOptimizeHelper::clean().ZugferdReader::extractInvoice()returns the invoice XML together with its embedded file name. Under TCPDF 7 every output passesPDFXrefHelper::rebuild(): tc-lib-pdf locates objects by a regex over the whole buffer and stores embedded files uncompressed in PDF/A-3 mode, so an attached PDF used to break the cross-reference table; the helper rebuilds it from a sequential pass (objects only where the previous one ends, streams skipped by their direct/Length) and rejects anything else - cross-reference streams, incremental revisions, indirect or wrong lengths, signatures - instead of guessing (since 0.31.0)
Additional Helpers
- Word boxes -
PDFWordBoxHelperandPDFTextProvider::wordBoxes()return every word with its rectangle relative to the page size (0–1), from the text layer (pdftotext -bbox-layout) or via OCR (Tesseract TSV on rendered pages, shared pipelineTesseractPageOcr); the basis for zone-based extraction such as "the invoice number of this supplier sits top right" (since 0.27.0) - Image OCR -
TesseractReader::extractTextFromImage()runs OCR on a single image file instead of a PDF; the image is only deskewed when it is skewed by at leastTesseractReader::IMAGE_DESKEW_MIN_ANGLE(0.5 degrees), because resampling a straight scan blurs thin characters such as the commas in amounts (needsconvertfor the measurement, otherwise every image is deskewed as before) - Searchable PDF -
PDFOcrHelper::makeSearchable()puts an invisible OCRmyPDF text layer over scanned pages (optionally with the recognised text as a sidecar file); it is the one OCRmyPDF path of the package,OcrMyPDFReaderuses it as well (see Searchable PDF and OCRmyPDF) - Encrypted Layers -
CipherLayerSolverresolves encrypted content layers so that text extraction also works on protected documents - Broken ToUnicode Tables -
GlyphNameLayer(viaPDFTextProvider::glyphNameText()) rebuilds the text layer from the fonts' glyph names, so a PDF whose ToUnicode table maps every glyph to the wrong character still yields its exact text - no OCR, no guessing. Needsmutool(mupdf-tools) - Page furniture -
PageFurnitureDetector::in()finds running headers and footers in extracted text: lines that stand at the same distance from the page edge on at least 60 % of the pages and never inside a page (digits ignored, so page counters and print dates match). It returns line indices of the cleaned text instead of removing anything, because callers often still need those lines (account number, statement number). Lines with an amount or a leading date are never furniture; column headers are recognised through an optional callback of the caller (since 0.28.0) - Pages -
PDFPageHelperarranges pages (order, rotation per page, drop, duplicate, blank pages), rotates, puts several pages on one sheet (nup, 1-16, optional frame; 1 per sheet resizes), stamps page numbers and text watermarks (stamp), renders thumbnails and page images (within the pixel limit, see Raster limits), extracts embedded images (extractImages, pdfimages);PDFCropHelper::cropToGrid()splits every page into a grid of parts. All page operations keep the content streams - nothing is rasterised, text stays text. Needsmutool; the scripts underdata/mupdf/run with MuPDF 1.17, 1.21 and 1.25 - Security -
PDFSecurityHelperprobes, sets (AES-256 with permissions) and removes password protection. Passwords never travel on the command line. Needsmutool.isSigned()detects digital signatures by the/ByteRangeentry and, withmutool, by signed signature fields and/Perms; it does not validate them (since 0.31.0) - Optimize -
PDFOptimizeHelpershrinks PDFs (strongvia Ghostscript,losslessvia OCRmyPDF's optimizer,customwith resolution and JPEG quality), repairs them (clean, rewrite without object streams) and converts to PDF/A-2b - PDF/A compliance -
PDFAComplianceHelper::verify()checks a file against ISO 19005 (the level declared in its XMP, else 2b) and returns the verdict with the violated clauses. veraPDF is the reference checker; PdfCraft CLI is the fallback without Java (2b/3b only, fewer rules), and the result always names the engine. Both are optional and self-installing viainstallscript/install-dependencies.sh(theinstallobjects inconfig/executables.json) - Metadata -
PDFMetadataHelpersets or clears the Info dictionary (and drops stale XMP); reading stays inPDFHelper::getMetadata() - Bookmarks -
PDFOutlineHelper::set()replaces the outline with one level of bookmarks (page and title, in page order) and opens the file with the bookmark pane. Needsmutool - Attachments -
PDFAttachmentHelperlists and extracts embedded files (pdfdetach, poppler) and embeds files (attach, EmbeddedFiles name tree; a file of the same name is replaced). The ZUGFeRD invoice XML keeps its own reader - Sanitize -
PDFSanitizeHelper::flatten()burns form fields and annotations into the page content (Ghostscript),sanitize()removes annotations, links, forms, JavaScript and actions, or embedded files (mutool). Content streams stay untouched - Phantom spaces -
PDFPhantomSpaceHelper::removePhantomSpaces()removes space glyphs that do not advance (the next glyph starts where the space starts) from the text layer of pages and form XObjects (mutool,data/mupdf/pdf-phantom-spaces.js). Some producers - e.g. the PDF24 printer driver for Word's optional hyphens - emit one at every hyphenation point;pdftotextdrops them by position, LibreOffice and MuPDF/pdf2docx keep them ("In halts ver zeich nis"). The pages render identically; the output is written only if something was removed. Meant as a pre-step for tools that take the text layer literally (PDF to DOCX/ODT) - Redaction -
PDFRedactHelper::findText()locates search terms per page frompdftotext -bbox-layout(word boxes in display space, or the whole visual line across all columns) andredact()removes text and image pixels under the boxes for real (MuPDF redaction annotations). Image pixels need MuPDF 1.19 or newer; older versions are refused for pages with images instead of covering them.isAvailable()also runs a probe (data/redact-probe.pdf, two words in oneTJarray): MuPDF 1.23 leaves the second word in place, so redaction reports itself unavailable there instead of leaking text. Needsmutoolandpdftotext - Barcodes -
BarcodeHelperreads bar and QR codes on an image or on the rendered pages of a PDF (zbarimgfrom zbar-tools, optional); separator sheets with a barcode are the scanning standard in tax offices. Since 0.32.1parseXml()reads thezbarimg --xmloutput with an XML parser (XmlHelper::safeLoadString(): no network, no entities; output with a DOCTYPE is dropped) instead of regular expressions and decodes base64 data. zbar writes data as base64 (format='base64') as soon as it contains control or C1 bytes - among them the UTF-8 of Ä, Ö, Ü, ß and € - or]]>. Before, a QR code whose content looked like XML (</data><symbol type='QR-Code'><data>BCD…) produced an invented second code, and a symbol with base64 data took the data of the next symbol. The unescaped file name zbarimg puts into<source href='…'>and invalid UTF-8 no longer break the parse.scanPages()reads the visible page only (CropBox): a code outside it, which no viewer shows, is not returned - OCR languages -
TesseractDataHelper::SUPPORTED_LANGUAGESis the allow-list for language codes that reach Tesseract and the tessdata download
Requirements
- PHP 8.2+
For Text Extraction (at least one)
pdftotext(apt install poppler-utils)tesseract-ocr(apt install tesseract-ocr tesseract-ocr-deu)ocrmypdf(apt install ocrmypdf)mutool(apt install mupdf-tools) - optional, needed forglyphNameText()on PDFs with broken ToUnicode tables and for all page and security helpersgs(apt install ghostscript) - optional, cropping and strong compression- Java + PDFBox JAR (optional)
verapdf(optional, PDF/A compliance; veraPDF 1.30 from software.verapdf.org, needs Java 8+) orpdfcraft-cli(optional fallback, glibc 2.35+) - both installed byinstallscript/install-dependencies.sh
For PDF Creation (at least one)
dompdf/dompdf(composer require dompdf/dompdf)tecnickcom/tcpdf(composer require tecnickcom/tcpdf)wkhtmltopdf(apt install wkhtmltopdf)
Installation
Via Composer
composer require daniel-jorg-schuppelius/php-pdf-toolkit
Clone with Submodules
git clone --recurse-submodules https://github.com/Daniel-Jorg-Schuppelius/php-pdf-toolkit.git
Or if already cloned:
git submodule update --init
Install System Dependencies
Use the included install script for system dependencies:
# Install PDF extraction tools (poppler-utils, tesseract, ocrmypdf)
sudo ./installscript/install-dependencies.sh
Install PHP Libraries for PDF Creation
# Dompdf (recommended, pure PHP) composer require dompdf/dompdf # Or TCPDF (alternative, pure PHP) composer require tecnickcom/tcpdf # Or wkhtmltopdf (external tool, best quality) sudo apt install wkhtmltopdf
Usage
Text Extraction
use PDFToolkit\Registries\PDFReaderRegistry; $registry = PDFReaderRegistry::getInstance(); $document = $registry->extractText('/path/to/file.pdf', [ 'language' => 'deu+eng' ]); if ($document->hasText()) { echo $document->text; echo "Reader: " . $document->reader; echo "Scanned: " . ($document->isScanned ? 'Yes' : 'No'); } // Ohne OCR-Fallback (schneller für Text-PDFs wie Kontoauszüge) $document = $registry->extractTextOnly('/path/to/bankstatement.pdf', [ 'layout' => false // Ohne Layout-Formatierung für bessere Regex-Extraktion ]);
PDF Creation
use PDFToolkit\Registries\PDFWriterRegistry; use PDFToolkit\Entities\PDFContent; $registry = PDFWriterRegistry::getInstance(); // Simple: HTML to PDF $registry->htmlToPdf('<h1>Hello World</h1><p>Content</p>', '/path/to/output.pdf'); // Simple: Text to PDF $registry->textToPdf('Plain text content', '/path/to/output.pdf'); // Advanced: With metadata and options $content = PDFContent::fromHtml($html, [ 'title' => 'My Document', 'author' => 'John Doe', 'subject' => 'Example PDF' ]); $registry->createPdf($content, '/path/to/output.pdf', [ 'paper_size' => 'A4', 'orientation' => 'portrait', 'margins' => ['top' => 15, 'bottom' => 15, 'left' => 15, 'right' => 15] ]); // Use specific writer $registry->createPdf($content, '/path/to/output.pdf', [], 'dompdf'); // Get PDF as string (for download/streaming) $pdfString = $registry->createPdfString($content); header('Content-Type: application/pdf'); echo $pdfString;
Check Available Tools
// Readers $readerRegistry = PDFReaderRegistry::getInstance(); foreach ($readerRegistry->getReaderInfo() as $info) { echo "{$info['name']}: " . ($info['available'] ? '✓' : '✗') . "\n"; } } // Writers $writerRegistry = PDFWriterRegistry::getInstance(); foreach ($writerRegistry->getWriterInfo() as $info) { echo "{$info['name']}: " . ($info['available'] ? '✓' : '✗') . "\n"; }
Configuration
Tool paths can be configured in config/executables.json:
{
"shellExecutables": {
"pdftotext": {
"path": "/usr/bin/pdftotext",
"required": true
},
"wkhtmltopdf": {
"path": "/usr/bin/wkhtmltopdf",
"required": false
}
}
}
Time Limits
Every external tool runs through CommonToolkit\Helper\Shell with a time limit (php-common-toolkit ≥ 2.9). The limits live in one place, PDFToolkit\Helper\ToolTimeout:
| Class | Limit | Used for |
|---|---|---|
PROBE |
30 s | pdfinfo, pdfdetach -list, password probe, --version |
PAGE |
120 s | one page: pdftoppm of a single page, first-page text probe, tesseract/mogrify per page |
forSize() |
180 s + 10 s per MB | whole-document text and page work: pdftotext, pdfbox, MuPDF scripts, pdftk, poppler split/merge, Ghostscript |
forRendering() |
180 s + 10 s per page | pdftoppm/pdfimages over many pages, mutool draw -F trace |
forOcr() |
180 s + 60 s per page | OCRmyPDF (also its optimizer and PDF/A, which OCR scanned pages via --skip-text) |
VERAPDF |
300 s | veraPDF (PDF/A compliance) |
PDFCRAFT |
120 s | PdfCraft CLI (PDF/A fallback) |
Scaled limits are capped at 4 h (ToolTimeout::MAX) and never drop below Shell::getDefaultTimeout(), so Shell::setDefaultTimeout(…) in a worker raises them for long OCR runs. When a limit is hit, the whole process tree is killed and the call reports a failure — OCR never returns the text of the remaining pages as a result; the optimizer and single-page rendering also remove a half-written output file. PDFOptimizeHelper::compress() tells a failure (including a time-out) apart from "nothing saved" through its optional by-reference parameter $failed.
phpstan.neon.dist includes the common-toolkit rule that reports direct process calls (exec, proc_open, backticks, Symfony Process …) outside Shell.
Raster limits (since 0.32.1)
pdfinfo reports the CropBox as the page size, while pdftoppm without -cropbox rasterises the MediaBox. A 1.6 KB PDF with an A4 CropBox and a 9000 × 9000 pt MediaBox therefore passed every page-size check and cost 2.4 GB of memory and 17 s per page at 200 dpi. Since 0.32.1 every raster path of the package checks the larger of both boxes against one pixel limit per page, kept in one place like the time limits: PDFToolkit\Helper\RasterLimit.
| Path | Above the limit |
|---|---|
PDFHelper::renderPageToImage()/renderPageToBase64(), PDFPageHelper::renderPages()/thumbnails(), BarcodeHelper::scanPages(), TesseractReader, TesseractPageOcr (word boxes, row-aligned OCR) |
The run gets the highest resolution at which no page of the range exceeds the limit (RasterLimit::dpiFor(), warning in the log). Pages below the limit keep the requested resolution; one oversized page lowers the resolution of its whole range |
PDFOcrHelper::makeSearchable() (also OcrMyPDFReader), PDFOptimizeHelper level lossless and toPdfA() |
OCRmyPDF chooses the resolution itself (at least 400 dpi for pages with text or vector content) and rasterises the MediaBox with Ghostscript. A file with a page above the limit at RasterLimit::OCRMYPDF_DPI (400) is refused before OCRmyPDF starts (RasterLimit::fits()); with a page list only those pages count |
If the boxes cannot be read (pdfinfo fails or prints inf), nothing is rasterised.
- Limit -
PDFSettings.raster_max_pixels, default 150 000 000 pixels per page (RasterLimit::DEFAULT_MAX_PIXELS). A0 at 300 dpi (139 MP) and A1 through OCRmyPDF (124 MP at 400 dpi) pass; A0 and larger is refused for OCRmyPDF.pdftoppmneeds a little under 4 bytes per pixel: the 9000 pt page above, rendered at the limit (12247 × 12247 pixels, 98 dpi), took 0.6 GB.RasterLimit::configure($maxPixels)sets the limit for one process. It can be raised, not switched off; a value that is not a positive number counts as the default. - Page boxes -
PDFHelper::getAllPageBoxes($file, $firstPage, $lastPage)returnsPageBoxes(MediaBox, CropBox, rotation) per page frompdfinfo -box, for every page of the range or none.PageBoxes::largest()is the size to check before rasterising,PageSize::pixelsAt($dpi)the pixel countpdftoppmproduces at that resolution.getAllPageSizes()still returns the CropBox, the visible page, for format detection. Applications that check page sizes before rasterising should checklargest(). - Visible page -
renderPageToImage(),renderPageToBase64()andrenderPages()takecropBox: trueto rasterise the CropBox only (pdftoppm -cropbox), as a viewer shows the page. The default stays the MediaBox: it is the coordinate system ofpdftotext -bboxand therefore of the word boxes.thumbnails()andBarcodeHelper::scanPages()use the visible page. OCR (Tesseract and OCRmyPDF) keeps reading the MediaBox, likepdftotext. The switch is passed as an extra argument, so overriddenpdftoppm-*entries in your own configuration keep working. - No memory limit per process - Limiting the address space of the tool (
ulimit -v,prlimit --as) was tried and not adopted:pdftoppm(poppler 25.03) answers a failed allocation with "Out of memory" on stderr, a 1 × 1 pixel image and exit code 0, so the failure would pass as an empty page.RLIMIT_ASalso counts reserved address space and breaks multi-threaded tools (OCRmyPDF, Tesseract, Java). Bound the memory of a worker in the service manager instead (cgroup, systemdMemoryMax=). - Known limits - The limit applies per page: a document with many pages near the limit still costs time and temporary disk space for each of them, and Tesseract recognises several pages in parallel. Large embedded images are decoded by poppler,
pdfimagesand OCRmyPDF regardless of the page size./UserUnitis ignored bypdftoppm(checked) but applied by Ghostscript and MuPDF, andpdfinfodoes not report it; for OCRmyPDF its own check remains the bound there (--max-image-mpixels, by default it refuses images above 500 MP).
Searchable PDF and OCRmyPDF (since 0.25.0)
PDFOcrHelper::makeSearchable() is the one OCRmyPDF path of the package: applications use it for a searchable PDF (an invisible text layer over the page images), OcrMyPDFReader uses it with --sidecar to read the recognised text. Language data (TESSDATA_PREFIX with configs/), one Tesseract thread per call and the time limit ToolTimeout::forOcr() are set in this one place. All four entries in config/executables.json (ocrmypdf, ocrmypdf-sidecar, ocrmypdf-force, ocrmypdf-force-sidecar) carry the same switches and differ only in --skip-text/--force-ocr and the sidecar file; PDFOcrHelperTest keeps it that way.
| Switch | Why |
|---|---|
--rotate-pages --rotate-pages-threshold 5 |
Turns pages that were scanned sideways or upside down before recognition. Tesseract's orientation detection names the right orientation reliably, but with low confidences: on 8 real scanned statement pages turned by 90/180/270 degrees OCRmyPDF reported 1.4 to 14.0 and, at its default threshold of 14, turned none of the 24 pages; at 5 it turns 22. Over 106 catalogue pages (first and last pages, 387 orientations) the detected orientation was right every time, and no upright page would have been turned at any threshold down to 2. |
--deskew |
Straightens skewed pages, in the output as well. |
no --clean |
unpaper only cleans the image handed to Tesseract. On the scans measured it found no additional amount, cost 20 to 50 % more CPU time and needs unpaper, which is only a recommended package of OCRmyPDF. |
Measured on 14.10.2026 on the scanned bank statements of the conversion catalogue: how many booking amounts of the converted statement appear in the text layer, and the CPU time. All four variants (no switch, the former application setting --deskew --rotate-pages, the former reader setting --deskew --clean, all three) found the same amounts within one or two per document (upright pages). --deskew --rotate-pages cost 35 to 70 % more CPU time than no switch, --clean another 20 to 50 % on top (six scans, 4 to 8 pages each). The page segmentation mode stays as in v1: ocrmypdf_psm (11), 12 for pages that are not A4.
OCR language data and models (since 0.30.0)
TesseractDataHelper downloads the official tessdata files for the codes in SUPPORTED_LANGUAGES into data/tesseract/ on demand. A model that is not on that list - typically a fine-tuned one such as deu_finetuned.traineddata - is accepted as soon as its file lies in data/tesseract/ (or the configured tesseract_data_path); it is never downloaded. The setting tesseract_models maps a language code to such a model ({"deu": "deu_finetuned"}, type json), so callers keep asking for deu+eng and TesseractDataHelper::resolveModels() swaps the model in everywhere the language reaches Tesseract and in every OCR cache key. See data/tesseract/README.md.
Temporary Files (since 0.25.0)
Rendered pages, OCR output and extracted text are document content. All work files of the tool calls go through TempPath: directories 0700, files created exclusively with 0600, names from random_bytes instead of uniqid(). A tool that writes into a pre-created file only truncates it, so the permissions stay. The cross-process OCR cache (OcrCache, default <temp>/pdftoolkit_ocr) is created with 0700 and its entries with 0600; if the default directory belongs to another user, the cache switches itself off instead of reading or writing there. Since 0.25.3 entries live 24 hours by default (PDFSettings.ocr_cache_ttl, seconds) and expired entries are removed while writing (at most once per hour per process) or with OcrCache::prune() - before, an entry was only deleted when the same file was read again, so a document converted once stayed on disk. Tools that want a longer cache (e.g. a catalog test run) call OcrCache::configure(ttl: ..., directory: ...) for their own process and directory. ZugferdReader no longer builds the output path from the attachment name inside the PDF.
Hardening of Ghostscript and ImageMagick (since 0.24.1)
The input PDFs usually come from third parties, so the two programs that can execute or load more than the page content are restricted explicitly:
- Ghostscript - all Ghostscript commands in
config/executables.json(gs-crop,gs-resize,gs-compress,gs-compress-custom) pass-dSAFERas their first argument instead of relying on the default of the installed version. If you override one of these commands in your own configuration, keep-dSAFER. - ImageMagick - the deskew before OCR (
mogrify-deskew,mogrify-deskew-denoiseinTesseractReader) runs withMAGICK_CONFIGURE_PATHpointing toconfig/imagemagick/of this package. Itspolicy.xmlonly adds restrictions: no delegates, no@filearguments, no file, network or script coders (URL,FILE,HTTP,HTTPS,FTP,EPHEMERAL,MSL,TEXT,LABEL,CAPTION,PANGO,SCRIPT), and the resource limits of the Debian default policy. ImageMagick applies it together with the server's own policy, so the restrictions hold even where the system policy allows everything. mogrify only sees PNG pages thatpdftoppmrendered itself; the raster coders stay available.config/imagemagick/is part of the Composer package and must be deployed with it - without the folder, mogrify runs under the server policy alone.
Architecture
PDFReaderRegistry → [Readers by Priority] → PDFDocument
↓
PDFToTextReader (10) # Fast, for text PDFs
PDFBoxReader (30) # Complex layouts
TesseractReader (50) # OCR for scans
OcrMyPDFReader (60) # Best OCR quality
PDFWriterRegistry → [Writers by Priority] → PDF File
↓
DompdfWriter (10) # HTML→PDF, pure PHP
TcpdfWriter (20) # Programmatic, pure PHP
WkhtmltopdfWriter (30) # Best HTML rendering
License
AGPL-3.0-or-later - see LICENSE file.
Versions
Releases are tagged in Git; git tag --sort=-v:refname lists them (latest: v0.25.3). There is no separate changelog file — the tags and the commit history are the record.
The CI runs the MuPDF-based tests against MuPDF 1.17 and 1.21 as well (scripts/ci/mupdf-alt.sh, packages from the Debian pool with pinned checksums), because servers often run an older MuPDF than the development machine.