diff --git a/ROADMAP.md b/ROADMAP.md index 248f3480b..b88e29fca 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -22,6 +22,7 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 #2080 candidate: explicit search jumps join per-tab history; rail and control-channel Back/Forward; estimates unchanged) > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M4 #1910: Organize Pages opens on the current page; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 candidate: coordinate control clicks keep press/release together on slow frames and preserve text multi-click selection; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M10 #2156 candidate: remember selected print paper by canonical name; estimates unchanged) > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M3 candidate: native opens retain chosen tools through password retries and defer startup intake while preparation is pending; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M4 #1310 #1311 #1312: undo, revert and the next save keep the last saved revision; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M3 #1980: exact quit modifiers preserve Ctrl+Alt+Q email input; M3 candidate: native opens retain chosen tools through password retries and defer startup intake while preparation is pending; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -270,6 +271,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-14 (community, M7, #128 Hebrew in Add text):** Add text and `page_add_text` draw Hebrew with the craft-fonts Hebr face (Noto Sans Hebrew), through the same shaped path as Arabic and Thai: right-to-left display order with numbers and Latin in place, points and cantillation kept on their letter as one Type 3 glyph, ToUnicode and per-line ActualText so extraction, search and the editor get typing order. Builds without the face refuse Hebrew rather than write `?`. Regular style only. Estimates unchanged. - **2026-10-14 (M9, #1787):** Changes after signing are judged by the role an object held when signed for a page's resources too: a rewritten font, XObject, graphics state or resources dictionary is page content however its new version is labelled, so a `/FT` or `/Subtype /Widget` added by the attacker no longer reads as a permitted form fill. A category dictionary that only gained a name is judged as before. - **2026-10-14 (M10, #1818):** Properties… reads a CUPS printer's driver options off the GUI thread, the dialog saying so meanwhile, and `lpoptions` is given ten seconds before it is killed and the PPD's defaults stand; a read that dies leaves the panel with nothing rather than a frozen window. The bounded wait is tested with a stand-in command that never answers. +- **2026-10-14 (M10 #2156 candidate):** Print paper selection survives Cancel and settings restoration; canonical names avoid preset-index dependence. Other print preferences and estimates unchanged. + - **2026-10-13 (M3, native-opening candidate):** Chosen tools survive preparation, conversion and password retries; startup intake waits for pending preparation. Regression tests cover completion, cancellation and document identity. Estimates unchanged. - **2026-10-13 (community, M3, #531 touchpad momentum):** On Wayland a two-finger swipe keeps gliding after the fingers lift and slows to a stop, measured from the gesture's last 0.1 s and decaying with a 0.35 s time constant through the #759 1:1 path. Any wheel input, click, key, zoom or edge ends it (fingers resting on the touchpad don't yet: the vendored winit binds no Wayland hold gestures); single-page paging, macOS (which sends its own momentum), the web, and Animation effects Off never glide. Unit tests for the physics and two headless view tests. Estimates unchanged. - **2026-10-13 (M3, native-open admission):** Later prepared replies stay queued while a modal or native picker owns the foreground. Picker completion, cancellation and abandonment wake retained opens. Regressions cover recipient ownership, exactly-once admission, source/history preservation and one-shot repaint requests. Estimates unchanged. diff --git a/crates/print/README.md b/crates/print/README.md index c8871629b..ae60e1a2c 100644 --- a/crates/print/README.md +++ b/crates/print/README.md @@ -36,6 +36,12 @@ Each source page becomes a Form XObject (its content wrapped in q/Q, plus the pr annotations); sheets place them with a clip. The result is a fresh, unencrypted, garbage-collected file (callers check the print permission). +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`; +unrecognized saved values retain the current selection (US Letter on a fresh app). +This change does not alter persistence of other print preferences. +Headless `doc_print` defaults are unchanged. + `spool` delegates discovery, driver options, preferences and submission to [`craft-print`](https://github.com/storytold/craft-libs/tree/main/crates/print). PdfCraft retains page selection, imposition, annotations and print-permission checks. diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 54c43d26d..f12e10968 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2323,6 +2323,8 @@ impl PdfCraftApp { // Set outside json! above, which is at serde_json's macro recursion limit. v["hide_empty_highlight_hover"] = serde_json::Value::Bool(self.comment_prefs.hide_empty_highlight_hover); v["organize_zoom"] = serde_json::json!(self.organize_zoom); + // Canonical names survive changes to the order of PAPERS and do not depend on UI language. + v["print_paper"] = serde_json::json!(pdfcraft_engine::print::PAPERS.get(self.print_draft.paper).map(|(name, _)| *name)); v.to_string() } @@ -2384,6 +2386,13 @@ impl PdfCraftApp { if let Ok(units) = serde_json::from_value::(v["units"].clone()) { self.units = units; } + if let Some(paper) = v + .get("print_paper") + .and_then(serde_json::Value::as_str) + .and_then(|name| pdfcraft_engine::print::PAPERS.iter().position(|(canonical, _)| *canonical == name)) + { + self.print_draft.paper = paper; + } if let Some(on) = v["highlight_fields"].as_bool() { self.view_defaults.highlight_fields = on; } diff --git a/crates/ui-egui/tests/print.rs b/crates/ui-egui/tests/print.rs index 45adcb066..ddd8aac2e 100644 --- a/crates/ui-egui/tests/print.rs +++ b/crates/ui-egui/tests/print.rs @@ -6,9 +6,16 @@ use egui_kittest::kittest::{NodeT, Queryable}; use pdfcraft_ui_egui::{Dialog, PdfCraftApp}; fn harness() -> Harness<'static, PdfCraftApp> { - let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { + harness_with_settings(None) +} + +fn harness_with_settings(settings: Option) -> Harness<'static, PdfCraftApp> { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); app.set_option("language", "en").unwrap(); + if let Some(json) = &settings { + app.restore(json); + } app.open_bytes("form.pdf", None, include_bytes!("data/form.pdf").to_vec()).unwrap(); app }); @@ -736,3 +743,125 @@ fn print_without_a_document_does_not_blame_security() { h.run_steps(30); assert!(h.query_by_label_contains("don't allow printing").is_none()); } + +#[test] +fn paper_choice_survives_cancel_and_settings_restore() { + use pdfcraft_engine::print::{Orientation, PAPERS}; + let a4 = PAPERS.iter().position(|(name, _)| *name == "A4").unwrap(); + let letter = PAPERS.iter().position(|(name, _)| *name == "US Letter").unwrap(); + let mut h = harness(); + assert!(h.state_mut().execute("print.dialog")); + h.run_steps(3); + h.get_by_value("US Letter").click(); + h.run_steps(2); + h.get_by_label("A4").click(); + h.run_steps(2); + assert_eq!(h.state().print_draft.paper, a4); + // Only paper should be added to persistence; these choices still belong to this session. + let draft = &mut h.state_mut().print_draft; + draft.copies = 5; + draft.which = pdfcraft_ui_egui::PrintWhich::Range; + draft.range = "1".into(); + draft.orientation = Orientation::Landscape; + draft.grayscale = true; + let before_cancel = draft.clone(); + h.get_by_label("Cancel").click(); + h.run_steps(2); + assert_eq!(h.state().dialog, None); + assert_eq!(h.state().print_draft, before_cancel, "cancel keeps the existing draft behavior"); + let saved = h.state().persist(); + let json: serde_json::Value = serde_json::from_str(&saved).unwrap(); + assert_eq!(json["print_paper"], "A4", "store the canonical name, not its position"); + let mut app = PdfCraftApp::new(); + app.restore(&saved); + let expected = pdfcraft_ui_egui::PrintDraft { paper: a4, ..Default::default() }; + assert_eq!(app.print_draft, expected, "no other print preference becomes persistent"); + let mut restarted = harness_with_settings(Some(saved)); + assert!(restarted.state_mut().execute("print.dialog")); + restarted.run_steps(3); + restarted.get_by_value("A4"); + assert_eq!(restarted.state().print_draft.paper, a4); + restarted.get_by_value("A4").click(); + restarted.run_steps(2); + restarted.get_by_label("US Letter").click(); + restarted.run_steps(2); + restarted.get_by_label("Cancel").click(); + restarted.run_steps(2); + let saved = restarted.state().persist(); + assert_eq!(serde_json::from_str::(&saved).unwrap()["print_paper"], "US Letter"); + let mut again = harness_with_settings(Some(saved)); + assert!(again.state_mut().execute("print.dialog")); + again.run_steps(3); + again.get_by_value("US Letter"); + assert_eq!(again.state().print_draft.paper, letter); +} + +#[test] +fn paper_restore_accepts_canonical_names_and_ignores_invalid_settings() { + use pdfcraft_engine::print::PAPERS; + for (index, (name, _)) in PAPERS.iter().enumerate() { + let mut app = PdfCraftApp::new(); + app.restore(&serde_json::json!({"print_paper": name}).to_string()); + assert_eq!(app.print_draft.paper, index); + assert_eq!(serde_json::from_str::(&app.persist()).unwrap()["print_paper"], *name); + } + for invalid in [ + "not JSON", + "{}", + r#"{"print_paper":null}"#, + r#"{"print_paper":true}"#, + r#"{"print_paper":[]}"#, + r#"{"print_paper":{}}"#, + r#"{"print_paper":"unknown"}"#, + r#"{"print_paper":"a4"}"#, + r#"{"print_paper":4}"#, + r#"{"print_paper":-1}"#, + r#"{"print_paper":4294967296}"#, + r#"{"print_paper":18446744073709551615}"#, + r#"{"print_paper":1e100}"#, + ] { + let mut app = PdfCraftApp::new(); + let factory = app.print_draft.paper; + app.restore(invalid); + assert_eq!(app.print_draft.paper, factory, "{invalid}"); + app.restore(r#"{"print_paper":"A4"}"#); + let selected = app.print_draft.paper; + app.restore(invalid); + assert_eq!(app.print_draft.paper, selected, "invalid input must not replace a valid choice: {invalid}"); + } + let mut app = PdfCraftApp::new(); + app.print_draft.paper = usize::MAX; + assert_eq!(serde_json::from_str::(&app.persist()).unwrap()["print_paper"], serde_json::Value::Null); +} + +#[test] +fn restored_paper_keeps_preview_and_saved_sheet_dimensions() { + use pdfcraft_engine::print::{self, PAPERS}; + let a4 = PAPERS.iter().find(|(name, _)| *name == "A4").unwrap().1; + let mut h = harness_with_settings(Some(r#"{"print_paper":"A4"}"#.into())); + assert!(h.state_mut().execute("print.dialog")); + h.run_steps(3); + h.get_by_value("A4"); + h.get_by_label("Sheet 1 of 1"); + let id = h.state().views[h.state().active.unwrap()].id; + let doc = h.state().session.get(id).unwrap(); + let sizes: Vec<_> = doc.info.pages.iter().map(|p| (f64::from(p.width), f64::from(p.height))).collect(); + let labels: Vec<_> = doc.info.pages.iter().map(|p| p.label.clone()).collect(); + let settings = h.state().print_draft.settings(sizes.len(), &labels).unwrap(); + assert_eq!(settings.paper, a4); + let sheets = print::layout(&sizes, &settings).unwrap(); + assert_eq!(sheets.len(), 1); + assert_eq!(sheets[0].size, a4, "preview uses the restored paper"); + let out = std::env::temp_dir().join(format!("pdfcraft-restored-paper-{}.pdf", std::process::id())); + h.state_mut().print_draft.printer = None; + h.state_mut().save_override = Some(out.to_string_lossy().into_owned()); + h.get_by_label("Save as PDF").click(); + h.run_steps(3); + assert_eq!(h.state().dialog, None); + let printed = pdfcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(&out).unwrap())).unwrap(); + let pages = pdfcraft_model::pages(&printed); + assert_eq!(pages.len(), sheets.len()); + let size = pages[0].display_size(&printed); + assert!((size.0 - a4.0).abs() < 0.01 && (size.1 - a4.1).abs() < 0.01, "saved sheet: {size:?}"); + std::fs::remove_file(out).unwrap(); +} diff --git a/docs/roadmap.md b/docs/roadmap.md index ae504008d..78e67dc04 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -20,6 +20,7 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 candidate: coordinate control clicks on slow frames and coalesced text selection; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-15 · **Change:** minor (M2: sampled functions index a flat table, one image decode serves every zoom level, patch meshes copy less; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 #2080 candidate: explicit search jumps join per-tab history; rail and control-channel Back/Forward; estimates unchanged) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M10 #2156 candidate: remember selected print paper by canonical name; estimates unchanged) > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M3 candidate: native opens retain chosen tools through password retries and defer startup intake while preparation is pending; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M4 #1310 #1311 #1312: undo, revert and the next save keep the last saved revision; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-15 · **Change:** minor (M14: PNG export writes opaque pages as RGB at deflate level 3, the same size 3–5× faster; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -149,7 +150,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.
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). 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.
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).
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 | +| 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).
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 #2156 candidate: last selected print paper persists across sessions; other print preferences unchanged. | | 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). 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.
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). Missing: writing proposed tags and parent tree, field autotag, visual Tags/Order/Content panels, Reading Order tool, compare, geospatial/3D measurement, search index, XFA | | 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
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. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index a4132c01b..5ee0d662f 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -7936,8 +7936,8 @@ milestone = "M10" status = "shipped" commands = ["print.dialog"] tools = ["doc_print", "printers"] -evidence = ["crates/ui-egui/tests/print.rs::print_dialog_lays_out_sheets_and_saves_a_pdf", "crates/engine/src/tests.rs::printing_lays_out_sheets_that_render", "crates/ui-egui/src/web_print.rs::a_job_draws_every_sheet_at_its_planned_resolution_then_builds_the_page", "crates/ui-egui/src/web_print.rs::the_print_page_turns_sideways_sheets_names_other_sizes_and_ends_without_a_blank_page"] -notes = "⌘P; settings and live sheet preview; on the web the same dialog prints through the browser's print dialog" +evidence = ["crates/ui-egui/tests/print.rs::paper_choice_survives_cancel_and_settings_restore", "crates/ui-egui/tests/print.rs::restored_paper_keeps_preview_and_saved_sheet_dimensions", "crates/ui-egui/tests/print.rs::print_dialog_lays_out_sheets_and_saves_a_pdf", "crates/engine/src/tests.rs::printing_lays_out_sheets_that_render", "crates/ui-egui/src/web_print.rs::a_job_draws_every_sheet_at_its_planned_resolution_then_builds_the_page", "crates/ui-egui/src/web_print.rs::the_print_page_turns_sideways_sheets_names_other_sizes_and_ends_without_a_blank_page"] +notes = "⌘P; settings and live sheet preview; on the web the same dialog prints through the browser's print dialog; last paper selection, including cancelled Print dialogs, persists by canonical name; this change does not alter persistence of other print preferences" [[feature]] id = "print.printer-selection"