Files
pdfcraft/crates/cos/README.md
T
Grzegorz MartensandClaude Opus 5.5 43202e10f9 M2.6: a page of many pictures stops at the export's allowance
The allowance's sharing was tested on a few images. This runs a whole page through the export: the
pictures past it are left out and counted, and the page is listed, which is what the warning goes
by. A few large pictures all come through. The README gives the numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-01 09:31:58 +01:00

101 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pdfcraft-cos
The PDF object layer (ISO 32000-2 §7): the COS object graph, parsed lazily and tolerantly, edited
through a copy-on-write overlay, and written back incrementally or as a full rewrite.
- **Layer:** L1, standalone. The layering table lets it use only `pdfcraft-filters`,
`pdfcraft-crypt` and `pdfcraft-geom` (`xtask/src/layers.rs::STANDALONE_DEPS`); it currently uses
the first two. That keeps the parser and the crypto independently auditable and reusable.
- `#![forbid(unsafe_code)]`, and the crate denies `unwrap`/`expect`/`panic!`/`todo!`. Builds for
`wasm32-unknown-unknown`.
**The object graph is the document model.** Nothing is lifted into a tidier representation and
written back out, because that is how data gets silently dropped. Typed views live above, in
`pdfcraft-model`.
## API
```rust
let doc = Document::open(bytes)?; // Arc<Vec<u8>>
let doc = Document::open_with_password(bytes, Some("owner"))?; // encrypted files
let root = doc.root(); // Option<ObjRef> — the catalog
let obj = doc.get(ObjRef { num: 12, gen: 0 }); // load (and cache): Arc<Object>
let real = doc.resolve(&obj); // follow one indirect reference
let d = doc.dict(&obj); // Option<Dict>, resolving first
let data = stream.decoded()?; // filters applied on demand
let data = doc.with_direct_parms(&stream).decoded()?; // /DecodeParms given by reference too
let px = doc.decoded_image(&stream)?; // an image's samples, a CCITT fax image's too
let fax = doc.fax_params(&stream); // a CCITT fax image's /DecodeParms, Table 11 defaults
doc.set(r, Object::Dict(d)); // edit: lands in the overlay
let r = doc.add(obj); // append a new object
doc.free(r);
let bytes = write_incremental(&doc, &SaveOptions::default())?; // append a revision
let bytes = write_full(&doc, &SaveOptions::default())?; // garbage-collected rewrite
```
Both writers **return the bytes**; the caller decides where they go, which is what lets the engine
produce a working file in memory after every edit and keep saving atomic.
Useful alongside: `doc.repair_log()` (what was reconstructed), `doc.revisions()` and
`doc.revision_ends()` (the incremental history), `doc.is_edited(num)` and `doc.modified_objects()`,
`doc.permissions()` and `doc.security()`, `doc.set_encryption()` / `doc.remove_encryption()`, and
`doc.require_full_save()` for edits that cannot be expressed incrementally.
`Document` is **cheap to clone**: the original bytes, the xref index and the parse caches are
shared, so a clone is a snapshot. Undo/redo and background jobs hold clones while the UI edits
another. Edits never touch the original bytes.
## Reading
- **Every cross-reference form:** classic tables with their quirky subsections, cross-reference
streams, hybrid-reference files (`/XRefStm`), and object streams.
- **Repair.** A file whose xref is wrong or missing is reconstructed by scanning for `obj`
headers. Repairs are *recorded*, never silent, so the UI can say the file was damaged.
- **Tolerant, never fatal.** The lexer works on a byte slice and a position; it allocates nothing
for skipped data, bounds-checks every index, and caps nesting depth. Malformed input yields
`CosError`, never a panic — this crate is the first thing a hostile file meets. Exhausted xref entries are skipped with a repair log; exhausted /Size hints allocate unused numbers without overflowing or replacing existing objects.
- **Lazy.** Objects are parsed on first use and cached. (Whole-file buffering is still the case;
`ByteSource` streaming is `core.lazy-loading`, not yet done.)
- **Encryption** is handled on load and save through `pdfcraft-crypt`: every standard-security
revision R2–R6 (RC4, AES-128/256), crypt filters, `/EFF` embedded-file encryption, and
`/EncryptMetadata`. `security.rs` knows what the spec exempts — the `/Encrypt` dictionary, xref
streams, the trailer `/ID`, and objects inside an object stream (the stream is encrypted whole).
## Writing
`write_incremental` appends only the edited objects, a cross-reference section **in the same
style as the file's last one**, and a trailer with `/Prev`. The original bytes are never modified,
which is what keeps a signature over an earlier revision valid.
`write_full` writes only what the trailer can reach, renumbered densely — the Save As and
garbage-collecting path. By default (`SaveOptions::object_streams`) it packs non-stream objects
into compressed object streams with a cross-reference stream (PDF 1.5+); turning that off writes a
classic table any reader accepts.
Dictionaries keep their key order and streams keep their *encoded* bytes, so rewritten objects stay
recognisable in a diff and byte-stable when re-serialized.
## Errors
`CosError`: `NotPdf`, `Syntax { offset, detail }`, `MissingObject`, `NotADictionary`, `Filter`,
`NeedsPassword`, `WrongPassword`, `Security`, `Poisoned`.
## Not done here
- Streaming very large files within a memory budget (`ByteSource`, `core.lazy-loading`); files are
read whole today.
- Linearized output, Fast Web View (`core.linearization`, M11).
- Structural validation against the Arlington model (`core.arlington-validation`), which will live
in the reserved `arlington` crate.
- Image codecs. `pdfcraft-filters` carries the non-image filters and CCITT fax and JBIG2 decoding
(`Document::decoded_image`, a JBIG2 image's `/JBIG2Globals` within `MAX_JBIG2_GLOBALS`, 32 MiB,
their filters undone once a document for the images that share them, up to 64 MiB kept, while
their segments are decoded for every image, and `ImageBudget` for an operation over many images,
which share one allowance of decoding, filters, decoders and pixels alike, and are refused past
it: `IMAGE_WORK`, 2^34 steps, or 2 to 5 minutes of JBIG2 and about 1,000 pages scanned at
300 dpi, plus `IMAGE_WORK_PER_BYTE`, 2^13 steps for each byte of the file);
DCT and JPX are still decoded through the bootstrap renderer (`core.image-filters`, M2.6).