mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 11:08:48 -06:00
Forms: measure a field's own simple font, not Helvetica, when laying out its text (fixes #2894)
This commit is contained in:
@@ -13,8 +13,9 @@
|
||||
//! Acrobat keeps a value its field font can't show: the value itself is stored as typed,
|
||||
//! [`field_appearance_drawn`] returns the first such character, and the caller marks the widget
|
||||
//! (`pdfcraft_fonts::UNDRAWN_MARK`) and asks other viewers to redraw the field
|
||||
//! (`/NeedAppearances`). Widths use the approximate metrics of `pdfcraft-fonts` (Helvetica, or one
|
||||
//! em per full-width character in composite fonts).
|
||||
//! (`/NeedAppearances`). Widths are the field font's own (its `/Widths`, or the standard-14
|
||||
//! metrics), Helvetica's when Helvetica stands in, and one em per full-width character in
|
||||
//! composite fonts.
|
||||
//!
|
||||
//! `/MK /R` (0, 90, 180, 270, counterclockwise) lays the content out in the upright box and sets
|
||||
//! the appearance `/Matrix` so that box lands on the widget rectangle. Any other value is 0, and
|
||||
@@ -191,7 +192,12 @@ impl FontText {
|
||||
|
||||
fn width(&self, s: &str, size: f64) -> f64 {
|
||||
match self {
|
||||
Self::WinAnsi | Self::Simple(_) => helvetica_width(s, size),
|
||||
Self::WinAnsi => helvetica_width(s, size),
|
||||
// The font's `/Widths`, or its standard-14 metrics (Courier, Times) when it has none.
|
||||
Self::Simple(metrics) => match metrics.encode(&s.replace(['\t', '\r', '\n'], " ")) {
|
||||
Some(bytes) => metrics.codes(&bytes).iter().map(|&(code, _)| metrics.width(code)).sum::<f64>() * size,
|
||||
None => helvetica_width(s, size),
|
||||
},
|
||||
Self::Unicode(_) => cjk_width(s, size),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +364,47 @@ fn multiline_wrapping_keeps_encodable_text_without_requiring_ascii_space() {
|
||||
assert_eq!(metrics.decode(&lines.concat()), value.replace('\n', ""));
|
||||
}
|
||||
|
||||
/// A field drawn in its `/DR` font is laid out with that font's widths, not Helvetica's (#2894):
|
||||
/// Courier is 0.6 em a character, so 10 pt text holds 94 to a line in a 564 pt box, and a
|
||||
/// right-aligned value ends at the padding in Courier and in Times.
|
||||
#[test]
|
||||
fn reused_standard_fonts_wrap_and_align_with_their_own_widths() {
|
||||
let objs: Vec<String> = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 612 792] >>".into(),
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [7 0 R 8 0 R 9 0 R] >>".into(),
|
||||
"<< /Fields [7 0 R 8 0 R 9 0 R] /DA (/Cour 10 Tf 0 g) /DR << /Font << /Cour 5 0 R /TiRo 6 0 R >> >> >>".into(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>".into(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >>".into(),
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (notes) /Rect [21 537 591 722] /Ff 4096 /DA (/Cour 10 Tf 0 g) /MK << /BC [0 0 0] >> /P 3 0 R >>"
|
||||
.into(),
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (code) /Q 2 /Rect [21 500 221 520] /DA (/Cour 10 Tf 0 g) /P 3 0 R >>".into(),
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (initials) /Q 2 /Rect [21 470 221 490] /DA (/TiRo 10 Tf 0 g) /P 3 0 R >>".into(),
|
||||
];
|
||||
let mut doc = document(&objs);
|
||||
let value = "The quick brown fox jumps over the lazy dog, then it runs into the field and keeps on going well past the right edge of the box. Every line here is plain mixed-case ASCII text in Courier at 10 points.";
|
||||
set_value(&mut doc, "notes", &FieldValue::Text(value.into())).unwrap();
|
||||
set_value(&mut doc, "code", &FieldValue::Text("iiii".into())).unwrap();
|
||||
set_value(&mut doc, "initials", &FieldValue::Text("iiii".into())).unwrap();
|
||||
let doc = reopen(&doc);
|
||||
let all = fields(&doc);
|
||||
let (_, lines) = appearance_text(&doc, &field(&all, "notes").widgets[0], b"Cour");
|
||||
let lengths: Vec<usize> = lines.iter().map(Vec::len).collect();
|
||||
assert!(lengths.iter().all(|&l| l <= 94), "every line fits the box in Courier: {lengths:?}");
|
||||
assert!(lengths[0] > 85, "and the first is not wrapped early: {lengths:?}");
|
||||
// Where each right-aligned value starts: the box is 200 pt with 2 pt padding, so `iiii` ends
|
||||
// at 198 and starts 24 pt (Courier) or 11.12 pt (Times) before it, against 8.88 in Helvetica.
|
||||
let start = |name: &str| {
|
||||
let w = &field(&all, name).widgets[0];
|
||||
let n = doc.get(w.obj).as_dict().unwrap().get(b"AP").unwrap().as_dict().unwrap().reference(b"N").unwrap();
|
||||
let Object::Stream(s) = &*doc.get(n) else { panic!() };
|
||||
let tm = pdfcraft_content::parse(&s.decoded().unwrap()).ops.into_iter().find(|op| op.is("Tm")).unwrap();
|
||||
tm.operands[4].as_f64().unwrap()
|
||||
};
|
||||
assert!((start("code") - 174.0).abs() < 1e-3, "Courier: {}", start("code"));
|
||||
assert!((start("initials") - 186.88).abs() < 1e-3, "Times: {}", start("initials"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_choices_use_the_unicode_cid_font() {
|
||||
// A choice field whose /DA names a non-embedded CID font with a predefined Unicode CMap
|
||||
|
||||
Reference in New Issue
Block a user