Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pdfparser Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Steps

  1. 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).

  2. Basic Text Extraction:

    use PrinsFrank\PdfParser\PdfParser;
    
    $document = (new PdfParser())->parseFile('path/to/file.pdf');
    $text = $document->getText();
    
  3. First Use Case: Extract metadata from a PDF:

    $title = $document->getInformationDictionary()?->getTitle();
    $author = $document->getInformationDictionary()?->getAuthor();
    $created = $document->getInformationDictionary()?->getCreationDate();
    

Where to Look First

  • README.md: Covers core functionality, installation, and basic usage. It was updated in v3.8.0 to document the new multi-column text extraction — read this first to see how column-aware output is exposed.
  • Documentation: Focus on the 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.
  • Benchmarks: Compare performance with alternatives in the benchmark repository.
  • Changelog: Pin to ^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.

Implementation Patterns

Common Workflows

  1. 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)
    }
    
  2. 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+
    
    • Column detection is heuristic: minimum gutter/column widths, minimum line counts, and a 20% of page width floor (a candidate column narrower than ~20% of the page is discarded). See Gotchas for tuning expectations.
    • Headers and footers are detected and separated for multi-column layouts — verify where they land in your output before assuming the first lines are body content.
  3. 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);
        }
    }
    
  4. Handling Encrypted PDFs:

    $document = (new PdfParser())->parseFile('encrypted.pdf', password: 'user_password');
    
  5. 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.

  6. 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".

  7. 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.

  8. 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.

Integration Tips

  • 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
        }
    }
    

Gotchas and Tips

Pitfalls

  1. Multi-Column Output Differs From 3.7 (new in 3.8.0):

    • Column-aware extraction and header/footer separation change the order and grouping of extracted text for multi-column PDFs. Snapshot tests, markdown fixtures, and cached extraction results must be regenerated. Single-column documents should be largely unaffected, but diff a real corpus before assuming so.
    • Post-processing that assumed naive reading order (e.g., regexes that stitch "column 1 + column 2" runs, or that strip the first N lines as header) can now mis-fire. Re-validate any custom de-interleaving logic.
  2. Column Detection Is Heuristic — Not Universal:

    • Tuned by min gutters/column widths, min line runs, and a 20% page-width floor for candidate columns. Very narrow sidebars, marginal notes, pull quotes, or tables may not be classified as columns, and pages with two genuinely distinct texts inside one column will not split. Don't rely on 100% recall — spot-check representative files.
    • If one resulting column is under ~20% of the page, it's deliberately discarded (treated as not-a-column). Layouts with a narrow sidebar will fall back to single-flow extraction, which may reorder content versus what you expect.
    • Headers/footers are separated for multi-column layouts; if you were previously stripping repeated first/last lines yourself, your stripping logic may now be redundant or remove body content.
  3. Page Boundary Calculation on Missing CropBox/MediaBox (new in 3.8.0):

    • Page boundaries now use actual offsets instead of a generic fallback when 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.
  4. GetFont Now Always Returns or Throws (new in 3.8.0):

    • Previously a 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.
  5. Whitespace in Dictionary Values (fixed in 3.5.0):

    • Prior releases could return values with leading/trailing whitespace, or drop array values that contained whitespace, producing false negatives in metadata extraction. Re-test any defensive 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).
  6. Date Metadata (improved in 3.5.0):

    • Date strings whose 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.
  7. Literal Strings and Line Continuations (fixed in v3.7.0):

    • End-of-line line continuations inside literal strings ((...\<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.
  8. Memory Usage:

    • Disable useInMemoryStream for large files to reduce memory footprint:
      $document = (new PdfParser())->parseFile('large_file.pdf', useInMemoryStream: false);
      
    • Use useFileCache for string parsing to avoid high memory usage:
      $document = (new PdfParser())->parseString($pdfString, useFileCache: true);
      
    • v3.8.0 adds internal caching (advance width, right edge, fonts) and early-exit optimisations in column grouping, so parsing of large multi-column documents is cheaper than in 3.7 — but it's still worth profiling before dropping your useFileCache/useInMemoryStream safeguards.
  9. Encrypted PDFs:

    • Always specify the password for encrypted files:
      $document = (new PdfParser())->parseFile('encrypted.pdf', password: 'correct_password');
      
    • Debugging: Check CONTRIBUTING.md for encryption-related issues; it also contains explicit guidelines on handling edge cases — worth reading before filing a parser bug.
  10. Text Extraction Quirks:

    • Subscript/superscript text may not group correctly in older PDFs. Use the improved algorithm in v3.0.0+.
    • Complex layouts (e.g., tables) may still require post-processing to reconstruct structure. Annotations and links are only as good as the PDF — plenty of generators emit link text without any /Annots entry at all, in which case no version gives you extra.
  11. Object Retrieval:

    • Objects are zero-indexed. Page 1 in the document is accessed via $document->getPage(0).
    • Type hints are strict. Ensure correct decorator classes are passed:
      $page = $document->getObject(42, Page::class); // Correct
      $page = $document->getObject(42); // May return a generic object
      
    • Annotation objects are reachable via the object graph — resolve them through the page decorators where possible rather than by raw object number.
  12. Image Handling:

    • Not all XObjects are images. Verify with $image->isImage() before processing.
    • Some images may lack metadata (e.g., getImageType() returns null). Handle gracefully:
      if (!$image->isImage() || !$image->getImageType()) continue;
      

Debugging Tips

  • 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.

Extension Points

  1. 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
        }
    }
    
  2. Override Parsing Logic: Subclass PdfParser to modify parsing behavior:

    class CustomPdfParser extends PdfParser {
        protected function parseObject(Object $object) {
            // Custom parsing logic
        }
    }
    
  3. 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.

  4. Hooks for Post-Processing: Use Laravel events or observer patterns to process extracted data after parsing:

    event(new PdfParsed($document));
    

Configuration Quirks

  • PHP Version: Requires PHP 8.2+. Use php -v to verify compatibility.
  • Dependencies: No external dependencies, but ensure ext-fileinfo is enabled for image type detection.
  • Performance Tuning: Adjust 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.
  • Version Pinning: v3.8.0 contains no breaking API changes, but the multi-column / header-footer detection changes text ordering and grouping, and the font-resolution and page-boundary changes alter edge-case behaviour. Pin ^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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin