diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index a4987a17f..86fc9dce4 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -934,16 +934,6 @@ fn display_bytes(editor: Option<&mut Editor>, bytes: &Arc>) -> Arc Option>> { - let e = doc.editor.as_ref()?; - let comments = pdfcraft_annot::with_missing_appearances(&e.cos); - let fields = pdfcraft_forms::with_printed_field_appearances(comments.as_ref().unwrap_or(&e.cos))?; - write_incremental(&fields, &SaveOptions::default()).ok().map(Arc::new) -} - /// `comments` (the display copy with comment appearances, if it needed any; else the document /// itself) with its form fields drawn from their values where the file leaves that to the viewer. fn with_field_appearances(comments: Option, doc: &pdfcraft_cos::Document) -> Option { diff --git a/crates/engine/src/print_raster.rs b/crates/engine/src/print_raster.rs index f54d64eed..4ed589586 100644 --- a/crates/engine/src/print_raster.rs +++ b/crates/engine/src/print_raster.rs @@ -23,9 +23,7 @@ pub fn rasterize(doc: &Document, pages: &[usize], raster: &Raster, content: Cont // The copy below holds only what prints, so the viewer's own comment hiding plays no part. config.hide_comments = false; let printed = guard(|| -> Result, String> { - // The display copy, less what it shows only on screen. - let shown = crate::printed_display_bytes(doc).unwrap_or_else(|| src.bytes.clone()); - let mut cos = pdfcraft_cos::Document::open_with_password(shown, config.password.as_deref()).map_err(|e| e.to_string())?; + let mut cos = pdfcraft_cos::Document::open_with_password(src.bytes.clone(), config.password.as_deref()).map_err(|e| e.to_string())?; pdfcraft_print::keep_printed(&mut cos, content); write_incremental(&cos, &SaveOptions::default()).map_err(|e| e.to_string()) })??; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 01789a3d7..154682601 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -7440,22 +7440,20 @@ fn a_page_truncated_for_forms_and_pictures_is_listed_once() { } /// ISO 32000-2 Table 192, 12.7.5.2.3 and Table 228 (Comb): a check box, a push button, a comb -/// field and a combo box the file leaves to the viewer. The page as displayed shows the check -/// box's caption symbol in its text colour, the button's grey face, the comb's cells in the border -/// colour and the combo box's drop-down button; printing leaves that button out, and saving -/// writes the file as it was. +/// field the file leaves to the viewer. The page as displayed and printed as an image shows the +/// check box's caption symbol in its text colour, the button's grey face and the comb's cells in +/// the border colour, and saving writes the file as it was. #[test] fn buttons_and_combs_left_to_the_viewer_are_displayed() { let pdf = b"%PDF-1.7\n\ 1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >> endobj\n\ 2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n\ - 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 400 200] /Annots [6 0 R 7 0 R 8 0 R 9 0 R] >> endobj\n\ - 4 0 obj << /Fields [6 0 R 7 0 R 8 0 R 9 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 5 0 R >> >> >> endobj\n\ + 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 400 200] /Annots [6 0 R 7 0 R 8 0 R] >> endobj\n\ + 4 0 obj << /Fields [6 0 R 7 0 R 8 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 5 0 R >> >> >> endobj\n\ 5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> endobj\n\ 6 0 obj << /Type /Annot /Subtype /Widget /FT /Btn /T (k) /V /Yes /AS /Yes /DA (/ZaDb 0 Tf 1 0 0 rg) /MK << /CA (8) /BC [0 0 0] >> /Rect [20 150 60 190] /F 4 /P 3 0 R >> endobj\n\ 7 0 obj << /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (o) /DA (/Helv 12 Tf 0 g) /MK << /CA (Press) /BG [0.8 0.8 0.8] /BC [0 0 0] >> /BS << /W 2 /S /B >> /Rect [100 150 220 190] /F 4 /P 3 0 R >> endobj\n\ 8 0 obj << /Type /Annot /Subtype /Widget /FT /Tx /Ff 16777216 /MaxLen 4 /T (d) /V (AB) /DA (/Helv 12 Tf 0 g) /MK << /BC [0 0 1] >> /Rect [20 80 220 110] /F 4 /P 3 0 R >> endobj\n\ - 9 0 obj << /Type /Annot /Subtype /Widget /FT /Ch /Ff 131072 /T (m) /Opt [(Apple) (Banana)] /V (Apple) /DA (/Helv 12 Tf 0 g) /MK << /BC [0 0 0] >> /Rect [20 20 220 50] /F 4 /P 3 0 R >> endobj\n\ trailer << /Root 1 0 R >>\n%%EOF" .to_vec(); let render = |bytes: Arc>| { @@ -7484,15 +7482,12 @@ fn buttons_and_combs_left_to_the_viewer_are_displayed() { assert!(count(&shown, (x - 1, 84, x + 1, 106), &blue) >= 20, "a cell line at {x}"); } assert_eq!(count(&shown, (95, 84, 97, 106), &blue), 0, "no line inside a cell"); - // The combo box's drop-down button: a 28 pt square inside the right edge. - let button = (193, 22, 218, 48); - assert!(count(&shown, button, &grey) > 300, "the drop-down button on screen"); // Print as Image: the page as a picture, at 72 dpi so it is the size of the page here. let raster = pdfcraft_print::Raster { dpi: 72.0, grayscale: false }; let pictures = crate::print_raster::rasterize(s.get(id).unwrap(), &[0], &raster, pdfcraft_print::Content::Document).unwrap(); let printed = render(Arc::new(pictures)); - assert_eq!(count(&printed, button, &grey), 0, "not on paper"); assert!(count(&printed, (24, 154, 56, 186), &red) > 60, "the check box prints"); + assert!(count(&printed, (104, 154, 216, 186), &grey) > 1000, "and the push button"); // The file is saved as it was: no widget has an appearance. let saved = pdfcraft_cos::Document::open(s.save_bytes(id).unwrap()).unwrap(); for f in pdfcraft_forms::fields(&saved) { diff --git a/crates/forms/README.md b/crates/forms/README.md index 88a086794..4aec223ee 100644 --- a/crates/forms/README.md +++ b/crates/forms/README.md @@ -58,9 +58,7 @@ let cells: Vec = grid.cells_for(names)?; // "Table1_1", "Table drawing while the objects it was drawn from are the same (`FieldAppearanceCache`), so typing in one field redraws that field, not the form. Check boxes and radio buttons with no appearance for their state are drawn from `/MK` (border, background, and when on the `/MK /CA` ZapfDingbats - symbol in the `/DA` colour), and push buttons with none from `/MK`, `/BS` and their caption. On - screen a drawn combo box also shows a drop-down button; `with_printed_field_appearances` leaves it - out for printing. + symbol in the `/DA` colour), and push buttons with none from `/MK`, `/BS` and their caption. - Check boxes and radio buttons switch `/V` and each widget's `/AS`. Widgets with no appearances get PdfCraft's own (a drawn check mark or dot; no symbol font needed). - Imported `/MaxLen 0` is treated as unlimited (also clearing an inherited limit), while the diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index 13464bbd3..7558b120a 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -390,25 +390,6 @@ fn comb_dividers(doc: &Document, wd: &Dict, w: f64, h: f64, cells: usize) -> Str c } -/// A combo box's drop-down button for the screen: a `size`-wide light grey face at the right -/// edge inside the `bw` border of a `w` × `h` widget, parted from the text by a line in the -/// border's colour, with a downward triangle. -fn drop_down_button(doc: &Document, wd: &Dict, w: f64, h: f64, bw: f64, size: f64) -> String { - let mk = wd.get(b"MK").map(|m| doc.resolve(m)).and_then(|m| m.as_dict().cloned()); - let (x0, y0, x1, y1) = (w - bw - size, bw, w - bw, h - bw); - let mut c = format!("0.85 g\n{} {} {} {} re f\n", n(x0), n(y0), n(size), n(y1 - y0)); - let lw = line_width(doc, wd); - if let Some(bc) = color_array(doc, mk.as_ref(), b"BC") - && lw > 0.0 - && lw.is_finite() - { - c.push_str(&format!("{}\n{} w\n{} {} m {} {} l S\n", stroke_op(&bc), n(lw), n(x0), n(y0), n(x0), n(y1))); - } - let (cx, cy, d) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0, size * 0.22); - c.push_str(&format!("0 g\n{} {} m {} {} l {} {} l h f\n", n(cx - d), n(cy + d / 2.0), n(cx + d), n(cy + d / 2.0), n(cx), n(cy - d / 2.0))); - c -} - /// Where a list box draws its options: one row per option from the top, starting with option /// `top` (`/TI`, §12.7.4.4). The appearance PdfCraft writes and the viewer's in-place list share /// it, so a click picks the option drawn under the pointer (#1524). @@ -499,19 +480,6 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri /// hold it: the form's font is a simple (WinAnsi) font, Helvetica stands in for one PdfCraft can't /// write with (an embedded subset, `Identity-H`), or a UCS-2 CMap meets a character beyond U+FFFF. pub fn field_appearance_drawn(doc: &Document, f: &Field, w: &Widget, values: &[String], format: bool) -> (Stream, Option) { - field_look(doc, f, w, values, format, false) -} - -/// [`field_appearance_drawn`] as the page view shows a field the file leaves to the viewer: a -/// combo box also gets its drop-down button at the right edge, which the text keeps clear of. -/// The button is something to click on screen, not part of the field's printed look, so it is -/// drawn only here and never into an appearance PdfCraft saves or prints. -pub(crate) fn field_appearance_on_screen(doc: &Document, f: &Field, w: &Widget, values: &[String], format: bool) -> (Stream, Option) { - field_look(doc, f, w, values, format, true) -} - -/// The appearance of a text or choice field, with a combo box's drop-down button when `screen`. -fn field_look(doc: &Document, f: &Field, w: &Widget, values: &[String], format: bool, screen: bool) -> (Stream, Option) { // The Format event: what is shown, not what is stored. let formatted: Vec; let values = if format @@ -552,15 +520,7 @@ fn field_look(doc: &Document, f: &Field, w: &Widget, values: &[String], format: let width_of = |text: &str, size: f64| enc.width(text, size); let (mut c, bw) = frame(doc, &wd, width, height); let pad = 2.0 + bw; - // A combo box's drop-down button on screen: a square as tall as the room inside the border, - // never wider than that room. - let button = if screen && f.kind == FieldKind::Combo { (height - 2.0 * bw).min(width - 2.0 * bw).max(0.0) } else { 0.0 }; - if button > 0.0 { - c.push_str(&drop_down_button(doc, &wd, width, height, bw, button)); - } - // The text and its clip keep to the room left of the button. - let text_w = width - button; - let inner_w = (text_w - 2.0 * pad).max(1.0); + let inner_w = (width - 2.0 * pad).max(1.0); let q = wd.get(b"Q").and_then(|o| doc.resolve(o).as_int()).unwrap_or(f.quadding); let mut body: Vec = Vec::new(); // The first character drawn that the font can't hold (drawn as `?`), for the caller to report. @@ -579,7 +539,7 @@ fn field_look(doc: &Document, f: &Field, w: &Widget, values: &[String], format: let tw = width_of(text, size); match q { 1 => pad + (inner_w - tw) / 2.0, - 2 => text_w - pad - tw, + 2 => width - pad - tw, _ => pad, } }; @@ -649,7 +609,7 @@ fn field_look(doc: &Document, f: &Field, w: &Widget, values: &[String], format: "/Tx BMC\nq\n{c}{} {} {} {} re W n\nBT\n/{} {} Tf\n{}\n", n(bw), n(bw), - n(text_w - 2.0 * bw), + n(width - 2.0 * bw), n(height - 2.0 * bw), font_name, n(size), diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 2a8761856..04de78ba1 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -1304,9 +1304,8 @@ pub fn need_appearances(doc: &Document) -> bool { /// writer left the appearances to the viewer), and otherwise each widget without a normal /// appearance (`/AP /N`), as typing the value would draw it (#3470). Check boxes and radio buttons /// with no normal appearance for their state (`/AS`) are drawn from their `/MK` background, border -/// and caption, and push buttons with none from their `/MK` and `/BS` (Table 192); the page view -/// also shows a drawn combo box's drop-down button. `doc` is not changed: saving writes the file -/// as it was. +/// and caption, and push buttons with none from their `/MK` and `/BS` (Table 192). `doc` is not +/// changed: saving writes the file as it was. pub fn with_field_appearances(doc: &Document) -> Option { with_field_appearances_cached(doc, &mut FieldAppearanceCache::default()) } @@ -1314,18 +1313,6 @@ pub fn with_field_appearances(doc: &Document) -> Option { /// [`with_field_appearances`], reusing the appearances `cache` drew for widgets whose inputs are /// the same objects as then (see [`FieldAppearanceCache`]). pub fn with_field_appearances_cached(doc: &Document, cache: &mut FieldAppearanceCache) -> Option { - fields_shown(doc, cache, true) -} - -/// [`with_field_appearances`] for printing: the same, without the drop-down buttons of combo -/// boxes, which are for clicking on screen and not part of a field's printed look. -pub fn with_printed_field_appearances(doc: &Document) -> Option { - fields_shown(doc, &mut FieldAppearanceCache::default(), false) -} - -/// [`with_field_appearances_cached`]; `screen` adds the on-screen parts (a combo box's button). -/// One cache serves one of the two: the engine keeps one for the screen and prints uncached. -fn fields_shown(doc: &Document, cache: &mut FieldAppearanceCache, screen: bool) -> Option { let all = need_appearances(doc); let mut copy: Option = None; for f in fields(doc) { @@ -1365,7 +1352,6 @@ fn fields_shown(doc: &Document, cache: &mut FieldAppearanceCache, screen: bool) let stream = match f.kind { FieldKind::CheckBox | FieldKind::Radio => appearance::button_state_shown(doc, &f, w, state.as_deref() != Some("Off")), FieldKind::PushButton => appearance::push_button_shown(doc, &f, w), - _ if screen => appearance::field_appearance_on_screen(doc, &f, w, &f.value, true).0, _ => appearance::field_appearance_drawn(doc, &f, w, &f.value, true).0, }; cache.insert(w.obj, inputs, &stream); diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 27b64f5a1..e3c7a1d13 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -3098,8 +3098,8 @@ fn a_skipped_field_leaves_its_widget_to_the_next() { assert_eq!(widgets, [("b", 1)]); } -/// Check boxes, radio buttons and push buttons whose file gives them no appearance, a comb field -/// and a combo box, for the display copy. +/// Check boxes, radio buttons and push buttons whose file gives them no appearance, a comb field, +/// a combo box and a list box, for the display copy. fn left_buttons() -> Document { let widget = |n: &str, rest: &str| format!("<< /Type /Annot /Subtype /Widget /T ({n}) /F 4 /P 3 0 R {rest} >>"); document(&[ @@ -3226,30 +3226,3 @@ fn comb_fields_show_their_cells() { let plain = saved_look(&doc, "d"); assert!(!plain.contains(" m ") && plain.contains("(A) Tj"), "{plain}"); } - -/// A combo box drawn for display shows its drop-down button at the right edge and keeps its text -/// clear of it; a list box, a text field, the combo box's saved look and its printed one have none. -#[test] -fn combo_boxes_shown_on_screen_have_a_drop_down_button() { - let doc = left_buttons(); - let screen = with_field_appearances(&doc).expect("fields to draw"); - let printed = with_printed_field_appearances(&doc).expect("fields to draw"); - let m = ap(&screen, &field(&fields(&screen), "m").widgets[0]); - // A 22 pt square inside the 1 pt border, a line in the border colour, a triangle. - assert!(m.contains("0.85 g\n97 1 22 22 re f\n0 0 0 RG\n1 w\n97 1 m 97 23 l S\n0 g\n"), "{m}"); - // The text's clip stops at the button. - assert!(m.contains("1 1 96 22 re W n"), "{m}"); - let printed_m = ap(&printed, &field(&fields(&printed), "m").widgets[0]); - assert!(!printed_m.contains("0.85 g") && printed_m.contains("1 1 118 22 re W n"), "{printed_m}"); - let saved = saved_look(&doc, "m"); - assert!(!saved.contains("0.85 g"), "{saved}"); - // Right-aligned text moves left by the button's width. - let x = |s: &str| -> f64 { - s.split(" Tm ").next().and_then(|s| s.rsplit("1 0 0 1 ").next()).and_then(|s| s.split(' ').next()).and_then(|s| s.parse().ok()).unwrap() - }; - assert!((x(&saved) - x(&m) - 22.0).abs() < 0.01, "{saved} / {m}"); - for other in ["n", "d"] { - let s = ap(&screen, &field(&fields(&screen), other).widgets[0]); - assert!(!s.contains("0.85 g"), "{other}: {s}"); - } -} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index a2281b009..9d98027d5 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -806,8 +806,8 @@ milestone = "M2" status = "shipped" commands = [] tools = [] -evidence = ["crates/render/src/raster.rs::widget_appearance_states", "crates/render/src/raster.rs::highlights_without_an_appearance_are_drawn", "crates/engine/src/tests.rs::comments_without_appearances_are_drawn_but_not_saved", "crates/engine/src/tests.rs::cli_display_adds_missing_appearances_without_changing_the_source", "crates/engine/src/tests.rs::fields_left_to_the_viewer_are_displayed_from_their_values", "crates/forms/src/tests.rs::need_appearances_draws_every_text_and_choice_field_in_the_display_copy", "crates/forms/src/tests.rs::the_field_appearance_cache_sees_every_input", "crates/engine/src/tests.rs::buttons_and_combs_left_to_the_viewer_are_displayed", "crates/forms/src/tests.rs::check_boxes_without_an_appearance_show_their_caption_symbol", "crates/forms/src/tests.rs::push_buttons_without_an_appearance_show_their_caption_and_frame", "crates/forms/src/tests.rs::combo_boxes_shown_on_screen_have_a_drop_down_button"] -notes = "comments without /AP (FreeText, Ink, notes, stamps, shapes, markup) are drawn from their dictionaries in the page view, page_render and page images, including the CLI render command, never saved; text and choice fields are drawn from their values when the form sets /NeedAppearances (every one) or a widget has no /N (#3470), also display-only and cached per widget (a field edit on a 1000-field /NeedAppearances form takes ~13 ms against ~7 ms on a form that needs none) (a read-only document still gets the renderer's Highlight fallback only); check boxes and radio buttons with no appearance for their /AS are drawn display-only from /MK (background, border, and when on the /MK /CA ZapfDingbats symbol in the /DA colour, a check or a dot by default), push buttons with none from /MK and /BS with their caption, and a drawn combo box shows a drop-down button on screen only (not in Print as Image); Link borders without /AP and printing are not covered yet" +evidence = ["crates/render/src/raster.rs::widget_appearance_states", "crates/render/src/raster.rs::highlights_without_an_appearance_are_drawn", "crates/engine/src/tests.rs::comments_without_appearances_are_drawn_but_not_saved", "crates/engine/src/tests.rs::cli_display_adds_missing_appearances_without_changing_the_source", "crates/engine/src/tests.rs::fields_left_to_the_viewer_are_displayed_from_their_values", "crates/forms/src/tests.rs::need_appearances_draws_every_text_and_choice_field_in_the_display_copy", "crates/forms/src/tests.rs::the_field_appearance_cache_sees_every_input", "crates/engine/src/tests.rs::buttons_and_combs_left_to_the_viewer_are_displayed", "crates/forms/src/tests.rs::check_boxes_without_an_appearance_show_their_caption_symbol", "crates/forms/src/tests.rs::push_buttons_without_an_appearance_show_their_caption_and_frame"] +notes = "comments without /AP (FreeText, Ink, notes, stamps, shapes, markup) are drawn from their dictionaries in the page view, page_render and page images, including the CLI render command, never saved; text and choice fields are drawn from their values when the form sets /NeedAppearances (every one) or a widget has no /N (#3470), also display-only and cached per widget (a field edit on a 1000-field /NeedAppearances form takes ~13 ms against ~7 ms on a form that needs none) (a read-only document still gets the renderer's Highlight fallback only); check boxes and radio buttons with no appearance for their /AS are drawn display-only from /MK (background, border, and when on the /MK /CA ZapfDingbats symbol in the /DA colour, a check or a dot by default), and push buttons with none from /MK and /BS with their caption; Link borders without /AP and printing are not covered yet" [[feature]] id = "view.render-error-isolation"