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...
Architecture fit. tecnickcom/tc-lib-pdf-filter is a stateless, leaf-level utility: bytes in, bytes out. It has no coupling to Laravel, no global state, no I/O, and no container awareness. That makes it architecturally safe to drop behind a thin adapter in the App\Support\Pdf layer, but it also defines its ceiling — the package occupies exactly one stage of a PDF ingestion pipeline (stream decoding). It performs no parsing of the PDF object graph, no xref resolution, no page traversal, and no rendering. A TPM should frame this internally as "the decompression primitive," not "PDF support." Any roadmap item phrased as "read/extract/search PDFs" is at best 10–15% addressed by this dependency; the remaining work (parser, stream extraction, /DecodeParms retrieval, output post-processing) is ours to build or buy elsewhere.
Scope correction worth raising before commitment. The package description claims "decode PDF compression and encryption filters," but the README feature list contains only compression/transform filters (FlateDecode, LZWDecode, RunLengthDecode, ASCIIHexDecode, ASCII85Decode, CCITTFaxDecode, DCTDecode, JPXDecode, JBIG2Decode). Password-based decryption in PDF requires the Standard Security Handler plus RC4/AES, which is not enumerated. Prior stakeholder framing that this "decrypts password-protected PDFs" should be treated as unvalidated. If decryption is the actual business driver (legal/finance workflows), this package is likely the wrong purchase and the business case needs rework before engineering time is spent.
Integration feasibility. composer require plus ext-zlib; no service provider is required, and the public surface is small (Filter::decodeAll(array $filters, string $data) plus typed exceptions). Effort to first working call: hours. Effort to a production-safe pipeline: weeks, because of the surrounding concerns below.
Notable discrepancy: internal notes reference a Tecnickcom\TCPDF\Filters namespace and new FlateDecode() style instantiation, while the README specifies \Com\Tecnick\Pdf\Filter\Filter with a filter-name-list API. The README is authoritative; the inconsistency signals that any internal design docs drafted from memory must be re-verified against source before estimates are trusted.
Technical risk.
| Risk | Severity | Notes / mitigation |
|---|---|---|
| Unsupported encryption despite description | High | Verify against fixtures early; block any roadmap claim until proven |
| Binary filter coverage (JPX/JBIG2/DCT) | Medium–High | Pure-PHP JPEG2000/JBIG2 decoding is unlikely; expect passthrough or partial output. Test explicitly |
/Predictor handling on FlateDecode |
Medium | PNG predictors are ubiquitous in xref and content streams; missing support breaks most real-world files |
| Documentation/namespace drift | Medium | Read source, not docs; write our own contract tests |
| License ambiguity | Medium | README says LGPL-3.0; registry reports NOASSERTION. Legal must confirm; LGPL is workable for hosted SaaS, requires review if we ship on-prem/appliance artifacts |
| Low adoption surface (6 stars, 0 dependents) | Medium | Stars understate backing — this is an extracted component of the TCPDF/tc-lib-pdf ecosystem (100M+ installs) — but the package itself has near-zero independent usage, so we will be an early adopter of the split |
| Single-maintainer bus factor | Medium | Contingency: vendor fork of a ~100KB library is cheap |
| Release metadata anomaly | Low | Latest release dated in the future (2026-10-17); release notes are auto-generated compare links only, so "patch cadence" is real but change intent is undocumented — diff review required before bumps |
| Memory / decompression bombs | Medium | Whole-buffer decode API; requires input caps and ratio guards at the boundary |
| PHP floor | Low | Requires PHP 8.2+; conflicts with stale assessments claiming 7.4. Confirm fleet parity |
Key questions.
smalot/pdfparser, setasign/fpdi)? Several already decode Flate internally — double-decoding is a real corruption risk./Filter values before committing.decodeAll correctly invert filter order and accept /DecodeParms? If not, we own that logic.Stack fit. Laravel 10+ on PHP 8.2/8.3, ext-zlib present in the base image, Composer-managed. The natural home is a single PdfFilterService bound as a singleton (the library is stateless and cheap; the binding is for injection ergonomics, not lifecycle). Everything else — storage, dispatch, persistence — stays native Laravel: Storage for stream acquisition, queued jobs for batching, events for post-decode hooks, an Artisan command for backfill/reprocessing.
Compatibility constraints to check before scheduling.
composer.json platform constraints vs. our PHP floor; 8.2 minimum is a hard gate on older services.tc-lib-pdf-* packages or TCPDF itself — pin a consistent 2.11.x line across the graph to avoid resolution conflicts.smalot/pdfparser is present, do not run both decode paths on the same stream.Migration path. This is additive, not a replacement. Nothing is migrated; the package slots in where we currently shell out to qpdf/pdftk for decompression, or where we have bespoke gzuncompress calls. Treat any existing hand-rolled gzuncompress usage as technical debt to consolidate behind the adapter — that is where the real cost saving is.
Sequencing.
qpdf --check or round-trip comparison. Kill the initiative here if the filter histogram or encryption result is unfavorable.PdfFilterService; tests pin real fixtures so a patch bump cannot silently change output.Maintenance. Low volume, low blast radius. Pin ^2.11, review the diff (not the release notes — they are compare links only) before each bump, and hold a vendored fork as contingency. The maintenance burden that actually accrues is not the dependency; it is the pipeline we build around it, which is ours entirely.
Support. Single maintainer, GitHub issues, generated API docs. No SLA, no commercial support. Our team owns every integration bug, and upstream will not triage our parser problems. Budget internal ownership accordingly — one named owner, not "the platform team."
Scaling. CPU-bound and per-worker; horizontal scale via queue concurrency. The binding constraint is memory: the decode API is buffer-oriented, so peak RSS scales with decoded stream size (often 5–20× the compressed input). Enforce upload limits, per-stream size caps, and expansion-ratio guards at the boundary — a small malicious PDF is a cheap OOM. Keep synchronous request-path use to small, known-shape documents; route anything else through Horizon with tuned memory and timeout.
Failure modes.
| Failure | Symptom | Handling |
|---|---|---|
| Unknown/unsupported filter | Typed exception | Classify as permanent; quarantine, do not retry |
| Truncated or malformed stream | Exception or partial output | Permanent failure; log hex prefix for triage |
Wrong filter order / missing DecodeParms |
Silent garbage bytes, no exception | Highest-risk mode — validate output, never trust decode success alone |
| Unsupported JPEG2000/JBIG2 | Passthrough or empty image | Detect and route to a different tool |
| Decompression bomb | OOM, worker kill | Pre-flight ratio cap; hard-fail |
| Timeout on large files | Job retry loop | Idempotent job, maxExceptions, dead-letter queue |
The silent-garbage mode is the one to brief stakeholders on: a successful return value is not evidence of correct output. Any downstream consumer (OCR, e-signature, archival metadata) must receive validated bytes or an explicit failure.
Ramp-up. A mid-level Laravel developer is productive with the API in half a day — it is a handful of methods. The real ramp is PDF-spec literacy (filter ordering, predictors, DecodeParms, encryption handlers) at roughly one to two weeks to own the pipeline confidently. Recommend designating a single owner and writing the fixture corpus before, not after, the feature is announced on the roadmap.
How can I help you explore Laravel packages today?