Laravel PDF package to merge, split, extract pages, watermark, encrypt, and optimize PDFs using qpdf.
johind/collate is a Laravel package for laravel pdf package to merge, split, extract pages, watermark, encrypt, and optimize pdfs using qpdf..
It currently has 12 GitHub stars and 931 downloads on Packagist (latest version v1.5.3).
Install it with composer require johind/collate.
Discover more Laravel packages by johind
or browse all Laravel packages to compare alternatives.
Last updated
Collate brings fluent PDF workflows to Laravel, giving your app expressive, chainable tools to compose, transform, secure, and deliver PDFs.
Powered by qpdf, it supports common operations including merging, splitting, extracting pages, watermarking, encryption, editing metadata, and web optimisation.
Install the package via Composer:
composer require johind/collate
Then run the install command to publish the configuration and verify that qpdf is available:
php artisan collate:install
You may also publish the config file manually:
php artisan vendor:publish --tag="collate-config"
The published config file (config/collate.php) contains three options:
return [
// Path to the qpdf binary (default: 'qpdf')
'binary_path' => env('COLLATE_BINARY_PATH', 'qpdf'),
// Default filesystem disk for reading/writing PDFs (default: null, uses your app's default disk)
'default_disk' => env('COLLATE_DISK'),
// Directory for temporary files during processing (automatically cleaned up)
'temp_directory' => env('COLLATE_TEMP_DIR', storage_path('app/collate')),
];
use Johind\Collate\Facades\Collate;
// Prepare an uploaded document for archival
Collate::open($request->file('document'))
->addPages('legal/standard-terms.pdf')
->withMetadata(title: 'Client Report 2025')
->encrypt('client-password')
->toDisk('s3')
->save('reports/final.pdf');
// Merge and optimize multiple files for web viewing
Collate::merge('cover.pdf', 'chapter-1.pdf', 'chapter-2.pdf')
->overlay('branding/watermark.pdf')
->linearize()
->save('book.pdf');
| Category | Features | |---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Getting started | open · choose a disk · save · download · stream · raw content | | Page operations | merge · split · add · remove · extract · rotate | | Overlays & watermarks | overlay & underlay | | Security | encrypt / decrypt · restrict permissions | | Metadata & inspection | read metadata · write metadata · strip metadata · inspect · page count | | Optimization | flatten · linearize · optimize | | Advanced | conditional operations · macros · debugging · error handling |
Use open() to manipulate an existing PDF, or merge() to combine multiple files. Both return a fluent builder you can
chain before saving or returning a response.
use Johind\Collate\Facades\Collate;
$pending = Collate::open('invoices/2024-001.pdf');
Files are resolved from your configured filesystem disk. You can also pass UploadedFile instances:
Collate::open($request->file('document'));
Switch disks on the fly using fromDisk():
Collate::fromDisk('s3')->open('reports/quarterly.pdf')->toDisk('local')->save('quarterly.pdf');
Collate::open('input.pdf')->save('output.pdf');
Return a download response from a controller. The filename defaults to document.pdf when omitted:
return Collate::open('invoice.pdf')
->encrypt('client-password')
->download('invoice-2024-001.pdf');
Display the PDF inline in the browser. The filename defaults to document.pdf when omitted:
return Collate::merge('cover.pdf', 'report.pdf')
->linearize()
->stream('quarterly-report.pdf');
Get the raw PDF binary contents as a string. Useful for APIs, email attachments, or custom storage:
$content = Collate::open('document.pdf')->content();
PendingCollate implements Laravel's Responsable interface, so you can return it directly from a controller. By
default, the PDF is displayed in the browser:
public function show()
{
return Collate::open('invoice.pdf');
}
Combine multiple files into a single document:
Collate::merge(
'documents/cover.pdf',
'documents/chapter-1.pdf',
'documents/chapter-2.pdf',
)->save('documents/book.pdf');
// Also accepts a single array of files
Collate::merge(['doc1.pdf', 'doc2.pdf'])->save('merged.pdf');
For more control, pass a closure to select specific pages:
use Johind\Collate\PendingCollate;
Collate::merge(function (PendingCollate $pdf) {
$pdf->addPage('documents/cover.pdf', 1);
$pdf->addPages('documents/appendix.pdf', range: '1-3');
})->save('documents/book.pdf');
Append entire files or specific pages to an existing document:
Collate::open('report.pdf')
->addPage('appendix.pdf', pageNumber: 3) // single page from another file
->addPages('terms.pdf', range: '1-5') // page range
->addPages(['exhibit-a.pdf', 'exhibit-b.pdf']) // multiple complete files
->save('final-report.pdf');
[!IMPORTANT] The
rangeparameter cannot be used when passing an array of files. Chain multipleaddPages()calls instead.
Remove specific pages from a document:
Collate::open('document.pdf')
->removePage(3)
->save('without-page-3.pdf');
Collate::open('document.pdf')
->removePages([1, 3, 5])
->save('trimmed.pdf');
// Remove a range of pages
Collate::open('document.pdf')
->removePages('5-10')
->save('trimmed.pdf');
Keep only the pages you need using onlyPages():
Collate::open('document.pdf')
->onlyPages([1, 2, 3])
->save('first-three-pages.pdf');
// Also accepts qpdf range expressions
Collate::open('document.pdf')
->onlyPages('1-5,8,11-z')
->save('selected-pages.pdf');
[!WARNING]
onlyPages()andremovePages()are mutually exclusive and neither can be called more than once — calling both, or calling either twice, on the same instance will throw aBadMethodCallException.
Anywhere a page range string is accepted (onlyPages(), addPages(), removePages(), rotate()), you can
use qpdf range syntax:
| Expression | Meaning |
|------------|-------------------|
| 1-5 | Pages 1 through 5 |
| 1,3,5 | Pages 1, 3, and 5 |
| 1-3,7-9 | Pages 1–3 and 7–9 |
| z | Last page |
| 1-z | All pages |
| 1-z:odd | Odd pages only |
| 1-z:even | Even pages only |
Split every page into its own file. The path supports a {page} placeholder for the page number:
$paths = Collate::open('multi-page.pdf')
->split('pages/page-{page}.pdf');
// $paths → Collection ['pages/page-1.pdf', 'pages/page-2.pdf', ...]
[!IMPORTANT] Always include
{page}in your path. Without it, every page will be written to the same destination, with each one overwriting the last.
All operations (page selection, rotation, overlays, etc.) are applied before splitting, so you can chain them freely:
Collate::open('scanned.pdf')
->rotate(90)
->onlyPages('1-5')
->split('pages/page-{page}.pdf');
Rotate pages by 0, 90, 180, or 270 degrees:
Collate::open('scanned.pdf')
->rotate(90)
->save('rotated.pdf');
// Rotate specific pages only
Collate::open('scanned.pdf')
->rotate(90, range: '1-3')
->rotate(180, range: '5')
->save('fixed.pdf');
Add watermarks, letterheads, or backgrounds. Both methods accept a disk path or an UploadedFile instance:
// Overlay (on top — watermarks, stamps)
Collate::open('document.pdf')
->overlay('watermark.pdf')
->save('watermarked.pdf');
// Underlay (behind — backgrounds, letterheads)
Collate::open('content.pdf')
->underlay('letterhead.pdf')
->save('branded.pdf');
Encrypt a document with a password:
Collate::open('confidential.pdf')
->encrypt('secret')
->save('protected.pdf');
For more control, use separate user and owner passwords and restrict specific permissions. Note that restrict() must
be called after encrypt():
Collate::open('confidential.pdf')
->encrypt(
userPassword: 'secret',
ownerPassword: 'more-secret',
bitLength: 256,
)
->restrict('print', 'extract')
->save('locked.pdf');
The following permissions can be passed to restrict():
| Permission | Effect |
|-----------------|----------------------------------------------------|
| print | Disallow printing |
| modify | Disallow modifications |
| extract | Disallow text and image extraction |
| annotate | Disallow adding annotations |
| assemble | Disallow page assembly (inserting, rotating, etc.) |
| print-highres | Disallow high-resolution printing |
| form | Disallow filling in form fields |
| modify-other | Disallow all other modifications |
Decrypt a password-protected document:
Collate::open('locked.pdf')
->decrypt('secret')
->save('unlocked.pdf');
Re-encrypt with a new password in one step:
Collate::open('locked.pdf')
->decrypt('old-password')
->encrypt('new-password')
->save('re-encrypted.pdf');
Use inspect() (a semantic alias for open()) for read-only operations like reading metadata or counting pages:
$meta = Collate::inspect('document.pdf')->metadata();
$meta->title; // 'Quarterly Report'
$meta->author; // 'Taylor Otwell'
$meta->subject;
$meta->keywords;
$meta->creator;
$meta->producer;
$meta->creationDate;
$meta->modDate;
$count = Collate::inspect('document.pdf')->pageCount();
pageCount() and metadata() are also available on the builder if you need them mid-chain, even after a merge():
Collate::merge('doc1.pdf', 'doc2.pdf')
->when(fn ($pdf) => $pdf->pageCount() > 10, fn ($pdf) => $pdf->rotate(90))
->save('merged.pdf');
Set metadata on the output document:
Collate::open('document.pdf')
->withMetadata(
title: 'Annual Report 2024',
author: 'Taylor Otwell',
)
->save('branded-report.pdf');
// Also accepts a PdfMetadata instance (named parameters override its values)
$meta = Collate::inspect('source.pdf')->metadata();
Collate::open('target.pdf')
->withMetadata($meta, author: 'New Author')
->withMetadata(title: 'Updated Title')
->save('output.pdf');
[!NOTE] When you pass a
PdfMetadatainstance, you can override any named fields in the same call excepttitle. To change the title, callwithMetadata()again withtitle:as shown above.
Remove all metadata from the output document:
Collate::open('document.pdf')
->withoutMetadata()
->save('clean.pdf');
[!WARNING]
withoutMetadata()andwithMetadata()are mutually exclusive. Calling both on the same instance will throw aBadMethodCallException.
Use inspect() to query properties of an existing document without modifying it:
$pdf = Collate::inspect('document.pdf');
$pdf->isEncrypted(); // true if the document is encrypted
$pdf->hasPassword(); // true if a password is required to open the document
$pdf->isLinearized(); // true if the document is linearized for fast web viewing
$pdf->pdfVersion(); // e.g. '1.7', '2.0'
$pdf->pageSize(); // PageSize { width: 612.0, height: 792.0 }
$pdf->pageSize(3); // dimensions of a specific page
inspect() is a semantic alias for open(). You can also call these methods mid-chain on any builder.
pageSize() returns the underlying page box dimensions in PDF points, not a rotation-adjusted display size. The returned
PageSize object includes conversion helpers:
$size = Collate::inspect('document.pdf')->pageSize();
$size->widthInInches();
$size->heightInInches();
$size->widthInMillimeters();
$size->heightInMillimeters();
Flatten form fields and annotations into the page content:
Collate::open('form-filled.pdf')->flatten()->save('flattened.pdf');
Optimize a PDF for fast web viewing (progressive loading):
Collate::open('large-report.pdf')->linearize()->save('web-optimized.pdf');
Reduce file size by removing redundant data and optimizing internal structures:
Collate::open('bloated.pdf')->optimize()->save('smaller.pdf');
When optimize() is combined with linearize(), qpdf's linearization requirements take precedence over object stream
generation. Other optimization steps still apply.
Collate::open('form-filled.pdf')
->flatten()
->optimize()
->linearize()
->save('optimized.pdf');
PendingCollate uses the Conditionable trait, so you can conditionally apply operations:
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');
Register macros on PendingCollate to add chainable operations:
use Johind\Collate\PendingCollate;
PendingCollate::macro('stamp', function () {
return $this->overlay('assets/stamp.pdf');
});
Collate::open('contract.pdf')->stamp()->save('stamped.pdf');
Register macros on Collate to add new entry points:
use Johind\Collate\Collate;
Collate::macro('openInvoice', function (int $invoiceId) {
return $this->open("invoices/{$invoiceId}.pdf");
});
Collate::openInvoice(2024001)->download();
Use dump() and dd() to inspect the underlying qpdf command that Collate builds, without executing it:
Collate::open('document.pdf')
->rotate(90)
->encrypt('secret')
->dump(); // dumps the command and continues the chain
Collate::open('document.pdf')
->overlay('watermark.pdf')
->dd(); // dumps the command and stops execution
[!WARNING] The output may contain sensitive data such as file paths and passwords.
All exceptions thrown by Collate extend Johind\Collate\Exceptions\CollateException, which itself extends PHP's
RuntimeException.
When a qpdf command fails, a Johind\Collate\Exceptions\ProcessFailedException is thrown, exposing the exitCode
and errorOutput from the underlying process. Invalid arguments (bad page ranges, unsupported rotation degrees, etc.)
throw standard InvalidArgumentException or BadMethodCallException instances.
use Johind\Collate\Exceptions\ProcessFailedException;
try {
Collate::open('corrupted.pdf')->save('output.pdf');
} catch (ProcessFailedException $e) {
$e->exitCode; // qpdf exit code
$e->errorOutput; // stderr from qpdf
}
Please see CHANGELOG for more information on what has changed recently.
Thank you for your help in keeping Collate stable! I am primarily looking for contributions that focus on fixing bugs, improving error handling or enhancing performance. If you have an idea for a new feature, please open an issue to discuss it with me first, since I want to ensure that the scope of the package remains focused. Please note that I do not provide monetary compensation for contributions.
Before submitting a code change, run composer check. Collate uses Pest for all tests; please do not add PHPUnit test
classes or run the suite through vendor/bin/phpunit.
If you discover a security vulnerability, please send an email rather than opening a GitHub issue.
The MIT License (MIT). Please see License File for more information.