retex-lua 1.3.0-alpha.2

Embedded Lua and the LuaTeX Lua API for retex
Documentation
//! Converting LuaTeX font tables (as returned by `define_font` callbacks
//! or `font.define`) into engine fonts (LuaTeX manual chapter 6).

use std::collections::HashMap;
use std::rc::Rc;

use mlua::{Lua, Table, Value};
use retex_engine::fonts::{
    CharMetrics, FontInfo, FontKind, LigKernStep, LigOp, PacketCommand, VirtualFont,
};

/// Builds a [`FontInfo`] from a Lua font table.
pub fn font_from_table(t: &Table) -> mlua::Result<FontInfo> {
    let mut info = FontInfo::null();
    info.name = t
        .get::<Option<mlua::String>>("name")?
        .map(|s| s.as_bytes().to_vec())
        .unwrap_or_default();
    info.size = t.get::<Option<i32>>("size")?.unwrap_or(655360);
    // LuaTeX's default design size is 10pt, whatever the size.
    info.design_size = t.get::<Option<i32>>("designsize")?.unwrap_or(655360);
    info.checksum = t.get::<Option<i64>>("checksum")?.unwrap_or(0) as u32;
    let mut params = vec![0; 8];
    // stretch, shrink and step replace a later \expandglyphsinfont.
    if let (Some(st), Some(sh), Some(sp)) = (
        t.get::<Option<i32>>("stretch")?,
        t.get::<Option<i32>>("shrink")?,
        t.get::<Option<i32>>("step")?,
    ) && (st > 0 || sh > 0)
        && sp > 0
    {
        info.expansion = Some(retex_engine::expansion::FontExpansion {
            stretch: st,
            shrink: sh,
            step: sp,
        });
    }
    if let Some(p) = t.get::<Option<Table>>("parameters")? {
        let names = [
            "slant",
            "space",
            "space_stretch",
            "space_shrink",
            "x_height",
            "quad",
            "extra_space",
        ];
        for (i, n) in names.iter().enumerate() {
            if let Some(v) = p.get::<Option<i32>>(*n)? {
                params[i + 1] = v;
            } else if let Some(v) = p.get::<Option<i32>>(i as i64 + 1)? {
                params[i + 1] = v;
            }
        }
    }
    info.params = params;
    let mut chars: HashMap<_, _, retex_engine::hash::FastHash> = HashMap::default();
    if let Some(cs) = t.get::<Option<Table>>("characters")? {
        for pair in cs.pairs::<i64, Table>() {
            let (c, ct) = pair?;
            chars.insert(
                c as u32,
                CharMetrics {
                    width: ct.get::<Option<i32>>("width")?.unwrap_or(0),
                    height: ct.get::<Option<i32>>("height")?.unwrap_or(0),
                    depth: ct.get::<Option<i32>>("depth")?.unwrap_or(0),
                    italic: ct.get::<Option<i32>>("italic")?.unwrap_or(0),
                    index: ct.get::<Option<u32>>("index")?.unwrap_or(c as u32),
                },
            );
            // `ligatures[next] = { char, type }` and `kerns[next]`.
            let mut steps = Vec::new();
            if let Some(l) = ct.get::<Option<Table>>("ligatures")? {
                for pair in l.pairs::<i64, Table>() {
                    let (next, lt) = pair?;
                    let chr = lt.get::<Option<u32>>("char")?.unwrap_or(0);
                    let op = lt.get::<Option<u8>>("type")?.unwrap_or(0);
                    steps.push(LigKernStep {
                        next: next as u32,
                        op: LigOp::Lig { op, chr },
                    });
                }
            }
            if let Some(k) = ct.get::<Option<Table>>("kerns")? {
                for pair in k.pairs::<i64, i32>() {
                    let (next, amount) = pair?;
                    steps.push(LigKernStep {
                        next: next as u32,
                        op: LigOp::Kern(amount),
                    });
                }
            }
            if !steps.is_empty() {
                steps.sort_by_key(|st| st.next);
                info.lig_kern.insert(c as u32, steps.into_boxed_slice());
            }
        }
    }
    info.chars = chars;
    if let Some(mc) = t.get::<Option<Table>>("MathConstants")? {
        let mut m = retex_engine::fonts::MathConstants::new();
        for pair in mc.pairs::<String, f64>() {
            let (k, v) = pair?;
            m.insert(k, v.round() as i32);
        }
        info.math_constants = Some(std::rc::Rc::new(m));
    }
    info.hyphen_char = t.get::<Option<i32>>("hyphenchar")?.unwrap_or(i32::MIN);
    info.skew_char = t.get::<Option<i32>>("skewchar")?.unwrap_or(i32::MIN);
    info.kind = FontKind::OpenType;
    Ok(info)
}

/// Makes font `f`, defined from Lua table `t`, a virtual font if some of
/// its characters have `commands` (LuaTeX manual, "Virtual fonts"): loads
/// the fonts of its `fonts` table (by default the font itself) and
/// attaches the packets. Runs with the engine lent to Lua.
pub fn attach_commands(lua: &Lua, t: &Table, f: u32) -> mlua::Result<()> {
    let Some(cs) = t.get::<Option<Table>>("characters")? else {
        return Ok(());
    };
    let mut packets: HashMap<u32, Box<[PacketCommand]>, retex_engine::hash::FastHash> =
        HashMap::default();
    for pair in cs.pairs::<i64, Table>() {
        let (c, ct) = pair?;
        if let Some(cmds) = ct.get::<Option<Table>>("commands")? {
            packets.insert(c as u32, packet(lua, &cmds)?.into_boxed_slice());
        }
    }
    if packets.is_empty() {
        return Ok(());
    }
    let mut fonts = Vec::new();
    match t.get::<Option<Table>>("fonts")? {
        Some(ft) => {
            for entry in ft.sequence_values::<Value>() {
                let Value::Table(entry) = entry? else {
                    fonts.push(0);
                    continue;
                };
                fonts.push(local_font(lua, &entry, f)?);
            }
        }
        None => fonts.push(f),
    }
    crate::with_engine(lua, |e| {
        e.fonts.fonts[f as usize].vf = Some(Rc::new(VirtualFont { fonts, packets }));
    })
}

/// The engine font of an entry of a virtual font's `fonts` table: `{ id
/// = n }` (0 is the font `f` itself) or `{ name = s, size = n }`, which
/// is loaded unless a font of that name and size is (as for the local
/// fonts of VF files).
fn local_font(lua: &Lua, entry: &Table, f: u32) -> mlua::Result<u32> {
    if let Some(id) = entry.get::<Option<i64>>("id")? {
        return crate::with_engine(lua, |e| match id {
            0 => f,
            _ if e.font_exists(id as i32) => id as u32,
            _ => 0,
        });
    }
    let Some(name) = entry.get::<Option<mlua::String>>("name")? else {
        return Ok(0);
    };
    let name = name.as_bytes().to_vec();
    let size = entry.get::<Option<i32>>("size")?.unwrap_or(655360);
    let existing = crate::with_engine(lua, |e| {
        (1..e.fonts.fonts.len()).find(|&g| {
            let fi = &e.fonts.fonts[g];
            fi.size == size && fi.name == name
        })
    })?;
    if let Some(g) = existing {
        return Ok(g as u32);
    }
    crate::with_engine(lua, |e| e.load_font(&name, size).unwrap_or(0))
}

/// The packet of one character's `commands` table.
fn packet(lua: &Lua, cmds: &Table) -> mlua::Result<Vec<PacketCommand>> {
    let num = |v: Option<&Value>| -> i64 {
        match v {
            Some(Value::Integer(n)) => *n,
            Some(Value::Number(x)) => *x as i64,
            Some(v @ Value::String(_)) => lua.coerce_integer(v.clone()).ok().flatten().unwrap_or(0),
            _ => 0,
        }
    };
    let bytes = |v: Option<&Value>| -> Option<Vec<u8>> {
        match v {
            Some(Value::String(s)) => Some(s.as_bytes().to_vec()),
            Some(v @ (Value::Integer(_) | Value::Number(_))) => lua
                .coerce_string(v.clone())
                .ok()
                .flatten()
                .map(|s| s.as_bytes().to_vec()),
            _ => None,
        }
    };
    let mut out = Vec::new();
    for cmd in cmds.sequence_values::<Value>() {
        let Value::Table(cmd) = cmd? else {
            continue;
        };
        let items: Vec<Value> = cmd
            .sequence_values::<Value>()
            .collect::<mlua::Result<_>>()?;
        let Some(Value::String(name)) = items.first() else {
            continue;
        };
        let arg = |i: usize| items.get(i);
        match name.as_bytes().as_ref() {
            b"font" => out.push(PacketCommand::Font(num(arg(1)).wrapping_sub(1) as usize)),
            b"char" => out.push(PacketCommand::Char(num(arg(1)) as u32)),
            b"slot" => {
                out.push(PacketCommand::Font(num(arg(1)).wrapping_sub(1) as usize));
                out.push(PacketCommand::Char(num(arg(2)) as u32));
            }
            b"right" => out.push(PacketCommand::Right(num(arg(1)) as i32)),
            b"down" => out.push(PacketCommand::Down(num(arg(1)) as i32)),
            b"push" => out.push(PacketCommand::Push),
            b"pop" => out.push(PacketCommand::Pop),
            b"rule" => out.push(PacketCommand::Rule {
                height: num(arg(1)) as i32,
                width: num(arg(2)) as i32,
            }),
            b"special" => {
                if let Some(s) = bytes(arg(1)) {
                    out.push(PacketCommand::Special(s));
                }
            }
            b"pdf" => {
                // `{ "pdf", text }` writes in origin mode, `{ "pdf",
                // mode, text }` in the named one.
                let (mode, text) = match (bytes(arg(1)), bytes(arg(2))) {
                    (Some(m), Some(text)) => {
                        let mode = crate::pdflib::MODES
                            .iter()
                            .chain(&[("font", 5)])
                            .find(|(n, _)| n.as_bytes() == m.as_slice())
                            .map(|&(_, m)| m);
                        match mode {
                            Some(mode) => (mode, text),
                            None => continue,
                        }
                    }
                    (Some(text), None) => (retex_engine::pdfext::literal_mode::ORIGIN, text),
                    _ => continue,
                };
                out.push(PacketCommand::Pdf(mode, text));
            }
            // Observed: each `lua` command holds a Lua reference.
            b"lua" => {
                if let Some(v @ (Value::String(_) | Value::Function(_))) = arg(1) {
                    out.push(PacketCommand::Lua(crate::nodelib::lua_ref(lua, v.clone())?));
                }
            }
            b"node" => {
                if let Some(p) = arg(1).and_then(crate::nodelib::to_ref) {
                    out.push(PacketCommand::Node(p));
                }
            }
            // `nop`, `comment`, and `image` (retex has no `img` library).
            _ => {}
        }
    }
    Ok(out)
}