mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
crates/forms/src/tests.rs: append conflict, kept both tests.
This commit is contained in:
@@ -288,6 +288,23 @@ impl Automation {
|
||||
let edit = if edits.len() == 1 { edits.remove(0) } else { Edit::Batch { label: "Fill in form".into(), edits } };
|
||||
let mut out = self.apply(a, edit)?;
|
||||
out["filled"] = json!(values.len());
|
||||
// A value the box cuts off at a fixed font size is saved whole but shows only in part:
|
||||
// said, not silent (#4018).
|
||||
let doc = self.doc(a)?;
|
||||
let cut: Vec<Value> = values
|
||||
.keys()
|
||||
.filter_map(|name| doc.field_overflow(name).map(|(page, o)| (name, page, o)))
|
||||
.map(|(name, page, o)| {
|
||||
let page = page.map(|p| format!(" (page {})", p.saturating_add(1))).unwrap_or_default();
|
||||
json!(format!("form field {name:?}{page}: {o}, so the box shows only part of it (the whole value is saved)"))
|
||||
})
|
||||
.collect();
|
||||
if !cut.is_empty() {
|
||||
match out.get_mut("warnings").and_then(Value::as_array_mut) {
|
||||
Some(w) => w.extend(cut),
|
||||
None => out["warnings"] = Value::Array(cut),
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
|
||||
@@ -2331,6 +2331,51 @@ fn forms_through_tools() {
|
||||
assert_ne!(v, "Filled by an agent");
|
||||
}
|
||||
|
||||
/// A PDF of the given objects (numbered from 1).
|
||||
fn pdf_of(objs: &[&str]) -> Vec<u8> {
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offsets {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
/// #4018: `form_fill` warns about a value its box cuts off at the field's fixed font size (the
|
||||
/// value is saved whole), and not about one that fits.
|
||||
#[test]
|
||||
fn form_fill_warns_when_a_value_does_not_fit() {
|
||||
let objs = [
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>",
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 400 300] >>",
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [5 0 R 6 0 R] >>",
|
||||
"<< /Fields [5 0 R 6 0 R] /DA (/Helv 12 Tf 0 g) /DR << /Font << /Helv 7 0 R >> >> >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (narrow) /Rect [10 250 150 268] /P 3 0 R >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (wide) /Rect [10 200 310 218] /P 3 0 R >>",
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>",
|
||||
];
|
||||
let dir = workdir("form-overflow");
|
||||
std::fs::write(dir.join("form.pdf"), pdf_of(&objs)).unwrap();
|
||||
let mut a = auto(&dir);
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "form.pdf" }))["doc"].as_u64().unwrap();
|
||||
let long = "Doe, Jane (represented by John Doe)";
|
||||
let r = ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "narrow": long, "wide": long } }));
|
||||
let warnings: Vec<&str> = r["warnings"].as_array().map(|w| w.iter().filter_map(Value::as_str).collect()).unwrap_or_default();
|
||||
assert_eq!(warnings.len(), 1, "{r}");
|
||||
assert!(warnings[0].starts_with("form field \"narrow\" (page 1): the value does not fit"), "{}", warnings[0]);
|
||||
let after = ok(&mut a, "form_fields", json!({ "doc": doc }));
|
||||
assert_eq!(after["fields"][0]["value"], long, "saved whole");
|
||||
let r = ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "narrow": "Ada" } }));
|
||||
assert!(r.get("warnings").is_none(), "{r}");
|
||||
}
|
||||
|
||||
/// A form whose fields are only page widgets (the `/Fields` list is empty) still lists, fills and
|
||||
/// resets through the tools, and the leniency shows up as a repair note.
|
||||
#[test]
|
||||
@@ -2346,19 +2391,7 @@ fn orphan_form_fields_through_tools() {
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (beta) /Rect [10 150 90 170] /P 3 0 R >>", // 6
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>", // 7
|
||||
];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offsets {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
out
|
||||
pdf_of(&objs)
|
||||
}
|
||||
let dir = workdir("orphan-forms");
|
||||
std::fs::write(dir.join("orphan.pdf"), orphan_form()).unwrap();
|
||||
|
||||
@@ -847,6 +847,14 @@ impl Document {
|
||||
matches!(f.kind, FormFieldKind::CheckBox | FormFieldKind::Radio).then(|| pdfcraft_forms::check_style(&e.cos, f))
|
||||
}
|
||||
|
||||
/// Whether text field `name`'s value is cut off by its box at the field's font size (#4018):
|
||||
/// the page (0-based) of the first widget that cuts it, and how.
|
||||
pub fn field_overflow(&self, name: &str) -> Option<(Option<usize>, pdfcraft_forms::appearance::Overflow)> {
|
||||
let e = self.editor.as_ref()?;
|
||||
let f = self.form.iter().find(|f| f.name == name)?;
|
||||
f.widgets.iter().find_map(|w| pdfcraft_forms::appearance::value_overflow(&e.cos, f, w).map(|o| (w.page, o)))
|
||||
}
|
||||
|
||||
/// Remove Hidden Information: what each category would remove.
|
||||
pub fn hidden_info(&self) -> Vec<(Hidden, usize)> {
|
||||
self.editor.as_ref().map(|e| pdfcraft_redact::sanitize::scan(&e.cos)).unwrap_or_default()
|
||||
|
||||
@@ -449,6 +449,45 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri
|
||||
/// hold it: the form's font is a simple (WinAnsi) font, Helvetica stands in for one PdfCraft can't
|
||||
/// write with (an embedded subset, `Identity-H`), or a UCS-2 CMap meets a character beyond U+FFFF.
|
||||
pub fn field_appearance_drawn(doc: &Document, f: &Field, w: &Widget, values: &[String], format: bool) -> (Stream, Option<char>) {
|
||||
let (stream, unencodable, _) = field_appearance_fit(doc, f, w, values, format);
|
||||
(stream, unencodable)
|
||||
}
|
||||
|
||||
/// A text value that its box cuts off at the field's font size (#4018).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Overflow {
|
||||
/// One line `width` points wide, with `room` for it, at `size` points.
|
||||
Wide { width: f64, room: f64, size: f64 },
|
||||
/// `lines` wrapped lines, of which the box shows `room`, at `size` points.
|
||||
Tall { lines: usize, room: usize, size: f64 },
|
||||
/// One line `height` points high (ascent to descent), with `room` for it, at `size` points.
|
||||
High { height: f64, room: f64, size: f64 },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Overflow {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Wide { width, room, size } => {
|
||||
write!(f, "the value does not fit: {} pt wide at {} pt, room for {} pt", fmt(*width), fmt(*size), fmt(*room))
|
||||
}
|
||||
Self::Tall { lines, room, size } => write!(f, "the value does not fit: {lines} lines at {} pt, room for {room}", fmt(*size)),
|
||||
Self::High { height, room, size } => {
|
||||
write!(f, "the value does not fit: {} pt high at {} pt, room for {} pt", fmt(*height), fmt(*size), fmt(*room))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the text field's value, drawn in widget `w`, is cut off by its box.
|
||||
pub fn value_overflow(doc: &Document, f: &Field, w: &Widget) -> Option<Overflow> {
|
||||
if f.kind != FieldKind::Text {
|
||||
return None;
|
||||
}
|
||||
field_appearance_fit(doc, f, w, &f.value, true).2
|
||||
}
|
||||
|
||||
/// [`field_appearance_drawn`], and whether the text drawn is cut off by the box.
|
||||
fn field_appearance_fit(doc: &Document, f: &Field, w: &Widget, values: &[String], format: bool) -> (Stream, Option<char>, Option<Overflow>) {
|
||||
// The Format event: what is shown, not what is stored.
|
||||
let formatted: Vec<String>;
|
||||
let values = if format
|
||||
@@ -513,6 +552,7 @@ pub fn field_appearance_drawn(doc: &Document, f: &Field, w: &Widget, values: &[S
|
||||
}
|
||||
};
|
||||
let mut size = da.size;
|
||||
let mut overflow = None;
|
||||
match f.kind {
|
||||
FieldKind::List => {
|
||||
// Every option, one per line from the top (from `/TI`); selected ones highlighted.
|
||||
@@ -539,13 +579,25 @@ pub fn field_appearance_drawn(doc: &Document, f: &Field, w: &Widget, values: &[S
|
||||
size = auto_text_size(&text, inner_w, height - 2.0 * pad, true, width_of);
|
||||
}
|
||||
let mut y = height - pad - size * 0.85;
|
||||
for line in wrap_with(&text, size, inner_w, width_of) {
|
||||
let lines = wrap_with(&text, size, inner_w, width_of);
|
||||
// The lines whose descent stays inside the border; the rest are cut off.
|
||||
let mut room = 0;
|
||||
for line in &lines {
|
||||
if y < -size {
|
||||
break;
|
||||
}
|
||||
show(&mut body, x_for(&line, size), y, &line);
|
||||
if y - 0.207 * size >= bw - 0.01 {
|
||||
room += 1;
|
||||
}
|
||||
show(&mut body, x_for(line, size), y, line);
|
||||
y -= size * 1.15;
|
||||
}
|
||||
if room < lines.len() {
|
||||
overflow = Some(Overflow::Tall { lines: lines.len(), room, size });
|
||||
} else if let Some(tw) = lines.iter().map(|l| width_of(l, size)).filter(|tw| *tw > inner_w + 0.01).reduce(f64::max) {
|
||||
// A character wider than the box is a line of its own, still cut at the sides.
|
||||
overflow = Some(Overflow::Wide { width: tw, room: inner_w, size });
|
||||
}
|
||||
} else {
|
||||
if size == 0.0 {
|
||||
// Comb cells hold one character each, so only the height limits them.
|
||||
@@ -557,6 +609,11 @@ pub fn field_appearance_drawn(doc: &Document, f: &Field, w: &Widget, values: &[S
|
||||
}
|
||||
// Centre Helvetica's ascent (0.718) and descent (0.207) vertically.
|
||||
let y = (height - 0.925 * size) / 2.0 + 0.207 * size;
|
||||
// The clip box is inside the border; a line taller than it is cut at the top and bottom.
|
||||
let room_h = height - 2.0 * bw;
|
||||
if 0.925 * size > room_h + 0.01 {
|
||||
overflow = Some(Overflow::High { height: 0.925 * size, room: room_h, size });
|
||||
}
|
||||
if comb {
|
||||
let cells = f.max_len.unwrap_or(1).max(1);
|
||||
let cell = width / cells as f64;
|
||||
@@ -565,6 +622,10 @@ pub fn field_appearance_drawn(doc: &Document, f: &Field, w: &Widget, values: &[S
|
||||
show(&mut body, cell * i as f64 + (cell - width_of(&s, size)) / 2.0, y, &s);
|
||||
}
|
||||
} else {
|
||||
let tw = width_of(&text.replace(['\n', '\r'], " "), size);
|
||||
if overflow.is_none() && tw > inner_w + 0.01 {
|
||||
overflow = Some(Overflow::Wide { width: tw, room: inner_w, size });
|
||||
}
|
||||
show(&mut body, x_for(&text, size), y, &text);
|
||||
}
|
||||
}
|
||||
@@ -595,7 +656,9 @@ pub fn field_appearance_drawn(doc: &Document, f: &Field, w: &Widget, values: &[S
|
||||
d.set(b"Subtype".to_vec(), Object::name("Form"));
|
||||
set_form_box(&mut d, place);
|
||||
d.set(b"Resources".to_vec(), Object::Dict(res));
|
||||
(Stream::flate(d, &content), unencodable)
|
||||
// Nothing drawn is nothing cut, however small the box.
|
||||
let overflow = overflow.filter(|_| !text.is_empty());
|
||||
(Stream::flate(d, &content), unencodable, overflow)
|
||||
}
|
||||
|
||||
/// On/Off appearances for a check box or radio button that has none (a check mark or a dot,
|
||||
|
||||
@@ -3747,3 +3747,87 @@ fn a_password_field_missing_from_the_field_list_is_still_cleared() {
|
||||
assert!(fields(&doc).iter().all(|f| f.value.iter().all(|v| v.is_empty())), "{what}: {:?}", fields(&doc));
|
||||
}
|
||||
}
|
||||
|
||||
/// #4018: a value that doesn't fit its box at the field's font size is reported (it is drawn cut
|
||||
/// at the box). An auto-sized field shrinks to fit, and a short or empty value fits.
|
||||
#[test]
|
||||
fn a_value_wider_or_taller_than_its_box_is_reported() {
|
||||
let long = "Doe, Jane (represented by John Doe)";
|
||||
let field = |name: &str, rect: &str, extra: &str, value: &str| {
|
||||
format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}) /V ({value}) /Rect [{rect}] /P 3 0 R {extra} >>")
|
||||
};
|
||||
let doc = document(&[
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [6 0 R 7 0 R 8 0 R 9 0 R 10 0 R] >>".into(),
|
||||
"<< /Fields [6 0 R 7 0 R 8 0 R 9 0 R 10 0 R] /DA (/Helv 12 Tf 0 g) /DR << /Font << /Helv 5 0 R >> >> >>".into(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
|
||||
field("single", "50 700 190 718", "", long),
|
||||
field("multi", "50 650 190 668", "/Ff 4096", long),
|
||||
field("auto", "50 600 190 618", "/DA (/Helv 0 Tf 0 g)", long),
|
||||
field("wide", "50 550 350 568", "", long),
|
||||
field("short", "50 500 190 518", "/Ff 4096", "Ada"),
|
||||
]);
|
||||
let overflow = |name: &str| {
|
||||
let f = fields(&doc).into_iter().find(|f| f.name == name).unwrap();
|
||||
appearance::value_overflow(&doc, &f, &f.widgets[0])
|
||||
};
|
||||
match overflow("single") {
|
||||
Some(appearance::Overflow::Wide { width, room, size }) => {
|
||||
assert_eq!(size, 12.0);
|
||||
assert!((width - 201.4).abs() < 1.0 && room < 140.0, "{width} in {room}");
|
||||
}
|
||||
other => panic!("single: {other:?}"),
|
||||
}
|
||||
assert_eq!(overflow("multi"), Some(appearance::Overflow::Tall { lines: 2, room: 1, size: 12.0 }));
|
||||
assert_eq!(overflow("auto"), None, "auto size shrinks to fit");
|
||||
assert_eq!(overflow("wide"), None);
|
||||
assert_eq!(overflow("short"), None);
|
||||
}
|
||||
|
||||
/// #4018: a single line taller than its box is cut at the top and bottom, and a character wider
|
||||
/// than a multiline box is a line of its own that is still cut at the sides; both are reported.
|
||||
/// An empty value draws nothing, so nothing is cut however small the box.
|
||||
#[test]
|
||||
fn a_line_taller_than_its_box_or_a_character_wider_than_it_is_reported() {
|
||||
let field = |name: &str, rect: &str, extra: &str, value: &str| {
|
||||
format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}) /V ({value}) /Rect [{rect}] /P 3 0 R {extra} >>")
|
||||
};
|
||||
let doc = document(&[
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R] >>".into(),
|
||||
"<< /Fields [6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R] /DA (/Helv 12 Tf 0 g) /DR << /Font << /Helv 5 0 R >> >> >>".into(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(),
|
||||
field("low", "50 700 190 708", "", "Ada"),
|
||||
field("comb", "50 650 190 658", "/Ff 16777216 /MaxLen 5", "Ada"),
|
||||
field("narrow", "50 500 56 560", "/Ff 4096", "WW"),
|
||||
field("roomy", "50 400 190 418", "", "Ada"),
|
||||
field("empty", "50 350 190 358", "", ""),
|
||||
field("empty-multi", "50 300 190 304", "/Ff 4096", ""),
|
||||
]);
|
||||
let overflow = |name: &str| {
|
||||
let f = fields(&doc).into_iter().find(|f| f.name == name).unwrap();
|
||||
appearance::value_overflow(&doc, &f, &f.widgets[0])
|
||||
};
|
||||
for name in ["low", "comb"] {
|
||||
match overflow(name) {
|
||||
Some(o @ appearance::Overflow::High { height, room, size }) => {
|
||||
assert_eq!(size, 12.0);
|
||||
assert!((height - 11.1).abs() < 0.01 && room <= 8.0, "{name}: {height} in {room}");
|
||||
assert!(o.to_string().starts_with("the value does not fit: 11.1 pt high at 12 pt"), "{o}");
|
||||
}
|
||||
other => panic!("{name}: {other:?}"),
|
||||
}
|
||||
}
|
||||
match overflow("narrow") {
|
||||
Some(appearance::Overflow::Wide { width, room, size }) => {
|
||||
assert_eq!(size, 12.0);
|
||||
assert!(width > room && (width - 11.32).abs() < 0.1, "{width} in {room}");
|
||||
}
|
||||
other => panic!("narrow: {other:?}"),
|
||||
}
|
||||
assert_eq!(overflow("roomy"), None);
|
||||
assert_eq!(overflow("empty"), None);
|
||||
assert_eq!(overflow("empty-multi"), None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user