mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 02:58:32 -06:00
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:
@@ -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);
|
||||
})?;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user