mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
Merge PR #4371: M2: Invisible annotations of unknown types and popups don't paint
Conflicts: vendor/README.md: hayro-interpret row keeps #4345's patch (55) and #4371's patch (56), with both test lists.
This commit is contained in:
+39
-1
@@ -719,19 +719,57 @@ pub fn keep_printed(doc: &mut Document, content: Content) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The standard annotation types of ISO 32000-2 Table 171.
|
||||
const STANDARD_ANNOTATION_TYPES: [&[u8]; 28] = [
|
||||
b"Text",
|
||||
b"Link",
|
||||
b"FreeText",
|
||||
b"Line",
|
||||
b"Square",
|
||||
b"Circle",
|
||||
b"Polygon",
|
||||
b"PolyLine",
|
||||
b"Highlight",
|
||||
b"Underline",
|
||||
b"Squiggly",
|
||||
b"StrikeOut",
|
||||
b"Caret",
|
||||
b"Stamp",
|
||||
b"Ink",
|
||||
b"Popup",
|
||||
b"FileAttachment",
|
||||
b"Sound",
|
||||
b"Movie",
|
||||
b"Screen",
|
||||
b"Widget",
|
||||
b"PrinterMark",
|
||||
b"TrapNet",
|
||||
b"Watermark",
|
||||
b"3D",
|
||||
b"Redact",
|
||||
b"Projection",
|
||||
b"RichMedia",
|
||||
];
|
||||
|
||||
/// Does annotation `d` print under `content`?
|
||||
fn prints(doc: &Document, d: &Dict, content: Content) -> bool {
|
||||
const INVISIBLE: i64 = 1;
|
||||
const HIDDEN: i64 = 2;
|
||||
const PRINT: i64 = 4;
|
||||
let flags = d.get(b"F").and_then(|f| doc.resolve(f).as_int()).unwrap_or(0);
|
||||
if flags & PRINT == 0 || flags & HIDDEN != 0 {
|
||||
return false;
|
||||
}
|
||||
let subtype = d.name(b"Subtype").unwrap_or(b"");
|
||||
// `/Subtype` may be an indirect reference (ISO 32000-2 §7.3.10), so resolve it first.
|
||||
let subtype_obj = d.get(b"Subtype").map(|s| doc.resolve(s));
|
||||
let subtype = subtype_obj.as_deref().and_then(Object::as_name).unwrap_or(b"");
|
||||
match subtype {
|
||||
b"Widget" => true,
|
||||
b"Link" | 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.
|
||||
_ if flags & INVISIBLE != 0 && !STANDARD_ANNOTATION_TYPES.contains(&subtype) => false,
|
||||
_ => content == Content::DocumentAndMarkups,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -961,3 +961,31 @@ fn odd_and_even_pages_are_page_numbers_not_places_in_the_range() {
|
||||
// A range with no page of the kind prints nothing.
|
||||
assert_eq!(select_pages(12, Some("3, 5"), &[], Subset::Even, false), Err(PrintError::NoPages));
|
||||
}
|
||||
|
||||
/// ISO 32000-2 Table 167: an Invisible annotation of a type that isn't standard does not print,
|
||||
/// even with its Print flag; Invisible means nothing for a standard type.
|
||||
#[test]
|
||||
fn invisible_annotations_of_unknown_types_do_not_print() {
|
||||
let doc = fixture(1);
|
||||
let annot = |subtype: Option<&str>, flags: i64| {
|
||||
let mut d = Dict::new();
|
||||
if let Some(s) = subtype {
|
||||
d.set(b"Subtype".to_vec(), Object::name(s));
|
||||
}
|
||||
d.set(b"F".to_vec(), Object::Int(flags));
|
||||
d
|
||||
};
|
||||
let all = Content::DocumentAndMarkups;
|
||||
assert!(prints(&doc, &annot(Some("MyCustomType"), 4), all), "an unknown type prints without Invisible");
|
||||
assert!(!prints(&doc, &annot(Some("MyCustomType"), 5), all), "Invisible + Print on an unknown type");
|
||||
assert!(!prints(&doc, &annot(None, 5), all), "no Subtype is no standard type");
|
||||
assert!(prints(&doc, &annot(Some("Square"), 5), all), "Invisible doesn't apply to a standard type");
|
||||
assert!(prints(&doc, &annot(Some("Widget"), 5), Content::FormFieldsOnly), "nor to a field");
|
||||
|
||||
// An indirect `/Subtype` (§7.3.10) is resolved: a standard type stays standard.
|
||||
let mut doc = doc;
|
||||
let square = doc.add(Object::name("Square"));
|
||||
let mut indirect = annot(None, 5);
|
||||
indirect.set(b"Subtype".to_vec(), Object::Ref(square));
|
||||
assert!(prints(&doc, &indirect, all), "an indirect standard Subtype");
|
||||
}
|
||||
|
||||
@@ -3349,6 +3349,40 @@ trailer << /Root 1 0 R >>
|
||||
assert_eq!(px(&hidden, 20, 80), vec![255, 0, 0, 255], "fields stay");
|
||||
}
|
||||
|
||||
/// ISO 32000-2 Table 167: Invisible hides an annotation of a type that isn't standard and
|
||||
/// nothing else; §12.5.6.14: a popup has no appearance of its own, so its `/AP` isn't painted.
|
||||
#[test]
|
||||
fn invisible_unknown_annotations_and_popups_paint_nothing() {
|
||||
let annots = [
|
||||
"/Subtype /MyCustomType /F 0",
|
||||
"/Subtype /MyCustomType /F 1",
|
||||
"/Subtype /MyCustomType /F 5",
|
||||
"/F 1",
|
||||
"/Subtype /Square /F 1",
|
||||
"/Subtype /Popup /F 4 /Open true",
|
||||
];
|
||||
let mut pdf = String::from("%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n");
|
||||
pdf.push_str("2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n");
|
||||
let refs: Vec<String> = (0..annots.len()).map(|i| format!("{} 0 R", 10 + i)).collect();
|
||||
pdf.push_str(&format!("3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 50] /Annots [{}] >> endobj\n", refs.join(" ")));
|
||||
pdf.push_str("4 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 30 30] /Length 24 >> stream\n0 1 0 rg 0 0 30 30 re f\nendstream endobj\n");
|
||||
for (i, a) in annots.iter().enumerate() {
|
||||
let x = 10 + 50 * i;
|
||||
pdf.push_str(&format!("{} 0 obj << /Type /Annot {a} /Rect [{x} 10 {} 40] /AP << /N 4 0 R >> >> endobj\n", 10 + i, x + 30));
|
||||
}
|
||||
pdf.push_str("trailer << /Root 1 0 R >>\n%%EOF");
|
||||
let mut r = PageRenderer::new(Arc::new(pdf.into_bytes()), RenderConfig::default());
|
||||
let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() });
|
||||
let px = |x: u32| p.rgba[((25 * p.width + x) * 4) as usize..][..4].to_vec();
|
||||
let (green, white) = (vec![0, 255, 0, 255], vec![255, 255, 255, 255]);
|
||||
assert_eq!(px(25), green, "an unknown type without Invisible is drawn");
|
||||
assert_eq!(px(75), white, "Invisible hides an unknown type");
|
||||
assert_eq!(px(125), white, "even with the Print flag");
|
||||
assert_eq!(px(175), white, "no Subtype is no standard type");
|
||||
assert_eq!(px(225), green, "Invisible means nothing for a standard type");
|
||||
assert_eq!(px(275), white, "a popup's appearance is not painted on the page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_pool_renders_in_priority_order() {
|
||||
let pool = RenderPool::new_inline(Arc::new(ONE_PAGE.to_vec()), RenderConfig::default());
|
||||
|
||||
@@ -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"]
|
||||
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); 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/render/src/raster.rs::invisible_unknown_annotations_and_popups_paint_nothing", "crates/print/src/tests.rs::invisible_annotations_of_unknown_types_do_not_print"]
|
||||
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); 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)"
|
||||
|
||||
[[feature]]
|
||||
id = "view.render-error-isolation"
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+45
@@ -190,6 +190,15 @@ pub fn interpret_page<'a>(
|
||||
if flags & (2 | 32) != 0 {
|
||||
continue;
|
||||
}
|
||||
// PdfCraft patch (56): Invisible (bit 1) hides an annotation of a type that isn't one
|
||||
// of the standard ones (ISO 32000-2 Table 167), and a popup has no appearance of its
|
||||
// own (§12.5.6.14): the viewer shows it as a window for its parent, not on the page.
|
||||
let subtype = annot.get::<hayro_syntax::object::Name<'_>>(SUBTYPE);
|
||||
if subtype.as_deref() == Some(b"Popup")
|
||||
|| (flags & 1 != 0 && !subtype.as_deref().is_some_and(is_standard_annotation_type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// PdfCraft patch: "Hide all comments" keeps widgets and links.
|
||||
if context.settings.hide_comments
|
||||
&& !matches!(annot.get::<hayro_syntax::object::Name<'_>>(SUBTYPE).as_deref(), Some(b"Widget" | b"Link"))
|
||||
@@ -286,6 +295,42 @@ pub fn interpret_page<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (56): whether `subtype` is one of the standard annotation types of
|
||||
/// ISO 32000-2 Table 171.
|
||||
fn is_standard_annotation_type(subtype: &[u8]) -> bool {
|
||||
matches!(
|
||||
subtype,
|
||||
b"Text"
|
||||
| b"Link"
|
||||
| b"FreeText"
|
||||
| b"Line"
|
||||
| b"Square"
|
||||
| b"Circle"
|
||||
| b"Polygon"
|
||||
| b"PolyLine"
|
||||
| b"Highlight"
|
||||
| b"Underline"
|
||||
| b"Squiggly"
|
||||
| b"StrikeOut"
|
||||
| b"Caret"
|
||||
| b"Stamp"
|
||||
| b"Ink"
|
||||
| b"Popup"
|
||||
| b"FileAttachment"
|
||||
| b"Sound"
|
||||
| b"Movie"
|
||||
| b"Screen"
|
||||
| b"Widget"
|
||||
| b"PrinterMark"
|
||||
| b"TrapNet"
|
||||
| b"Watermark"
|
||||
| b"3D"
|
||||
| b"Redact"
|
||||
| b"Projection"
|
||||
| b"RichMedia"
|
||||
)
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the normal appearance of a Highlight annotation that has none (ISO 32000-2
|
||||
/// §12.5.6.10), drawn as PdfCraft draws its own highlights (pdfcraft-annot): each /QuadPoints
|
||||
/// quadrilateral filled with /C at /CA opacity, blended with Multiply so the marked-up content
|
||||
|
||||
Reference in New Issue
Block a user