retex-lua 0.9.0

Embedded Lua and the LuaTeX Lua API for retex
Documentation
//! The `font` library (LuaTeX manual ยง6.3).

use mlua::{Lua, Table, Value};

use crate::with_engine;

/// Builds the Lua font table of engine font `f` (a subset of LuaTeX's
/// fields: name, size, designsize, parameters, characters, hyphenchar,
/// skewchar, psname, filename).
fn font_table(lua: &Lua, f: u32) -> mlua::Result<Option<Table>> {
    struct Snap {
        name: Vec<u8>,
        size: i32,
        dsize: i32,
        params: Vec<i32>,
        hyphen: i32,
        skew: i32,
        file: Option<(String, String)>,
        math: Option<Vec<(&'static str, retex_font::texfont::MathValue)>>,
        expansion: Option<retex_engine::expansion::FontExpansion>,
    }
    let snap = with_engine(lua, |e| {
        if !e.font_exists(f as i32) {
            return None;
        }
        let fi = e.font(f);
        let file = crate::luaotfload::loaded_font(e, f).map(|lf| {
            (
                lf.tex.path.to_string_lossy().into_owned(),
                lf.tex.psname.clone(),
            )
        });
        let math = crate::luaotfload::loaded_font(e, f).and_then(|lf| lf.tex.math.clone());
        Some(Snap {
            name: fi.name.clone(),
            size: fi.size,
            dsize: fi.design_size,
            params: fi.params.clone(),
            hyphen: fi.hyphen_char,
            skew: fi.skew_char,
            file,
            math,
            expansion: fi.expansion,
        })
    })?;
    let Some(s) = snap else {
        return Ok(None);
    };
    let t = lua.create_table()?;
    t.set("name", lua.create_string(&s.name)?)?;
    t.set("size", s.size)?;
    t.set("designsize", s.dsize)?;
    t.set("hyphenchar", s.hyphen)?;
    t.set("skewchar", s.skew)?;
    if let Some(x) = s.expansion {
        t.set("stretch", x.stretch)?;
        t.set("shrink", x.shrink)?;
        t.set("step", x.step)?;
    }
    let p = lua.create_table()?;
    let names = [
        "slant",
        "space",
        "space_stretch",
        "space_shrink",
        "x_height",
        "quad",
        "extra_space",
    ];
    for (i, v) in s.params.iter().enumerate().skip(1) {
        if let Some(n) = names.get(i - 1) {
            p.set(*n, *v)?;
        } else {
            p.set(i, *v)?;
        }
    }
    t.set("parameters", p)?;
    // The character table is large: it is built when first used.
    let meta = lua.create_table()?;
    meta.set(
        "__index",
        lua.create_function(move |lua, (t, k): (Table, Value)| {
            if matches!(&k, Value::String(k) if k.as_bytes() == b"characters".as_slice()) {
                let chars = characters_table(lua, f)?;
                t.raw_set("characters", chars.clone())?;
                return Ok(Value::Table(chars));
            }
            Ok(Value::Nil)
        })?,
    )?;
    t.set_metatable(Some(meta))?;
    if let Some(m) = s.math {
        // luaotfload's MathConstants: percentages as integers, lengths
        // as unrounded floats.
        let mc = lua.create_table()?;
        for (k, v) in m {
            match v {
                retex_font::texfont::MathValue::Percent(p) => mc.set(k, p)?,
                retex_font::texfont::MathValue::Scaled(x) => mc.set(k, x)?,
            }
        }
        t.set("MathConstants", mc)?;
    }
    if let Some((file, ps)) = s.file {
        t.set("filename", file)?;
        t.set("psname", ps.clone())?;
        t.set("fullname", ps)?;
        t.set("format", "opentype")?;
    }
    Ok(Some(t))
}

/// The `characters` table of font `f`.
fn characters_table(lua: &Lua, f: u32) -> mlua::Result<Table> {
    let s = with_engine(lua, |e| {
        let fi = e.font(f);
        CharsSnap {
            chars: fi.chars.iter().map(|(c, m)| (*c, *m)).collect(),
            lig_kern: fi.lig_kern.clone(),
            math_chars: fi.math_chars.clone(),
        }
    })?;
    let chars = lua.create_table()?;
    for (c, m) in s.chars {
        let ct = lua.create_table()?;
        ct.set("width", m.width)?;
        ct.set("height", m.height)?;
        ct.set("depth", m.depth)?;
        ct.set("italic", m.italic)?;
        ct.set("index", m.index)?;
        // The TFM lig/kern program as `kerns[next]` and
        // `ligatures[next] = { char = โ€ฆ, type = โ€ฆ }`.
        if let Some(prog) = s.lig_kern.get(&c) {
            let kerns = lua.create_table()?;
            let ligs = lua.create_table()?;
            for st in prog.iter() {
                match st.op {
                    retex_engine::fonts::LigOp::Kern(k) => kerns.set(st.next, k)?,
                    retex_engine::fonts::LigOp::Lig { op, chr } => {
                        let l = lua.create_table()?;
                        l.set("char", chr)?;
                        l.set("type", op)?;
                        ligs.set(st.next, l)?;
                    }
                }
            }
            if kerns.pairs::<Value, Value>().next().is_some() {
                ct.set("kerns", kerns)?;
            }
            if ligs.pairs::<Value, Value>().next().is_some() {
                ct.set("ligatures", ligs)?;
            }
        }
        if let Some(mi) = s.math_chars.as_ref().and_then(|m| m.get(&c)) {
            if let Some(n) = mi.next {
                ct.set("next", n)?;
            }
            if let Some(t) = mi.top_accent {
                ct.set("top_accent", t)?;
            }
            for (key, parts) in [("vert_variants", &mi.vert), ("horiz_variants", &mi.horiz)] {
                if let Some(ps) = parts {
                    let pt = lua.create_table()?;
                    for (i, p) in ps.iter().enumerate() {
                        let e = lua.create_table()?;
                        e.set("glyph", p.code)?;
                        e.set("start", p.start)?;
                        e.set("end", p.end)?;
                        e.set("advance", p.advance)?;
                        if p.extender {
                            e.set("extender", 1)?;
                        }
                        pt.set(i + 1, e)?;
                    }
                    ct.set(key, pt)?;
                }
            }
        }
        chars.set(c, ct)?;
    }
    Ok(chars)
}

struct CharsSnap {
    chars: Vec<(u32, retex_engine::fonts::CharMetrics)>,
    lig_kern: retex_engine::fonts::LigKernMap,
    math_chars:
        Option<std::rc::Rc<std::collections::HashMap<u32, retex_engine::fonts::MathCharInfo>>>,
}

/// `font.read_vf(name, size)`: the virtual font `name` loaded at `size`,
/// as LuaTeX reports it (observed with LuaTeX 1.22): `name`, `checksum`
/// (signed), `designsize`, `size`, `header` (the preamble comment),
/// `fonts` (`{ name = โ€ฆ, size = โ€ฆ }` in definition order) and
/// `characters`, each with its `width` as the raw fix_word of the VF file
/// and its `commands` in sp (see [`retex_font::vf::Vf::commands`]).
/// Returns nothing if there is no such file.
fn read_vf(lua: &Lua, name: &str, size: i32) -> mlua::Result<Value> {
    use retex_font::vf::Command;
    let data = with_engine(lua, |e| {
        let path = e
            .kpse
            .as_ref()?
            .borrow_mut()
            .find_file(name, retex_kpath::FileFormat::Vf)?;
        e.read_file_recorded(&path)
    })?;
    let Some(vf) = data.and_then(|d| retex_font::vf::Vf::parse(&d).ok()) else {
        return Ok(Value::Nil);
    };
    let t = lua.create_table()?;
    t.set("name", name)?;
    t.set("checksum", vf.checksum as i32)?;
    t.set("designsize", vf.design_size_sp())?;
    t.set("size", size)?;
    t.set("header", lua.create_string(&vf.comment)?)?;
    let fonts = lua.create_table()?;
    for (i, f) in vf.fonts.iter().enumerate() {
        let ft = lua.create_table()?;
        ft.set("name", f.name.as_str())?;
        ft.set("size", vf.font_size(i, size))?;
        fonts.set(i + 1, ft)?;
    }
    t.set("fonts", fonts)?;
    let chars = lua.create_table()?;
    for (&c, p) in &vf.packets {
        let ct = lua.create_table()?;
        ct.set("width", p.width)?;
        let cmds = lua.create_table()?;
        for (k, cmd) in vf.commands(c, size).unwrap_or_default().iter().enumerate() {
            let e = lua.create_table()?;
            match cmd {
                Command::Font(i) => {
                    e.push("font")?;
                    e.push(i + 1)?;
                }
                Command::Char(ch) => {
                    e.push("char")?;
                    e.push(*ch)?;
                }
                Command::Rule { height, width } => {
                    e.push("rule")?;
                    e.push(*height)?;
                    e.push(*width)?;
                }
                Command::Right(x) => {
                    e.push("right")?;
                    e.push(*x)?;
                }
                Command::Down(y) => {
                    e.push("down")?;
                    e.push(*y)?;
                }
                Command::Push => e.push("push")?,
                Command::Pop => e.push("pop")?,
                Command::Special(s) => {
                    e.push("special")?;
                    e.push(lua.create_string(s)?)?;
                }
            }
            cmds.set(k + 1, e)?;
        }
        ct.set("commands", cmds)?;
        chars.set(c, ct)?;
    }
    t.set("characters", chars)?;
    Ok(Value::Table(t))
}

/// Installs `font`.
pub fn install(lua: &Lua) -> mlua::Result<()> {
    let t = lua.create_table()?;
    t.set(
        "getfont",
        lua.create_function(|lua, f: i64| font_table(lua, f as u32))?,
    )?;
    t.set(
        "getcopy",
        lua.create_function(|lua, f: i64| font_table(lua, f as u32))?,
    )?;
    t.set(
        "current",
        lua.create_function(|lua, f: Option<i64>| {
            with_engine(lua, |e| match f {
                Some(f) => {
                    e.eqtb.cur_font.val = f as u32;
                    None
                }
                None => Some(i64::from(e.eqtb.cur_font.val)),
            })
        })?,
    )?;
    t.set(
        "nextid",
        lua.create_function(|lua, _reserve: Option<bool>| {
            with_engine(lua, |e| e.fonts.fonts.len() as i64)
        })?,
    )?;
    t.set(
        "max",
        lua.create_function(|lua, ()| with_engine(lua, |e| e.fonts.fonts.len() as i64 - 1))?,
    )?;
    t.set(
        "id",
        lua.create_function(|lua, name: mlua::String| {
            let n = name.as_bytes().to_vec();
            with_engine(lua, |e| {
                let cs = e.hash.lookup(&n)?;
                let m = e.eqtb.meaning(cs);
                (m.cmd == retex_engine::cmd::Cmd::SetFont).then_some(i64::from(m.chr))
            })
            .map(|o| o.unwrap_or(-1))
        })?,
    )?;
    t.set(
        "define",
        lua.create_function(|lua, args: mlua::MultiValue| {
            let tbl = args.into_iter().find_map(|v| match v {
                Value::Table(t) => Some(t),
                _ => None,
            });
            let Some(tbl) = tbl else {
                return Err(mlua::Error::runtime("font.define: table expected"));
            };
            let info = crate::fonttable::font_from_table(&tbl)?;
            with_engine(lua, |e| i64::from(e.register_font(info)))
        })?,
    )?;
    t.set(
        "read_vf",
        lua.create_function(|lua, (name, size): (mlua::String, i64)| {
            read_vf(lua, &name.to_str()?, size as i32)
        })?,
    )?;
    t.set("frozen", lua.create_function(|_, _f: i64| Ok(false))?)?;
    t.set(
        "fonts",
        lua.create_function(|lua, ()| {
            let n = with_engine(lua, |e| e.fonts.fonts.len() as i64)?;
            let state = lua.create_table()?;
            state.set("i", 0)?;
            state.set("n", n)?;
            let iter = lua.create_function(|lua, st: Table| {
                let i: i64 = st.get("i")?;
                let n: i64 = st.get("n")?;
                let i = i + 1;
                st.set("i", i)?;
                if i >= n {
                    return Ok((Value::Nil, Value::Nil));
                }
                let t = font_table(lua, i as u32)?;
                Ok((Value::Integer(i), t.map_or(Value::Nil, Value::Table)))
            })?;
            Ok((iter, state))
        })?,
    )?;
    // `font.each()` is the documented name of the iterator.
    t.set("each", t.get::<mlua::Function>("fonts")?)?;
    lua.globals().set("font", t)?;
    Ok(())
}