prinsfrank/pdfparser
prinsfrank/pdfparser is an MIT-licensed PDF parsing library for PHP 8.1+ with no external dependencies. It is on average 2.6x faster and 5x more memory-efficient than smalot/pdfparser, supports encrypted PDFs, and is actively maintained.
Installation:
composer require prinsfrank/pdfparser:^3.8
Ensure your project meets the PHP 8.2+ requirement (unchanged since v3.0, still true in v3.8.0).
Basic Text Extraction:
use PrinsFrank\PdfParser\PdfParser;
$document = (new PdfParser())->parseFile('path/to/file.pdf');
$text = $document->getText();
First Use Case: Extract metadata from a PDF:
$title = $document->getInformationDictionary()?->getTitle();
$author = $document->getInformationDictionary()?->getAuthor();
$created = $document->getInformationDictionary()?->getCreationDate();
Document class and its methods (getText(), getPages(), getInformationDictionary()), the annotation/link handling added in v3.7.0, and the multi-column / header-footer detection introduced in v3.8.0.^3.8. The 3.4 → 3.8 line is almost entirely parsing-correctness, format-coverage, and layout-detection work — no breaking API changes, but extracted output can legitimately differ from earlier runs (see Gotchas). v3.8.0 in particular changes text ordering/structure for multi-column documents.Batch Processing:
$parser = new PdfParser();
$files = glob('path/to/pdf/*.pdf');
foreach ($files as $file) {
$document = $parser->parseFile($file);
$text = $document->getText();
// Process text (e.g., save to DB, search for keywords)
}
Multi-Column Text Extraction (new in v3.8.0): The parser now detects column layouts (gutter-based) and separates headers/footers, so newsletters, academic papers, reports, and newspaper-style PDFs no longer return interleaved, unreadable text. This is the headline reason to upgrade.
$document = (new PdfParser())->parseFile('two-column-report.pdf');
$text = $document->getText(); // column-aware ordering in 3.8+
Extracting Images:
foreach ($document->getPages() as $page) {
foreach ($page->getImages() as $image) {
$content = $image->getContent();
$extension = $image->getImageType()?->getFileExtension();
file_put_contents("images/{$page->getPageNumber()}_{$extension}", $content);
}
}
Handling Encrypted PDFs:
$document = (new PdfParser())->parseFile('encrypted.pdf', password: 'user_password');
Positioned Text Extraction:
foreach ($document->getPages() as $page) {
$textElements = $page->getPositionedTextElements();
foreach ($textElements as $element) {
echo "Text: {$element->getText()}, BBox: {$element->getBoundingBox()}\n";
}
}
v3.8.0 caches advance width, right edge, and fonts on positioned text elements, so tight loops over getPositionedTextElements() are measurably cheaper than in 3.7 — prefer this API over recomputing bounding boxes yourself.
Reading Annotations and Links (v3.7.0): Annotation objects are parsed as first-class objects instead of being skipped. Useful for contracts ("initial here"), invoices (payment links), and any document where the link target is as important as the visible text:
foreach ($document->getPages() as $page) {
foreach ($page->getAnnotations() as $annotation) {
// Inspect the annotation dictionary for /Subtype, /A (/URI), /Dest, /Rect
$rect = $annotation->getDictionary()->get('Rect');
$action = $annotation->getDictionary()->get('A');
// Persist uri + rect so you can map links back to extracted text
}
}
Match annotation rectangles against getPositionedTextElements() bounding boxes if you need "which visible text does this link belong to".
Markdown Output with Links (v3.7.0): Link elements are carried through into markdown output rather than being flattened to bare text, so converted documents keep clickable references:
// Wherever you render to markdown, links now appear as [text](uri)
$markdown = $document->toMarkdown(); // verify exact method name in README for 3.8
If you previously post-processed plain text with regexes to re-attach links from a separate pass, you can often drop that step.
Standalone Extraction (phar): For one-off jobs, ops scripts, or CI steps where you don't want the package in your application's composer.json, use the .phar build documented in the README. Keep it out of app/ — it's a tool, not a runtime dependency.
Laravel Service Provider: Bind the parser as a singleton for reusable access:
$this->app->singleton(PdfParser::class, function ($app) {
return new PdfParser();
});
Queue Jobs for Large Files: Use Laravel queues to process large PDFs asynchronously:
ParsePdfJob::dispatch('path/to/large_file.pdf')->onQueue('pdf-processing');
Store Extracted Data: Save metadata or text to a database:
$metadata = [
'title' => $document->getInformationDictionary()?->getTitle(),
'author' => $document->getInformationDictionary()?->getAuthor(),
'text' => $document->getText(),
];
PdfMetadata::create($metadata);
Detect Layout Before Choosing a Renderer (new in v3.8.0):
If you feed extracted text into an LLM or a search index, layout matters. For column-heavy PDFs, 3.8's column-aware getText() output is generally more faithful than 3.7's; for single-column documents nothing should change. Consider branching on whether headers/footers/columns were detected before post-processing.
Normalising Dates in Ingest Pipelines: Since v3.5.0, partial dates and quoted Z offsets parse correctly, so metadata ingestion no longer needs pre-cleaning:
$created = $document->getInformationDictionary()?->getCreationDate();
// May be partial (e.g. year only) — store with reduced precision rather than casting blindly
Defensive Reads for Null Dictionary Values (v3.7.0):
/Null entries inside dictionary values are supported instead of tripping the value parser. A previously throwing document may now parse and return a structurally "null" value. Keep null-coalescing on every dictionary read you surface to users.
Legacy PDFs with Mixed Cross-Reference Tables/Streams (v3.7.0): Documents whose xref chain mixes classic tables and xref streams — and streams whose headers are written without the usual whitespace — now resolve. If you maintain a "these PDFs fail" blocklist, retest it before keeping workarounds.
Custom Decorators:
Extend existing decorators (e.g., Page) to add domain-specific logic:
class CustomPageDecorator extends Page {
public function getCustomData() {
// Add logic here
}
}
Multi-Column Output Differs From 3.7 (new in 3.8.0):
Column Detection Is Heuristic — Not Universal:
Page Boundary Calculation on Missing CropBox/MediaBox (new in 3.8.0):
CropBox/MediaBox are absent. This can shift bounding boxes reported by getPositionedTextElements() slightly for malformed PDFs — re-baseline any coordinate assertions or annotation-rect overlap calculations.GetFont Now Always Returns or Throws (new in 3.8.0):
null return followed by a throw; now it throws directly when a font can't be resolved. If you wrapped font access in null checks, they won't fire — catch the exception path instead. This is a behavioural change in edge cases even though the public API signature is unchanged.Whitespace in Dictionary Values (fixed in 3.5.0):
trim() / regex workarounds on 3.5+ — they may now be unnecessary, and previously "missing" fields may start appearing (check your nullable handling and DB column defaults).Date Metadata (improved in 3.5.0):
Z offset sign is followed by quoted hours/minutes now parse, and partial dates (year-only, year-month) are supported. Consequence: getCreationDate() can now return a value where it previously returned null — and that value may have less than day precision. Don't assume full precision when writing to datetime columns.Literal Strings and Line Continuations (fixed in v3.7.0):
(...\<newline>...)) are now ignored per spec. Prior versions could embed stray backslash-newline pairs or split a logical string into two, which showed up as broken words and truncated metadata values. On upgrade, diff your extracted text: values that used to be split or contain a literal \ + newline should now be clean, so any regex you wrote to stitch them back together should be removed or it may double-process.Memory Usage:
useInMemoryStream for large files to reduce memory footprint:
$document = (new PdfParser())->parseFile('large_file.pdf', useInMemoryStream: false);
useFileCache for string parsing to avoid high memory usage:
$document = (new PdfParser())->parseString($pdfString, useFileCache: true);
useFileCache/useInMemoryStream safeguards.Encrypted PDFs:
$document = (new PdfParser())->parseFile('encrypted.pdf', password: 'correct_password');
CONTRIBUTING.md for encryption-related issues; it also contains explicit guidelines on handling edge cases — worth reading before filing a parser bug.Text Extraction Quirks:
/Annots entry at all, in which case no version gives you extra.Object Retrieval:
$document->getPage(0).$page = $document->getObject(42, Page::class); // Correct
$page = $document->getObject(42); // May return a generic object
Image Handling:
$image->isImage() before processing.getImageType() returns null). Handle gracefully:
if (!$image->isImage() || !$image->getImageType()) continue;
Regression-Test Your Own Fixtures on Upgrade: v3.8.0 changes text ordering/structure for multi-column documents (column detection, header/footer separation, 20% page-width cutoff, corrected page boundaries), while 3.7.0 changed parsing (null dictionary values, mixed xref chains, whitespace-free stream headers, literal-string continuations) and 3.5.0 changed value parsing. Run a small corpus of real customer PDFs through both versions and diff getText(), markdown output, metadata, and any bounding-box-derived data before shipping the bump.
Log Parsing Issues: Inspect raw objects for inspection:
$document = (new PdfParser())->parseFile('problematic.pdf');
$rawObject = $document->getObject(123); // Inspect problematic object
Check PDF Validity: Use tools like PDF.js or PDFtk to validate PDF structure before parsing. If a document extracts sensibly in a viewer but not here, compare column structure visually before filing — column detection is heuristic.
Memory Profiling: Use Laravel Telescope or Xdebug to monitor memory usage during parsing.
Custom Object Decorators:
Extend existing decorators (e.g., Page, XObject) to add domain-specific methods:
class CustomPage extends Page {
public function getHighlightedText(string $keyword) {
// Implement custom logic
}
}
Override Parsing Logic:
Subclass PdfParser to modify parsing behavior:
class CustomPdfParser extends PdfParser {
protected function parseObject(Object $object) {
// Custom parsing logic
}
}
Add New Object Types:
Implement new decorators for unsupported PDF objects by extending the base Object class. v3.5.0 added missing typeNameValue entries and v3.7.0 added annotation objects and /Null dictionary values — check whether a type you previously handled manually is now supported natively before adding a decorator.
Hooks for Post-Processing: Use Laravel events or observer patterns to process extracted data after parsing:
event(new PdfParsed($document));
php -v to verify compatibility.ext-fileinfo is enabled for image type detection.useInMemoryStream and useFileCache based on file size and server resources. v3.8.0 introduces additional internal caching (advance widths, right edges, fonts) with no config knob — beneficial for getPositionedTextElements()-heavy code paths.^3.8 and re-run any snapshot tests, markdown fixtures, or cached extraction results after the bump. If you need 3.7's flat reading order, pin ^3.7 (or <3.8) explicitly rather than relying on range resolution.How can I help you explore Laravel packages today?