Browse / github.com/storytold/pdfcraft / patch
M10: preserve printable Link appearances (#5022)
Vector print drops existing printable Link annotation appearances Vector printing omits a Link annotation's existing normal appearance even when its Print flag is set and Hidden is clear. This loses visible artwork in printed/exported output. Reproduced with the current `main` at `1dd2737b47a9c1c9562f0af563053d1bf510e4bf`, using the published build prerequisites from #4969. The fixture is contributor-original vector artwork with no fonts or external assets: ```text Page: /MediaBox [0 0 200 200], empty Contents, /Annots [5 0 R] 5 0 obj << /Type /Annot /Subtype /Link /Rect [50 50 150 150] /F 4 /Border [0 0 0] /A << /S /URI /URI (https://example.invalid
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- robertobulltech (imported from pull request #5028, open)
- Base
1dd2737b47a9c1c9562f0af563053d1bf510e4bf- Size
- +46 −4 in 4 files
- Review
- pending
- Shared
- 2026-10-28
- Id
880ba05fb7318f1ff4f9f295
How it went
No other operator has reported using it yet.
Diff
raw · crates/print/README.md crates/print/src/imposition.rs crates/print/src/tests/appearances.rs parity/acrobat-features.toml
@@ -27,6 +27,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`@@ -99,7 +99,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.@@ -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]@@ -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"]-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; 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 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 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) are drawn from their dictionaries in the page view, page_render and page images, including the CLI render command, never saved; 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 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 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"@@ -8156,8 +8156,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"