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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

---------

Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-11-09 08:29:31 +08:00
1 parent 8d5d39ef5e
commit b07a84f003
8 files changed
+222 -22

No files matched your search

+16 -3
View File
@@ -205,11 +205,24 @@ fn quads(context: &AppearanceContext<'_>) -> Option<Vec<[f64; 8]>> {
(!q.is_empty()).then_some(q)
}
/// `/C`-style colour arrays: none (transparent), gray, RGB or CMYK.
fn color(d: &Dict, key: &[u8]) -> Option<Option<Rgb>> {
/// `/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<Option<Rgb>> {
let c = match d.get(key) {
None => return Some(None),
Some(o) => o.as_array()?.iter().map(|o| o.as_f64()).collect::<Option<Vec<f64>>>()?,
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::<Option<Vec<f64>>>()?,
a => a.iter().map(|o| o.as_f64()).collect::<Option<Vec<f64>>>()?,
},
};
Some(match c.as_slice() {
[] => None,
+29 -11
View File
@@ -105,7 +105,10 @@ pub fn summaries(doc: &Document) -> Vec<Summary> {
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<Props> {
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<f64> = 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<Props> {
})
}
/// 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<Rgb> {
let key: &[u8] = if d.name(b"Subtype") == Some(b"FreeText") { b"C" } else { b"IC" };
let c: Vec<f64> = 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<Rgb> {
let rgb = appearance::color(d, key).flatten()?;
finite(&rgb).then(|| rgb.map(|x| x.clamp(0.0, 1.0)))
}
+65
View File
@@ -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<Option<[f32; 3]>> = 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<Option<[f32; 3]>> = 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<f64> {
content.split(" Tm").filter_map(|s| s.rsplit(' ').next()?.parse().ok()).collect()
+9 -1
View File
@@ -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<String> {
let a = doc.resolve(mk?.get(key)?);
let v: Vec<f64> = a.as_array()?.iter().filter_map(|x| doc.resolve(x).as_f64()).collect();
let a = a.as_array()?;
let v: Vec<f64> = 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))),
+32
View File
@@ -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<String> = 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}");
}
+35 -5
View File
@@ -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<f32> = if c.len() == 4 {
c.iter()
.map(|o| match o {
Object::Reference(_) => None,
o => Some(o.as_float().unwrap_or(0.0)),
})
.collect::<Option<_>>()?
} else {
c.iter().map(|o| o.as_float().ok()).collect::<Option<_>>()?
};
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")
+34
View File
@@ -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<Option<[f32; 3]>> = 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]
+2 -2
View File
@@ -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"