Export wrote polygon and polyline comments without their points and import had nowhere to read them from, so an XFDF round trip left the comments with no /Vertices and no appearance. Export now writes the points as XFDF <vertices>x,y;x,y;…</vertices>, and import reads them back (whole x,y pairs only) before the appearance is drawn. Co-authored-by: DWAA1660 <DWAA1660@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
259002da97
commit
4b74f655de
|
|
@ -264,6 +264,12 @@ fn xfdf_annots(doc: &Document, out: &mut String) {
|
|||
}
|
||||
out.push_str("</inklist>");
|
||||
}
|
||||
// Polygon and polyline points (XFDF `<vertices>`: `x,y;x,y;…`).
|
||||
let vertices = nums_of(doc, d.get(b"Vertices"));
|
||||
let pts: Vec<String> = vertices.as_chunks::<2>().0.iter().map(|p| format!("{},{}", n(p[0]), n(p[1]))).collect();
|
||||
if !pts.is_empty() {
|
||||
let _ = write!(out, "<vertices>{}</vertices>", pts.join(";"));
|
||||
}
|
||||
if let Some(pd) = d.get(b"Popup").map(|p| doc.resolve(p)).and_then(|p| p.as_dict().cloned()) {
|
||||
let r = nums_of(doc, pd.get(b"Rect"));
|
||||
if r.len() == 4 {
|
||||
|
|
@ -617,6 +623,14 @@ fn annot_from_xml(node: roxmltree::Node, subtype: &str, page_ref: ObjRef) -> Opt
|
|||
.collect();
|
||||
d.set(b"InkList".to_vec(), Object::Array(list));
|
||||
}
|
||||
"vertices" => {
|
||||
// Whole x,y pairs only; a stray trailing number is dropped.
|
||||
let mut v = csv(child.text().unwrap_or(""));
|
||||
v.truncate(v.len() - v.len() % 2);
|
||||
if !v.is_empty() {
|
||||
d.set(b"Vertices".to_vec(), arr(&v));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -352,3 +352,59 @@ fn reimporting_a_note_replaces_its_popup() {
|
|||
let parent = doc.get(popups[0]).as_dict().and_then(|d| d.reference(b"Parent")).unwrap();
|
||||
assert!(refs.contains(&parent), "the pop-up belongs to a note that is still on the page");
|
||||
}
|
||||
|
||||
/// The `/Vertices` of every annotation on the document's pages, by subtype.
|
||||
fn vertices(doc: &Document) -> Vec<(String, Vec<f64>)> {
|
||||
let mut out = Vec::new();
|
||||
for p in pdfcraft_model::pages(doc) {
|
||||
for a in p.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default() {
|
||||
let Some(d) = doc.resolve(&a).as_dict().cloned() else { continue };
|
||||
let sub = String::from_utf8_lossy(d.name(b"Subtype").unwrap_or_default()).into_owned();
|
||||
let v = nums_of(doc, d.get(b"Vertices"));
|
||||
if !v.is_empty() {
|
||||
out.push((sub, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// storytold/pdfcraft#809: XFDF exchange dropped polygon and polyline vertices, so the
|
||||
/// imported comments had no shape (and no appearance).
|
||||
#[test]
|
||||
fn xfdf_keeps_polygon_and_polyline_vertices() {
|
||||
let mut src = blank();
|
||||
let meta = |id: &str| Meta { date: Some("D:20261002120000Z".into()), id: id.into() };
|
||||
let polygon = Shape::Polygon { vertices: vec![[100.0, 100.0], [200.0, 120.0], [150.0, 220.5]], cloud: false };
|
||||
let polyline = Shape::PolyLine {
|
||||
vertices: vec![[50.0, 300.0], [120.0, 380.0], [200.0, 310.0]],
|
||||
start: pdfcraft_annot::LineEnding::None,
|
||||
end: pdfcraft_annot::LineEnding::ClosedArrow,
|
||||
};
|
||||
for (page, shape, id) in [(0, polygon, "pg"), (1, polyline, "pl")] {
|
||||
let new = NewAnnotation { page, style: Style::default_for(&shape), shape, contents: id.into(), author: "Ada".into() };
|
||||
add_annotation(&mut src, &new, &meta(id)).unwrap();
|
||||
}
|
||||
let xfdf = export_xfdf(&src, true, false, "shapes.pdf");
|
||||
assert!(xfdf.contains("<vertices>100,100;200,120;150,220.5</vertices>"), "{xfdf}");
|
||||
assert!(xfdf.contains("<vertices>50,300;120,380;200,310</vertices>"), "{xfdf}");
|
||||
let mut dst = blank();
|
||||
let r = import(&mut dst, xfdf.as_bytes()).unwrap();
|
||||
assert_eq!(r.comments, 2, "{r:?}");
|
||||
assert_eq!(vertices(&dst), vertices(&src));
|
||||
// The imported shapes are drawn again.
|
||||
for p in pdfcraft_model::pages(&dst) {
|
||||
for a in p.dict.get(b"Annots").map(|a| dst.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default() {
|
||||
let d = dst.resolve(&a).as_dict().cloned().unwrap();
|
||||
assert!(d.get(b"AP").is_some(), "{d:?}");
|
||||
}
|
||||
}
|
||||
// Acrobat-style input: a stray trailing number is dropped and only whole points are kept.
|
||||
let odd = r#"<?xml version="1.0"?><xfdf xmlns="http://ns.adobe.com/xfdf/"><annots>
|
||||
<polygon page="0" rect="0,0,50,50" name="odd"><vertices>1,2;3,4;5,6;7</vertices></polygon>
|
||||
<polygon page="0" rect="0,0,50,50" name="none"><vertices>junk</vertices></polygon>
|
||||
</annots></xfdf>"#;
|
||||
let mut dst = blank();
|
||||
import(&mut dst, odd.as_bytes()).unwrap();
|
||||
assert_eq!(vertices(&dst), vec![("Polygon".to_string(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue