* M10: enhance camera images: straighten a page from four corners (ocr.camera-perspective) Four corners on a page (any order, a convex quadrilateral) are warped to an upright rectangle that replaces what the page shows, read at the page picture's own resolution (at most 600 dpi) and kept at that physical size. One undoable step through Replace Pages, so comments, links and bookmarks stay. - geom: a pure 3x3 homography solve and corner ordering (no dependencies); collinear, concave and non-finite input gives None. - engine: camera::warp (bilinear, over white, capped at 40 MP) and Session::enhance_camera_page. - automation: page_enhance_camera (corners in top-left-origin points). - parity: ocr.camera-perspective is partial; the UI handles, automatic edge detection and contrast cleanup are still missing. Refs #1008. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M10: Enhance camera image: drag four corners on the page, then Enhance page (ocr.camera-perspective) Scan & OCR ▸ Enhance camera image (ocr.enhance_camera) shows four corner handles on the current page, inset 5% from its edges, with what will be cut away dimmed. Enhance page straightens what they enclose through Session::enhance_camera_page (one undoable step); Cancel and Esc leave the page alone. The buttons float at the top right, clear of a centred page's corners, and the instructions are a notice, as for Crop. A command of its own, beside Enhance scanned file (ocr.enhance), which stays for scan correction (#1012). - ui-egui: camera_ui (handles, dimming, bar), QuickTool::EnhanceCamera, quick_tool "enhance-camera" for ui.state and the shot example. - Catalogs: the three new labels in every language with UI coverage (plus es, pl, zh-hant); ATTRIBUTION.toml hashes updated. - automation: page_enhance_camera is linked to the command. - parity: ocr.camera-perspective lists the command and the UI test. Refs #1008. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M10: Enhance camera image finds the document's edges (ocr.camera-perspective) The corners now start on the photographed document's edges when they can be found, else inset 5% as before. camera_edges::detect_document, pure Rust and deterministic: shrink to 400 px, blur, Sobel edges thinned and thresholded against the 98th percentile; a Hough transform where each edge votes within 6 degrees of its own normal; pairs of near-horizontal and near-vertical lines make convex quadrilaterals (at least 12% of the picture), kept when every side lies on a matching edge along at least 55% of its length and scored by area times the square of the mean support; each side is then refitted through its edge points (total least squares). None when nothing convinces. - Session::detect_camera_corners (user space); page_enhance_camera finds the corners itself when `corners` is omitted and reports the corners it used and whether they were detected; the UI command starts from them. - Tests on synthetic photos: plain table, text on the sheet, wood grain with a shadow, strong perspective, a 30 degree turn, a sheet running off the picture, no document, white on white, bad input; end to end through the engine and the tool. - Parity notes: what is still missing (turning the result upright, rounded corners, clean-up). Refs #1008. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M10: after Enhance page, rotate, crop or organize the page (ocr.camera-perspective) Enhance camera image no longer closes on Enhance page: the bar then offers rotate counterclockwise and clockwise for the enhanced page (one undo step each), Crop pages and Organize pages (which leave the tool for those tools), and Done; Esc leaves too. A photo of a card often comes out a quarter turn off until the page is turned upright automatically. The two icon buttons are named for screen readers. Every label already exists in the catalogs. Refs #1008. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M10: Enhance camera image cleans the photo up and chooses its look (#1034) After Enhance page the bar offers a look (Original, Colour, Greyscale, Black and white) and a Clean-up slider (0-100, default Colour at 50). Choosing a look or letting go of the slider enhances the page again from the straightened photo and turns it as before, so the history keeps one enhancement (and one turn), never a pile of them. camera_clean, pure Rust: - clean: the paper's illumination is estimated per channel on a coarse grid (each block's 90th percentile, lifted to the 75th percentile of the 5x5 blocks around it so a stamp or picture isn't taken for paper, then smoothed) and divided out, so casts, shadows and darker edges go and the paper turns white; a contrast stretch grows with the level, and from 60 isolated dark pixels are removed. Colour ink stays. - apply(look): Original is the photo as taken; Greyscale is the cleaned page in grey; Black and white evens the lighting fully, thresholds the whole page (the level sets the threshold: higher keeps fainter strokes) and despeckles. Black and white is stored as PNG (lossless): JPEG would smear grey around every letter. - Session::enhance_camera_page_at(look, level); page_enhance_camera takes `mode` (original, colour, greyscale, bw) and `level` (0-100), refusing anything else. - "Clean-up", "Original", "Greyscale" and "Black and white" in every catalog with UI coverage, plus es, pl and zh-hant. - Tests: shadow and yellow cast become white paper with dark text, a blue stamp stays blue, strength grows with the level, specks go but text stays, the four looks (black and white is only black and white, the threshold moves with the level), bad sizes; a shadowed sheet through the engine; level and mode through the tool; the slider and the looks re-enhancing from the photo and keeping the turn. - Parity: ocr.background-removal, ocr.edge-shadow-removal and ocr.despeckle are partial (camera images only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M10: Enhance camera image cleans the photo up and chooses its look (#1034) After Enhance page the bar offers a look (Original, Colour, Greyscale, Black and white) and a Clean-up slider (0-100, default Colour at 50). Choosing a look or letting go of the slider enhances the page again from the straightened photo and turns it as before, so the history keeps one enhancement (and one turn), never a pile of them. camera_clean, pure Rust: - clean: the paper's illumination is estimated per channel on a coarse grid (each block's 90th percentile, lifted to the 75th percentile of the 5x5 blocks around it so a stamp or picture isn't taken for paper, then smoothed) and divided out, so casts, shadows and darker edges go and the paper turns white; a contrast stretch grows with the level, and from 60 isolated dark pixels are removed. Colour ink stays. - apply(look): Original is the photo as taken; Greyscale is the cleaned page in grey; Black and white evens the lighting fully, thresholds the whole page (the level sets the threshold: higher keeps fainter strokes) and despeckles. Black and white is stored as PNG (lossless): JPEG would smear grey around every letter. - Session::enhance_camera_page_at(look, level); page_enhance_camera takes `mode` (original, colour, greyscale, bw) and `level` (0-100), refusing anything else. - "Clean-up", "Original", "Greyscale" and "Black and white" in every catalog with UI coverage, plus es, pl and zh-hant. - Tests: shadow and yellow cast become white paper with dark text, a blue stamp stays blue, strength grows with the level, specks go but text stays, the four looks (black and white is only black and white, the threshold moves with the level), bad sizes; a shadowed sheet through the engine; level and mode through the tool; the slider and the looks re-enhancing from the photo and keeping the turn. - Parity: ocr.background-removal, ocr.edge-shadow-removal and ocr.despeckle are partial (camera images only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Camera level: when another edit broke the run, redo what was undone instead of warping the page again --------- Co-authored-by: magdale76 <16279778+magdale76@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.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.
- 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).