Merge PR #3245: Draw a note without an appearance as a fixed-size icon at the corner of its Rect (#3175)

This commit is contained in:
aiongg
2026-10-22 01:44:50 +00:00
3 changed files with 58 additions and 0 deletions
+14
View File
@@ -1355,8 +1355,22 @@ fn apply_appearance(doc: &mut Document, r: ObjRef, d: &Dict, stream: Stream) ->
apd.remove(b"D");
apd.remove(b"R");
apd.set(b"N".to_vec(), Object::Ref(ap));
// A note icon has a fixed size (`NOTE_SIZE`); viewers draw it at the upper-left corner of `/Rect`.
// The appearance's box is mapped onto `/Rect`, so any other `/Rect` (a large one, or one with
// no width, as LaTeX `pdfcomment` writes) would stretch the icon or shrink it to nothing.
let icon_rect = matches!(d.name(b"Subtype"), Some(b"Text" | b"FileAttachment"))
.then(|| d.get(b"Rect")?.as_array()?.iter().map(|o| o.as_f64()).collect::<Option<Vec<f64>>>())
.flatten()
.filter(|q| q.len() == 4 && finite(q))
.map(|q| {
let (x, top) = (q[0].min(q[2]), q[1].max(q[3]));
[x, top - NOTE_SIZE, x + NOTE_SIZE, top]
});
doc.update_dict(r, |d| {
d.set(b"AP".to_vec(), Object::Dict(apd));
if let Some(rect) = icon_rect {
d.set(b"Rect".to_vec(), num_array(&rect));
}
d.remove(b"AS");
pdfcraft_fonts::set_undrawn_mark(d, missing);
})?;
+23
View File
@@ -991,6 +991,29 @@ fn malformed_optional_interior_color_does_not_hide_shape_strokes() {
assert!(list(&doc, 0)[0].get(b"AP").is_none(), "display generation leaves the source document unchanged");
}
/// #3175: viewers draw a note without an appearance as a fixed-size icon at the upper-left corner
/// of its `/Rect`. The display copy must not stretch the icon over a large `/Rect` or shrink it
/// to nothing in one with no width.
#[test]
fn a_note_without_an_appearance_is_shown_at_icon_size() {
let doc = document_of(&[
b"<< /Type /Catalog /Pages 2 0 R >>",
b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 600] >>",
b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R 6 0 R] >>",
b"<< /Type /Annot /Subtype /Text /Rect [-23 240 480 468] /C [1 0 0] /Open true >>",
b"<< /Type /Annot /Subtype /Text /Rect [310 380 310 392] /Name /Comment /F 4 >>",
b"<< /Type /Annot /Subtype /FileAttachment /Rect [100 50 300 10] >>",
]);
let display = with_missing_appearances(&doc).expect("the notes get a display appearance");
let rects = |doc: &Document| -> Vec<Vec<f64>> {
let rect = |d: &Dict| d.get(b"Rect").and_then(|r| r.as_array()).map(|a| a.iter().filter_map(Object::as_f64).collect::<Vec<f64>>());
list(doc, 0).iter().map(|d| rect(d).unwrap_or_default()).collect()
};
assert_eq!(rects(&display), [vec![-23.0, 448.0, -3.0, 468.0], vec![310.0, 372.0, 330.0, 392.0], vec![100.0, 30.0, 120.0, 50.0]]);
let original = [vec![-23.0, 240.0, 480.0, 468.0], vec![310.0, 380.0, 310.0, 392.0], vec![100.0, 50.0, 300.0, 10.0]];
assert_eq!(rects(&doc), original, "the source document is unchanged");
}
#[test]
fn every_line_ending_is_drawn() {
let filled = [LineEnding::Square, LineEnding::Circle, LineEnding::Diamond, LineEnding::ClosedArrow, LineEnding::RClosedArrow];
+21
View File
@@ -284,6 +284,27 @@ fn flattening_keeps_a_note_whose_appearance_is_missing() {
assert!(flat.contains("q 1 0 0 1 10 10 cm /PCFl0 Do Q"), "{flat}");
}
/// #3175: a note without an appearance is drawn as a fixed-size icon at the upper-left corner of
/// its `/Rect`. A rectangle with no width (LaTeX `pdfcomment`) used to scale the icon to nothing
/// while flattening still counted it as drawn and deleted the note.
#[test]
fn flattening_a_note_with_a_zero_width_rect_draws_a_fixed_size_icon() {
let page = stream("", " ");
let mut doc = build(&[
"<< /Type /Catalog /Pages 2 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R /Annots [5 0 R 6 0 R] >>",
&page,
"<< /Type /Annot /Subtype /Text /Rect [50 60 50 72] /Contents (Zero width) /Name /Comment /F 4 >>",
"<< /Type /Annot /Subtype /Text /Rect [10 20 150 120] /Contents (Large) /F 4 >>",
]);
assert_eq!(flatten(&mut doc, &[0], true, false).unwrap(), 2, "both icons are drawn");
let doc = reopen(&doc);
let flat = streams(&doc, 0).pop().unwrap();
assert!(flat.contains("q 1 0 0 1 50 52 cm /PCFl0 Do Q"), "{flat}");
assert!(flat.contains("q 1 0 0 1 10 100 cm /PCFl1 Do Q"), "{flat}");
}
/// #1470: a form field whose widget is on no page (left behind by page extraction or combining)
/// has nothing to draw, but flattening every form field still takes it out of `/AcroForm /Fields`,
/// or other software keeps treating the flattened file as a form. A hidden widget on a page is