tecnickcom/tc-lib-pdf-filter
Pure-PHP decoder for standard PDF stream filters, including FlateDecode, LZWDecode, RunLengthDecode, ASCIIHexDecode, ASCII85Decode, CCITTFaxDecode, DCTDecode, JPXDecode and JBIG2Decode. Chain multiple filters in sequence, with typed exceptions for invalid or unknown filters. Requires PHP 8.2+ and zl...
Install and confirm the runtime contract first
composer require tecnickcom/tc-lib-pdf-filter
Two hard requirements from the README: PHP 8.2+ and the zlib extension. On a Laravel app still on 8.1 this is a non-starter, so check composer.json's platform block and your CI matrix before promising anything. In config/app.php-era terms there's nothing to register — it's a plain library, no service provider, no config file.
The one API you need to know
use Com\Tecnick\Pdf\Filter\Filter;
$filter = new Filter();
$decoded = $filter->decodeAll(['ASCIIHexDecode', 'FlateDecode'], $rawStreamBytes);
That is the entire public surface described in the README: pass an ordered array of filter names plus the raw bytes from a PDF stream object, get bytes back. A single filter is just a one-element array. Do not trust third-party snippets using Tecnickcom\TCPDF\Filters\* — the real namespace is \Com\Tecnick\Pdf\Filter.
Where to look first
src/ — the concrete decoders (Flate, LZW, ASCIIHex, ASCII85, RunLength, CCITT, DCT, JPX, JBIG2). Skim each to see what arguments they actually consume before designing around them.catch blocks.A realistic first use case
Not "decode a whole PDF" — the package can't parse a document. Your first win is much smaller: take a PDF whose content streams are /FlateDecode, pull the stream bytes out with whatever parser you already have (smalot/pdfparser, setasign/fpdi, or your own object walker), run them through decodeAll(), and get readable content-stream operators for text extraction, redaction auditing, or debugging a broken export from a client's legacy system.
Wrap it in a single domain service, not scattered calls
The library is stateless and cheap to instantiate, but you don't want new Filter() in fifteen controllers. Bind one class:
namespace App\Services\Pdf;
use Com\Tecnick\Pdf\Filter\Filter;
class StreamDecoder
{
public function __construct(private Filter $filter) {}
/** @param string[] $filters */
public function decode(array $filters, string $bytes): string
{
if ($filters === []) {
return $bytes;
}
return $this->filter->decodeAll($filters, $bytes);
}
}
// AppServiceProvider::register()
$this->app->singleton(StreamDecoder::class, fn () => new StreamDecoder(new Filter()));
Everything below hangs off that one seam — swapping implementations later, or mocking it in tests, costs one binding.
The real workflow: parse → collect filters → decode
decodeAll() is only one step. The surrounding pipeline you own:
/Filter — it may be a name (/FlateDecode) or an array ([/ASCII85Decode /FlateDecode])./DecodeParms alongside it (per-filter params, in the same order)./Encrypt is present (see Gotchas — this package does not do that).decodeAll($filtersInDocumentOrder, $bytes).Note the order: PDF lists filters in the order they were applied at encode time, and decoding walks that list front-to-back. Getting this backwards silently produces garbage rather than an error.
Queue the work, always
Anything that touches real PDFs is I/O- and CPU-heavy and prone to throwing on malformed input. Put it in a job:
class DecodePdfStreams implements ShouldQueue
{
use Queueable;
public function __construct(public string $disk, public string $path) {}
public function handle(StreamDecoder $decoder): void
{
$bytes = Storage::disk($this->disk)->get($this->path);
// ...walk objects, decode, emit results
event(new PdfStreamsDecoded($this->disk, $this->path));
}
}
Laravel's filesystem layer keeps large blobs out of your request lifecycle, and the event gives downstream steps (OCR, indexing, e-signature prep) a clean hook.
Expose it as an Artisan command for ops
php artisan pdf:decode storage/app/in/legacy.pdf --filter=FlateDecode
Batch/forensic use cases are almost always driven from the CLI, not the UI. One command with --dump-filters (list filters found, don't decode) is the single most valuable debugging tool you can build here.
Abstract over it if PDF work is core to your product
If decoding is load-bearing, define an interface (PdfStreamDecoder) with a tc-lib implementation and a qpdf-via-Process implementation behind it. You get an escape hatch without a rewrite, and you can A/B the pure-PHP path against the C tool on your own corpus.
The description overpromises on encryption
Packagist says "compression and encryption filters" but the README's feature list is decoders only, and PDF encryption is not a stream filter — it's the security handler (RC4/AES-128/AES-256) applied around the stream before /Filter runs. Assume you must decrypt elsewhere (qpdf, setasign/fpdi, or your own implementation) and validate this on a password-protected sample before it lands in an estimate. The Crypt class referenced in circulating AI-generated assessments is not in the documented API.
/DecodeParms, especially /Predictor, is the classic silent corruption
Flate/LZW streams for images and xref tables frequently carry /Predictor 12 (PNG Up) or /Predictor 2 (TIFF). If predictor post-processing isn't applied, the inflate succeeds and the bytes still look like noise. The README's decodeAll($filters, $data) signature shows no parameter channel for this — read the source before assuming the decoders accept params. If they don't, you own the predictor un-filtering step (/Columns, /Colors, /BitsPerComponent, /Predictor) as a post-pass.
Image codecs are a trap
DCTDecode (JPEG), JPXDecode (JPEG2000), and JBIG2Decode are image compression formats, not byte transforms. Pure PHP will either pass them through or reject them. Treat a stream with these filters as "already decoded pixels in a container" and hand it to GD/Imagick rather than expecting bytes from this library.
It's a decoder, not a PDF reader
It cannot find the streams for you, cannot resolve object references, cannot decompress an object stream's contents into usable objects without your parser feeding it. Budget for the parsing layer separately.
Binary safety around the decoded output
The return value is arbitrary bytes. Never log it raw, never mb_* it, never put it in a JSON column, and never let Laravel's default Strict-mode string handling touch it. Write straight to disk or a LONGBLOB; compare with hash_equals/md5 rather than string equality. When debugging, hex-dump the first 64 bytes rather than echoing.
No streaming API means a memory ceiling
decodeAll() takes and returns whole strings. A 200 MB Flate stream can inflate to several GB. Enforce an input size cap, raise memory_limit explicitly in the job (not globally), and fail fast with a domain exception when a stream exceeds your threshold. Never run this in a web request.
Wrap every call — malformed PDFs are the norm
Unknown or invalid filter names throw typed exceptions per the README. Catch narrowly around the decode step, log the filter name and object ID, and continue with the next stream. One bad object should not kill a 400-page document.
Validate output, don't trust it
qpdf --check or pdftk on reconstructed output, plus a known-good fixture pair (raw stream → expected bytes) in your test suite. Regression tests here are worth more than coverage metrics, because a decoding bug looks exactly like a rendering bug three layers up.
Licensing and bus factor
Packagist reports NOASSERTION; the README states LGPL-3.0. That matters if you link it into a proprietary product — get it reviewed rather than assuming permissive. And with ~6 stars and effectively no dependents, you are the support team: pin ^2.11, commit the lockfile, watch release notes, and keep the interface abstraction described above so a swap is a day's work, not a quarter's.
How can I help you explore Laravel packages today?