mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 22:19:10 -06:00
M10: preserve page artwork when printing annotations
This commit is contained in:
+6
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~25% (ready for real work 50% → 75%) and ~400–700 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-22 · **Last updated:** 2026-10-22 · **Change:** minor (M14, #3558: rebase guarded macOS per-file association recovery onto current main; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M10: vector printing preserves existing page XObjects when adding annotation appearances; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-21 · **Last updated:** 2026-10-21 · **Change:** minor (M3, #3808: the Pages inspector has an independent 50–150% thumbnail size slider and buttons; selection and current-page following are retained, with headless ui.set coverage. Estimates unchanged.)
|
||||
> **Last reviewed:** 2026-10-20 | **Last updated:** 2026-10-20 | **Change:** minor (M11 #3507: image merging preserves content stream boundaries; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-20 | **Last updated:** 2026-10-20 | **Change:** minor (M10, #3532 partial: vector printing retains existing appearances with absent Subtype; estimates unchanged)
|
||||
@@ -191,6 +191,10 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
|
||||
## Progress log
|
||||
|
||||
- **2026-10-23 (M10, print resources):** Vector printing preserves original page artwork when existing XObjects use annotation resource names.
|
||||
The regression checks two distinct appearances, inherited resources, print/reopen pixels and unchanged source data.
|
||||
Estimates unchanged.
|
||||
|
||||
- **2026-10-22 (M4, #3916 partial):** Document edits and Undo/Redo cancel an Organize page drag. Pointer-release regressions check exact page order and save/reopen.
|
||||
- **2026-10-22 (M14, macOS association recovery candidate):** Successful desktop open/save/sign can queue recovery of an existing per-file binding to the running PDFCraft bundle through NSWorkspace. Metadata-only watcher changes retain undo and avoid false overwrite prompts; real same-length/same-mtime edits remain protected. Rebased on 0.12.0 and reuses #3042's shared atomic metadata preservation; the bounded recovery parser does not narrow save limits. Prior native Preview TEST-signature and Finder workflows passed on macOS 27.0.1; other versions remain unqualified. Estimates unchanged.
|
||||
- **2026-10-13 (M7, free image rotation):** A selected page image has a rotate knob (Shift: 15 degree steps) and `image_edit` a `rotate_by` action; the angle is written as one undoable `cm`. Snapping to page edges while moving is in #1740; resize snapping is not done. Estimates unchanged.
|
||||
@@ -640,6 +644,7 @@ Newest first: the date, what moved, and the new overall estimate.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | minor | M10: annotation appearances use unoccupied XObject names, preserving original page artwork and distinct appearances through inherited resources and print/reopen. Estimates unchanged. |
|
||||
| 2026-10-22 | minor | M4, #3916 partial: document edits and Undo/Redo cancel Organize drags. Pointer-release and save/reopen regressions check page order. Estimates unchanged. |
|
||||
| 2026-10-20 | minor | M4: initial-view window and interface options resolve indirect boolean ViewerPreferences values; synthetic true/false and save/reopen regressions retain defaults for malformed entries. Estimates unchanged. |
|
||||
| 2026-10-20 | minor | M6, #3502: form detection preserves comments across content streams; estimates unchanged. |
|
||||
|
||||
@@ -35,6 +35,7 @@ spool::submit(&pdf, &spool::Job { printer: None, copies: 2, ..Default::default()
|
||||
Each source page becomes a Form XObject (its content wrapped in q/Q, plus the printable
|
||||
annotations); sheets place them with a clip. The result is a fresh, unencrypted,
|
||||
garbage-collected file (callers check the print permission).
|
||||
Annotation appearances receive unused XObject names, preserving existing page resources even when they use the same naming prefix.
|
||||
|
||||
The GUI Print dialog remembers its last selected paper size across application sessions,
|
||||
including when Print is cancelled. The preference stores a canonical name such as `A4`;
|
||||
|
||||
+10
-3
@@ -801,7 +801,7 @@ fn page_form(
|
||||
}
|
||||
let annots = page.dict.get(b"Annots").and_then(|a| doc.resolve(a).as_array().cloned()).unwrap_or_default();
|
||||
let mut xobjects = res.get(b"XObject").and_then(|x| doc.resolve(x).as_dict().cloned()).unwrap_or_default();
|
||||
let mut k = 0;
|
||||
let mut k = 0usize;
|
||||
for a in annots {
|
||||
let Some(d) = doc.resolve(&a).as_dict().cloned() else { continue };
|
||||
if !prints(doc, &d, content) {
|
||||
@@ -833,8 +833,15 @@ fn page_form(
|
||||
} else {
|
||||
ap
|
||||
};
|
||||
let name = format!("PCPrA{k}");
|
||||
k += 1;
|
||||
// Keep the page's own XObjects, including names previous print operations generated.
|
||||
// Check the growing map so each appearance also keeps the name just assigned to it.
|
||||
let name = loop {
|
||||
let name = format!("PCPrA{k}");
|
||||
k = k.checked_add(1).ok_or_else(|| PrintError::Invalid("too many annotation appearance resources".into()))?;
|
||||
if !xobjects.contains(name.as_bytes()) {
|
||||
break name;
|
||||
}
|
||||
};
|
||||
let [a0, b0, c0, d0, e0, f0] = m.0;
|
||||
body.extend(format!("q {} {} {} {} {} {} cm /{name} Do Q\n", n(a0), n(b0), n(c0), n(d0), n(e0), n(f0)).bytes());
|
||||
xobjects.set(name.into_bytes(), Object::Ref(ap));
|
||||
|
||||
@@ -85,6 +85,66 @@ fn xobject(doc: &Document, owner: &Dict, name: &[u8]) -> ObjRef {
|
||||
objects.as_dict().unwrap().get(name).unwrap().as_ref().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotation_appearances_do_not_replace_page_xobjects() {
|
||||
for inherited in [false, true] {
|
||||
let (mut doc, ap, widget) = widget_fixture(Some(Object::name("Form")), None);
|
||||
let page = pdfcraft_model::pages(&doc)[0].obj;
|
||||
let mut xobjects = Dict::new();
|
||||
for (name, color) in [("PCPrA0", "1 0 0"), ("PCPrA1", "0 0 1"), ("PCPrA3", "1 0 0")] {
|
||||
let dict = Dict::from_iter([
|
||||
(b"Type".to_vec(), Object::name("XObject")),
|
||||
(b"Subtype".to_vec(), Object::name("Form")),
|
||||
(b"BBox".to_vec(), numbers(&[0.0, 0.0, 10.0, 10.0])),
|
||||
]);
|
||||
let form = doc.add(Object::Stream(Stream::flate(dict, format!("{color} rg 0 0 10 10 re f").as_bytes())));
|
||||
xobjects.set(name.as_bytes().to_vec(), Object::Ref(form));
|
||||
}
|
||||
let xobjects = doc.add(Object::Dict(xobjects));
|
||||
let resources = doc.add(Object::Dict(Dict::from_iter([(b"XObject".to_vec(), Object::Ref(xobjects))])));
|
||||
let contents = doc.add(Object::Stream(Stream::flate(
|
||||
Dict::new(),
|
||||
b"q 1 0 0 1 60 20 cm /PCPrA0 Do Q\nq 1 0 0 1 70 20 cm /PCPrA1 Do Q\nq 1 0 0 1 80 20 cm /PCPrA3 Do Q",
|
||||
)));
|
||||
let second_ap = doc.add(Object::Stream(Stream::flate(doc.get(ap).as_dict().unwrap().clone(), b"/Ink cs 1 1 0 scn 0 0 20 10 re f")));
|
||||
let mut second_widget = doc.get(widget).as_dict().unwrap().clone();
|
||||
second_widget.set(b"Rect".to_vec(), numbers(&[10.0, 50.0, 50.0, 70.0]));
|
||||
second_widget.set(b"AP".to_vec(), Object::Dict(Dict::from_iter([(b"N".to_vec(), Object::Ref(second_ap))])));
|
||||
let second_widget = doc.add(Object::Dict(second_widget));
|
||||
doc.update_dict(page, |d| {
|
||||
d.set(b"Resources".to_vec(), Object::Ref(resources));
|
||||
d.set(b"Contents".to_vec(), Object::Ref(contents));
|
||||
d.set(b"Annots".to_vec(), Object::Array(vec![Object::Ref(widget), Object::Ref(second_widget)]));
|
||||
})
|
||||
.unwrap();
|
||||
if inherited {
|
||||
let parent = doc.get(page).as_dict().unwrap().reference(b"Parent").unwrap();
|
||||
doc.update_dict(parent, |d| d.set(b"Resources".to_vec(), Object::Ref(resources))).unwrap();
|
||||
doc.update_dict(page, |d| {
|
||||
d.remove(b"Resources");
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let before = saved(&doc);
|
||||
let source = render(before.clone());
|
||||
for (x, color) in [(65, [255, 0, 0, 255]), (75, [0, 0, 255, 255]), (85, [255, 0, 0, 255])] {
|
||||
assert_eq!(pixel(&source, x, 75), color, "source page artwork");
|
||||
}
|
||||
for (y, color) in [(40, [255, 255, 0, 255]), (70, [0, 255, 0, 255])] {
|
||||
assert_eq!(pixel(&source, 30, y), color, "source widget appearance");
|
||||
}
|
||||
let printed = render(impose(&doc, &print_settings(Content::Document)).unwrap());
|
||||
for (x, color) in [(65, [255, 0, 0, 255]), (75, [0, 0, 255, 255]), (85, [255, 0, 0, 255])] {
|
||||
assert_eq!(pixel(&printed, x, 75), color, "printed page artwork, inherited={inherited}");
|
||||
}
|
||||
for (y, color) in [(40, [255, 255, 0, 255]), (70, [0, 255, 0, 255])] {
|
||||
assert_eq!(pixel(&printed, 30, y), color, "printed widget appearance, inherited={inherited}");
|
||||
}
|
||||
assert_eq!(printed.rgba, source.rgba, "printing must retain page artwork and both appearances, inherited={inherited}");
|
||||
assert_eq!(saved(&doc), before, "printing must leave the source resources unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_subtype_appearance_survives_vector_print() {
|
||||
let (control, _, _) = widget_fixture(Some(Object::name("Form")), None);
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# PdfCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-22 · **Last updated:** 2026-10-22 · **Change:** minor (M14, #3558: rebase guarded macOS per-file association recovery onto current main; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M10: vector printing preserves existing page XObjects when adding annotation appearances; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-20 · **Last updated:** 2026-10-20 · **Change:** minor (M12 #3673: Character encoding accepts Identity-H/V with Adobe CJK collections; estimates unchanged)
|
||||
|
||||
> **Last reviewed:** 2026-10-20 | **Last updated:** 2026-10-20 | **Change:** minor (M10, #3532 partial: vector printing retains existing appearances with absent Subtype; estimates unchanged)
|
||||
@@ -184,7 +184,7 @@ total matches the parity estimate.
|
||||
| M7 | Content editing (text, images, header/footer, watermark) | 250–500 | 20% | 100–180 | Done: header & footer, watermarks, backgrounds, Bates; added text and images that stay editable (move, resize, format, rotate, flip, crop, replace); links (Link tool, Link Properties, create from URLs, remove all). Missing: editing existing text and images in place (the longest pole), image/PDF watermarks |
|
||||
| M8 | Security + redaction | 100–180 | 68% | 15–30 | Done: opening protected documents, permissions, Protect Using Password, Remove security; redaction (mark text/areas/pages, Search & Redact with patterns, apply removing glyphs, image pixels, vectors, XObject content, annotations and fields, verification, full rewrite on save); Remove hidden information and Sanitize.<br>Done since: redaction codes (U.S. FOIA and Privacy Act sets as overlay text, in Redaction properties and `redact_mark`). Redaction cleans indirect and form structure references, preserving surviving form tags. Redaction retires inherited resource dictionaries, so a parent `/Pages` can't keep a removed XObject reachable; unredacted pages are left untouched. Image redaction removes alternate images and unsupported masks only on affected images. Changed marked-content spans lose replacement descriptions; untouched spans keep theirs. Hidden-text cleanup retires unused named descriptions across inherited/shared page and form resources (64 focused tests and scoped Rust 1.96 Clippy/wasm checks pass; publication CI pending). The redaction audit counts a filled cover that hides half of any one word of a long line (#2662), applied or not. Missing: certificate security, custom redaction code sets and pattern locales, DCT re-encoding |
|
||||
| M9 | Signatures (PAdES, validation) | 160–280 | 60% | 40–70 | Done: new `sign` crate (DER, X.509, CMS, PKCS #12 on RustCrypto; aws-lc-rs for RSA private keys); PAdES B-B signing (visible, invisible, existing fields, certification with DocMDP); validation with trust store and changes-after-signing classification; self-signed digital IDs; Signatures panel, message bar, sign dialogs; agent tools. Checked with pdfsig and OpenSSL.<br>Done since: macOS Keychain and Windows Current User Personal store signing (software-backed CNG keys), certificate viewer, signed documents protected from rewrites, page validation overlays separate from original signature artwork (baked Not Verified captions remain; #79 open), snapshot tabs that reuse instead of multiplying. Missing: timestamps (B-T), LTV (DSS, OCSP, CRL), FieldMDP, smart cards, PKCS #11 |
|
||||
| M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, the CUPS and Windows spoolers, print-ready PDF).<br>Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; architectural/ANSI/large ISO sheet presets, Match page at 100% and PDF-scale preview (#599 follow-up); Create PDF from multiple files (PDFs, images and text, edited as pages in the grid); OCR can also read pictures on pages that have text, without adding that text twice (#1028). Done since: Word invoice export uses borderless inferred tables, floating positioned text/artwork only where text overlaps an image or is rotated, and per-page sizes, covered by synthetic invoice automation regressions (#773); printing on the web through the browser's print dialog, sheets drawn by PdfCraft (gap 4). Lossless image export applies per-component Decode, including Indexed mapping (#1201 partial). Existing 16-bit precision and color-management limits remain. #3532 partial: vector printing retains existing appearances with absent Subtype through a print-owned Form copy; source artwork stays unchanged. #3120: Office export reads form XObjects 50 levels deep, as the renderer does, and up to 50 000 a page; pages past that are listed in `truncated_pages`. Missing: OCR languages beyond Latin, editable-text OCR output, full Office layout fidelity/import. Done since: dotted-leader contents entries stay in separate complete paragraphs (#1757) #2156 candidate: last selected print paper persists across sessions; other print preferences unchanged. |
|
||||
| M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, the CUPS and Windows spoolers, print-ready PDF).<br>Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; architectural/ANSI/large ISO sheet presets, Match page at 100% and PDF-scale preview (#599 follow-up); Create PDF from multiple files (PDFs, images and text, edited as pages in the grid); OCR can also read pictures on pages that have text, without adding that text twice (#1028). Done since: Word invoice export uses borderless inferred tables, floating positioned text/artwork only where text overlaps an image or is rotated, and per-page sizes, covered by synthetic invoice automation regressions (#773); printing on the web through the browser's print dialog, sheets drawn by PdfCraft (gap 4). Lossless image export applies per-component Decode, including Indexed mapping (#1201 partial). Existing 16-bit precision and color-management limits remain. #3532 partial: vector printing retains existing appearances with absent Subtype through a print-owned Form copy; source artwork stays unchanged. #3120: Office export reads form XObjects 50 levels deep, as the renderer does, and up to 50 000 a page; pages past that are listed in `truncated_pages`. Missing: OCR languages beyond Latin, editable-text OCR output, full Office layout fidelity/import. Done since: dotted-leader contents entries stay in separate complete paragraphs (#1757) #2156 candidate: last selected print paper persists across sessions; other print preferences unchanged. Vector printing gives annotation appearances unoccupied XObject names so existing page artwork survives. |
|
||||
| M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 80–140 | Done: new `optimize` crate: Reduce File Size and the PDF Optimizer (images measured where drawn, bicubic downsampling, JPEG/ZIP recompression only when smaller, discard objects and user data, Flate clean-up, resource merging, object streams). Content stream boundaries preserve comments, split operands and graphics state when image fragments merge. Done since: reject invalid image and soft-mask dimensions with checked sizing (#2178). Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA |
|
||||
| M12 | Accessibility, compare, measure, search, XFA | 200–380 | 30% | 90–160 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Locatable figure, annotation and form findings highlight their page bounds. Character encoding accepts Identity-H/V with Adobe CJK collections and predefined CJK CMaps independently (#3673); per-glyph validation and embedded-font extraction remain separate.<br>Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export; a bounded, text-only auto-tag proposal tool for one page. Marking a figure decorative rewrites only that stream (#1324). An XFA radio item whose value contains a space is written back unchanged (#1319). Done since: #2569: Growable multiline XFA text reflows after edits, with undo/redo, save/reopen and print coverage; oversized fields still become read-only page text. Structured-data synchronization warnings retain accepted whitespace without relayout from stale data. Done since: #2572: Every repeated page-master button resolves its own field to the shared XFA click script, including print buttons. Done since: #2570: Dynamic XFA rich-text fields carry exData/maxLength into AcroForm MaxLen for reported and enforced Unicode character limits, retained after save/reopen. Missing: writing proposed tags and parent tree, field autotag, visual Tags/Order/Content panels, Reading Order tool, compare, geospatial/3D measurement, search index, XFA<br>Reading colours: pending page backgrounds follow the configured colour (#2059). |
|
||||
| M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers<br>Done since: CLI run and MCP reject missing, empty, repeated and option-valued --root arguments before any tool/script execution or MCP initialization. A single-hyphen directory name remains valid; synthetic CLI regressions preserve a would-be overwritten file. `text_find` also caps the per-page match ranges it materializes at the remaining result limit plus one continuation match. CLI robustness scans visit each directory once through overlapping roots and symlink aliases. |
|
||||
@@ -239,6 +239,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | minor | M10: annotation appearances use unoccupied XObject names, preserving original page artwork and distinct appearances through inherited resources and print/reopen. Estimates unchanged. |
|
||||
| 2026-10-22 | minor | M4, #3916 partial: edits and Undo/Redo cancel Organize page drags. Pointer-release and save/reopen regressions check page order. Estimates unchanged. |
|
||||
| 2026-10-22 | minor | M14 #3558: rebased macOS association recovery; reuse #3042 shared metadata preservation instead of a duplicate save wrapper. Estimates unchanged. |
|
||||
| 2026-10-20 | minor | M4: initial-view window and interface options resolve indirect boolean ViewerPreferences values; synthetic true/false and save/reopen regressions retain defaults for malformed entries. Estimates unchanged. |
|
||||
|
||||
@@ -8109,8 +8109,8 @@ milestone = "M10"
|
||||
status = "shipped"
|
||||
commands = ["print.dialog"]
|
||||
tools = ["doc_print"]
|
||||
evidence = ["crates/print/src/tests.rs::imposed_pdf_has_the_sheets_and_honours_comments_and_forms"]
|
||||
notes = "Print flag honoured"
|
||||
evidence = ["crates/print/src/tests.rs::imposed_pdf_has_the_sheets_and_honours_comments_and_forms", "crates/print/src/tests/appearances.rs::annotation_appearances_do_not_replace_page_xobjects"]
|
||||
notes = "Print flag honoured; appearance resource names preserve existing page XObjects and distinct annotations, including inherited resources"
|
||||
|
||||
[[feature]]
|
||||
id = "print.print-with-comment-summary"
|
||||
|
||||
Reference in New Issue
Block a user