mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 10:39:01 -06:00
* M7: share Windows local-link fixture portability fixes (#1265) Use the identical test-only prerequisite already proposed in #1263: Unix-only mutable vectors, ordinary Windows drive paths and escaped PDF literal strings. This is shared CI support for the separate xref overflow fix; production link handling is unchanged. Generated with Codex. * M1: return an error for exhausted xref object numbers (#1337) Check the next-object addition and return CosError at the xref offset rather than overflowing or wrapping to object zero. Classic-table and stream regressions reproduced the original panic; ordinary, zero-offset and repaired-range controls preserve catalog access and allocation. Validation: 2732 passed, 0 failed, 13 ignored; strict all-target Clippy, fmt, 31 WASM crates, layering, assets, deny and parity passed. The rebuilt CLI opened and rendered a clean synthetic page. Generated with Codex. * M1: place overflow notes beside stable milestone entries (#1337) Keep the tested code unchanged while avoiding progress-note conflicts with newly landed main updates. Formatting and diff checks passed. Generated with Codex. * Merge main: adapt exhausted-xref repair to the compact xref index Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
92 lines
5.2 KiB
Markdown
92 lines
5.2 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
|
||
|
||
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; DCT, JPX, JBIG2 and CCITT are
|
||
still decoded through the bootstrap renderer (`core.image-filters`, M2.6).
|