diff --git a/crates/pdf/src/font.rs b/crates/pdf/src/font.rs index 480873b0..c2716fc8 100644 --- a/crates/pdf/src/font.rs +++ b/crates/pdf/src/font.rs @@ -57,6 +57,10 @@ pub(crate) struct Font { codespace: Vec<(usize, u32, u32)>, cid_ranges: Vec<(u32, u32, u32)>, pub vertical: bool, + /// Vertical metrics (`/W2`, in text space): CID → (w1y, vx, vy). + cid_vmetrics: HashMap, + /// `/DW2`: default (vy, w1y). + dw2: (f64, f64), cache: RefCell>>>, /// Program glyph advances (font units → text space) for fonts without `/Widths`. has_widths: bool, @@ -194,6 +198,8 @@ struct CMap { codespace: Vec<(usize, u32, u32)>, unicode: HashMap, cids: Vec<(u32, u32, u32)>, + /// `/WMode 1`: vertical writing. + wmode: bool, } fn be(b: &[u8]) -> u32 { @@ -285,6 +291,14 @@ fn parse_cmap(data: &[u8]) -> CMap { } mode = ""; } + "def" if mode.is_empty() => { + if let [.., Obj::Name(k), Obj::Num(v)] = args.as_slice() + && k == "WMode" + { + m.wmode = *v == 1.0; + } + args.clear(); + } _ => { if mode.is_empty() { args.clear(); @@ -315,6 +329,8 @@ impl Font { codespace: vec![], cid_ranges: vec![], vertical: false, + cid_vmetrics: HashMap::new(), + dw2: (0.88, -1.0), cache: RefCell::new(HashMap::new()), has_widths: false, base_name: base_name.clone(), @@ -366,6 +382,37 @@ impl Font { } } } + // Vertical metrics (§9.7.4.3): /DW2 [vy w1y] and /W2 entries + // `c [w1y vx vy …]` or `cfirst clast w1y vx vy`. + let dw2 = file.get(&desc_dict, "DW2").map(|x| file.nums(x)).unwrap_or_default(); + if dw2.len() == 2 { + f.dw2 = (dw2[0] / 1000.0, dw2[1] / 1000.0); + } + if let Some(w) = file.get(&desc_dict, "W2").and_then(Obj::array) { + let num = |i: usize| w.get(i).and_then(|x| file.resolve(x).num()); + let mut i = 0; + while i < w.len() { + let Some(first) = num(i) else { break }; + let first = first.max(0.0) as u32; + match w.get(i + 1).map(|x| file.resolve(x)) { + Some(Obj::Array(list)) => { + let v: Vec = list.iter().filter_map(|x| file.resolve(x).num()).collect(); + for (k, t) in v.chunks_exact(3).enumerate() { + f.cid_vmetrics.insert(first + k as u32, (t[0] / 1000.0, t[1] / 1000.0, t[2] / 1000.0)); + } + i += 2; + } + Some(Obj::Num(last)) => { + let (Some(w1), Some(vx), Some(vy)) = (num(i + 2), num(i + 3), num(i + 4)) else { break }; + for c in first..=(last.max(0.0) as u32).min(first + 65535) { + f.cid_vmetrics.insert(c, (w1 / 1000.0, vx / 1000.0, vy / 1000.0)); + } + i += 5; + } + _ => break, + } + } + } f.has_widths = true; // Encoding CMap. match file.get(d, "Encoding") { @@ -378,6 +425,7 @@ impl Font { Some(Obj::Stream(sd, raw)) => { if let Some(data) = decode_stream(file, sd, raw) { let cm = parse_cmap(&data); + f.vertical = cm.wmode || file.get_num(sd, "WMode") == Some(1.0); f.codespace = cm.codespace; f.cid_ranges = cm.cids; } @@ -530,6 +578,17 @@ impl Font { self.cid_ranges.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).map(|(lo, _, c)| c + (code - lo)).unwrap_or(0) } + /// Vertical metrics of a code in text space (before the font size): the vertical + /// displacement `w1y` (negative: downwards) and the position vector `(vx, vy)` from the + /// horizontal origin to the vertical origin (§9.7.4.3; defaults from `/DW2`, `vx` half the + /// horizontal advance). + pub fn vmetrics(&self, code: u32) -> (f64, f64, f64) { + match self.cid_vmetrics.get(&self.cid(code)) { + Some(m) => *m, + None => (self.dw2.1, self.width(code) / 2.0, self.dw2.0), + } + } + /// The advance of a code in text space (before the font size). pub fn width(&self, code: u32) -> f64 { if self.composite { diff --git a/crates/pdf/src/page.rs b/crates/pdf/src/page.rs index b4a7799d..c754c1dd 100644 --- a/crates/pdf/src/page.rs +++ b/crates/pdf/src/page.rs @@ -1047,7 +1047,8 @@ impl<'a> Interp<'a> { for (code, nbytes) in font.codes(s) { let w0 = font.width(code); let trm = self.tm * Affine::new([fs * th, 0.0, 0.0, fs, 0.0, rise]); - let trm = if font.vertical { trm * Affine::translate((-w0 / 2.0, -0.88)) } else { trm }; + let (w1y, vx, vy) = if font.vertical { font.vmetrics(code) } else { (0.0, 0.0, 0.0) }; + let trm = if font.vertical { trm * Affine::translate((-vx, -vy)) } else { trm }; if let Some((proc_, t3res)) = font.type3_proc(code) { let proc_ = proc_.to_vec(); let r = t3res.cloned().unwrap_or_else(|| res.clone()); @@ -1060,7 +1061,7 @@ impl<'a> Interp<'a> { text.push_str(&font.unicode(code)); let tw = if nbytes == 1 && code == 32 { self.gs.word_spacing } else { 0.0 }; if font.vertical { - self.tm *= Affine::translate((0.0, -(fs + self.gs.char_spacing + tw))); + self.tm *= Affine::translate((0.0, w1y * fs - self.gs.char_spacing - tw)); } else { self.tm *= Affine::translate(((w0 * fs + self.gs.char_spacing + tw) * th, 0.0)); } diff --git a/crates/pdf/src/tests_groups.rs b/crates/pdf/src/tests_groups.rs index 3e27dd7c..ce928139 100644 --- a/crates/pdf/src/tests_groups.rs +++ b/crates/pdf/src/tests_groups.rs @@ -97,3 +97,57 @@ fn soft_mask_applies_to_the_masked_group_result() { let p = img.get(75, 50); assert!((p[3] - 0.5).abs() < 0.02 && p[0] < 0.02 && (p[2] - 0.5).abs() < 0.02, "{p:?}"); } + +/// The ink bounding box `(x0, y0, x1, y1)` of a render (pixels with alpha > 0.5). +fn ink_box(img: &effectcraft_raster::Image) -> (i64, i64, i64, i64) { + let (mut x0, mut y0, mut x1, mut y1) = (i64::MAX, i64::MAX, i64::MIN, i64::MIN); + for y in 0..img.height as i64 { + for x in 0..img.width as i64 { + if img.get(x, y)[3] > 0.5 { + (x0, y0, x1, y1) = (x0.min(x), y0.min(y), x1.max(x), y1.max(y)); + } + } + } + (x0, y0, x1, y1) +} + +#[test] +fn vertical_metrics_w2_and_dw2() { + use skrifa::MetadataProvider; + let inter = effectcraft_text::fonts::INTER_REGULAR.to_vec(); + let font = skrifa::FontRef::new(&inter).unwrap(); + let h = font.charmap().map('H').unwrap().to_u32(); + let doc = |desc_extra: &str| { + let bytes = page_pdf( + &format!("BT /F1 20 Tf 100 95 Td <{h:04X}{h:04X}> Tj ET"), + "/Font << /F1 10 0 R >>", + vec![ + (10, "<< /Type /Font /Subtype /Type0 /BaseFont /Inter /Encoding /Identity-V /DescendantFonts [13 0 R] >>".into(), None), + ( + 13, + format!( + "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /Inter /CIDToGIDMap /Identity /W [{h} [700]] {desc_extra} /FontDescriptor 11 0 R >>" + ), + None, + ), + (11, "<< /Type /FontDescriptor /Flags 32 /FontFile2 12 0 R >>".into(), None), + (12, "<< >>".into(), Some(inter.clone())), + ], + ); + let d = parse(&bytes).unwrap(); + assert!(d.skipped.is_empty(), "{:?}", d.skipped); + ink_box(&render(&d)) + }; + // Defaults: an advance of 1 em down; the vertical origin (350, 880) from the horizontal one. + let base = doc(""); + // /W2: a 2 em advance and vx 250. + let w2 = doc(&format!("/W2 [{h} [-2000 250 880]]")); + assert_eq!(w2.1, base.1, "the first glyph's top stays: {base:?} {w2:?}"); + assert!(((w2.3 - base.3) - 20).abs() <= 1, "second glyph 20 pt lower: {base:?} {w2:?}"); + assert!(((w2.0 - base.0) - 2).abs() <= 1, "vx 250 vs 350 → 2 pt right: {base:?} {w2:?}"); + // The range form gives the same; /DW2 with vy 780 draws the glyphs 2 pt higher. + let range = doc(&format!("/W2 [{h} {h} -2000 250 880]")); + assert_eq!(range, w2); + let dw2 = doc("/DW2 [780 -1000]"); + assert!(((base.1 - dw2.1) - 2).abs() <= 1, "vy 780 → 2 pt higher: {base:?} {dw2:?}"); +}