Class PdfOptimizer


  • public final class PdfOptimizer
    extends java.lang.Object
    Cleans a PDF down to exactly what's actually reachable, and collapses its cross-reference/trailer history into one fresh table -- built entirely on the pd4ml com.pd4ml.pdf.cos object model/parser (no PDFBox, no other PDF library), the same foundation com.pd4ml.pdf.sign and com.pd4ml.pdf.merge are built on.

    Two things accumulate in a PDF that has been edited via incremental update (e.g. by com.pd4ml.pdf.sign or CosCli's set/ delete, or by any other tool that patches a file this way rather than rewriting it from scratch):

    • Unreferenced objects -- an incremental update only adds new object bodies and xref entries; when it drops a reference (e.g. replacing a page dictionary that used to point at some content), the object that's no longer pointed to anywhere still has a live "in use" xref entry and its bytes are still sitting in the file. COSParser happily parses it into the document's object table right along with everything still actually reachable.
    • Trailer/xref history -- each incremental update appends its own xref section and trailer, chained to the previous one via /Prev. COSParser already resolves that chain down to the current value of each object number (older, superseded bodies are simply never parsed in the first place), but the chain of old xref sections/trailers itself is still physically part of the file until something rewrites it away.

    optimize(...) parses the input, then deep-clones only what's reachable by walking from the trailer's /Root (and /Info, which is otherwise unreferenced by design -- it's a trailer-only entry) into a fresh COSDocument with compact object numbering (see COSObjectImporter), and writes that out as a single, from-scratch xref table and trailer (see COSDocumentWriter) -- anything not reachable is simply never visited, and every old revision's xref/trailer is gone because the output isn't an incremental update at all.

    By default, that reachable set is then also deduplicated by content (see ContentDeduplicator): two distinct objects with byte-identical content -- typically the same font, image, or color space embedded twice under two different object numbers -- are merged into one shared copy, every reference to the duplicate is redirected to the survivor, and a second reachability pass drops the now-unreferenced duplicate and renumbers everything compactly again. Pass deduplicateContent(false) to skip this (a large-document performance trade-off, since it hashes every reachable object's full content) and keep only the reachability cleanup.

    Encrypted input is supported (pass the password to the password-taking overload; the COS reader decrypts transparently while parsing). The output is not re-encrypted by default -- pass PdfOptimizer.Options.encryptOutput(java.lang.String, java.lang.String) if the cleaned file should stay protected.

    Not optimized (out of scope): stream recompression -- an uncompressed or suboptimally-compressed content/image stream is copied through as-is; a size optimization orthogonal to both reachability cleanup and content dedup, not attempted here.

    Thread-safety: PdfOptimizer holds no state at all (every method is static); call optimize(...) freely and concurrently from any number of threads. Each call builds its own local COSDocument/COSObjectImporter and touches nothing shared across calls.

    Example

    
     PdfOptimizeResult result = PdfOptimizer.optimize(pdfBytes);
     System.out.println("Removed " + result.getRemovedObjectCount() + " unreferenced object(s)");
     Files.write(Paths.get("cleaned.pdf"), result.getOptimizedPdf());