use std::path::{Path, PathBuf};
use std::process::Command;
use rustyfi_backend::{FontKey, FontMetrics, HorzBox, Length, Page, PageGeometry, PlacedLine, PureHorzBox, VertVariantPolicy};
use rustyfi_lang::value::Value;
use rustyfi_lang::{elaborate, eval, primitives, typecheck, CompileError};
use rustyfi_pdf::{render_pdf_ttf, TtfFontStore};
use ttf_parser::Face;
fn expected_at_least_gid(face: &Face, c: char, size: Length, target: Length) -> u16 {
let gid = face.glyph_index(c).expect("cmap has the char");
let construction = face
.tables()
.math
.expect("MATH table")
.variants
.expect("MathVariants subtable")
.vertical_constructions
.get(gid)
.expect("char has a vertical GlyphConstruction");
let n = construction.variants.len();
let upem = face.units_per_em() as f64;
let min_du = (target.0 / size.0) * upem;
let mut chosen = construction
.variants
.get(n - 1)
.expect("at least one variant record")
.variant_glyph
.0;
for i in 0..n {
let v = construction.variants.get(i).expect("index < n");
if v.advance_measurement as f64 >= min_du {
chosen = v.variant_glyph.0;
break;
}
}
chosen
}
fn find_family(family: &str, fallbacks: &[&str]) -> Option<PathBuf> {
if let Ok(output) = Command::new("fc-match")
.args(["--format=%{file}", family])
.output()
{
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty()
&& Path::new(&path).is_file()
&& (path.contains("Math") || path.contains("math"))
{
return Some(PathBuf::from(path));
}
}
}
for candidate in fallbacks {
if Path::new(candidate).is_file() {
return Some(PathBuf::from(candidate));
}
}
None
}
fn find_dejavu_math() -> Option<PathBuf> {
find_family(
"DejaVu Math TeX Gyre",
&[
"/usr/share/texmf/fonts/opentype/public/dejavu-otf/DejaVuMathTeXGyre.ttf",
"/usr/share/fonts/opentype/dejavu-math-tex-gyre/DejaVuMathTeXGyre.ttf",
"/usr/share/fonts/truetype/tex-gyre/texgyredejavu-math.otf",
],
)
}
fn find_noto_math() -> Option<PathBuf> {
find_family(
"Noto Sans Math",
&[
"/usr/share/fonts/noto/NotoSansMath-Regular.ttf",
"/usr/share/fonts/truetype/noto/NotoSansMath-Regular.ttf",
"/usr/share/fonts/opentype/noto/NotoSansMath-Regular.ttf",
"/usr/share/fonts/OTF/NotoSansMath-Regular.otf",
"/usr/share/fonts/noto-fonts/NotoSansMath-Regular.ttf",
"/run/current-system/sw/share/fonts/truetype/NotoSansMath-Regular.ttf",
],
)
}
fn find_math_font() -> Option<PathBuf> {
let is_cff = |p: &Path| {
std::fs::read(p)
.map(|b| b.starts_with(b"OTTO"))
.unwrap_or(false)
};
let bundled_lmmath = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../lib-rustyfi/dist/fonts/latinmodern-math.otf");
if bundled_lmmath.is_file() && is_cff(&bundled_lmmath) {
return Some(bundled_lmmath);
}
for family in ["Noto Sans Math", "DejaVu Math TeX Gyre"] {
if let Some(p) = find_family(family, &[]) {
if is_cff(&p) {
return Some(p);
}
}
}
find_dejavu_math()
.or_else(find_noto_math)
.filter(|p| is_cff(p))
}
macro_rules! need_font {
($finder:expr, $label:expr) => {
match $finder {
Some(path) => path,
None => {
eprintln!(
"skipping: no {} font found on this system (tried fc-match \
and common nix/distro paths)",
$label
);
return;
}
}
};
}
fn assert_vertical_variant_unit(path: &Path) {
let store = TtfFontStore::load(path, None, None).expect("load math font");
let face = store.face(FontKey(0)).expect("parse face");
let size = Length::pt(12.0);
let upem = face.units_per_em() as f64;
let sum_gid = face
.glyph_index('∑')
.unwrap_or_else(|| panic!("{path:?}: cmap has no ∑"));
let math_table = face
.tables()
.math
.unwrap_or_else(|| panic!("{path:?}: no MATH table"));
let variants_table = math_table
.variants
.unwrap_or_else(|| panic!("{path:?}: no MathVariants subtable"));
let sum_construction = variants_table
.vertical_constructions
.get(sum_gid)
.unwrap_or_else(|| panic!("{path:?}: ∑ has no vertical GlyphConstruction"));
let n = sum_construction.variants.len();
assert!(
n >= 1,
"{path:?}: expected at least one prepared variant record for ∑, got {n}"
);
let sum_variant_gids: Vec<u16> = sum_construction
.variants
.into_iter()
.map(|v| v.variant_glyph.0)
.collect();
let base_rec = sum_construction
.variants
.get(0)
.expect("record[0] must exist since n >= 1");
let base_bbox = face
.glyph_bounding_box(base_rec.variant_glyph)
.unwrap_or_else(|| panic!("{path:?}: base ∑ record has no bbox"));
let base_h = size.0 * (base_bbox.y_max.max(0) as f64) / upem;
let base_d = size.0 * ((-(base_bbox.y_min.min(0) as i32)) as f64) / upem;
let big = store
.math_vertical_variant(FontKey(0), '∑', size, VertVariantPolicy::BigOp)
.unwrap_or_else(|| panic!("{path:?}: expected Some for BigOp('∑')"));
assert_ne!(
big.gid, sum_gid.0,
"{path:?}: BigOp variant gid should differ from the plain cmap gid"
);
assert!(
sum_variant_gids.contains(&big.gid),
"{path:?}: BigOp gid {} not among ∑'s enumerated variant records {sum_variant_gids:?}",
big.gid
);
assert!(
big.height.0 + big.depth.0 > base_h + base_d,
"{path:?}: BigOp variant (h+d={}) should exceed record[0]'s (h+d={})",
big.height.0 + big.depth.0,
base_h + base_d
);
let paren_gid = face
.glyph_index('(')
.unwrap_or_else(|| panic!("{path:?}: cmap has no '('"));
let paren_construction = variants_table
.vertical_constructions
.get(paren_gid)
.unwrap_or_else(|| panic!("{path:?}: '(' has no vertical GlyphConstruction"));
let record0_gid = paren_construction
.variants
.get(0)
.unwrap_or_else(|| panic!("{path:?}: '(' construction has no record[0]"))
.variant_glyph
.0;
let target = size * 2.0;
let expected_gid = expected_at_least_gid(&face, '(', size, target);
assert_ne!(
expected_gid, record0_gid,
"{path:?}: test target {target:?} should force a non-record[0] selection \
(pick a larger target if this ever fails)"
);
let at_least = store
.math_vertical_variant(FontKey(0), '(', size, VertVariantPolicy::AtLeast(target))
.unwrap_or_else(|| panic!("{path:?}: expected Some for AtLeast(2*size) on '('"));
assert_eq!(
at_least.gid, expected_gid,
"{path:?}: AtLeast(2*size) should select the smallest record whose \
advance_measurement covers 2*size (independently computed gid {expected_gid}), got {}",
at_least.gid
);
let expected_advance = size
* (face
.glyph_hor_advance(ttf_parser::GlyphId(at_least.gid))
.expect("selected variant has an hmtx advance") as f64
/ upem);
assert!(
(at_least.advance.0 - expected_advance.0).abs() < 1e-6,
"{path:?}: `.advance` should be the selected variant glyph's own hmtx advance \
({expected_advance:?}), got {:?}",
at_least.advance
);
let tiny = Length::pt(1e-6);
let at_tiny = store
.math_vertical_variant(FontKey(0), '(', size, VertVariantPolicy::AtLeast(tiny))
.unwrap_or_else(|| panic!("{path:?}: expected Some for AtLeast(tiny) on '('"));
assert_eq!(
at_tiny.gid, record0_gid,
"{path:?}: AtLeast(tiny) should return record[0]'s gid ({record0_gid}), got {}",
at_tiny.gid
);
let none = store.math_vertical_variant(FontKey(0), 'a', size, VertVariantPolicy::BigOp);
assert!(
none.is_none(),
"{path:?}: expected None for 'a' (no vertical construction), got {none:?}"
);
}
struct ParenAssembly {
part_gids: Vec<u16>,
extender_gids: Vec<u16>,
non_extender_gids: Vec<u16>,
largest_variant_advance_du: f64,
upem: f64,
}
fn read_paren_assembly(face: &Face, c: char) -> ParenAssembly {
let gid = face.glyph_index(c).expect("cmap has the char");
let variants = face
.tables()
.math
.expect("MATH table")
.variants
.expect("MathVariants subtable");
let construction = variants
.vertical_constructions
.get(gid)
.expect("char has a vertical GlyphConstruction");
let assembly = construction
.assembly
.expect("this delimiter has a GlyphAssembly (both DejaVu Math & Noto Math do)");
let mut part_gids = Vec::new();
let mut extender_gids = Vec::new();
let mut non_extender_gids = Vec::new();
for p in assembly.parts {
part_gids.push(p.glyph_id.0);
if p.part_flags.extender() {
extender_gids.push(p.glyph_id.0);
} else {
non_extender_gids.push(p.glyph_id.0);
}
}
let n = construction.variants.len();
let largest_variant_advance_du = if n == 0 {
0.0
} else {
construction
.variants
.get(n - 1)
.expect("largest variant")
.advance_measurement as f64
};
ParenAssembly {
part_gids,
extender_gids,
non_extender_gids,
largest_variant_advance_du,
upem: face.units_per_em() as f64,
}
}
fn assert_vertical_assembly_unit(path: &Path) {
let store = TtfFontStore::load(path, None, None).expect("load math font");
let face = store.face(FontKey(0)).expect("parse face");
let size = Length::pt(12.0);
let asm = read_paren_assembly(&face, '(');
assert!(
!asm.extender_gids.is_empty(),
"{path:?}: '(' assembly should have at least one extender part"
);
assert!(
!asm.non_extender_gids.is_empty(),
"{path:?}: '(' assembly should have at least one non-extender (hook) part"
);
let largest_pt = size.0 * asm.largest_variant_advance_du / asm.upem;
let target = Length::pt(largest_pt * 6.0);
let parts = store
.math_vertical_assembly(FontKey(0), '(', size, target)
.unwrap_or_else(|| panic!("{path:?}: expected Some assembly for a tall '('"));
for (gid, _dy, _adv) in &parts {
assert!(
asm.part_gids.contains(gid),
"{path:?}: placed part gid {gid} is not among '('s assembly parts {:?}",
asm.part_gids
);
}
assert!(
parts.len() > asm.part_gids.len(),
"{path:?}: a very tall '(' must emit MORE placed parts ({}) than the {} distinct \
assembly parts — i.e. the extender is repeated",
parts.len(),
asm.part_gids.len()
);
let placed_gids: Vec<u16> = parts.iter().map(|(g, _, _)| *g).collect();
for hook in &asm.non_extender_gids {
let count = placed_gids.iter().filter(|g| *g == hook).count();
assert_eq!(
count, 1,
"{path:?}: non-extender hook gid {hook} should be placed exactly once, got {count}"
);
}
let repeated_extender = asm
.extender_gids
.iter()
.any(|ext| placed_gids.iter().filter(|g| *g == ext).count() > 1);
assert!(
repeated_extender,
"{path:?}: a very tall '(' must repeat an extender part; placed gids = {placed_gids:?}"
);
let total = parts
.last()
.map(|(_, dy, adv)| dy.0 + adv.0)
.expect("at least one part");
assert!(
total >= target.0 - 1e-6,
"{path:?}: stacked assembly extent ({total}) should cover target ({})",
target.0
);
let mut prev_dy = f64::NEG_INFINITY;
for (_, dy, _) in &parts {
assert!(
dy.0 >= prev_dy - 1e-6,
"{path:?}: parts must be placed bottom-to-top (non-decreasing dy)"
);
prev_dy = dy.0;
}
assert!(
store
.math_vertical_assembly(FontKey(0), 'a', size, target)
.is_none(),
"{path:?}: 'a' has no assembly -> None"
);
}
#[test]
fn vertical_assembly_unit_dejavu() {
let path = need_font!(find_dejavu_math(), "DejaVu Math TeX Gyre");
assert_vertical_assembly_unit(&path);
}
#[test]
fn vertical_assembly_unit_noto() {
let path = need_font!(find_noto_math(), "Noto Sans Math");
assert_vertical_assembly_unit(&path);
}
#[test]
fn vertical_variant_unit_dejavu() {
let path = need_font!(find_dejavu_math(), "DejaVu Math TeX Gyre");
assert_vertical_variant_unit(&path);
}
#[test]
fn vertical_variant_unit_noto() {
let path = need_font!(find_noto_math(), "Noto Sans Math");
assert_vertical_variant_unit(&path);
}
fn run_math(src: &str, metrics: &dyn FontMetrics) -> Result<Value, CompileError> {
let file = rustyfi_syntax::parse_file(src)?;
let env = primitives::base_env();
let store = rustyfi_lang::symbol::SymbolStore::new();
let scope = elaborate::Scope::new(&store, env.names());
let program = elaborate::elaborate_program(&file, &scope)?;
typecheck::typecheck(&program)?;
let mut interp = eval::Interp::new(metrics);
Ok(interp.eval(&env, &rustyfi_lang::ast::debrand(&program.body, &store))?)
}
fn with_ctx(body: &str) -> String {
format!(
"let-inline ctx \\dummy m = inline-nil\n\
in\n\
let ctx = get-initial-context 200pt (command \\dummy) in\n\
{body}"
)
}
fn math_box(v: Value) -> PureHorzBox {
match v {
Value::InlineBoxes(boxes) => {
assert_eq!(boxes.len(), 1, "expected exactly one box, got {boxes:?}");
match boxes.into_iter().next().unwrap() {
HorzBox::Pure(m @ PureHorzBox::Math { .. }) => m,
other => panic!("expected a PureHorzBox::Math, got {other:?}"),
}
}
other => panic!("expected inline-boxes, got {other:?}"),
}
}
fn as_math_parts(bx: PureHorzBox) -> (Length, Length, Length, Vec<rustyfi_backend::MathGlyph>) {
match bx {
PureHorzBox::Math {
width,
height,
depth,
glyphs,
..
} => (width, height, depth, glyphs),
other => panic!("expected PureHorzBox::Math, got {other:?}"),
}
}
fn page_for(bx: PureHorzBox, geometry: &PageGeometry) -> Page {
Page {
body_lines: usize::MAX,
lines: vec![PlacedLine {
x: geometry.text_origin.0,
baseline_y: geometry.text_origin.1 + Length::pt(60.0),
contents: vec![(Length::ZERO, bx)],
}],
}
}
fn pdf_str_repr(bytes: &[u8]) -> Vec<u8> {
if bytes.iter().all(|b| b.is_ascii()) {
let is_balanced = {
let mut depth = 0i32;
let mut ok = true;
for &b in bytes {
match b {
b'(' => depth += 1,
b')' => {
if depth > 0 {
depth -= 1;
} else {
ok = false;
}
}
_ => {}
}
}
ok && depth == 0
};
let mut out = vec![b'('];
let mut balanced_flag: Option<bool> = None;
for &byte in bytes {
match byte {
b'(' | b')' => {
let bal =
*balanced_flag.get_or_insert_with(|| byte != b')' && is_balanced);
if !bal {
out.push(b'\\');
}
out.push(byte);
}
b'\\' => out.extend(b"\\\\"),
b' '..=b'~' => out.push(byte),
b'\n' => out.extend(b"\\n"),
b'\r' => out.extend(b"\\r"),
b'\t' => out.extend(b"\\t"),
0x08 => out.extend(b"\\b"),
0x0c => out.extend(b"\\f"),
_ => {
out.push(b'\\');
out.push(b'0' + (byte >> 6));
out.push(b'0' + ((byte >> 3) & 7));
out.push(b'0' + (byte & 7));
}
}
}
out.push(b')');
out
} else {
let mut out = vec![b'<'];
let hex = |b: u8| -> u8 {
if b < 10 {
b'0' + b
} else {
b'A' + (b - 10)
}
};
for &byte in bytes {
out.push(hex(byte >> 4));
out.push(hex(byte & 0xF));
}
out.push(b'>');
out
}
}
fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || needle.len() > haystack.len() {
return false;
}
haystack.windows(needle.len()).any(|w| w == needle)
}
fn extract_streams(pdf: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut i = 0;
while let Some(rel) = find_subslice(&pdf[i..], b"stream") {
let mut start = i + rel + b"stream".len();
if pdf.get(start) == Some(&b'\r') {
start += 1;
}
if pdf.get(start) == Some(&b'\n') {
start += 1;
}
match find_subslice(&pdf[start..], b"endstream") {
Some(end_rel) => {
let end = start + end_rel;
out.push(&pdf[start..end]);
i = end + b"endstream".len();
}
None => break,
}
}
out
}
fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || needle.len() > haystack.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
fn content_stream(pdf: &[u8]) -> Vec<u8> {
let streams = extract_streams(pdf);
let content = streams
.iter()
.min_by_key(|s| s.len())
.expect("expected at least one PDF stream object");
assert!(
streams.len() >= 2,
"expected >= 2 stream objects (content + embedded font), got {}",
streams.len()
);
assert!(
contains_subslice(content, b"BT") && contains_subslice(content, b"Tf"),
"the shortest stream object doesn't look like a content stream \
(missing BT/Tf) -- got {} bytes: {:?}",
content.len(),
String::from_utf8_lossy(content)
);
content.to_vec()
}
fn approx(a: Length, b: Length, tol: f64) -> bool {
(a.0 - b.0).abs() < tol
}
fn original_gids_used(face: &Face, glyphs: &[rustyfi_backend::MathGlyph]) -> Vec<u16> {
glyphs
.iter()
.flat_map(|g| match g.gid {
Some(gid) => vec![gid],
None => g
.text
.chars()
.map(|c| face.glyph_index(c).expect("MathGlyph.text char has a gid").0)
.collect(),
})
.collect()
}
fn expected_cid(font_bytes: &[u8], used_gids: &[u16], original_gid: u16) -> u16 {
let remapper = subsetter::GlyphRemapper::new_from_glyphs_sorted(used_gids);
match subsetter::subset(font_bytes, 0, &remapper) {
Ok(_) => remapper.get(original_gid).unwrap_or(original_gid),
Err(_) => original_gid,
}
}
#[test]
fn big_char_sum_variant_grows_and_emits_variant_gid() {
let path = need_font!(find_math_font(), "MATH");
let store = TtfFontStore::load(&path, None, None).expect("load math font");
let face = store.face(FontKey(0)).expect("parse face");
let size = Length::pt(12.0);
let base_gid = face.glyph_index('∑').expect("cmap has ∑");
let construction = face
.tables()
.math
.expect("MATH table")
.variants
.expect("MathVariants")
.vertical_constructions
.get(base_gid)
.expect("∑ has a vertical GlyphConstruction");
let n = construction.variants.len();
let expected_variant = construction
.variants
.get(if n >= 2 { 1 } else { 0 })
.expect("record[1] (or [0])")
.variant_glyph;
let base_src = with_ctx("embed-math ctx (math-char MathOp `∑`)");
let base_v = run_math(&base_src, &store).expect("plain ∑ should compile");
let (_, _base_h, _base_d, base_glyphs) = as_math_parts(math_box(base_v));
assert_eq!(base_glyphs.len(), 1);
assert_eq!(
base_glyphs[0].gid, None,
"plain (non-big) ∑ should stay on the cmap path (gid: None)"
);
let big_src = with_ctx("embed-math ctx (math-big-char MathOp `∑`)");
let big_v = run_math(&big_src, &store).expect("math-big-char MathOp `∑` should compile");
let (_, big_h, big_d, big_glyphs) = as_math_parts(math_box(big_v));
assert_eq!(big_glyphs.len(), 1, "expected exactly 1 glyph, got {big_glyphs:?}");
assert_eq!(
big_glyphs[0].gid,
Some(expected_variant.0),
"expected the BigOp policy's record[1] (or [0] if only one) variant gid"
);
assert_ne!(
big_glyphs[0].gid,
Some(base_gid.0),
"the big-char glyph must NOT be the plain base gid"
);
let base_bbox = face
.glyph_bounding_box(base_gid)
.expect("base ∑ glyph has a bbox");
let base_ink_h = size.0 * (base_bbox.y_max.max(0) as f64) / (face.units_per_em() as f64);
let base_ink_d =
size.0 * ((-(base_bbox.y_min.min(0) as i32)) as f64) / (face.units_per_em() as f64);
let base_ink_extent = base_ink_h + base_ink_d;
let big_extent = big_h.0 + big_d.0;
assert!(
big_extent > base_ink_extent * 1.2,
"expected the big ∑ variant's height+depth ({big_extent}) to exceed the base \
(non-variant) glyph's own ink height+depth ({base_ink_extent}) by more than 20%"
);
let geometry = PageGeometry::default();
let e2e_box = math_box(run_math(&big_src, &store).unwrap());
let (_, _, _, e2e_glyphs) = as_math_parts(e2e_box.clone());
let used_gids = original_gids_used(&face, &e2e_glyphs);
let page = page_for(e2e_box, &geometry);
let pdf_bytes = render_pdf_ttf(&geometry, &[page], &store, &[]).expect("render");
assert!(pdf_bytes.starts_with(b"%PDF-"));
let content = content_stream(&pdf_bytes);
let font_bytes = std::fs::read(&path).expect("read font file for subsetter cross-check");
let variant_cid = expected_cid(&font_bytes, &used_gids, expected_variant.0);
let variant_bytes = variant_cid.to_be_bytes();
let variant_repr = pdf_str_repr(&variant_bytes);
assert!(
contains_subslice(&content, &variant_repr),
"expected the content stream to contain the variant gid's (remapped) Tj operand \
{variant_repr:02x?} ({:?}); content stream = {:?}",
String::from_utf8_lossy(&variant_repr),
String::from_utf8_lossy(&content)
);
let base_bytes = base_gid.0.to_be_bytes();
let base_repr = pdf_str_repr(&base_bytes);
if base_repr != variant_repr {
assert!(
!contains_subslice(&content, &base_repr),
"the content stream should NOT contain the plain base gid's Tj \
operand {base_repr:02x?}; content stream = {:?}",
String::from_utf8_lossy(&content)
);
}
}
const DUMMY_PAREN: &str = "(fun hgt dpt hgtaxis fontsize color -> \
(fun s -> (inline-nil, (fun x -> x))) (string-sub `x` 9 9))";
#[test]
fn paren_stretches_around_tall_inner_and_short_inner_stays_record0() {
let path = need_font!(find_math_font(), "MATH");
let store = TtfFontStore::load(&path, None, None).expect("load math font");
let face = store.face(FontKey(0)).expect("parse face");
let size = Length::pt(12.0);
let record0_gid_of = |c: char| -> u16 {
let gid = face.glyph_index(c).expect("cmap has the char");
face.tables()
.math
.expect("MATH table")
.variants
.expect("MathVariants")
.vertical_constructions
.get(gid)
.expect("char has a vertical GlyphConstruction")
.variants
.get(0)
.expect("record[0]")
.variant_glyph
.0
};
let open_record0_gid = record0_gid_of('(');
let close_record0_gid = record0_gid_of(')');
let mc = store
.math_constants(FontKey(0))
.expect("MATH font should expose MathConstants");
let axis = size * mc.axis_height;
let short_src = with_ctx(&format!(
"embed-math ctx (math-paren {DUMMY_PAREN} {DUMMY_PAREN} ${{}})"
));
let short_v =
run_math(&short_src, &store).expect("math-paren over an empty inner should compile");
let (_, _, _, short_glyphs) = as_math_parts(math_box(short_v));
assert_eq!(
short_glyphs.len(),
2,
"expected '(', ')' -- 2 glyphs (empty inner), got {short_glyphs:?}"
);
assert_eq!(
short_glyphs[0].gid,
Some(open_record0_gid),
"short inner: the '(' should stay at record[0] (gid {open_record0_gid})"
);
assert_eq!(
short_glyphs[1].gid,
Some(close_record0_gid),
"short inner: the ')' should stay at record[0] (gid {close_record0_gid})"
);
let tall_inner_src = "(math-big-char MathOp `∑`)";
let tall_src = with_ctx(&format!(
"embed-math ctx (math-paren {DUMMY_PAREN} {DUMMY_PAREN} {tall_inner_src})"
));
let tall_v =
run_math(&tall_src, &store).expect("math-paren over a tall inner should compile");
let (_, paren_h, paren_d, tall_glyphs) = as_math_parts(math_box(tall_v));
assert_eq!(
tall_glyphs.len(),
3,
"expected '(', ∑, ')' -- 3 glyphs, got {tall_glyphs:?}"
);
let open = &tall_glyphs[0];
let close = &tall_glyphs[2];
assert_ne!(
open.gid,
Some(open_record0_gid),
"tall inner: the '(' should have stretched past record[0] (gid {open_record0_gid}), got {:?}",
open.gid
);
assert_ne!(
close.gid,
Some(close_record0_gid),
"tall inner: the ')' should have stretched past record[0] (gid {close_record0_gid}), got {:?}",
close.gid
);
let inner_alone_src = with_ctx(&format!("embed-math ctx {tall_inner_src}"));
let inner_alone_v =
run_math(&inner_alone_src, &store).expect("the tall inner alone should compile");
let (_, inner_h, inner_d, _) = as_math_parts(math_box(inner_alone_v));
assert!(
paren_h.0 + paren_d.0 >= inner_h.0 + inner_d.0 - 1e-6,
"expected the paren box's height+depth ({}) to cover the tall inner's own \
({}) ",
paren_h.0 + paren_d.0,
inner_h.0 + inner_d.0
);
for (label, g) in [("'('", open), ("')'", close)] {
let expected_dy = axis - (g.height - g.depth) * 0.5;
assert!(
approx(g.dy, expected_dy, 1e-6),
"{label}: expected dy = axis - (h-d)/2 = {expected_dy:?}, got {:?}",
g.dy
);
let placed_bottom = g.dy - g.depth;
let placed_top = g.dy + g.height;
assert!(
placed_bottom.0 <= axis.0 + 1e-6 && placed_top.0 >= axis.0 - 1e-6,
"{label}: placed ink [{:?}, {:?}] should straddle the axis ({axis:?}) -- \
NOT mirrored entirely below the baseline",
placed_bottom,
placed_top
);
}
let geometry = PageGeometry::default();
let e2e_box = math_box(run_math(&tall_src, &store).unwrap());
let (_, _, _, e2e_glyphs) = as_math_parts(e2e_box.clone());
let used_gids = original_gids_used(&face, &e2e_glyphs);
let page = page_for(e2e_box, &geometry);
let pdf_bytes = render_pdf_ttf(&geometry, &[page], &store, &[]).expect("render");
assert!(pdf_bytes.starts_with(b"%PDF-"));
let content = content_stream(&pdf_bytes);
let open_gid = open.gid.expect("open paren has a variant gid");
let font_bytes = std::fs::read(&path).expect("read font file for subsetter cross-check");
let open_cid = expected_cid(&font_bytes, &used_gids, open_gid);
let open_repr = pdf_str_repr(&open_cid.to_be_bytes());
assert!(
contains_subslice(&content, &open_repr),
"expected the content stream to contain the '(' variant gid's (remapped) Tj operand \
{open_repr:02x?}; content stream = {:?}",
String::from_utf8_lossy(&content)
);
}
fn paren_assembly_and_variant_gids(face: &Face, c: char) -> (Vec<u16>, Vec<u16>) {
let gid = face.glyph_index(c).expect("cmap has the char");
let construction = face
.tables()
.math
.expect("MATH table")
.variants
.expect("MathVariants")
.vertical_constructions
.get(gid)
.expect("char has a vertical GlyphConstruction");
let assembly_gids: Vec<u16> = construction
.assembly
.expect("delimiter has a GlyphAssembly")
.parts
.into_iter()
.map(|p| p.glyph_id.0)
.collect();
let variant_gids: Vec<u16> = construction
.variants
.into_iter()
.map(|v| v.variant_glyph.0)
.collect();
(assembly_gids, variant_gids)
}
#[test]
fn very_tall_paren_is_built_from_assembly_parts() {
let path = need_font!(find_math_font(), "MATH");
let store = TtfFontStore::load(&path, None, None).expect("load math font");
let face = store.face(FontKey(0)).expect("parse face");
let (open_asm_gids, open_variant_gids) = paren_assembly_and_variant_gids(&face, '(');
let big = "(math-big-char MathOp `∑`)";
let row = format!("(math-frac {big} {big})");
let tall_inner = format!("(math-frac {row} {row})");
let src = with_ctx(&format!(
"embed-math ctx (math-paren {DUMMY_PAREN} {DUMMY_PAREN} {tall_inner})"
));
let v = run_math(&src, &store).expect("tall nested-fraction paren should compile");
let (_, _, _, glyphs) = as_math_parts(math_box(v));
let placed_open_parts: Vec<u16> = glyphs
.iter()
.filter_map(|g| g.gid)
.filter(|gid| open_asm_gids.contains(gid))
.collect();
assert!(
placed_open_parts.len() > open_asm_gids.len(),
"expected the tall '(' to be built from MULTIPLE assembly parts (extender repeated): \
placed {placed_open_parts:?} vs {} distinct assembly parts {open_asm_gids:?}",
open_asm_gids.len()
);
let placed_open_variants: Vec<u16> = glyphs
.iter()
.filter_map(|g| g.gid)
.filter(|gid| open_variant_gids.contains(gid) && !open_asm_gids.contains(gid))
.collect();
assert!(
placed_open_variants.is_empty(),
"the tall '(' should have used assembly parts, not a discrete variant glyph, but found \
discrete-variant gids {placed_open_variants:?}"
);
let geometry = PageGeometry::default();
let page = page_for(math_box(run_math(&src, &store).unwrap()), &geometry);
let pdf_bytes = render_pdf_ttf(&geometry, &[page], &store, &[]).expect("render");
assert!(pdf_bytes.starts_with(b"%PDF-"));
let cmap = extract_streams(&pdf_bytes)
.into_iter()
.find(|s| contains_subslice(s, b"beginbfchar"))
.expect("a ToUnicode CMap stream (beginbfchar)")
.to_vec();
let some_part = placed_open_parts[0];
let font_bytes = std::fs::read(&path).expect("read font file for subsetter cross-check");
let used_gids = original_gids_used(&face, &glyphs);
let some_part_cid = expected_cid(&font_bytes, &used_gids, some_part);
let gid_hex = format!("<{:04X}>", some_part_cid).into_bytes();
assert!(
contains_subslice(&cmap, &gid_hex),
"expected the ToUnicode CMap to map the assembly part gid {some_part} (CID {some_part_cid}, \
{}); CMap = {:?}",
String::from_utf8_lossy(&gid_hex),
String::from_utf8_lossy(&cmap)
);
}