Comments: line breaks saved as CR show in comment text (#1113)

Acrobat saves the line breaks in a comment's /Contents as CR, e.g.
"first line\r\rsecond paragraph". egui draws CR as nothing and only breaks
lines at LF, so the comments panel, the hover popup and the edit box ran
the paragraphs together ("first linesecond paragraph").

Both readers of comment text (pdfcraft_render::inspect on open and
pdfcraft_annot::summaries after an edit) now turn CRLF and CR into LF.

Regression tests: carriage_returns_in_comment_text_become_line_breaks in
annot and render (both fail before this change).
This commit is contained in:
Nicholas Yoannou authored and GitHub committed 2026-10-11 14:46:05 -04:00
1 parent 0e8c978c6a
commit 92c9de7aad
3 files changed
+48 -2

No files matched your search

+3 -1
View File
@@ -1931,7 +1931,9 @@ pub fn summaries(doc: &Document) -> Vec<Summary> {
index,
subtype,
author: text_value(doc, d, b"T"),
contents: text_value(doc, d, b"Contents"),
// Acrobat ends lines with CR, which the interface would draw as nothing (keep in
// step with `pdfcraft_render::inspect`).
contents: text_value(doc, d, b"Contents").map(|s| s.replace("\r\n", "\n").replace('\r', "\n")),
modified: text_value(doc, d, b"M"),
name: text_value(doc, d, b"NM"),
in_reply_to,
+13
View File
@@ -432,6 +432,19 @@ fn real_annotation_flags_do_not_lock_comments() {
assert!(set_info(&mut doc, 0, 0, Some("Ada"), None, None, &meta("")).is_ok());
}
#[test]
fn carriage_returns_in_comment_text_become_line_breaks() {
let mut doc = fixture();
let page = page_refs(&doc).unwrap()[0];
let mut entries = annots(&doc, page);
let Object::Dict(annotation) = &mut entries[0] else { panic!("fixture annotation is inline") };
// Acrobat ends lines in /Contents with CR; the interface only breaks lines at LF.
annotation.set(b"Contents".to_vec(), PdfString::text("one\r\rtwo\r\nthree"));
set_annots(&mut doc, page, entries).unwrap();
assert_eq!(summaries(&doc)[0].contents.as_deref(), Some("one\n\ntwo\nthree"));
}
#[test]
fn a_locked_text_box_still_refits_its_text() {
let mut doc = fixture();
+32 -1
View File
@@ -921,7 +921,9 @@ impl<'a> Inspector<'a> {
page,
subtype,
author: self.text(d, b"T"),
contents: self.text(d, b"Contents"),
// Acrobat ends lines with CR, which the interface would draw as nothing (keep
// in step with `pdfcraft_annot::summaries`).
contents: self.text(d, b"Contents").map(|s| s.replace("\r\n", "\n").replace('\r', "\n")),
modified: self.text(d, b"M").map(|m| pretty_date(&m)),
name: self.text(d, b"NM"),
in_reply_to,
@@ -2233,6 +2235,35 @@ trailer << /Root 1 0 R >>
assert!(info.warnings.is_empty(), "{:?}", info.warnings);
}
#[test]
fn carriage_returns_in_comment_text_become_line_breaks() {
use lopdf::dictionary;
let mut doc = Document::with_version("1.7");
let pages_id = doc.new_object_id();
let annot_id = doc.add_object(dictionary! {
"Type" => "Annot",
"Subtype" => "Text",
"Rect" => vec![10.into(), 10.into(), 30.into(), 30.into()],
// Acrobat ends lines in /Contents with CR; the interface only breaks lines at LF.
"Contents" => Object::string_literal("one\r\rtwo\r\nthree"),
});
let page_id = doc.add_object(dictionary! {
"Type" => "Page",
"Parent" => pages_id,
"MediaBox" => vec![0.into(), 0.into(), 200.into(), 300.into()],
"Annots" => vec![annot_id.into()],
});
doc.objects.insert(pages_id, dictionary! { "Type" => "Pages", "Kids" => vec![page_id.into()], "Count" => 1 }.into());
let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id });
doc.trailer.set("Root", catalog_id);
let mut bytes = Vec::new();
doc.save_to(&mut bytes).expect("write synthetic fixture");
let info = inspect(Arc::new(bytes), None).expect("opens");
assert_eq!(info.annotations[0].contents.as_deref(), Some("one\n\ntwo\nthree"));
}
#[test]
fn dates_are_prettified() {
assert_eq!(pretty_date("D:20260930104512-04'00'"), "2026-09-30 10:45");