pdfcraft/crates/engine
Mawfyy b8b6fb2f45
Never crash & persist: Make Current Properties Default and page-display choices survive a restart (#340, #266) (#365)
* Never crash & persist: Make Current Properties Default and page display choices survive a restart

#340:
- Expose per-tool comment styles via CommentPrefs::styles().
- persist() writes a `comment_styles` array (tool, color, opacity, width, fill).
- restore() reads each entry through the clamping setters; a new rgb_of helper
  at the read boundary refuses malformed colors whole (string, wrong length,
  null, non-finite) and clamps finite out-of-range values into 0..=1, so a
  hostile settings file can't smuggle a NaN or saturated color.

#266:
- The page display chosen in a document (view.layout.*, view.fit_one_page,
  view.fit_width_scrolling) now mirrors into view_defaults, which the existing
  default-layout/default-zoom keys already persist. New documents and the next
  launch open in the persisted display; transient zooms do not rewrite the
  default (matches Acrobat).

Tests:
- make_current_properties_default: persist + fresh app reuses the style.
- the_page_display_you_choose_becomes_the_default: choice mirrors, persists,
  new document opens in the persisted display; Acrobat fit modes persist too.
- comment_style_settings_clamp_and_refuse_hostile_values: hostile JSON is
  clamped or refused.

cargo fmt --check and cargo clippy -p pdfcraft-ui-egui --all-targets are clean.

* Fix clippy: chunks_exact(4) -> as_chunks::<4>() in signature_image.rs

* 365: keep the comment-style persistence (#340) only; a saved no-fill clears a default fill

The #266 half (every View choice rewriting the default page display)
is dropped: Preferences already sets that default explicitly. rgb_triple
moves out of the module declarations with one doc comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fill & Sign dates: Italian names, since Italian is now an interface language (#494)

date_languages_are_the_interface_languages requires every interface
language to have date names; the unknown-language example in the
session test moves from it to xx.

---------

Co-authored-by: Mawfyy <mawfyy@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:26:23 -04:00
..
src Never crash & persist: Make Current Properties Default and page-display choices survive a restart (#340, #266) (#365) 2026-10-09 21:26:23 -04:00
tests Fix main CI: move the rotated-field render test out of pdfcraft-forms (#591) 2026-10-09 14:13:58 -04:00
Cargo.toml Add image signatures with placement preview and resize controls (#335) 2026-10-08 16:36:26 -04:00
README.md D5: READMEs for cos and engine (#332) 2026-10-09 14:44:37 -04:00

README.md

pdfcraft-engine

The façade every frontend talks to. It holds the open documents, their edit history and the tool catalogue; frontends never reach past it into the parsing, rendering or editing crates.

  • Layer: L6. It depends on most of L1–L4 and on nothing above itself. No UI toolkit — egui, winit, eframe and rfd are forbidden below L7, and xtask layers enforces it.
  • The crate denies unwrap/expect/panic!/todo!. Entry points that run untrusted-document code keep a last-resort catch_unwind guard so an escaped panic becomes an error and the user keeps their document.

Two frontends exist on top of it today: pdfcraft-ui-egui (desktop and web) and pdfcraft-automation (headless tools, the CLI and MCP). Anything one can do, the other must be able to do — see AGENTS.md §3.

The editing model

Each open document holds a pdfcraft_cos::Document: the object graph plus a copy-on-write overlay.

  1. An edit runs on a clone. On success the previous state is pushed onto the undo stack. Clones share all unchanged data, so a snapshot is cheap — this is what makes 100 levels of undo affordable on a 200 MB file.
  2. After every edit the working file is produced by an incremental write: the original bytes plus one appended revision.
  3. The view is refreshed from those bytes, so what you see is exactly what Save will write.
  4. Saving rebases onto the written bytes, so the next save appends only new edits.

Consequences worth preserving: the original bytes of an opened file are never rewritten in place; a full rewrite happens only where it must (redaction, PDF/A conversion, print imposition, a changed password); and unknown data survives a round trip because nothing is reconstructed that wasn't understood.

API

let mut s = Session::new();
let id = s.open("report.pdf", Some(path), bytes, None)?;   // -> DocId

s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 })?;
let label = s.undo(id)?;                                    // "Rotate pages"
s.redo(id)?;

let doc = s.get(id).ok_or("gone")?;                         // &Document: queries, never edits
doc.can_undo(); doc.editable(); doc.allows_printing();
doc.text_blocks(page); doc.page_boxes(page); doc.security_summary();

let bytes = s.save_bytes(id)?;        // incremental: keeps signatures over earlier revisions valid
let bytes = s.save_full_bytes(id)?;   // full rewrite, garbage-collected
s.mark_saved(id, bytes.clone(), path)?;

Edit is one enum of 75 variants — every undoable change in the product, from RotatePages to AddAnnotation, SetFieldValue, ApplyRedactions and AddWatermark. Adding a feature means adding a variant and handling it in apply, which is what keeps undo, the dirty flag, autosave and the automation tools consistent for free.

Session also owns the operations that produce a new document rather than editing one: create_blank, create_from_images, create_from_text, combine, extract, split, split_by_size, reduced_bytes, optimized_bytes, print_pdf, export_data, and the signing family (sign, sign_with_timestamp, timestamp_document, embed_ltv).

Modules

Module What it holds
commands The command registry: id, label, menu, shortcut, and a Needs precondition encoding document security. Menus, the ⌘K palette, shortcuts and the control channel all read it, so every surface agrees on what exists and when it is enabled
catalog Acrobat's "All tools" information architecture as data — groups, sections, icons, hues, and an Availability of Ready, Planned(milestone) or Provider. The UI renders what is not built yet, honestly
actions PDF action dispatch (go-to, URI, named, set-layer-state, JavaScript)
links Link creation, properties, and finding URLs in page text
js The bridge to pdfcraft-js. forms never depends on js directly; the engine injects it, so a build without JavaScript is possible
xfa, ocr, compare, export Façades over the L3/L4 crates of the same names

Not done here

  • Rendering is still borrowed. pdfcraft-render wraps hayro and inspects through lopdf (ADR-0004). The engine only sees inspect() -> DocInfo, RenderPool and text::PageText, and that API stays frozen while M2 replaces what is behind it.
  • Performance budgets (misc.performance-budgets) are not set or enforced.
  • Some shipped features are still reachable only from the UI, not through an automation tool; cargo xtask parity lists them; closing that gap is tracked in the execution plan (D4).