DEV Community

johind
johind

Posted on

I Built a Laravel Package for PDF Manipulation (Because Nothing Else Felt Right)

I was working on a client project where we needed to process uploaded PDFs before sending them to an AI model. The documents were multi-page city directory scans, and the AI worked best when fed individual pages rather than entire documents. Simple enough, I just needed to split PDFs.

So I started looking for a Laravel package. I found a few options, but nothing that felt right.

The Problem with PDF in the Laravel Ecosystem

The Laravel ecosystem offers excellent packages for generating PDFs, such as Spatie's laravel-pdf. However, when it comes to manipulating existing PDFs (e.g. splitting, merging, extracting pages or adding watermarks), the story is different.

Some of this can be done with existing tools. For example, FPDI can combine pages, and you can wrap CLI tools like qpdf or Ghostscript to split documents. But that leaves you writing all the integration code yourself. There's no filesystem disk support, no service container integration, and no fluent API.

I wanted something with a facade, chainable methods, and proper Storage integration. Something that feels like writing Eloquent queries, not fighting a CLI tool.

Meet Collate

Collate is a Laravel package for PDF manipulation, powered by qpdf under the hood. Here's what my original problem looks like with it:

use Johind\Collate\Facades\Collate;

$pages = Collate::open($request->file('directory-scan'))
    ->split('ai-processing/scan-page-{page}.pdf');

// $pages โ†’ Collection ['ai-processing/scan-page-1.pdf', ...]
// Now feed each page to your AI pipeline
Enter fullscreen mode Exit fullscreen mode

That's it. The uploaded file is handled, pages are split, temp files are cleaned up automatically, and you get a collection of paths back.

But once I had the foundation in place, I kept going. I felt like this was missing in the ecosystem, and honestly it was just fun to build, so it grew into a full toolkit.

What Else Can You Do?

Merge multiple documents:

Collate::merge('cover.pdf', 'report.pdf', 'appendix.pdf')
    ->save('complete-report.pdf');
Enter fullscreen mode Exit fullscreen mode

Watermark a document with an overlay:

Collate::open('proposal.pdf')
    ->overlay('branding/confidential-stamp.pdf')
    ->save('stamped-proposal.pdf');
Enter fullscreen mode Exit fullscreen mode

Chain a whole workflow together. Add standard terms, set metadata, encrypt, and push to S3 in one go:

Collate::open($request->file('document'))
    ->addPages('legal/standard-terms.pdf')
    ->withMetadata(title: 'Client Report 2025')
    ->encrypt('client-password')
    ->toDisk('s3')
    ->save('reports/final.pdf');
Enter fullscreen mode Exit fullscreen mode

Extract or remove specific pages:

Collate::open('document.pdf')
    ->onlyPages([1, 2, 3])
    ->save('first-three-pages.pdf');

Collate::open('document.pdf')
    ->removePages('5-10')
    ->save('trimmed.pdf');
Enter fullscreen mode Exit fullscreen mode

Rotate pages, flatten forms, optimize for web:

Collate::open('scanned.pdf')
    ->rotate(90, range: '1-3')
    ->flatten()
    ->linearize()
    ->save('cleaned-up.pdf');
Enter fullscreen mode Exit fullscreen mode

Read metadata and page counts without modifying anything:

$meta = Collate::inspect('document.pdf')->metadata();
$meta->title;  // 'Quarterly Report'
$meta->author; // 'Taylor Otwell'

$count = Collate::inspect('document.pdf')->pageCount();
Enter fullscreen mode Exit fullscreen mode

Everything chains.

Built for Laravel

A few things that make it feel native:

Storage disk integration. Read from S3, write to local, or the other way around. Collate handles the temp file dance for remote disks transparently:

Collate::fromDisk('s3')
    ->open('uploads/contract.pdf')
    ->toDisk('local')
    ->save('processed/contract.pdf');
Enter fullscreen mode Exit fullscreen mode

It's Responsable. Return a PendingCollate directly from a controller and Laravel streams it to the browser. There's also download() and stream() if you need more control:

public function show()
{
    return Collate::open('invoice.pdf');
}
Enter fullscreen mode Exit fullscreen mode

Conditional operations via Laravel's Conditionable trait:

Collate::open('document.pdf')
    ->when($request->boolean('watermark'), fn ($pdf) => $pdf->overlay('watermark.pdf'))
    ->when($request->boolean('flatten'), fn ($pdf) => $pdf->flatten())
    ->save('output.pdf');
Enter fullscreen mode Exit fullscreen mode

Macros for your own reusable operations:

PendingCollate::macro('stamp', function () {
    return $this->overlay('assets/stamp.pdf');
});

Collate::open('contract.pdf')->stamp()->save('stamped.pdf');
Enter fullscreen mode Exit fullscreen mode

Requirements

Collate requires PHP 8.4+, Laravel 11 or 12, and qpdf v11.0.0+ installed on your system. Installation is straightforward:

composer require johind/collate
php artisan collate:install
Enter fullscreen mode Exit fullscreen mode

The install command publishes the config and verifies that qpdf is available.

Wrapping Up

I built Collate because I needed it for a real project and couldn't find something that felt at home in Laravel. What started as a PDF splitter turned into something I think the ecosystem was missing. The package ships with a full test suite, so you can trust it in production.

If you work with existing PDFs in Laravel, such as merging or splitting documents for processing, adding watermarks, encrypting or optimising them for the web, give it a try. The full list of operations can be found in the README.

โ†’ GitHub ยท Packagist

I'm actively working on the package, so feedback and issues are very welcome.

Top comments (0)