collective

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

diff --git a/crates/print/README.md b/crates/print/README.md--- a/crates/print/README.md+++ b/crates/print/README.md@@ -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`diff --git a/crates/print/src/imposition.rs b/crates/print/src/imposition.rs--- a/crates/print/src/imposition.rs+++ b/crates/print/src/imposition.rs@@ -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.diff --git a/crates/print/src/tests/appearances.rs b/crates/print/src/tests/appearances.rs--- 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--- 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"]-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"