Skip to main content

retex_lua/
luaotfload.rs

1//! A native replacement for luaotfload.
2//!
3//! The LaTeX kernel loads luaotfload in `\everyjob` with
4//! `require('luaotfload-main')` and `luaotfload.main()`. Here that module is
5//! provided natively: `main()` makes the same `luatexbase` registrations as
6//! luaotfload (so logs show the same lines), and the registered functions
7//! are implemented in Rust on top of `retex-font`:
8//!
9//! * `luaotfload.define_font` loads OpenType fonts with luaotfload's
10//!   request syntax and metrics,
11//! * `luaotfload.node_processor` shapes glyph runs (ligatures, kerning) in
12//!   node mode.
13//!
14//! The registration sequence and messages were taken from the log of a
15//! lualatex run.
16
17use std::cell::RefCell;
18use std::collections::HashMap;
19use std::rc::Rc;
20use std::sync::Arc;
21
22use mlua::{Lua, Value};
23use retex_engine::Engine;
24use retex_engine::fonts::{CharMetrics, FontData, FontInfo, FontKind};
25use retex_engine::nodes::NodeRef;
26use retex_font::LoadedFont;
27
28use crate::with_engine;
29
30/// Host-side state of the font loader.
31#[derive(Default)]
32pub struct FontState {
33    loader: Option<retex_font::FontLoader>,
34    /// Engine font id of each loaded font (by `LoadedFont` identity):
35    /// luaotfload returns the id of a font already loaded for an
36    /// equivalent request (e.g. `Latin Modern Math/B` when there is no
37    /// bold) instead of a new font.
38    ids: HashMap<usize, u32>,
39}
40
41/// The Lua glue: the module table and `main`, which performs luaotfload's
42/// registrations through `luatexbase` in the order lualatex shows them.
43const MAIN_LUA: &str = r#"
44local native = ...
45local lb = luatexbase
46local function noop(...) return ... end
47local function log(cat, msg)
48  texio.write_nl("log", "luaotfload | " .. cat .. " : " .. msg)
49end
50luaotfload = luaotfload or {}
51luaotfload.define_font = native.define_font
52luaotfload.node_processor = native.node_processor
53luaotfload.version = luaotfload.version or "3.29"
54-- Letterspacing options (microtype replaces keepligature).
55luaotfload.letterspace = luaotfload.letterspace or
56  { keepligature = true, keeptogether = false, keepwordspacing = false }
57luaotfload.features = luaotfload.features or { defaults = {}, global = {} }
58local aux = luaotfload.aux or {}
59luaotfload.aux = aux
60for _, k in ipairs{"provides_script", "provides_language", "provides_feature",
61    "get_math_dimension", "font_has_glyph", "slot_of_name", "gid_of_name"} do
62  aux[k] = native["aux_" .. k]
63end
64aux.sprint_math_dimension = function(f, name)
65  tex.sprint(-2, tostring(aux.get_math_dimension(f, name)))
66end
67-- The parts of the fontloader's `fonts` table that packages touch.
68fonts = fonts or {}
69fonts.names = fonts.names or {}
70fonts.names.set_location_precedence = fonts.names.set_location_precedence or function() end
71fonts.hashes = fonts.hashes or {}
72fonts.hashes.identifiers = fonts.hashes.identifiers or setmetatable({}, {
73  __index = function(_, id) if type(id) == "number" then return font.getfont(id) end end })
74luaotfload.main = function()
75  lb.provides_module({ name = "luaotfload", date = "2024-12-03", version = "3.29",
76    description = "Lua based OpenType font support" })
77  lb.provides_module({ name = "lualibs", date = "2023-07-13", version = "2.76",
78    description = "ConTeXt Lua standard libraries." })
79  lb.provides_module({ name = "lualibs-extended", date = "2023-07-13", version = "2.76",
80    description = "ConTeXt Lua libraries -- extended collection." })
81  local cache = native.cache_dir()
82  log("conf", 'Root cache directory is "' .. cache .. '".')
83  local loader = kpse.find_file("fontloader-2023-12-28.lua", "lua") or "fontloader-2023-12-28.lua"
84  log("init", 'Loading fontloader "fontloader-2023-12-28.lua" from kpse-resolved path "' .. loader .. '".')
85  lb.new_attribute("luaotfload@noligature")
86  log("init", "Context OpenType loader version 3.134")
87  lb.add_to_callback("pre_linebreak_filter", native.node_processor, "luaotfload.node_processor")
88  lb.add_to_callback("hpack_filter", native.node_processor, "luaotfload.node_processor")
89  lb.add_to_callback("glyph_stream_provider", noop, "luaotfload.glyph_stream")
90  lb.add_to_callback("define_font", native.define_font, "luaotfload.define_font")
91  lb.new_attribute("luaotfload_color_attribute")
92  log("conf", 'Root cache directory is "' .. cache .. '".')
93  lb.add_to_callback("find_opentype_file", noop, "luaotfload.harf.strip_prefix")
94  lb.add_to_callback("find_truetype_file", noop, "luaotfload.harf.strip_prefix")
95  lb.remove_from_callback("glyph_stream_provider", "luaotfload.glyph_stream")
96  lb.add_to_callback("glyph_stream_provider", noop, "luaotfload.harf.glyphstream")
97  lb.add_to_callback("post_linebreak_filter", noop, "luaotfload.harf.finalize_vlist")
98  lb.add_to_callback("hpack_filter", noop, "luaotfload.harf.finalize_hlist")
99  lb.add_to_callback("wrapup_run", noop, "luaotfload.cleanup_files")
100  lb.add_to_callback("finish_pdffile", noop, "luaotfload.harf.finalize_unicode")
101  -- Box displays show the character as UTF-8 (observed: "^^@" for 0
102  -- and ^^^^^^ notation beyond Unicode).
103  lb.add_to_callback("glyph_info", function(n)
104    local c = n.char
105    if c == 0 then return "^^@" end
106    if c > 0x10FFFF then return string.format("^^^^^^%06x", c) end
107    return utf8.char(c)
108  end, "luaotfload.glyphinfo")
109  lb.new_attribute("luaotfload.letterspace_done")
110  lb.create_callback("luaotfload.patch_font", "simple", noop)
111  lb.create_callback("luaotfload.patch_font_unsafe", "simple", noop)
112  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_sscale_dimens")
113  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_font_index")
114  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.patch_cambria_domh")
115  lb.add_to_callback("luaotfload.patch_font_unsafe", noop, "luaotfload.aux.fixup_fontdata")
116  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_capheight")
117  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_xheight")
118  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.rewrite_fontname")
119  lb.create_callback("luaotfload.parse_color", "exclusive", noop)
120  return true
121end
122return luaotfload
123"#;
124
125/// Installs the `luaotfload-main` module (in `package.preload`).
126pub fn install(lua: &Lua) -> mlua::Result<()> {
127    lua.set_app_data(Rc::new(RefCell::new(FontState::default())));
128    let native = lua.create_table()?;
129    native.set(
130        "define_font",
131        lua.create_function(|lua, (name, size, _id): (mlua::String, i64, Option<i64>)| {
132            let name = name.to_str()?.to_owned();
133            let t0 = std::time::Instant::now();
134            let r = define_font(lua, &name, size as i32);
135            retex_engine::timing::FONTS.with(|a| a.add(t0));
136            r
137        })?,
138    )?;
139    native.set(
140        "node_processor",
141        lua.create_function(|lua, (head, _group): (Value, Value)| {
142            let h = match &head {
143                Value::UserData(u) => u
144                    .borrow::<crate::nodelib::LNode>()
145                    .ok()
146                    .map(|n| NodeRef(n.0)),
147                _ => None,
148            };
149            if let Some(h) = h {
150                let nh = with_engine(lua, |e| process_list(e, h))?;
151                if nh != h {
152                    return Ok(Value::UserData(
153                        lua.create_userdata(crate::nodelib::LNode(nh.0))?,
154                    ));
155                }
156            }
157            Ok(Value::Boolean(true))
158        })?,
159    )?;
160    native.set(
161        "cache_dir",
162        lua.create_function(|lua, ()| {
163            with_engine(lua, |e| {
164                e.kpse
165                    .as_ref()
166                    .and_then(|k| k.borrow().var_value("TEXMFVAR"))
167                    .map(|v| format!("{v}/luatex-cache/generic/names"))
168                    .unwrap_or_default()
169            })
170        })?,
171    )?;
172    crate::otfaux::install(lua, &native)?;
173    let loader = lua.create_function(move |lua, ()| {
174        let f = lua
175            .load(MAIN_LUA)
176            .set_name("=luaotfload-main")
177            .into_function()?;
178        let native: mlua::Table = lua.named_registry_value("retex.luaotfload.native")?;
179        f.call::<Value>(native)
180    })?;
181    lua.set_named_registry_value("retex.luaotfload.native", native)?;
182    let package: mlua::Table = lua.globals().get("package")?;
183    let preload: mlua::Table = package.get("preload")?;
184    preload.set("luaotfload-main", loader)?;
185    Ok(())
186}
187
188/// Loads a font for the `define_font` callback and registers it with the
189/// engine. Returns the engine font id, or nil if the font cannot be loaded.
190fn define_font(lua: &Lua, name: &str, size: i32) -> mlua::Result<Value> {
191    let state = lua
192        .app_data_ref::<Rc<RefCell<FontState>>>()
193        .map(|s| s.clone())
194        .ok_or_else(|| mlua::Error::runtime("font state missing"))?;
195    // An anonymous request is a TeX metric file if there is one (the
196    // first step of luaotfload's "tex,path,name" anonymous lookup).
197    if let Ok(req) = retex_font::FontRequest::parse(name)
198        && let retex_font::FontSource::Any(base) = &req.source
199    {
200        let id = with_engine(lua, |e| {
201            e.load_tfm_font(base.as_bytes(), size, name.as_bytes())
202        })?;
203        if let Some(i) = id {
204            return Ok(Value::Integer(i64::from(i)));
205        }
206    }
207    let loaded: Result<Arc<LoadedFont>, String> = with_engine(lua, |e| {
208        let mut st = state.borrow_mut();
209        if st.loader.is_none() {
210            let k = e
211                .kpse
212                .clone()
213                .ok_or_else(|| "kpathsea unavailable".to_owned())?;
214            st.loader = Some(retex_font::FontLoader::new(k));
215        }
216        st.loader
217            .as_mut()
218            .expect("loader initialized")
219            .load(name, size)
220            .map_err(|e| e.to_string())
221    })?;
222    let Ok(font) = loaded else {
223        // luaotfload falls back to TeX metric files (e.g. `cmr10`).
224        let plain = name.strip_prefix("file:").unwrap_or(name);
225        let plain = plain.split(':').next().unwrap_or(plain).to_owned();
226        let id = with_engine(lua, |e| {
227            e.load_tfm_font(plain.as_bytes(), size, name.as_bytes())
228        })?;
229        return Ok(id.map_or(Value::Nil, |i| Value::Integer(i64::from(i))));
230    };
231    let key = Arc::as_ptr(&font) as usize;
232    if let Some(&id) = state.borrow().ids.get(&key) {
233        return Ok(Value::Integer(i64::from(id)));
234    }
235    let id = with_engine(lua, |e| {
236        let tex = &font.tex;
237        let mut params = vec![0; tex.params.len() + 1];
238        for (i, p) in tex.params.iter().enumerate() {
239            params[i + 1] = *p;
240        }
241        let mut chars = HashMap::with_capacity_and_hasher(
242            tex.num_chars(),
243            retex_engine::hash::FastHash::default(),
244        );
245        for (code, m) in tex.chars() {
246            chars.insert(
247                code,
248                CharMetrics {
249                    width: m.width,
250                    height: m.height,
251                    depth: m.depth,
252                    italic: m.italic,
253                    index: u32::from(m.glyph),
254                },
255            );
256        }
257        let info = FontInfo {
258            name: tex.name.clone().into_bytes(),
259            size: tex.size,
260            design_size: tex.design_size,
261            checksum: 0,
262            params,
263            chars,
264            hyphen_char: i32::MIN,
265            skew_char: i32::MIN,
266            kind: FontKind::OpenType,
267            handle: 0,
268            char_codes: Default::default(),
269            ignore_ligatures: 0,
270            data: Some(FontData(Rc::new(font.clone()))),
271            lig_kern: Default::default(),
272            right_boundary: None,
273            expansion: None,
274            size_spec: i32::MIN,
275            vf: None,
276            math_chars: {
277                let mc = tex.math_chars(&font.file);
278                (!mc.is_empty()).then(|| Rc::new(convert_math_chars(mc)))
279            },
280            math_constants: tex.math.as_ref().map(|m| {
281                Rc::new(
282                    m.iter()
283                        .map(|(k, v)| ((*k).to_owned(), v.to_sp()))
284                        .collect(),
285                )
286            }),
287        };
288        e.register_font(info)
289    })?;
290    state.borrow_mut().ids.insert(key, id);
291    Ok(Value::Integer(i64::from(id)))
292}
293
294/// Converts the font crate's math data to the engine's, rounding the
295/// dimensions to scaled points.
296fn convert_math_chars(
297    mc: HashMap<u32, retex_font::texfont::MathChar>,
298) -> HashMap<u32, retex_engine::fonts::MathCharInfo> {
299    use retex_engine::fonts::{ExtPart, MathCharInfo};
300    let parts = |v: &Option<Vec<retex_font::texfont::MathPart>>| {
301        v.as_ref().map(|ps| {
302            ps.iter()
303                .map(|p| ExtPart {
304                    code: p.code,
305                    start: retex_font::texfont::round_sp(p.start),
306                    end: retex_font::texfont::round_sp(p.end),
307                    advance: retex_font::texfont::round_sp(p.advance),
308                    extender: p.extender,
309                })
310                .collect::<Rc<[ExtPart]>>()
311        })
312    };
313    mc.into_iter()
314        .map(|(c, m)| {
315            (
316                c,
317                MathCharInfo {
318                    next: m.next,
319                    top_accent: m.top_accent.map(retex_font::texfont::round_sp),
320                    vert: parts(&m.vert),
321                    horiz: parts(&m.horiz),
322                },
323            )
324        })
325        .collect()
326}
327
328/// The OpenType font behind engine font `f`, if any.
329pub fn loaded_font(e: &Engine, f: u32) -> Option<Arc<LoadedFont>> {
330    let data = e.font(f).data.as_ref()?;
331    data.0.downcast_ref::<Arc<LoadedFont>>().cloned()
332}
333
334pub use crate::otfnodes::process_list;