com.pd4ml.pdf.cos on its own: the PDF object model, parser,
COS-path query language, and incremental-update writer.
1. Overview
com.pd4ml.pdf.cos is a hand-rolled PDF COS (Carousel Object Structure) object model,
parser, and writer. It has exactly one
third-party dependency, BouncyCastle, used solely inside com.pd4ml.pdf.cos.security
for standard-security-handler decryption (RC4/AES) of an already-encrypted
input document.
com.pd4ml.pdf.cos
is fully usable standalone for any PDF-reading or incremental-update-writing task:
form inspection, metadata extraction, structural validation, adding
annotations, or a custom incremental-update.
2. The object model
| Type | Represents |
|---|---|
COSDictionary | a PDF dictionary (<< /Key value ... >>) |
COSArray | a PDF array ([ ... ]) |
COSStream | a COSDictionary plus a byte stream (extends it) |
COSString | a PDF string, stored as raw bytes - getBytes()/getString() |
COSName | a PDF name (/Foo) - interned via COSName.getPDFName(String) |
COSInteger / COSFloat | numbers (both extend COSNumber) |
COSBoolean | true/false - the two singletons COSBoolean.TRUE/FALSE |
COSNull | the null object (singleton COSNull.NULL) |
COSObject | an indirect reference (N G R) - resolve() follows it |
COSObjectKey | an object number + generation pair, the identity of an indirect object |
COSDocument | the whole document: object table + trailer |
Every COSBase (the common superclass) can carry an assigned COSObjectKey
(getKey()/setKey()) - that's what makes it an indirect object
when serialized; without one, it's written inline wherever it appears as a value.
COSDictionary offers both indirection-transparent and indirection-aware accessors:
getItem(key) returns the raw value (possibly a COSObject reference),
getDictionaryObject(key) resolves it first. Typed convenience getters
(getString, getInt, getLong, getFloat,
getBoolean, getDictionary, getArray, getStream,
getNameAsString, getCOSName, …) all resolve indirection automatically
and return a sensible default/null on a type mismatch rather than throwing - the
common case (an optional key) never needs an instanceof check. COSArray
mirrors this on the array side (get(index) raw, getObject(index) resolved,
plus getInt/getFloat/getName convenience accessors and
toFloatArray() for a MediaBox-style entry).
3. Parsing
COSDocument doc = COSParser.load(pdfBytes); // empty password
COSDocument doc = COSParser.load(pdfBytes, "ownerPassword"); // tried as user AND owner password
COSDocument.isEncrypted() reports whether the trailer named an /Encrypt
dictionary; getEncryptionError() is non-null if a password was given but didn't
authenticate (strings/streams are then still in their raw encrypted form). If encryption succeeded
(or the document was never encrypted), every accessor throughout the API already returns plaintext
- decryption happens once, in place, as the document is parsed.
COSDocument.getCatalog() resolves trailer → /Root.
getTrailer() returns the trailer dictionary directly. getObjectKeys()/
getObjects() enumerate the whole indirect-object table (this is what pd4coscli
list walks); getObject(key)/hasObject(key) look up one entry
directly, and getHighestObjectNumber() reports the largest object number currently in
use.
4. COS-path queries
A small, filesystem-like path language for navigating the object graph - used by
pd4coscli inspect/dump and internally by pd4signcli inspect.
COSBase result = COSPathEvaluator.evaluate(doc, "/Root/Pages/Kids[0]/MediaBox"); // null on any miss
COSBase result = COSPathEvaluator.evaluateStrict(doc, "..."); // throws COSPathException instead
Root (where navigation starts)
| Form | Starts at |
|---|---|
(empty), a leading /, or trailer/... | the trailer dictionary |
Root/... or catalog/... | trailer/Root (the catalog) |
N G R/... | an explicit indirect object N G R |
Steps (repeated after the root)
| Form | Meaning |
|---|---|
Name or /Name | look up a key in the current dictionary/stream node |
[N], or a bare integer N | index into the current array node |
N G R | jump directly to an indirect object, ignoring the current node |
A name step may contain \/, \[, \\ escapes and
#xx hex escapes (the same convention PDF name objects themselves use). Indirect
references are followed transparently at every step - a path never needs .resolve()
spelled out. Examples: /Root/Pages/Kids[0]/Contents,
root/Pages/Kids/0/MediaBox (bare-integer index, no brackets needed),
catalog/Outlines/First/Title, 12 0 R/Filter, trailer/Info.
evaluate(...) is lenient: any failure (a missing key, an out-of-range index, a step
applied to the wrong kind of node) yields COSNull.NULL rather than throwing -
convenient for exploratory lookups. evaluateStrict(...) instead throws
COSPathException naming exactly which step failed and why, e.g. "No such key 'Foo'
(after /Root/Pages)" - what pd4coscli/pd4signcli use, since a
silent null would be a confusing CLI result.
5. Writing
com.pd4ml.pdf.cos.writer is where new or changed content gets serialized.
5.1 COSWriter
A generic ICOSVisitor<Void> implementation: give it any COSBase and
it writes correct PDF syntax for it, deciding reference-vs-inline per value. Building blocks:
writeIndirectObject(key, value), writeValue(value),
writeName(...), writeString(...), writeHex(...), static
formatFloat(float), plus raw writeAscii/writeBytes escape
hatches and position() for exact byte-offset tracking.
5.2 IncrementalUpdateWriter
The generic incremental-update mechanics (ISO 32000-1 §7.5.6): allocate object numbers, track byte offsets, emit a correct cross-reference table and trailer - with zero PDF-semantic knowledge of what's actually being added or changed. The original file's bytes are never modified; this only appends.
IncrementalUpdateWriter update = new IncrementalUpdateWriter(originalBytes, document);
COSDictionary newThing = new COSDictionary();
update.assignNewKey(newThing); // gives it an indirect identity
someExistingDict.setItem("Foo", newThing); // wire it into the graph
update.writeObject(someExistingDict); // rewrite: it changed
update.writeObject(newThing); // write: it's new
byte[] result = update.finish(); // writes xref + trailer, returns the whole file
writeObject(obj) requires obj.getKey() != null - only an
already-indirect object can be independently rewritten this way. writeObjectCustom(key,
bodyWriter) is the escape hatch for precise control over an object's own byte layout (exactly
how IncrementalPdfSigner reserves the /Contents//ByteRange
placeholders).
finish() writes the cross-reference table, then a trailer (/Size,
/Root, /Info if present, a fresh /ID, and /Prev
chained back to the base file's own startxref), and returns the complete file. It throws
if nothing was written, the base file's startxref couldn't be located, or the document
has no indirect /Root.
5.3 Editing recipe: locate, mutate, rewrite
The general pattern behind pd4coscli set/delete, for changing one existing
key or array element rather than building new structure from scratch:
COSDocument document = COSParser.load(pdfBytes);
// 1. Locate the container to edit -- must resolve to an INDIRECT
// COSDictionary or COSArray (its own N G obj), not a value embedded
// inline inside something else.
COSBase parent = COSPathEvaluator.evaluateStrict(document, "/Root/Pages/Kids[0]");
if (parent.getKey() == null) {
throw new IllegalStateException("not independently addressable");
}
// 2. Mutate it in place.
((COSDictionary) parent).setItem("Rotate", COSInteger.get(90));
// 3. Rewrite just that object as an incremental update.
IncrementalUpdateWriter update = new IncrementalUpdateWriter(pdfBytes, document);
update.writeObject(parent);
byte[] result = update.finish();
The same shape works for COSArray (set(index, value)/add(value)
to replace or append, remove(index) to delete) in place of
COSDictionary.setItem/removeItem.
COSWriter applies no encryption, so appending freshly-plaintext objects into a
document whose base trailer still declares /Encrypt would produce a file most readers
can't parse correctly. pd4coscli set/delete refuse encrypted input outright
for this reason, rather than emit a broken file.
6. Page tree
COSDictionary page = PageTree.findPage(catalog, 0); // zero-based index
Walks /Root/Pages/Kids recursively (a page tree can nest intermediate
/Pages nodes), resolving indirect references at every step, with an identity-based cycle
guard against a malformed/cyclic tree. Returns null if the catalog has no usable page
tree or the index is out of range. General-purpose COS navigation, useful for form filling,
annotation placement, or per-page metadata - not just a visible signature's placement.
7. Filters
com.pd4ml.pdf.cos.filter implements FlateDecode, ASCII85Decode,
ASCIIHexDecode, and the PNG/TIFF predictors layered on top of Flate/LZW.
COSStream.getDecodedBytes() applies the full /Filter chain automatically
(each filter's output feeding the next), stopping - and returning whatever was decoded so far
- at the first unsupported filter, typically an image codec such as
DCTDecode expected to be handed to an image decoder directly.
isFullyDecodable() tells you in advance whether getDecodedBytes() will
reach the end of the chain; getFilterNames() lists the chain itself.
8. Security (reading encrypted PDFs)
com.pd4ml.pdf.cos.security implements the PDF standard security handler - RC4 and
AES, including both the legacy and the AES-256 (ISO 32000-2 hardened-hash) key derivation
algorithms - for reading an encrypted PDF: given the right password,
COSParser.load(bytes, password) transparently decrypts every string and stream as it
parses, so nothing elsewhere in the API needs to know the document was ever encrypted.
There is deliberately no encryption/write support: com.pd4ml.pdf.cos
cannot produce a re-encrypted or newly-encrypted PDF, and (§5.3) cannot correctly append an
incremental update to an encrypted one either, since the writer never applies encryption to what it
writes. This is also why com.pd4ml.pdf.sign's signer refuses encrypted input up front (see
the PD4ML Signing Manual's §5) - the same underlying
limitation.
9. Known limits
- No signature verification, and (§8) no encryption/write support -
com.pd4ml.pdf.cosreads an encrypted PDF but never writes one. - Incremental-update editing (§5.3, and
pd4coscli set/delete) requires the target to already be an indirect object; a value embedded inline in its parent isn't independently addressable and can't be rewritten without also rewriting that parent (which the caller must locate and target itself). DCTDecodeand other image codecs are intentionally left undecoded bygetDecodedBytes()(§7) - they're expected to be handed directly to an image decoder, not treated as generic filtered PDF data.
10. See also
- pd4coscli Reference - the command-line tool for everything in this manual, no code required
- PD4ML COS Examples - runnable
com.pd4ml.pdf.cosuse-case examples, no signing involved - PD4ML Signing Manual - the
com.pd4ml.pdf.signdigital-signing and PAdES-LT layer built on top ofcom.pd4ml.pdf.cos - PD4ML Signing Examples - runnable use-case examples, including two (
Ex17_CosInspectionAndPathQuery,Ex18_ListSignatureFields) that usecom.pd4ml.pdf.coswith no signing involved at all - pd4signcli Reference - command-line reference for
com.pd4ml.pdf.sign.cli.PdfSignCli - PD4ML PDF Merge Manual - combining page ranges from multiple PDFs, also built on
com.pd4ml.pdf.cos - PD4ML PDF Optimizer Manual - cleaning up unreferenced objects and incremental-update history
- PD4ML XFDF Manual -
com.pd4ml.pdf.xfdf, importing/exporting annotations and form field values, also built oncom.pd4ml.pdf.cos
