diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index 0cc456cba6..674cfa6c71 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -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(); diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index fb7281be6e..071c62170d 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -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 = 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