mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 04:28:20 -06:00
Integration: a comment listed through a reference chain keeps its replies after an edit (#4061)
This commit is contained in:
+17
-4
@@ -2328,6 +2328,18 @@ pub fn is_comment_subtype(subtype: &str) -> bool {
|
||||
!matches!(subtype, "Link" | "Widget" | "Popup" | "Screen" | "Movie" | "RichMedia" | "3D" | "PrinterMark" | "TrapNet" | "Watermark")
|
||||
}
|
||||
|
||||
/// Where a reference to a reference (`5 0 obj 6 0 R endobj`) ends, as `pdfcraft_render::inspect`
|
||||
/// follows it, within a few steps (a cycle stops there).
|
||||
fn final_ref(doc: &Document, mut r: ObjRef) -> ObjRef {
|
||||
for _ in 0..8 {
|
||||
match &*doc.get(r) {
|
||||
Object::Ref(next) => r = *next,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// Every comment (see [`is_comment_subtype`]), ordered by page and then top edge.
|
||||
pub fn summaries(doc: &Document) -> Vec<Summary> {
|
||||
let mut out = Vec::new();
|
||||
@@ -2338,13 +2350,13 @@ pub fn summaries(doc: &Document) -> Vec<Summary> {
|
||||
for (page, list) in lists.iter().enumerate() {
|
||||
for (index, entry) in list.iter().enumerate() {
|
||||
if let Some(r) = entry.as_ref() {
|
||||
slots.entry(r).or_insert((page, index));
|
||||
slots.entry(final_ref(doc, r)).or_insert((page, index));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (page, list) in lists.iter().enumerate() {
|
||||
for (index, entry) in list.iter().enumerate() {
|
||||
let obj = doc.resolve(entry);
|
||||
let obj = entry.as_ref().map_or_else(|| doc.resolve(entry), |r| doc.get(final_ref(doc, r)));
|
||||
let Some(d) = obj.as_dict() else { continue };
|
||||
let Some(subtype) = d.name(b"Subtype").map(|s| String::from_utf8_lossy(s).into_owned()) else { continue };
|
||||
if !is_comment_subtype(&subtype) {
|
||||
@@ -2378,8 +2390,9 @@ pub fn summaries(doc: &Document) -> Vec<Summary> {
|
||||
"PolyLine" | "Polygon" => nums(b"Vertices").as_chunks::<2>().0.to_vec(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let in_reply_to = d.get(b"IRT").map(|o| doc.resolve(o)).and_then(|o| o.as_dict().cloned()).and_then(|p| text_value(doc, &p, b"NM"));
|
||||
let parent = d.get(b"IRT").and_then(Object::as_ref).and_then(|r| slots.get(&r).copied());
|
||||
let irt = d.get(b"IRT").map(|o| o.as_ref().map_or_else(|| doc.resolve(o), |r| doc.get(final_ref(doc, r))));
|
||||
let in_reply_to = irt.and_then(|o| o.as_dict().cloned()).and_then(|p| text_value(doc, &p, b"NM"));
|
||||
let parent = d.get(b"IRT").and_then(Object::as_ref).and_then(|r| slots.get(&final_ref(doc, r)).copied());
|
||||
out.push(Summary {
|
||||
page,
|
||||
index,
|
||||
|
||||
@@ -3839,6 +3839,37 @@ trailer << /Root 1 0 R >>
|
||||
check(&s, 2);
|
||||
}
|
||||
|
||||
/// A comment listed through a reference chain (`/Annots [5 0 R]`, `5 0 obj 6 0 R`) keeps its
|
||||
/// replies after an edit, whether their `/IRT` names the comment or the chain, as on open.
|
||||
#[test]
|
||||
fn replies_thread_through_a_reference_chain_after_an_edit() {
|
||||
let pdf = b"%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 300 200] /Annots [5 0 R 7 0 R 8 0 R 9 0 R] >> endobj
|
||||
5 0 obj 6 0 R endobj
|
||||
6 0 obj << /Type /Annot /Subtype /Text /Rect [20 140 40 160] /NM (one) /Contents (CHAINED) >> endobj
|
||||
7 0 obj << /Type /Annot /Subtype /Text /Rect [20 120 40 130] /IRT 6 0 R /Contents (TO THE COMMENT) >> endobj
|
||||
8 0 obj << /Type /Annot /Subtype /Text /Rect [20 100 40 110] /IRT 5 0 R /Contents (THROUGH THE CHAIN) >> endobj
|
||||
9 0 obj << /Type /Annot /Subtype /Text /Rect [20 60 40 80] /NM (two) /Contents (PLAIN) >> endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000);
|
||||
let id = s.open("chain.pdf", None, Arc::new(pdf.to_vec()), None).expect("opens");
|
||||
let check = |s: &Session| {
|
||||
let all = &s.get(id).unwrap().info.annotations;
|
||||
let comment = all.iter().find(|a| a.contents.as_deref() == Some("CHAINED")).expect("listed");
|
||||
assert_eq!((comment.page, comment.index, comment.is_reply()), (0, 0, false), "{all:?}");
|
||||
for reply in ["TO THE COMMENT", "THROUGH THE CHAIN"] {
|
||||
let a = all.iter().find(|a| a.contents.as_deref() == Some(reply)).expect("listed");
|
||||
assert_eq!((a.parent, a.in_reply_to.as_deref()), (Some((0, 0)), Some("one")), "{reply}: {all:?}");
|
||||
}
|
||||
};
|
||||
check(&s);
|
||||
s.apply(id, Edit::ReplyToAnnotation { page: 0, index: 3, text: "LATER".into(), author: "Delta".into() }).unwrap();
|
||||
check(&s);
|
||||
}
|
||||
|
||||
/// #820: the generated summary includes replies to replies, indented one level deeper.
|
||||
#[test]
|
||||
fn comment_summary_includes_replies_to_replies() {
|
||||
|
||||
Reference in New Issue
Block a user