Files
pdfcraft/crates/engine
c3c36f77cf M5: a comment's text, border or line, and fill each at an opacity of its own (#4542)
* A comment's text, border or line, and fill each at an opacity of its own

Comment Properties showed one opacity for a whole comment, so a text box's
see-through background took its text and border with it, and a rectangle
marking an area on a plan couldn't have an opaque line over a faint fill.
Now a text box, typewriter text or callout has a Text, Border and Fill
opacity, a rectangle, oval, polygon or cloud a Line and Fill opacity (a part
not drawn is greyed); everything else keeps one Opacity.

The line's stays /CA, the fill's is /ca (PDF 2.0's opacity for what is
filled) and the text's /PCTextOpacity, each only where it differs from the
one it would follow, and the appearance draws them with /GS0 (strokes at the
line's, fills at the fill's) and /GS1 for the text, so every viewer shows
them. The quick bar's Opacity and StyleAnnotation's set the whole comment's.

Engine: Style::{fill_opacity, text_opacity}, Edit::PartOpacity,
annot::{set_part_opacity, appearance::part_opacity}, Props::{fill_opacity,
text_opacity}. Make Properties Default and the settings carry the parts.
Headless: comment_add / comment_edit fill_opacity, text_opacity; comment_list
reports opacity, fill_opacity, text_opacity. Four labels in every catalog
(AI-assisted translations from English, noted in ATTRIBUTION.toml).

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

* Review: the text's opacity slider shows what the text is drawn at; comment_list skips replies' properties; ui-parity's change record

A text box without fill and border whose text kept an opacity of its own showed the border's
in its Text opacity slider; it now shows the text's, and a change sets the whole comment's.
The opacity sliders are named by their labels. comment_list reads a comment's properties only
for comments, not for every reply.

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

* Rebase: comment_list tells replies by is_reply, as main now threads them

main threads replies by their /IRT reference (#4061); the opacity fields skip a reply the
same way.

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

* Review: a text-only text box made default gives the tool its text's opacity

With neither a border nor a fill, a text box's text is the whole comment:
Comment Properties shows its drawn opacity (the text's own, when set), and a
change there sets the whole comment's. Make Properties Default, and the
context menu's Make Current Properties Default, stored /CA instead, so a
text box whose text was at 50 % gave the tool 100 %. The default is now the
text's drawn opacity in that case.

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

* Rebase: the part opacities in main's annot layout (Style's fields, the text box drawing's parts, set_part_opacity exported)

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

* Rebase: main's new comment test builds a Style with the part opacities

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 14:39:36 +01:00
..
…

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, 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.

When dynamic XFA layout is active, JsRunner does not initialize Acrobat JavaScript with scripts from the PDF container's /Names /JavaScript tree. Those scripts belong to the fallback PDF view and can otherwise show a misleading viewer-compatibility warning. They remain in the document and in Document::document_scripts. Requested console scripts, field actions and XFA events can still show messages; ordinary PDFs, static XFA and shells without an active layout keep their document scripts. Converting XFA to a static form and undoing that conversion switches the context with the document state.

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).

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.