* M4: the Bookmarks panel shows each bookmark's colour and style (#3193) ISO 32000-2 §12.3.3: an outline item's /C is "An array of three numbers in the range 0.0 to 1.0, representing the components in the DeviceRGB colour space of the colour that shall be used for the outline entry's text" (Table 151), and its /F flags say italic (bit 1) and bold (bit 2) (Table 152). Both were ignored: every title was drawn in the theme's text colour and face. The inspector (OutlineItem) and the bookmark list (organize::Bookmark, used once a document is edited) now read them: each component is kept within 0-1, and black (the default) or a /C that isn't three numbers reads as no colour. The panel draws bold titles in the semibold face, italic ones slanted, and the colour as given, lightened towards the text colour only as far as it needs to read on the panel (3:1), so dark blue stays visible in the dark theme. bookmark_list reports color, bold and italic where they differ from the defaults. Mutants, each failing a test: italic or bold bit ignored (both readers, the panel), black kept as a colour, no clamping, a four-number /C accepted, colour ignored or truncated in the panel, the contrast floor removed, always jumping to the text colour. Checks for non-finite numbers were equivalent (clamping already covers any size), so they are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M4: set a bookmark's style and colour from its menu or bookmark_set_style (#3193) #3193 asks for a way to mark bookmarks by colour, for example chapters that are rarely used. ISO 32000-2 §12.3.3 gives every outline item a title colour (/C, Table 151, DeviceRGB, default black) and style flags (/F, Table 152: bit 1 italic, bit 2 bold, default 0); the previous commit shows them, and this one lets the user change them. - organize: set_bookmark_color writes /C, removing it for black (the default) and refusing a component outside 0-1; set_bookmark_style sets or clears the bold and italic bits, keeps any bit the table doesn't define, and removes /F when no bit is left. - engine: SetBookmarkColor and SetBookmarkStyle edits, undoable, under the same assemble permission as the other bookmark edits. Their undo labels are the menus' own names, already translated in every catalogue. - ui-egui: the bookmark menu gains Style (Bold and Italic check boxes) and Colour (the comment swatch grid, with Other colour...). Only existing UI strings are used. - automation: bookmark_set_style takes any of color ([r, g, b], 0-1), bold and italic, and changes only what it is given. Mutants, each failing a test: no range check (library and tool), black kept as /C, a bit not set, a bit not cleared, an untouched bit cleared, /F 0 written, the bits swapped (library and menu), the tool accepting nothing to change or ignoring bold/italic, the menu's colour ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M4: a colour and a style set in one bookmark_set_style call are one undo step (#3193) Review on #4171 (CodeRabbit). Given both, the tool applied two edits, so undo took back only the style and left the colour, and a failing second edit left the first applied. They now go in one Edit::Batch named "Style", all or nothing. "Colour" and "Style" join EDIT_LABELS, and "Style" BATCH_LABELS, so the history can translate them. The test sets colour and style together and undoes once; with the two edits applied separately it fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
word_count::count provides cancellable, page-at-a-time Unicode word and character statistics
with optional semantic exclusions; see word-count behavior and limits.
- Layer: L6. It depends on most of L1–L4 and on nothing above itself. No UI toolkit —
egui,winit,eframeandrfdare forbidden below L7, andxtask layersenforces it. - The crate denies
unwrap/expect/panic!/todo!. Entry points that run untrusted-document code keep a last-resortcatch_unwindguard 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.
- 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.
- After every edit the working file is produced by an incremental write: the original bytes plus one appended revision.
- The view is refreshed from those bytes, so what you see is exactly what Save will write.
- 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-renderwrapshayroand inspects throughlopdf(ADR-0004). The engine only seesinspect() -> DocInfo,RenderPoolandtext::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
automationtool;cargo xtask paritylists them; closing that gap is tracked in the execution plan (D4).
doc.text_edit_preview(page, block) prepares a read-only page background with the selected
paragraph’s glyphs removed, preserving text positioning, images and colours. It shares the
working document’s render configuration and never enters the undo history.