From 851d6ceca528a992e338240218545d2690f7aa01 Mon Sep 17 00:00:00 2001 From: robertobulltech Date: Tue, 10 Nov 2026 04:01:52 +0100 Subject: [PATCH] M10 #5022: preserve printable Link appearances (#5028) --- crates/print/README.md | 2 ++ crates/print/src/imposition.rs | 4 ++- crates/print/src/tests/appearances.rs | 38 +++++++++++++++++++++++++++ parity/acrobat-features.toml | 6 ++--- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/crates/print/README.md b/crates/print/README.md index e1bbe844cc..53155071f2 100644 --- a/crates/print/README.md +++ b/crates/print/README.md @@ -32,6 +32,8 @@ spool::submit(&pdf, &spool::Job { printer: None, copies: 2, ..Default::default() - **Poster**: tile scale, overlap shared by neighbouring tiles, cut marks. - **Comments & forms**: document, + markups, + stamps, or form fields only; annotations print only with their Print flag, drawn from their appearance streams (Algorithm 8.1). + Existing Link appearances are document content in the first three modes; Link appearances + are not synthesized when absent, and form fields only excludes them. - **Properties… on CUPS** reads the queue's PPD and its current defaults (`lpoptions -l`) off the GUI thread, and gives `lpoptions` ten seconds: a CUPS that has stopped answering leaves the PPD's own defaults in force instead of holding the dialog (#1818; `spool::output_within` diff --git a/crates/print/src/imposition.rs b/crates/print/src/imposition.rs index 19a75af32c..d5ad4e2648 100644 --- a/crates/print/src/imposition.rs +++ b/crates/print/src/imposition.rs @@ -100,7 +100,9 @@ pub(crate) fn prints(doc: &Document, d: &Dict, content: Content) -> bool { let subtype = subtype_obj.as_deref().and_then(Object::as_name).unwrap_or(b""); match subtype { b"Widget" => true, - b"Link" | b"Popup" => false, + // Existing Link appearances are part of the document, not markup or fields. + b"Link" => content != Content::FormFieldsOnly, + b"Popup" => false, b"Stamp" => content != Content::Document && content != Content::FormFieldsOnly, // ISO 32000-2 Table 167: an Invisible annotation of a type that isn't standard (Table // 171) is not printed even with its Print flag set. diff --git a/crates/print/src/tests/appearances.rs b/crates/print/src/tests/appearances.rs index 0d3107d1d7..f63ed14733 100644 --- a/crates/print/src/tests/appearances.rs +++ b/crates/print/src/tests/appearances.rs @@ -85,6 +85,44 @@ fn xobject(doc: &Document, owner: &Dict, name: &[u8]) -> ObjRef { objects.as_dict().unwrap().get(name).unwrap().as_ref().unwrap() } +/// Existing Link artwork prints as document content only when its flags allow printing. +#[test] +fn printable_link_appearance_survives_vector_print() { + for flags in [0, 2, 4, 6, 5] { + for content in [Content::Document, Content::DocumentAndMarkups, Content::DocumentAndStamps, Content::FormFieldsOnly] { + let (mut doc, _, annot) = widget_fixture(Some(Object::name("Form")), None); + doc.update_dict(annot, |d| { + d.set(b"Subtype".to_vec(), Object::name("Link")); + d.set(b"F".to_vec(), Object::Int(flags)); + }) + .unwrap(); + let before = saved(&doc); + let expected = flags & 4 != 0 && flags & 2 == 0 && content != Content::FormFieldsOnly; + let printed = render(impose(&doc, &print_settings(content)).unwrap()); + let color = if expected { [0, 255, 0, 255] } else { [255, 255, 255, 255] }; + assert_eq!(pixel(&printed, 30, 70), color, "flags={flags}, content={content:?}"); + let mut raster_source = doc.clone(); + keep_printed(&mut raster_source, content); + let page = &pdfcraft_model::pages(&raster_source)[0]; + let annots = raster_source.resolve(page.dict.get(b"Annots").unwrap()); + assert_eq!(annots.as_array().unwrap().len(), usize::from(expected), "raster flags={flags}, content={content:?}"); + assert_eq!(saved(&doc), before, "source remains unchanged"); + } + } + for subtype in ["Link", "Popup"] { + let (mut doc, _, annot) = widget_fixture(Some(Object::name("Form")), None); + doc.update_dict(annot, |d| { + d.set(b"Subtype".to_vec(), Object::name(subtype)); + if subtype == "Link" { + d.remove(b"AP"); + } + }) + .unwrap(); + let printed = render(impose(&doc, &print_settings(Content::DocumentAndMarkups)).unwrap()); + assert_eq!(pixel(&printed, 30, 70), [255, 255, 255, 255], "{subtype} must not acquire an appearance"); + } +} + /// Printing keeps occupied page resource names and distinct annotation appearances intact, /// whether the page resources are direct or inherited from the page tree. #[test] diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 7c59700dcc..030b05a1f2 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -819,7 +819,7 @@ status = "shipped" commands = [] tools = [] evidence = ["crates/render/src/raster/tests.rs::widget_appearance_states", "crates/render/src/raster/tests.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::need_appearances_draws_check_boxes_and_radio_buttons_from_their_captions", "crates/engine/src/tests.rs::need_appearances_buttons_are_displayed_from_their_captions", "crates/render/src/raster/tests.rs::invisible_unknown_annotations_and_popups_paint_nothing", "crates/print/src/tests.rs::invisible_annotations_of_unknown_types_do_not_print", "crates/engine/src/tests.rs::stamps_without_appearances_draw_the_stamp_they_name", "crates/automation/tests/it/automation.rs::page_render_draws_stamps_without_an_appearance", "crates/annot/src/tests.rs::a_four_entry_colour_with_a_null_is_cmyk_with_zero", "crates/forms/src/tests.rs::a_four_entry_mk_colour_with_a_null_is_cmyk_with_zero", "crates/annot/src/tests.rs::text_markup_whose_quads_reach_past_its_rect_is_drawn_over_them", "crates/annot/src/tests.rs::text_markup_reach_stops_at_the_page_and_the_quad_cap", "crates/annot/src/tests.rs::text_markup_reach_reads_quads_like_the_drawing", "crates/annot/src/tests.rs::ink_whose_strokes_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::polyline_whose_vertices_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::polygon_whose_vertices_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::line_whose_points_reach_past_its_rect_is_drawn_through_them", "crates/annot/src/tests.rs::path_reach_stops_at_the_point_cap_and_is_saved_on_restyle", "crates/annot/src/tests.rs::indirect_border_style_and_colour_are_drawn", "crates/annot/src/tests.rs::callout_without_rd_is_drawn", "crates/forms/src/tests.rs::normal_appearances_that_give_no_stream_are_drawn", "crates/engine/src/tests.rs::text_boxes_and_redaction_marks_without_appearances_show_their_outlines", "crates/annot/src/tests.rs::free_text_da_fonts_draw_in_their_standard_face", "crates/annot/src/tests.rs::free_text_ds_style_is_drawn", "crates/annot/src/tests.rs::redaction_marks_with_quadpoints_take_their_outline_from_oc_then_c", "crates/annot/src/tests.rs::unapplied_redaction_marks_follow_their_oc_outline_colour"] -notes = "comments without /AP (FreeText, Ink, notes, stamps, shapes, markup; text markup, lines, polylines, polygons and ink through their /QuadPoints, /L, /Vertices or /InkList where these reach past /Rect, as far as the crop box) are drawn from their dictionaries in the page view, page_render and page images, including the CLI render command, never saved, with style values given by reference (a /BS, colour, /RD or /CL as an indirect object, ISO 32000-2 §7.3.10) followed and a callout without /RD (optional, Table 177) boxed at its /Rect; a four-entry colour with a non-number in it (/C, /IC, /MK /BC and /BG written [1 0 0 null]) is read as CMYK with that entry 0; a stamp from another program draws the stamp its /Name names (the fourteen standard names of ISO 32000-2 Table 184, Draft when there is none) or, for any other name, a grey frame with both diagonals; text boxes without /BS or /Border get the /Border default 1-pt border (ISO 32000-2 Table 166), their /DA font when /DR or its resource name makes it one of the twelve standard text faces (others stay Helvetica), and /DS font, size and colour over /DA (no /RC rich text yet); unapplied redaction marks without /AP are outlined in an array /OC colour (three finite numbers) when they have one, with or without /QuadPoints; without one, marks with /QuadPoints keep /C (else red) and marks without /QuadPoints (Table 195: /Rect is the region) draw nothing; an /OC dictionary stays optional content (Table 166). Basis: ISO 32000-2 §12.5.6.23 says only that before redaction is applied the user can see, move and redefine these annotations, and Table 195 has no outline-colour entry, so the colour comes from black-box observation of two synthetic fixtures. noap3.pdf (generator attached to #4364), rows J/K/L, unapplied, without /QuadPoints: Adobe Acrobat Pro J nothing, K red outline, L nothing; Chrome (PDFium) and Edge nothing on all three; PDF-XChange Editor and Right PDF Editor a red outline on all three. oc.pdf, six marks without /AP (1 /OC blue; 2 /C green; 3 /OC blue + /C green; 4 /OC blue + /IC black; 5 no colour; 6 /OC blue + /QuadPoints; rebuilt inline in unapplied_redaction_marks_follow_their_oc_outline_colour): Acrobat blue on 1, 3, 4 and 6 and nothing on 2 and 5; Chrome and Edge nothing on all six; PDF-XChange Editor green on 2 and 3, nothing on the others; Right PDF Editor red on all six. The three readers tested that support redaction marks disagree, so PdfCraft follows the /OC colour, the one Acrobat uses; text and choice fields are drawn from their values when the form sets /NeedAppearances (every one) or a widget's /N gives no stream to draw (none, null, a dictionary without its stream data, or a state subdictionary without a stream for /AS; #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 when the form sets /NeedAppearances so is every one that has an /MK, over the appearance the file carries (ISO 32000-2 Table 224; one without /MK keeps its own), and push buttons with none from /MK and /BS with their caption; Link borders without /AP and printing are not covered yet; an annotation of a non-standard type with the Invisible flag (ISO 32000-2 Table 167) is neither drawn nor printed, and a popup's own /AP is never painted on the page (its parent's text shows in the popup window, §12.5.6.14)" +notes = "comments without /AP (FreeText, Ink, notes, stamps, shapes, markup; text markup, lines, polylines, polygons and ink through their /QuadPoints, /L, /Vertices or /InkList where these reach past /Rect, as far as the crop box) are drawn from their dictionaries in the page view, page_render and page images, including the CLI render command, never saved, with style values given by reference (a /BS, colour, /RD or /CL as an indirect object, ISO 32000-2 §7.3.10) followed and a callout without /RD (optional, Table 177) boxed at its /Rect; a four-entry colour with a non-number in it (/C, /IC, /MK /BC and /BG written [1 0 0 null]) is read as CMYK with that entry 0; a stamp from another program draws the stamp its /Name names (the fourteen standard names of ISO 32000-2 Table 184, Draft when there is none) or, for any other name, a grey frame with both diagonals; text boxes without /BS or /Border get the /Border default 1-pt border (ISO 32000-2 Table 166), their /DA font when /DR or its resource name makes it one of the twelve standard text faces (others stay Helvetica), and /DS font, size and colour over /DA (no /RC rich text yet); unapplied redaction marks without /AP are outlined in an array /OC colour (three finite numbers) when they have one, with or without /QuadPoints; without one, marks with /QuadPoints keep /C (else red) and marks without /QuadPoints (Table 195: /Rect is the region) draw nothing; an /OC dictionary stays optional content (Table 166). Basis: ISO 32000-2 §12.5.6.23 says only that before redaction is applied the user can see, move and redefine these annotations, and Table 195 has no outline-colour entry, so the colour comes from black-box observation of two synthetic fixtures. noap3.pdf (generator attached to #4364), rows J/K/L, unapplied, without /QuadPoints: Adobe Acrobat Pro J nothing, K red outline, L nothing; Chrome (PDFium) and Edge nothing on all three; PDF-XChange Editor and Right PDF Editor a red outline on all three. oc.pdf, six marks without /AP (1 /OC blue; 2 /C green; 3 /OC blue + /C green; 4 /OC blue + /IC black; 5 no colour; 6 /OC blue + /QuadPoints; rebuilt inline in unapplied_redaction_marks_follow_their_oc_outline_colour): Acrobat blue on 1, 3, 4 and 6 and nothing on 2 and 5; Chrome and Edge nothing on all six; PDF-XChange Editor green on 2 and 3, nothing on the others; Right PDF Editor red on all six. The three readers tested that support redaction marks disagree, so PdfCraft follows the /OC colour, the one Acrobat uses; text and choice fields are drawn from their values when the form sets /NeedAppearances (every one) or a widget's /N gives no stream to draw (none, null, a dictionary without its stream data, or a state subdictionary without a stream for /AS; #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 when the form sets /NeedAppearances so is every one that has an /MK, over the appearance the file carries (ISO 32000-2 Table 224; one without /MK keeps its own), and push buttons with none from /MK and /BS with their caption; Link borders without /AP are not synthesized for display or printing; an annotation of a non-standard type with the Invisible flag (ISO 32000-2 Table 167) is neither drawn nor printed, and a popup's own /AP is never painted on the page (its parent's text shows in the popup window, §12.5.6.14)" [[feature]] id = "view.render-error-isolation" @@ -8333,8 +8333,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", "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" +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", "crates/print/src/tests/appearances.rs::printable_link_appearance_survives_vector_print"] +notes = "Print and Hidden flags honoured; existing Link appearances print as document content, but missing Link appearances are not synthesized. Appearance resource names preserve existing page XObjects and distinct annotations, including inherited resources" [[feature]] id = "print.print-with-comment-summary"