mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
1 parent
4b74f655de
commit
f6caba43b2
3 files changed
+69
-2
No files matched your search
@@ -12,7 +12,8 @@ let report = import(&mut doc, &bytes)?; // XFDF, FDF, XML, CSV o
|
||||
- **XFDF** (ISO 19444-1): every comment type PdfCraft knows (text, free text, line, square,
|
||||
circle, polygon, polyline, markup, stamp, caret, ink, attachments, sound, redact) with rect,
|
||||
name, author, subject, dates, flags, colours, opacity, width, icon, quads, line ends, ink,
|
||||
default appearance, pop-ups and reply threads (`inreplyto`); field values nested by name.
|
||||
default appearance, callouts (`callout`, `fringe`, `intent`, `head`), pop-ups and reply threads
|
||||
(`inreplyto`); field values nested by name.
|
||||
- **FDF**: fields (with `/Kids` hierarchies) and comments as direct objects.
|
||||
- **Form data**: Acrobat's XML (`xfdf:original` keeps names that aren't XML names), CSV and
|
||||
tab-delimited text (a row of names, a row of values).
|
||||
|
||||
+30
-1
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! - **XFDF** (ISO 19444-1, Acrobat's "Export all to data file" / "Import comments", and form
|
||||
//! data): comments with their geometry, colours, flags, authors, dates, replies, pop-ups,
|
||||
//! ink, quadrilaterals and line ends; field values, nested by name.
|
||||
//! ink, quadrilaterals, line ends and callouts; field values, nested by name.
|
||||
//! - **FDF** (ISO 32000-2 §12.7.8): fields and comments in PDF syntax.
|
||||
//! - Form data as **XML**, **CSV** and **tab-delimited text** (Acrobat's Export data formats).
|
||||
//!
|
||||
@@ -244,6 +244,21 @@ fn xfdf_annots(doc: &Document, out: &mut String) {
|
||||
let name = |o: &Object| o.as_name().map(|x| String::from_utf8_lossy(x).into_owned()).unwrap_or_else(|| "None".into());
|
||||
let _ = write!(attrs, " head=\"{}\" tail=\"{}\"", name(&le[0]), name(&le[1]));
|
||||
}
|
||||
// A callout: its leader path, text-box inset, intent and (single-name) line ending.
|
||||
let cl = nums_of(doc, d.get(b"CL"));
|
||||
if cl.len() == 4 || cl.len() == 6 {
|
||||
let _ = write!(attrs, " callout=\"{}\"", cl.iter().map(|v| n(*v)).collect::<Vec<_>>().join(","));
|
||||
let rd = nums_of(doc, d.get(b"RD"));
|
||||
if rd.len() == 4 {
|
||||
let _ = write!(attrs, " fringe=\"{}\"", rd.iter().map(|v| n(*v)).collect::<Vec<_>>().join(","));
|
||||
}
|
||||
if let Some(it) = d.get(b"IT").map(|o| doc.resolve(o)).and_then(|o| o.as_name().map(|x| String::from_utf8_lossy(x).into_owned())) {
|
||||
let _ = write!(attrs, " intent=\"{}\"", esc(&it));
|
||||
}
|
||||
if let Some(le) = d.get(b"LE").map(|o| doc.resolve(o)).and_then(|o| o.as_name().map(|x| String::from_utf8_lossy(x).into_owned())) {
|
||||
let _ = write!(attrs, " head=\"{}\"", esc(&le));
|
||||
}
|
||||
}
|
||||
if let Some(irt) = d.get(b"IRT").and_then(Object::as_ref).and_then(|r| names.get(&r)) {
|
||||
let _ = write!(attrs, " inreplyto=\"{}\"", esc(irt));
|
||||
}
|
||||
@@ -607,6 +622,20 @@ fn annot_from_xml(node: roxmltree::Node, subtype: &str, page_ref: ObjRef) -> Opt
|
||||
Object::Array(vec![Object::name(node.attribute("head").unwrap_or("None")), Object::name(node.attribute("tail").unwrap_or("None"))]),
|
||||
);
|
||||
}
|
||||
if let Some(cl) = node.attribute("callout").map(csv).filter(|c| c.len() == 4 || c.len() == 6) {
|
||||
d.set(b"CL".to_vec(), arr(&cl));
|
||||
if let Some(rd) = node.attribute("fringe").map(csv).filter(|r| r.len() == 4) {
|
||||
d.set(b"RD".to_vec(), arr(&rd));
|
||||
}
|
||||
d.set(b"IT".to_vec(), Object::name(node.attribute("intent").unwrap_or("FreeTextCallout")));
|
||||
// A callout's /LE is one name, not the head/tail pair of a line.
|
||||
match node.attribute("head") {
|
||||
Some(h) => d.set(b"LE".to_vec(), Object::name(h)),
|
||||
None => {
|
||||
d.remove(b"LE");
|
||||
}
|
||||
}
|
||||
}
|
||||
for child in node.children().filter(|c| c.is_element()) {
|
||||
match child.tag_name().name() {
|
||||
"contents" => {
|
||||
|
||||
@@ -408,3 +408,40 @@ fn xfdf_keeps_polygon_and_polyline_vertices() {
|
||||
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])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xfdf_keeps_callout_leader_inset_and_ending() {
|
||||
let mut src = blank();
|
||||
let shape = Shape::Callout {
|
||||
rect: [40.0, 300.0, 180.0, 360.0],
|
||||
knee: [260.0, 330.0],
|
||||
point: [320.0, 200.0],
|
||||
font_size: 12.0,
|
||||
ending: pdfcraft_annot::LineEnding::ClosedArrow,
|
||||
};
|
||||
let meta = Meta { date: Some("D:20261002120000Z".into()), id: "c1".into() };
|
||||
add_annotation(
|
||||
&mut src,
|
||||
&NewAnnotation { page: 0, style: Style::default_for(&shape), shape, contents: "leader".into(), author: "Ada".into() },
|
||||
&meta,
|
||||
)
|
||||
.unwrap();
|
||||
let xfdf = export_xfdf(&src, true, false, "form.pdf");
|
||||
let mut dst = blank();
|
||||
import(&mut dst, xfdf.as_bytes()).unwrap();
|
||||
let callout_keys = |doc: &Document| {
|
||||
let page = pdfcraft_model::pages(doc).remove(0);
|
||||
let annots = page.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default();
|
||||
let d = annots.iter().filter_map(|a| doc.resolve(a).as_dict().cloned()).find(|d| d.name(b"Subtype") == Some(b"FreeText")).unwrap();
|
||||
(
|
||||
nums_of(doc, d.get(b"Rect")),
|
||||
nums_of(doc, d.get(b"CL")),
|
||||
nums_of(doc, d.get(b"RD")),
|
||||
d.name(b"IT").map(<[u8]>::to_vec),
|
||||
d.get(b"LE").and_then(|o| o.as_name()).map(<[u8]>::to_vec),
|
||||
)
|
||||
};
|
||||
let want = callout_keys(&src);
|
||||
assert_eq!(want.1.len(), 6, "{want:?}");
|
||||
assert_eq!(callout_keys(&dst), want, "{xfdf}");
|
||||
}
|
||||
Reference in new issue
Block a user