mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-05 18:19:50 +00:00
From CodeRabbit's review of the stack: - Every JBIG2 image read its /JBIG2Globals afresh, within 32 MiB each but with no bound across an export or a redaction. A globals stream of the file, not edited since it was read and with its /DecodeParms direct, now has its filters undone once a document and is kept for the images that share it (in every clone, which share the file), up to 64 MiB in all. An edited one, one added in memory, or one with /DecodeParms by reference (another object, which can change under it) is read each time as before. This saves the filters only: the decoder reads the segments afresh for every image, so a real symbol dictionary still costs its decoding once an image. - view.copy-image dropped "JBIG2 inside other filters" from its missing list on the strength of Export's test; Copy Image now has its own: a JBIG2 picture behind ASCIIHexDecode is found under the pointer and copied as its pixels and as a PNG, and one whose page information is in its globals is read through them. A JBIG2 image's /Width and /Height are read as given directly; one by reference is an error rather than an image of no rows. A /JBIG2Globals reference to no object still reads as no globals: it is a reference to null (ISO 32000-2 §7.3.10), which an entry may hold for being absent (§7.3.7), and the renderer draws the page so. An image that needed those globals then mostly fails to decode; in a probe of twelve pictures that decoded with them, eleven were refused and one decoded to other pixels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
98 lines
5.7 KiB
Markdown
98 lines
5.7 KiB
Markdown
# 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);
|
||
DCT and JPX are still decoded through the bootstrap renderer (`core.image-filters`, M2.6).
|