1tomany/pdf-pack-bundle
Symfony bundle wrapping the 1tomany/pdf-pack library for PDF extraction. Autowires read and convert actions to fetch metadata, rasterize pages to PNG, and extract text, with configurable Poppler binaries.
First, the honest framing: 1tomany/pdf-pack-bundle is a Symfony bundle, not a Laravel package. There is no ServiceProvider, no config/onetomany_pdfpack.php publisher, and no facade. The bundle itself is also archived with 1 star and 0 dependents. So for a Laravel codebase you have two realistic paths:
composer require 1tomany/pdf-pack (the underlying library) and wire the two action interfaces into Laravel's container yourself. You get 95% of the bundle's value with ~20 lines of glue.spatie/pdf-to-image is more idiomatic here.If you go with option 1, this is your minimal bootstrap.
1. Install the system binary. The whole stack shells out to Poppler (pdfinfo, pdftoppm, pdftotext). Don't assume your host has it:
RUN apt-get update && apt-get install -y --no-install-recommends poppler-utils \
&& rm -rf /var/lib/apt/lists/*
2. Install the library and publish config.
composer require 1tomany/pdf-pack
// config/pdfpack.php
return [
'client' => env('PDFPACK_CLIENT', 'poppler'),
'poppler' => [
'pdfinfo_binary' => env('PDFINFO_BINARY', 'pdfinfo'),
'pdftoppm_binary' => env('PDFTOPPM_BINARY', 'pdftoppm'),
'pdftotext_binary' => env('PDFTOTEXT_BINARY', 'pdftotext'),
],
];
3. Bind the interfaces. This is the direct Laravel translation of the bundle's autowiring:
// app/Providers/PdfPackServiceProvider.php
public function register(): void
{
$this->app->singleton(PopplerClient::class, fn () => new PopplerClient(
pdfinfoBinary: config('pdfpack.poppler.pdfinfo_binary'),
pdftoppmBinary: config('pdfpack.poppler.pdftoppm_binary'),
pdftotextBinary: config('pdfpack.poppler.pdftotext_binary'),
));
$this->app->bind(ReadActionInterface::class, fn ($app) =>
new ReadAction($app->make(PopplerClient::class)));
$this->app->bind(ExtractActionInterface::class, fn ($app) =>
new ExtractAction($app->make(PopplerClient::class)));
}
Where to look first. Read OneToMany\PdfPack\Contract\Action\ReadActionInterface and ExtractActionInterface, then the Request\ReadRequest / Request\ExtractRequest classes — they are immutable fluent builders, so the method chain in the README is the documentation.
First use case. The highest-value 30 minutes is: user uploads an invoice → ReadRequest to get page count and metadata → ExtractRequest(...)->asTextOutput() to pull searchable text into your DB. Everything else (rasterizing, thumbnails) is a variation on that.
Keep the handler shape from the bundle. The README's UploadFileHandler is a good pattern regardless of framework — a small readonly class that takes the action interfaces and returns a result object. In Laravel, inject it wherever you need it:
final readonly class PdfIngestHandler
{
public function __construct(
private ReadActionInterface $read,
private ExtractActionInterface $extract,
) {}
public function __invoke(string $path): DocumentMeta
{
$read = $this->read->act(new ReadRequest($path));
$text = '';
foreach ($this->extract->act(new ExtractRequest($path)->asTextOutput()) as $page) {
$text .= $page->getData();
}
return new DocumentMeta($read->getPageCount(), $text);
}
}
Always run extraction in a queued job. Poppler is CPU-bound and blocks a PHP-FPM worker for the duration. This is the single most important integration decision:
final class ExtractPdfJob implements ShouldQueue
{
public int $timeout = 300;
public int $tries = 3;
public array $backoff = [10, 60, 300];
public function __construct(public string $diskPath) {}
public function handle(ExtractActionInterface $extract): void
{
$local = Storage::path($this->diskPath);
$request = (new ExtractRequest($local))
->asPngOutput()
->atResolution(150);
foreach ($extract->act($request) as $index => $page) {
Storage::disk('s3')->put(
"previews/{$this->diskPath}/page-{$index}.png",
$page->getData()
);
}
}
}
Chunk by page range, not by "all pages." fromPage()/toPage() exists precisely so you can shard work. For a 400-page scan, dispatch N jobs of 20 pages each — memory and retry granularity both improve dramatically.
Swap the client via config, not code. The bundle's custom-client mechanism is a DI tag; in Laravel it's a container binding. Keep the same escape hatch so your config key decides:
$this->app->bind(ClientInterface::class, function ($app) {
return match (config('pdfpack.client')) {
'mock' => new MockClient(),
'magick' => $app->make(MagickClient::class),
default => $app->make(PopplerClient::class),
};
});
Then bind ReadActionInterface/ExtractActionInterface against ClientInterface::class rather than PopplerClient directly, so the swap works everywhere.
Use toDataUri() for inline previews. It saves you a round-trip to S3 when you're rendering a page thumbnail into a Blade view or an API payload.
Testing. Mirror the bundle's mock client idea with when@test semantics via phpunit.xml env:
<env name="PDFPACK_CLIENT" value="mock"/>
If MockClient doesn't cover your assertions, fake the interface instead — far more controllable:
$this->mock(ExtractActionInterface::class, function ($mock) {
$mock->shouldReceive('act')->andReturn((function () {
yield new ExtractResponse(data: 'fake-png-bytes');
})());
});
Validate before you extract. Reject on MIME + extension + a size ceiling before the request object ever gets built. Then confirm page count and cap it — a 5,000-page PDF is a denial-of-service vector against your queue.
The README config has a typo. pdftoppm_binary: "pdftppm" is wrong — it should be "pdftoppm". If you copy the sample verbatim you get a confusing "command not found" at runtime. Verify all three binary names resolve.
The package is archived. No upstream fixes, 1 star, 0 dependents, and releases are pinned to Symfony 8. Pin an exact version in composer.json and treat the library as vendored code you may need to patch. Do not build a roadmap dependency on it.
You cannot install the bundle into Laravel. It requires symfony/framework-bundle; Composer will refuse or you'll drag in a parallel DI container. Only the underlying 1tomany/pdf-pack library is usable. Any tutorial referencing config/packages/onetomany_pdfpack.yaml applies to you only as a vocabulary reference.
Generators fail mid-iteration. extract() returns a \Generator, meaning a failing pdftoppm process throws during the foreach, after you have already persisted pages 1–7. Wrap the loop in try/catch, and make your persistence idempotent (write to a temp prefix, then move on success) so retries don't duplicate files.
PATH differs between CLI and FPM. php artisan queue:work and php-fpm often resolve binaries differently. Always set absolute paths from env in production and run a deploy-time sanity check:
throw_unless(is_executable(config('pdfpack.poppler.pdftoppm_binary')), RuntimeException::class, 'pdftoppm missing');
Also check proc_open/exec aren't in disable_functions — shared hosts love to break this silently.
Resolution is a file-size cliff. 150 DPI is a sensible default for previews; 300 DPI on a 50-page document can produce hundreds of MB of PNGs. If you only need thumbnails, consider rendering page 1 only (fromPage(1)->toPage(1)), and prefer JPEG output if the library exposes it.
Temp files leak. Requests take a filesystem path, not a stream. If you're pulling from S3, you must download to a local temp path and clean it up in a finally block — the library will not delete it for you. Use storage_path('app/tmp') and prune, not sys_get_temp_dir() blindly.
Treat PDFs as hostile input. Poppler has a CVE history. Run extraction in an isolated worker/container with ulimit-style CPU and memory caps, never as a privileged process, and never on a web-facing synchronous request path.
Silent mock in production is a real risk. If PDFPACK_CLIENT falls back to mock because an env var is missing, you get plausible-looking fake output instead of an error. Add an explicit guard in AppServiceProvider::boot() that throws when app()->isProduction() && config('pdfpack.client') === 'mock'.
Debugging a failing extraction. Log the resolved binary path plus the full page range and DPI before calling act(). Most reported failures are one of: binary not found, wrong path, corrupted PDF, or an encrypted PDF — and Poppler's stderr is often swallowed by the wrapper. If you're stuck, run the equivalent pdftoppm -png -r 150 in.pdf out by hand inside the container to confirm it's a system problem, not a PHP one.
Extension point you'll actually use. Rather than a custom ClientInterface implementation, the pragmatic Laravel move is a decorator that adds logging/metrics around PopplerClient and gets bound in its place — same swap mechanism, zero reimplementation.
How can I help you explore Laravel packages today?