M13.12: pdf /W2 and /DW2 vertical metrics, CMap WMode
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d15b69c734
commit
3d536cfe5a
|
|
@ -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<u32, (f64, f64, f64)>,
|
||||
/// `/DW2`: default (vy, w1y).
|
||||
dw2: (f64, f64),
|
||||
cache: RefCell<HashMap<u32, Option<Arc<BezPath>>>>,
|
||||
/// 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<u32, String>,
|
||||
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<f64> = 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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:?}");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue