From b07a84f00316949cee4f2320d557e899d4ad4ee3 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:29:31 +0800 Subject: [PATCH] M5: a comment whose colour is written [r g b null] is drawn, as CMYK with the null read as 0 (view.render-annotation-appearances) (#6503) * M5: a comment whose colour is written [r g b null] is drawn, as CMYK with the null read as 0 (view.render-annotation-appearances) ISO 32000-2 Table 166 /C (and /IC, Tables 178, 180, 181; /MK /BC and /BG, Table 192) is an array of 0, 1, 3 or 4 numbers whose length picks the colour space. A four-entry array with a non-number in it, such as [1 0 0 null] (PDFBOX-2699), made the /C parser give up, so a shape without an appearance stream was not drawn at all. PDFium, MuPDF and Poppler all read such an array as CMYK with the non-number as 0. - annot: a four-entry /C or /IC reads non-numbers as 0; other lengths still need every entry to be a number. - annot: the properties' fill uses the same parser (it read [r g b null] as RGB before), and /C no longer drops a non-number to make three numbers. - forms: /MK /BC and /BG read a four-entry array the same way instead of dropping the non-number and drawing RGB. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn * M5: comment properties report the stroke colour as drawn; a reference among four colour entries is not read as 0 (view.render-annotation-appearances) props() reads a non-FreeText /C through the appearance parser, so a [1 0 0 null] stroke reports CMYK cyan as drawn, and grey and CMYK strokes report their converted colour. A reference among the four entries of a /C array (ISO 32000-2 7.3.10) is left unreadable instead of standing in as 0, so /C [1 0 0 6 0 R] is never drawn cyan. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn * M5: re-run CI (fill_sign Escape test depends on the clock's second, passes locally) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn * M5: the comment list reports a comment's colour as drawn (view.render-annotation-appearances) summaries() accepted only a three-entry /C, so a Square with /C [1 0 0 null], drawn cyan and reported cyan by props(), had no colour in the comment list; grey and CMYK colours were dropped the same way. Non-FreeText comments now read /C through drawn_color (the array itself followed when it is given by reference), keeping the three-number fallback and the FreeText /DA handling. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn * M5: the render inspector lists a comment's colour as drawn (view.render-annotation-appearances) The inspector accepted only a three-entry /C, with a referenced entry read as 0, so a /C [1 0 0 null], a grey or a CMYK colour listed none, unlike the comment list and the drawing. Non-FreeText comments now read /C by the appearance builder's rule (one entry grey, four CMYK with a non-number as 0 and a reference unreadable, finite and clamped), with the three-number fallback reading referenced entries. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn --------- Co-authored-by: mtlcl410 Co-authored-by: Claude Opus 5.5 --- crates/annot/src/appearance.rs | 19 +++++-- crates/annot/src/info.rs | 40 +++++++++++---- crates/annot/src/tests.rs | 65 ++++++++++++++++++++++++ crates/forms/src/appearance.rs | 10 +++- crates/forms/src/tests.rs | 32 ++++++++++++ crates/render/src/inspect/annotations.rs | 40 +++++++++++++-- crates/render/src/inspect/tests.rs | 34 +++++++++++++ parity/acrobat-features.toml | 4 +- 8 files changed, 222 insertions(+), 22 deletions(-) diff --git a/crates/annot/src/appearance.rs b/crates/annot/src/appearance.rs index dafc4b01b1..7996567f04 100644 --- a/crates/annot/src/appearance.rs +++ b/crates/annot/src/appearance.rs @@ -205,11 +205,24 @@ fn quads(context: &AppearanceContext<'_>) -> Option> { (!q.is_empty()).then_some(q) } -/// `/C`-style colour arrays: none (transparent), gray, RGB or CMYK. -fn color(d: &Dict, key: &[u8]) -> Option> { +/// `/C`-style colour arrays (ISO 32000-2 Table 166): none (transparent), gray, RGB or CMYK. +/// The entries should all be numbers; a four-entry array with a non-number in it, such as +/// `[1 0 0 null]`, is still read as CMYK with that entry 0, as PDFium, MuPDF and Poppler read it. +/// A reference is not such an entry: it stands for a value this builder can't see (§7.3.10), so +/// the colour is unreadable, as the other colour and number readers here treat one. +pub(crate) fn color(d: &Dict, key: &[u8]) -> Option> { let c = match d.get(key) { None => return Some(None), - Some(o) => o.as_array()?.iter().map(|o| o.as_f64()).collect::>>()?, + Some(o) => match o.as_array()? { + a if a.len() == 4 => a + .iter() + .map(|o| match o { + Object::Ref(_) => None, + o => Some(o.as_f64().unwrap_or(0.0)), + }) + .collect::>>()?, + a => a.iter().map(|o| o.as_f64()).collect::>>()?, + }, }; Some(match c.as_slice() { [] => None, diff --git a/crates/annot/src/info.rs b/crates/annot/src/info.rs index 8af76cb2ed..c4913e7ab8 100644 --- a/crates/annot/src/info.rs +++ b/crates/annot/src/info.rs @@ -105,7 +105,10 @@ pub fn summaries(doc: &Document) -> Vec { let r = nums(b"Rect"); let rect = if r.len() == 4 { [r[0].min(r[2]), r[1].min(r[3]), r[0].max(r[2]), r[1].max(r[3])] } else { [0.0; 4] }; let c = nums(b"C"); - let mut color = (c.len() == 3).then(|| [c[0], c[1], c[2]]); + let rgb = (c.len() == 3).then(|| [c[0], c[1], c[2]]); + // Other comments report the stroke colour as it is drawn (grey, CMYK, a null entry + // read as 0), like `props`; three numbers, some given by reference, are still read. + let mut color = if subtype == "FreeText" { rgb } else { summary_color(doc, d).or(rgb) }; if subtype == "FreeText" && let Some(da) = text_value(doc, d, b"DA") { @@ -244,8 +247,7 @@ pub fn props(doc: &Document, page: usize, index: usize) -> Option { let obj = doc.resolve(&entry); let d = obj.as_dict()?; let subtype = String::from_utf8_lossy(d.name(b"Subtype")?).into_owned(); - let c: Vec = d.get(b"C").and_then(|o| o.as_array().map(|a| a.iter().filter_map(|x| x.as_f64()).collect())).unwrap_or_default(); - let color = if subtype == "FreeText" { Some(appearance::parse_da(d).0) } else { (c.len() == 3).then(|| [c[0], c[1], c[2]]) }; + let color = if subtype == "FreeText" { Some(appearance::parse_da(d).0) } else { drawn_color(d, b"C") }; let width = matches!(subtype.as_str(), "Square" | "Circle" | "Line" | "Ink" | "Polygon" | "PolyLine" | "FreeText").then(|| { if subtype == "FreeText" { if !d.contains(b"BS") && !d.contains(b"Border") { 0.0 } else { appearance::border_width(d) } @@ -275,16 +277,32 @@ pub fn props(doc: &Document, page: usize, index: usize) -> Option { }) } -/// A shape’s `/IC` or FreeText `/C` as RGB. Grey and CMYK fills are converted. +/// A shape’s `/IC` or FreeText `/C` as RGB, read as it is drawn. Grey and CMYK fills are converted. fn fill_of(d: &Dict) -> Option { let key: &[u8] = if d.name(b"Subtype") == Some(b"FreeText") { b"C" } else { b"IC" }; - let c: Vec = d.get(key)?.as_array()?.iter().filter_map(|x| x.as_f64()).collect(); - let rgb = match c.as_slice() { - [g] => [*g; 3], - [r, g, b] => [*r, *g, *b], - [c, m, y, k] => [(1.0 - c) * (1.0 - k), (1.0 - m) * (1.0 - k), (1.0 - y) * (1.0 - k)], - _ => return None, - }; + drawn_color(d, key) +} + +/// `/C` as [`drawn_color`] reads it, the array itself followed when it is given by reference. +/// Only an array of at most four entries is copied out of the reference. +fn summary_color(doc: &Document, d: &Dict) -> Option<[f32; 3]> { + let rgb = match d.get(b"C") { + Some(o @ Object::Ref(_)) => { + let held = doc.resolve(o); + let entries = held.as_array().filter(|a| a.len() <= 4)?; + let mut direct = Dict::new(); + direct.set(b"C".to_vec(), Object::Array(entries.clone())); + drawn_color(&direct, b"C") + } + _ => drawn_color(d, b"C"), + }?; + Some(rgb.map(|x| x as f32)) +} + +/// The colour array at `key` as RGB, read as the appearance builder draws it: grey and CMYK are +/// converted, and a four-entry array with a non-number in it is CMYK with that entry 0. +fn drawn_color(d: &Dict, key: &[u8]) -> Option { + let rgb = appearance::color(d, key).flatten()?; finite(&rgb).then(|| rgb.map(|x| x.clamp(0.0, 1.0))) } diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index 0c9ac7ed5a..9e414078e9 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -2786,6 +2786,71 @@ fn indirect_typewriter_names_keep_outer_whitespace_in_summaries() { check(&reopen(&doc)); } +/// PDFBOX-2699: a `/C` written `[1 0 0 null]` (Table 166 asks for numbers) is still a four-entry +/// colour, CMYK with the null as 0 as PDFium, MuPDF and Poppler read it, so a shape without an +/// appearance is drawn instead of dropped. `/IC` is read the same way, and three numbers stay RGB. +/// The properties and the comment list report the colours as drawn. +#[test] +fn a_four_entry_colour_with_a_null_is_cmyk_with_zero() { + let doc = document_of(&[ + b"<< /Type /Catalog /Pages 2 0 R >>", + b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 100 40] >>", + b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >>", + b"<< /Type /Annot /Subtype /Square /Rect [5 5 45 35] /C [1 0 0 null] /IC [0 0 1 null] >>", + b"<< /Type /Annot /Subtype /Square /Rect [55 5 95 35] /C [1 0 0] >>", + ]); + let display = with_missing_appearances(&doc).expect("both squares get a fallback appearance"); + let squares = list(&display, 0); + let cmyk = ap_content(&display, &squares[0]); + assert!(cmyk.contains("0 1 1 RG") && cmyk.contains("1 1 0 rg"), "CMYK (1,0,0,0) stroke and (0,0,1,0) fill: {cmyk}"); + let rgb = ap_content(&display, &squares[1]); + assert!(rgb.contains("1 0 0 RG"), "three numbers stay RGB: {rgb}"); + // The properties read the stroke and the fill as they are drawn. + let p = props(&doc, 0, 0).expect("the square has properties"); + assert_eq!((p.color, p.fill), (Some([0.0, 1.0, 1.0]), Some([1.0, 1.0, 0.0]))); + assert_eq!(props(&doc, 0, 1).and_then(|p| p.color), Some([1.0, 0.0, 0.0])); + // The comment list reports the same colours. + let colors: Vec> = summaries(&doc).iter().map(|s| s.color).collect(); + assert_eq!(colors, [Some([0.0, 1.0, 1.0]), Some([1.0, 0.0, 0.0])]); +} + +/// A `/C` given by reference is followed for the comment list, as three numbers were, and read +/// like a direct one: `[0 0 1 null]` is CMYK yellow, and a referenced grey level is grey. +#[test] +fn a_referenced_colour_is_listed_like_a_direct_one() { + let doc = document_of(&[ + b"<< /Type /Catalog /Pages 2 0 R >>", + b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 100 40] >>", + b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >>", + b"<< /Type /Annot /Subtype /Square /Rect [5 5 45 35] /C 6 0 R >>", + b"<< /Type /Annot /Subtype /Circle /Rect [55 5 95 35] /C 7 0 R >>", + b"[0 0 1 null]", + b"[0.5]", + ]); + let colors: Vec> = summaries(&doc).iter().map(|s| s.color).collect(); + assert_eq!(colors, [Some([1.0, 1.0, 0.0]), Some([0.5, 0.5, 0.5])]); +} + +/// A reference among the four entries is not a non-number read as 0: `/C [1 0 0 6 0 R]` with +/// object 6 the number 1 is CMYK black (§7.3.10), so drawing it cyan would be wrong. The builder +/// doesn't follow it and draws no appearance, as for any colour it can't read. +#[test] +fn a_four_entry_colour_with_a_reference_is_not_read_as_zero() { + let doc = document_of(&[ + b"<< /Type /Catalog /Pages 2 0 R >>", + b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 100 40] >>", + b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >>", + b"<< /Type /Annot /Subtype /Square /Rect [5 5 45 35] /C [1 0 0 6 0 R] >>", + b"<< /Type /Annot /Subtype /Square /Rect [55 5 95 35] /C [1 0 0] >>", + b"1", + ]); + let display = with_missing_appearances(&doc).expect("the RGB square gets a fallback appearance"); + let squares = list(&display, 0); + assert!(!squares[0].contains(b"AP"), "no cyan stand-in for CMYK black"); + assert_eq!(props(&doc, 0, 0).and_then(|p| p.color), None); + assert_eq!(summaries(&doc)[0].color, None, "nor in the comment list"); +} + /// The `y` of every `1 0 0 1 x y Tm` in `content`: where each piece of text sits. fn baselines(content: &str) -> Vec { content.split(" Tm").filter_map(|s| s.rsplit(' ').next()?.parse().ok()).collect() diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index 91cc352701..891a533eaa 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -399,9 +399,17 @@ fn helvetica() -> Object { Object::Dict(f) } +/// An `/MK /BC` or `/BG` colour (ISO 32000-2 Table 192) as a fill operator. A four-entry array +/// with a non-number in it, such as `[1 0 0 null]`, is CMYK with that entry 0, as annotation `/C` +/// colours are read. fn color_array(doc: &Document, mk: Option<&Dict>, key: &[u8]) -> Option { let a = doc.resolve(mk?.get(key)?); - let v: Vec = a.as_array()?.iter().filter_map(|x| doc.resolve(x).as_f64()).collect(); + let a = a.as_array()?; + let v: Vec = if a.len() == 4 { + a.iter().map(|x| doc.resolve(x).as_f64().unwrap_or(0.0)).collect() + } else { + a.iter().filter_map(|x| doc.resolve(x).as_f64()).collect() + }; match v.as_slice() { [g] => Some(format!("{} g", n(*g))), [r, g, b] => Some(format!("{} {} {} rg", n(*r), n(*g), n(*b))), diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 353545f2b3..6cf207d0de 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -5706,3 +5706,35 @@ fn locked_button_export_edits_leave_the_document_unchanged() { assert_eq!(write_incremental(&doc, &SaveOptions::default()).unwrap(), before); } } + +/// A `/MK /BC` or `/BG` written `[1 0 0 null]` is CMYK with the null as 0, as annotation `/C` +/// colours are read (PDFBOX-2699), not RGB from the three numbers left; three numbers stay RGB. +#[test] +fn a_four_entry_mk_colour_with_a_null_is_cmyk_with_zero() { + let widget = |name: &str, mk: &str| { + format!("<< /Type /Annot /Subtype /Widget /FT /Btn /T ({name}) /Rect [50 700 70 720] /P 3 0 R /MK << {mk} >> /BS << /W 1 /S /S >> >>") + }; + let objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), + "<< /Type /Page /Parent 2 0 R /Annots [5 0 R 6 0 R] >>".into(), + "<< /Fields [5 0 R 6 0 R] >>".into(), + widget("cmyk", "/BG [0 0 1 null] /BC [1 0 0 null]"), + widget("rgb", "/BG [0 0 1] /BC [1 0 0]"), + ]; + let mut doc = document(&objs); + let off = |doc: &mut Document, name: &str| { + redraw_field(doc, name).unwrap(); + let all = fields(doc); + let w = field(&all, name).widgets[0].obj; + let wd = doc.get(w).as_dict().cloned().unwrap(); + let n = doc.resolve(wd.get(b"AP").unwrap().as_dict().unwrap().get(b"N").unwrap()); + let r = n.as_dict().unwrap().reference(b"Off").expect("an Off appearance"); + let Object::Stream(s) = &*doc.get(r) else { panic!("Off is a stream") }; + String::from_utf8_lossy(&s.decoded().unwrap()).into_owned() + }; + let cmyk = off(&mut doc, "cmyk"); + assert!(cmyk.contains("0 0 1 0 k") && cmyk.contains("1 0 0 0 K"), "CMYK background and border: {cmyk}"); + let rgb = off(&mut doc, "rgb"); + assert!(rgb.contains("0 0 1 rg") && rgb.contains("1 0 0 RG"), "three numbers stay RGB: {rgb}"); +} diff --git a/crates/render/src/inspect/annotations.rs b/crates/render/src/inspect/annotations.rs index 7c4543b733..5ed635fdcb 100644 --- a/crates/render/src/inspect/annotations.rs +++ b/crates/render/src/inspect/annotations.rs @@ -22,6 +22,31 @@ pub(super) const MAX_ANNOTATION_BYTES: usize = 64 << 20; /// key does not fit is not listed. pub(super) const MAX_LINK_ATTACHMENT_TEXT_BYTES: usize = 16 << 20; +/// A colour array as RGB, read as pdfcraft-annot's appearance builder reads it (`appearance::color` +/// and `drawn_color`): one entry is grey, three RGB, four CMYK; among four entries a non-number is +/// 0 but a reference is unreadable, otherwise every entry must be a number. `None` for anything +/// else, an empty array or a value out of range; each component is clamped to 0..=1. +fn drawn_color(c: &[Object]) -> Option<[f32; 3]> { + let v: Vec = if c.len() == 4 { + c.iter() + .map(|o| match o { + Object::Reference(_) => None, + o => Some(o.as_float().unwrap_or(0.0)), + }) + .collect::>()? + } else { + c.iter().map(|o| o.as_float().ok()).collect::>()? + }; + let rgb = match v.as_slice() { + [g] => [*g; 3], + [r, g, b] => [*r, *g, *b], + [c, m, y, k] => [(1.0 - c) * (1.0 - k), (1.0 - m) * (1.0 - k), (1.0 - y) * (1.0 - k)], + _ => return None, + }; + // pdfcraft-annot's `finite`: finite and below 1e7 in magnitude. + rgb.iter().all(|x| x.is_finite() && x.abs() < 1e7).then(|| rgb.map(|x| x.clamp(0.0, 1.0))) +} + /// The bytes a listed annotation keeps. pub(super) fn retained(a: &Annotation) -> usize { let text = [&a.author, &a.contents, &a.modified, &a.name, &a.in_reply_to, &a.state, &a.intent]; @@ -119,13 +144,18 @@ impl<'a> Inspector<'a> { continue; } let rect = rect4(self.resolve(d.get(b"Rect").unwrap_or(&Object::Null))); - let mut color = match d.get(b"C").map(|o| self.resolve(o)) { - Ok(Object::Array(c)) if c.len() == 3 => { - let f = |i: usize| c[i].as_float().unwrap_or(0.0); - Some([f(0), f(1), f(2)]) - } + let c = match d.get(b"C").map(|o| self.resolve(o)) { + Ok(Object::Array(c)) => Some(c), _ => None, }; + // Three numbers, some given by reference, with a non-number read as 0. + let rgb = c.filter(|c| c.len() == 3).map(|c| { + let f = |i: usize| c.get(i).map_or(0.0, |o| self.resolve(o).as_float().unwrap_or(0.0)); + [f(0), f(1), f(2)] + }); + // Other comments list the stroke colour as pdfcraft-annot draws it and lists it + // (`summaries`): grey, CMYK, a non-number among four entries read as 0. + let mut color = if subtype == "FreeText" { rgb } else { c.and_then(|c| drawn_color(c)).or(rgb) }; // A text box's /C is its background; its text colour is in /DA. if subtype == "FreeText" && let Some(da) = self.text(d, b"DA") diff --git a/crates/render/src/inspect/tests.rs b/crates/render/src/inspect/tests.rs index 907401f8bb..012e40f79a 100644 --- a/crates/render/src/inspect/tests.rs +++ b/crates/render/src/inspect/tests.rs @@ -877,6 +877,40 @@ fn an_annotation_reached_through_a_reference_chain_is_listed_once() { } } +/// Comments list their `/C` as pdfcraft-annot draws and lists it: four entries with a null are +/// CMYK with the null as 0, one entry is grey, a reference among four entries is unreadable, +/// three numbers (some by reference) are RGB, and a text box keeps its three-number `/C`. +#[test] +fn comment_colours_are_listed_as_drawn() { + let pdf = "%PDF-1.7 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj + 2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj + 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R] >> endobj + 4 0 obj << /Type /Annot /Subtype /Square /Rect [0 0 10 10] /C [1 0 0 null] >> endobj + 5 0 obj << /Type /Annot /Subtype /Square /Rect [0 20 10 30] /C 10 0 R >> endobj + 6 0 obj << /Type /Annot /Subtype /Square /Rect [0 40 10 50] /C [1 0 0 11 0 R] >> endobj + 7 0 obj << /Type /Annot /Subtype /Square /Rect [0 60 10 70] /C [11 0 R 0 0] >> endobj + 8 0 obj << /Type /Annot /Subtype /Circle /Rect [0 80 10 90] /C [0 0 0 1] >> endobj + 9 0 obj << /Type /Annot /Subtype /FreeText /Rect [0 100 10 110] /C [0 1 0] >> endobj + 10 0 obj [0.5] endobj + 11 0 obj 1 endobj + trailer << /Root 1 0 R >> +%%EOF"; + let mut doc = Document::load_mem(pdf.as_bytes()).expect("loads"); + let mut saved = Vec::new(); + doc.save_to(&mut saved).expect("saves"); + let lazy = LazyStructure::new(Arc::new(saved), None).expect("opens"); + let page = PageInfo { width: 200.0, height: 200.0, crop: [0.0, 0.0, 200.0, 200.0], rotation: 0, label: String::new() }; + for reader in [&doc as &dyn Structure, &lazy] { + let mut info = DocInfo { pages: vec![page.clone()], ..Default::default() }; + Inspector::new(reader).fill(&mut info); + let mut listed: Vec<(f32, Option<[f32; 3]>)> = info.annotations.iter().map(|a| (a.rect[1], a.color)).collect(); + listed.sort_by(|a, b| a.0.total_cmp(&b.0)); + let colors: Vec> = listed.into_iter().map(|(_, c)| c).collect(); + assert_eq!(colors, [Some([0.0, 1.0, 1.0]), Some([0.5, 0.5, 0.5]), None, Some([1.0, 0.0, 0.0]), Some([0.0, 0.0, 0.0]), Some([0.0, 1.0, 0.0])]); + } +} + /// A reply finds its comment where the listing keeps it: on the first page of a page-shared /// `/Annots` array, whether its `/IRT` names the comment or an object holding a reference to it. #[test] diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 67ee899f85..af026b3a85 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -818,8 +818,8 @@ milestone = "M2" 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)" +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"] +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 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 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)" [[feature]] id = "view.render-error-isolation"