M6: form_fill also warns about lines too tall or too wide for their box (#4018)

Two cases still drew a value cut off without saying so. A single-line
value at a fixed font size taller than the box (12 pt text in an 8 pt
field) was checked only for width, so it fit sideways and was cut at the
top and bottom. In a multiline field, a character wider than the box
becomes a line of its own, which fits vertically but is cut at the sides.

The single-line and comb branch now compares the line's ascent to
descent against the clip box (a new `Overflow::High`), and the multiline
branch reports the widest wrapped line that is still wider than the box.
An empty value draws nothing, so it is never reported, however small the
box.

Fixes #4018
This commit is contained in:
Ridzzz0Alam
2026-10-22 15:47:54 +02:00
parent bd00a131b6
commit b98d8a2819
2 changed files with 63 additions and 1 deletions
+16 -1
View File
@@ -460,6 +460,8 @@ pub enum Overflow {
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 {
@@ -469,6 +471,9 @@ impl std::fmt::Display for Overflow {
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))
}
}
}
}
@@ -589,6 +594,9 @@ fn field_appearance_fit(doc: &Document, f: &Field, w: &Widget, values: &[String]
}
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 {
@@ -601,6 +609,11 @@ fn field_appearance_fit(doc: &Document, f: &Field, w: &Widget, values: &[String]
}
// 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;
@@ -610,7 +623,7 @@ fn field_appearance_fit(doc: &Document, f: &Field, w: &Widget, values: &[String]
}
} else {
let tw = width_of(&text.replace(['\n', '\r'], " "), size);
if tw > inner_w + 0.01 {
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);
@@ -643,6 +656,8 @@ fn field_appearance_fit(doc: &Document, f: &Field, w: &Widget, values: &[String]
d.set(b"Subtype".to_vec(), Object::name("Form"));
set_form_box(&mut d, place);
d.set(b"Resources".to_vec(), Object::Dict(res));
// Nothing drawn is nothing cut, however small the box.
let overflow = overflow.filter(|_| !text.is_empty());
(Stream::flate(d, &content), unencodable, overflow)
}
+47
View File
@@ -3018,3 +3018,50 @@ fn a_value_wider_or_taller_than_its_box_is_reported() {
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);
}