mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 12:08:46 -06:00
M6: form_fill warns when a value does not fit its field (#4018)
With a fixed font size, a text field's appearance cuts a long value at the edge of its box, but form_fill only answered "filled": n. A person sees the cut; an agent filling forms through the CLI or MCP did not. The text appearance builder now notes when the text it lays out does not fit: one line wider than the box, or wrapped lines below it. forms::appearance::value_overflow asks that for a field's widget, Document::field_overflow for a field by name, and form_fill adds a warning for each filled field that is cut off. The value is still saved whole and drawn as before; auto-sized fields shrink to fit and are not reported. The automation tests' inline PDF builder moves to a shared pdf_of. Fixes #4018
This commit is contained in:
@@ -449,6 +449,40 @@ 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 },
|
||||
}
|
||||
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 +547,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 +574,22 @@ 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 size == 0.0 {
|
||||
// Comb cells hold one character each, so only the height limits them.
|
||||
@@ -565,6 +609,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 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 +643,7 @@ 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)
|
||||
(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,
|
||||
|
||||
@@ -2981,3 +2981,40 @@ fn page_walk_skips_repeated_and_cyclic_kids_without_queueing_them() {
|
||||
assert_eq!(walk.next(), None);
|
||||
assert_eq!(page_refs(&doc), [page]);
|
||||
}
|
||||
|
||||
/// #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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user