daegun 1.1.6

Your all-in-one text engine. Rust, no_std, zero dependencies.
Documentation
use daegun::daerizer::daegpu::{GpuBatch, HULL_VERTICES};
use super::fonts_dir;

fn each_glyph(file: &str, mut check: impl FnMut(u16, Vec<[f32; 2]>, Vec<[f32; 2]>)) {
    let bytes = std::fs::read(format!("{}/{file}", fonts_dir())).expect("read font");
    let tables = daegun::daecore::daetype::decoder::extract_ttf_tables(&bytes).expect("tables");
    let head = tables.get("head").expect("head");
    let format = daegun::daecore::daetype::decoder::read_i16_be(head, 50).expect("loca format");
    let upm = f32::from(daegun::daecore::daetype::decoder::read_u16_be(head, 18).expect("upm"));
    let count = daegun::daecore::daetype::decoder::read_u16_be(tables.get("maxp").expect("maxp"), 4)
        .expect("num glyphs") as usize;
    let loca = daegun::daecore::daetype::instancer::parse_loca(&tables, format, count).expect("loca");

    let mut seen = 0;
    for gid in 0..count.min(400) as u16 {
        let mut pen = daegun::daerizer::daegpu::collector(upm);
        if daegun::daecore::daetype::outline::outline_glyf_glyph_with_loca(&tables, &loca, gid, &mut pen)
            .is_err()
        {
            continue;
        }
        let Ok(mut curves) = pen.finish() else { continue };
        let mut batch = GpuBatch::new();
        if batch.append(&mut curves).is_none() {
            continue;
        }
        let pts: Vec<[f32; 2]> = batch.curves().iter().map(|c| [c.x, c.y]).collect();
        let h = batch.hulls();
        assert_eq!(h.len(), HULL_VERTICES, "gid {gid} emitted the wrong vertex count");

        let mut cyclic: Vec<[f32; 2]> = vec![h[0].pos, h[1].pos];
        cyclic.extend((3..HULL_VERTICES).step_by(2).map(|i| h[i].pos));
        let mut back: Vec<[f32; 2]> = (2..HULL_VERTICES).step_by(2).map(|i| h[i].pos).collect();
        back.reverse();
        cyclic.extend(back);
        check(gid, pts, cyclic);
        seen += 1;
    }
    assert!(seen > 50, "only {seen} glyphs reached the check in {file}");
}

fn containment(file: &str) {
    each_glyph(file, |gid, pts, poly| {
        let n = poly.len();
        let live: Vec<[f32; 2]> = {
            let mut v: Vec<[f32; 2]> = Vec::with_capacity(n);
            for p in &poly {
                if v.last().is_none_or(|q: &[f32; 2]| q != p) {
                    v.push(*p);
                }
            }
            while v.len() > 1 && v.first() == v.last() {
                v.pop();
            }
            v
        };
        assert!(live.len() >= 3, "gid {gid} degenerated to {} vertices", live.len());
        let m = live.len();

        let mut area = 0.0f32;
        for i in 0..m {
            let (a, b, c) = (live[i], live[(i + 1) % m], live[(i + 2) % m]);
            area += a[0] * b[1] - b[0] * a[1];
            let turn = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]);
            assert!(turn >= -1e-6, "gid {gid} is not convex: turn {turn} at vertex {i}");
        }
        assert!(area > 0.0, "gid {gid} is wound backwards, area {}", area * 0.5);

        for p in &pts {
            for i in 0..m {
                let (a, b) = (live[i], live[(i + 1) % m]);
                let (ex, ey) = (b[0] - a[0], b[1] - a[1]);
                let len = (ex * ex + ey * ey).sqrt();
                if len < 1e-9 {
                    continue;
                }
                let outside = ((p[0] - a[0]) * ey - (p[1] - a[1]) * ex) / len;
                assert!(
                    outside <= 1e-3,
                    "gid {gid} leaks a control point {outside} em outside edge {i}"
                );
            }
        }
    });
}

#[test]
fn the_drawn_polygon_contains_every_control_point() {
    containment("eb-garamond/EBGaramond.ttf");
    containment("inter/InterVariable.ttf");
}

#[test]
fn the_box_fallback_is_a_rectangle_that_covers_the_glyph() {
    let mut curves = vec![
        [[0.0, 0.0], [0.5, 0.0], [1.0, 0.0]],
        [[1.0, 0.0], [1.0, 0.5], [1.0, 1.0]],
        [[1.0, 1.0], [0.5, 1.0], [0.0, 1.0]],
        [[0.0, 1.0], [0.0, 0.5], [0.0, 0.0]],
    ];
    let mut batch = GpuBatch::new();
    batch.append(&mut curves).expect("a unit square is drawable");
    let h = batch.hulls();
    assert_eq!(h.len(), HULL_VERTICES);
    for v in h {
        assert!(v.pos[0] == 0.0 || v.pos[0] == 1.0, "corner off the square: {:?}", v.pos);
        assert!(v.pos[1] == 0.0 || v.pos[1] == 1.0, "corner off the square: {:?}", v.pos);
        let [xx, xy, yx, yy] = v.dilate;
        assert!(xy.abs() < 1e-6 && yx.abs() < 1e-6, "a square dilates on-axis, got {:?}", v.dilate);
        assert!(
            (xx.abs() - 1.0).abs() < 1e-6 && (yy.abs() - 1.0).abs() < 1e-6,
            "a square corner travels one pad per axis, got {:?}",
            v.dilate
        );
        assert_eq!(xx < 0.0, v.pos[0] == 0.0, "corner {:?} dilates inward in x", v.pos);
        assert_eq!(yy < 0.0, v.pos[1] == 0.0, "corner {:?} dilates inward in y", v.pos);
    }
}

#[test]
fn each_glyph_in_a_shared_batch_indexes_its_own_hull() {
    let bytes = std::fs::read(format!("{}/eb-garamond/EBGaramond.ttf", fonts_dir())).expect("font");
    let tables = daegun::daecore::daetype::decoder::extract_ttf_tables(&bytes).expect("tables");
    let head = tables.get("head").expect("head");
    let format = daegun::daecore::daetype::decoder::read_i16_be(head, 50).expect("loca format");
    let upm = f32::from(daegun::daecore::daetype::decoder::read_u16_be(head, 18).expect("upm"));
    let count = daegun::daecore::daetype::decoder::read_u16_be(tables.get("maxp").expect("maxp"), 4)
        .expect("num glyphs") as usize;
    let loca = daegun::daecore::daetype::instancer::parse_loca(&tables, format, count).expect("loca");

    let curves_of = |gid: u16| {
        let mut pen = daegun::daerizer::daegpu::collector(upm);
        daegun::daecore::daetype::outline::outline_glyf_glyph_with_loca(&tables, &loca, gid, &mut pen)
            .ok()?;
        pen.finish().ok()
    };

    let mut shared = GpuBatch::new();
    let mut expected: Vec<(u16, Vec<daegun::daerizer::daegpu::HullVertex>)> = Vec::new();
    let mut slots = Vec::new();

    for gid in 1..120u16 {
        let Some(mut curves) = curves_of(gid) else { continue };
        let mut alone = GpuBatch::new();
        let mut solo_curves = curves.clone();
        let Some(solo) = alone.append(&mut solo_curves) else { continue };
        let Some(slot) = shared.append(&mut curves) else { continue };

        assert_eq!(solo.hull_base, 0, "gid {gid}: the first glyph of a batch must start at zero");
        expected.push((gid, alone.hulls().to_vec()));
        slots.push((gid, slot));
    }
    assert!(slots.len() > 60, "only {} glyphs entered the shared batch", slots.len());

    assert_eq!(
        shared.hulls().len(), slots.len() * HULL_VERTICES,
        "the shared batch holds {} vertices for {} glyphs", shared.hulls().len(), slots.len(),
    );

    for (i, ((gid, slot), (egid, want))) in slots.iter().zip(expected.iter()).enumerate() {
        assert_eq!(gid, egid, "the two runs disagreed about glyph order");
        assert_eq!(
            slot.hull_base as usize, i * HULL_VERTICES,
            "gid {gid} is glyph {i} of the batch and claims hull_base {}", slot.hull_base,
        );

        let base = slot.hull_base as usize;
        let got = &shared.hulls()[base..base + HULL_VERTICES];
        for (v, (g, w)) in got.iter().zip(want.iter()).enumerate() {
            assert_eq!(
                (g.pos[0].to_bits(), g.pos[1].to_bits()),
                (w.pos[0].to_bits(), w.pos[1].to_bits()),
                "gid {gid} vertex {v}: shared batch has {:?} at hull_base {base}, alone it is {:?}",
                g.pos, w.pos,
            );
            assert_eq!(g.dilate, w.dilate, "gid {gid} vertex {v}: dilation differs in a shared batch");
        }
    }
}