Fonts: measure a remapped standard-14 code by the glyph it draws

A standard-14 font with no /Widths was measured by code in the WinAnsi-indexed
table, so a /Differences remap took the width of the glyph WinAnsi gives that
code: Helvetica with [65 /W 87 /A] laid out "AAAA" four W's wide. The code is
now looked up through the font's encoding first; /Widths still go by code, and
Symbol, ZapfDingbats and glyphs WinAnsi has no code for keep the code itself.
This commit is contained in:
Marco Arnulfo
2026-10-18 13:21:34 +02:00
parent b2150f3ffd
commit a61bd92fdd
2 changed files with 78 additions and 1 deletions
+43 -1
View File
@@ -766,7 +766,7 @@ impl Metrics {
match self.std14 {
// A font with no /Widths is one of the standard 14; use its published metrics.
// Codes the font's encoding leaves undefined fall back to /MissingWidth.
Some(f) => match u8::try_from(code).map(|c| f.width(c)).unwrap_or(0.0) {
Some(f) => match u8::try_from(self.std14_code(f, code)).map(|c| f.width(c)).unwrap_or(0.0) {
0.0 => self.default * self.scale,
w => w / 1000.0,
},
@@ -774,6 +774,17 @@ impl Metrics {
}
}
/// Where the standard-14 table, indexed by WinAnsi, holds the glyph `code` draws in this font:
/// its encoding (base and `/Differences`) names the glyph, so a remapped code is measured as
/// the glyph it now shows (#2894). Symbol and ZapfDingbats are indexed by their own encodings,
/// and a glyph WinAnsi has no code for keeps the code itself.
fn std14_code(&self, face: crate::Std14, code: u32) -> u32 {
if matches!(face, crate::Std14::Symbol | crate::Std14::ZapfDingbats) {
return code;
}
self.unicode.encoding.get(&code).and_then(|t| only_char(t)).and_then(crate::win_ansi_byte).map_or(code, u32::from)
}
/// Word spacing applies to the single-byte code 32 (§9.3.3).
pub fn is_space(&self, code: u32, len: usize) -> bool {
code == 32 && len == 1
@@ -1325,6 +1336,37 @@ mod tests {
assert_eq!(m.encode("Ω").as_deref(), Some(&b"B"[..]));
}
/// A standard-14 font with no `/Widths` is measured by the glyph each code draws, not by the
/// glyph WinAnsi gives that code (#2894); `/Widths`, when present, still go by code.
#[test]
fn standard_fonts_without_widths_measure_the_glyph_their_differences_name() {
let mut doc = Document::new_empty();
let mut enc = Dict::new();
enc.set(b"BaseEncoding".to_vec(), Object::name("WinAnsiEncoding"));
let diffs =
vec![Object::Int(65), Object::name("W"), Object::Int(87), Object::name("A"), Object::name("uni03A9"), Object::name("not.a.glyph")];
enc.set(b"Differences".to_vec(), Object::Array(diffs));
let f = font(
&mut doc,
vec![("Subtype", Object::name("Type1")), ("BaseFont", Object::name("Helvetica")), ("Encoding", Object::Dict(enc.clone()))],
);
let m = Metrics::from_dict(&doc, &f);
assert_eq!((m.width(65), m.width(87)), (0.944, 0.667), "A and W swap their widths");
// Omega has no WinAnsi code and the last name reads as nothing: both keep their code's width.
assert_eq!((m.width(88), m.width(89)), (0.667, 0.667));
let f = font(
&mut doc,
vec![
("Subtype", Object::name("Type1")),
("BaseFont", Object::name("Helvetica")),
("FirstChar", Object::Int(87)),
("Widths", Object::Array(vec![Object::Int(500)])),
("Encoding", Object::Dict(enc)),
],
);
assert_eq!(Metrics::from_dict(&doc, &f).width(87), 0.5, "/Widths go by code");
}
#[test]
fn subset_fonts_only_encode_the_glyphs_they_have() {
let mut doc = Document::new_empty();
+35
View File
@@ -405,6 +405,41 @@ fn reused_standard_fonts_wrap_and_align_with_their_own_widths() {
assert!((start("initials") - 186.88).abs() < 1e-3, "Times: {}", start("initials"));
}
/// A standard-14 font whose `/Differences` swap A and W draws `AAAA` with code 87, so it is
/// measured as four A's (0.667 em), not as the W that code has in WinAnsi. The same remap with
/// `/Widths` takes the width the font gives code 87, here 0.5 em.
#[test]
fn remapped_standard_fonts_are_measured_by_the_glyph_each_code_draws() {
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] >>".into(),
"<< /Fields [7 0 R 8 0 R] /DA (/Dif 10 Tf 0 g) /DR << /Font << /Dif 5 0 R /DifW 6 0 R >> >> >>".into(),
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding << /BaseEncoding /WinAnsiEncoding /Differences [65 /W 87 /A] >> >>"
.into(),
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /FirstChar 87 /LastChar 87 /Widths [500] /Encoding << /BaseEncoding /WinAnsiEncoding /Differences [65 /W 87 /A] >> >>"
.into(),
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (std) /Q 2 /Rect [21 500 221 520] /DA (/Dif 10 Tf 0 g) /P 3 0 R >>".into(),
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (widths) /Q 2 /Rect [21 470 221 490] /DA (/DifW 10 Tf 0 g) /P 3 0 R >>".into(),
];
let mut doc = document(&objs);
set_value(&mut doc, "std", &FieldValue::Text("AAAA".into())).unwrap();
set_value(&mut doc, "widths", &FieldValue::Text("AAAA".into())).unwrap();
let doc = reopen(&doc);
let all = fields(&doc);
assert_eq!(appearance_text(&doc, &field(&all, "std").widgets[0], b"Dif").1, vec![b"WWWW".to_vec()], "A is drawn by code 87");
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()
};
// The text ends at 198 (200 pt box, 2 pt padding): 26.68 pt of A's, or 20 pt by /Widths.
assert!((start("std") - 171.32).abs() < 1e-3, "no /Widths: {}", start("std"));
assert!((start("widths") - 178.0).abs() < 1e-3, "/Widths: {}", start("widths"));
}
#[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