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, and
35    /// whether the request was `scaled`): luaotfload returns the id of a
36    /// font already loaded for an equivalent request (e.g. `Latin Modern
37    /// Math/B` when there is no bold) instead of a new font. Observed: a
38    /// `scaled` request (a computed size) and an `at` request for the same
39    /// size give two fonts.
40    ids: HashMap<(usize, bool), u32>,
41    /// Engine font id of each TeX metric file request (as written, and
42    /// its size or negated scale): luaotfload returns the font of an
43    /// identical request (observed: `cmr10` and `cmr10 scaled 1000` are
44    /// one font, `cmr10 at 10pt` and `file:cmr10` others).
45    tfm_ids: HashMap<(String, i32), u32>,
46    /// Whether the installed luaotfload is older than 3.29 (read once
47    /// from `luaotfload.sty`): those versions give fonts their design
48    /// size (so `\fontname` has no `at` clause at that size), 3.29 gives
49    /// them 0 (observed with 3.28 and 3.29).
50    legacy_design_size: Option<bool>,
51    /// Whether the log tells that the font name database was loaded.
52    db_announced: bool,
53    /// Whether a font was looked up by name (luaotfload then loads its
54    /// lookup cache, if there is one, and says so in the log).
55    lookup_announced: bool,
56}
57
58/// The Lua glue: the module table and `main`, which performs luaotfload's
59/// registrations through `luatexbase` in the order lualatex shows them.
60const MAIN_LUA: &str = r#"
61local native = ...
62local lb = luatexbase
63local function noop(...) return ... end
64local function log(cat, msg)
65  texio.write_nl("log", "luaotfload | " .. cat .. " : " .. msg)
66end
67luaotfload = luaotfload or {}
68luaotfload.define_font = native.define_font
69luaotfload.node_processor = native.node_processor
70luaotfload.version = luaotfload.version or "3.29"
71-- Letterspacing options (microtype replaces keepligature).
72luaotfload.letterspace = luaotfload.letterspace or
73  { keepligature = true, keeptogether = false, keepwordspacing = false }
74luaotfload.features = luaotfload.features or { defaults = {}, global = {} }
75local aux = luaotfload.aux or {}
76luaotfload.aux = aux
77for _, k in ipairs{"provides_script", "provides_language", "provides_feature",
78    "get_math_dimension", "font_has_glyph", "slot_of_name", "gid_of_name"} do
79  aux[k] = native["aux_" .. k]
80end
81aux.sprint_math_dimension = function(f, name)
82  tex.sprint(-2, tostring(aux.get_math_dimension(f, name)))
83end
84-- The parts of the fontloader's `fonts` table that packages touch.
85fonts = fonts or {}
86fonts.names = fonts.names or {}
87fonts.names.set_location_precedence = fonts.names.set_location_precedence or function() end
88fonts.hashes = fonts.hashes or {}
89fonts.hashes.identifiers = fonts.hashes.identifiers or setmetatable({}, {
90  __index = function(_, id) if type(id) == "number" then return font.getfont(id) end end })
91-- The fontloader's parameters (unrounded) and characters of a font, by
92-- id, kept once asked for; nil for fonts it did not load (polyglossia's
93-- punctuation spacing reads the parameters).
94local function font_hash(get)
95  return setmetatable({}, { __index = function(t, id)
96    if type(id) ~= "number" then return nil end
97    local v = get(id)
98    if v ~= nil then rawset(t, id, v) end
99    return v
100  end })
101end
102fonts.hashes.parameters = fonts.hashes.parameters or font_hash(native.font_parameters)
103fonts.hashes.characters = fonts.hashes.characters or font_hash(function(id)
104  if native.font_parameters(id) then return font.getfont(id).characters end
105end)
106fonts.hashes.quads = fonts.hashes.quads or {}
107local initialized = false
108luaotfload.main = function()
109  if initialized then
110    log("load", "Luaotfload initialization requested but is already loaded, ignoring.")
111    return
112  end
113  initialized = true
114  lb.provides_module({ name = "luaotfload", date = "2024-12-03", version = "3.29",
115    description = "Lua based OpenType font support" })
116  lb.provides_module({ name = "lualibs", date = "2023-07-13", version = "2.76",
117    description = "ConTeXt Lua standard libraries." })
118  lb.provides_module({ name = "lualibs-extended", date = "2023-07-13", version = "2.76",
119    description = "ConTeXt Lua libraries -- extended collection." })
120  -- lualibs' global `utf` (luamml uses it).
121  if not utf then utf = native.lualibs_utf() end
122  -- lualibs' `table.copy(t)`: a deep copy, keys included, that keeps
123  -- shared and cyclic tables shared and the metatables (luapstricks).
124  if not table.copy then
125    local function copy(t, tables)
126      tables = tables or {}
127      local c = {}
128      if not tables[t] then tables[t] = c end
129      for k, v in next, t do
130        if type(k) == "table" then k = tables[k] or copy(k, tables) end
131        if type(v) == "table" then v = tables[v] or copy(v, tables) end
132        c[k] = v
133      end
134      local mt = getmetatable(t)
135      if mt then setmetatable(c, mt) end
136      return c
137    end
138    table.copy = copy
139  end
140  -- The fontloader's definer, which luapstricks loads its fonts with: a
141  -- font defined here is registered already, so `read` gives its id.
142  fonts.definers = fonts.definers or {}
143  fonts.definers.read = fonts.definers.read or function(name, size)
144    return native.define_font(name, size)
145  end
146  fonts.definers.register = fonts.definers.register or function(_, id) return id end
147  local cache = native.cache_dir()
148  log("conf", 'Root cache directory is "' .. cache .. '".')
149  -- As luaotfload does before loading the fontloader, which wants the
150  -- attribute with index zero to be zero.
151  tex.attribute[0] = 0
152  -- luaotfload's set-up holds six Lua references (observed in the
153  -- numbers box displays show for Lua data).
154  native.reserve_lua_refs(6)
155  local loader = kpse.find_file("fontloader-2023-12-28.lua", "lua") or "fontloader-2023-12-28.lua"
156  log("init", 'Loading fontloader "fontloader-2023-12-28.lua" from kpse-resolved path "' .. loader .. '".')
157  -- As the fontloader does: node properties are flushed with their
158  -- nodes and inherited by copies, for good.
159  node.direct.set_properties_mode(true, true)
160  function node.direct.set_properties_mode() end
161  lb.new_attribute("luaotfload@noligature")
162  log("init", "Context OpenType loader version 3.134")
163  -- luaotfload's node processor runs the pre_shaping_filter and
164  -- post_shaping_filter callbacks around the shaping. They are only
165  -- called once something is added to them.
166  if not lb.callbacktypes.pre_shaping_filter then
167    lb.create_callback("pre_shaping_filter", "list")
168    lb.create_callback("post_shaping_filter", "reverselist")
169  end
170  local shape = native.node_processor
171  local dispatch = shape
172  local call_callback = lb.call_callback
173  local function with_shaping_filters(head, groupcode, _, _, direction)
174    if not direction then
175      direction = tex.get("textdirection")
176    else
177      direction = direction == "TRT" and 1 or 0
178    end
179    local result = call_callback("pre_shaping_filter", head, groupcode, direction)
180    if result == false then
181      return false
182    elseif result ~= true then
183      head = result
184    end
185    result = shape(head, groupcode)
186    if result == false then
187      node.flush_list(head)
188      return nil
189    elseif result ~= true then
190      head = result
191    end
192    result = call_callback("post_shaping_filter", head, groupcode, direction)
193    if result == false then
194      node.flush_list(head)
195      return nil
196    elseif result == true then
197      return head
198    else
199      return result
200    end
201  end
202  local add_to_callback = lb.add_to_callback
203  function lb.add_to_callback(name, ...)
204    if name == "pre_shaping_filter" or name == "post_shaping_filter" then
205      dispatch = with_shaping_filters
206    end
207    return add_to_callback(name, ...)
208  end
209  local function node_processor(head, groupcode, size, packtype, direction)
210    return dispatch(head, groupcode, size, packtype, direction)
211  end
212  lb.add_to_callback("pre_linebreak_filter", node_processor, "luaotfload.node_processor")
213  lb.add_to_callback("hpack_filter", node_processor, "luaotfload.node_processor")
214  lb.add_to_callback("glyph_stream_provider", noop, "luaotfload.glyph_stream")
215  -- Font colours (`color=RRGGBB[AA]`): a font with one adds the colour
216  -- handler to the callbacks below, which brackets its glyphs with
217  -- colour stack 0 whatsits (observed; the alpha part is not supported).
218  local colors = {}
219  local color_handler
220  -- The colour literal for `value`, a colour in hex (RRGGBB, the alpha
221  -- of RRGGBBAA left out) or a name: the `luaotfload.parse_color`
222  -- callback (LaTeX's l3color) makes it if set, else hex is converted.
223  local function color_literal(value)
224    local hex = value:match("^%x%x%x%x%x%x$") or value:match("^(%x%x%x%x%x%x)%x%x$")
225    local ok, s = pcall(lb.call_callback, "luaotfload.parse_color", hex or value)
226    if ok and type(s) == "string" then return s end
227    if not hex then return nil end
228    local function c(i)
229      local v = tonumber(hex:sub(i, i + 1), 16) / 255
230      return (string.format("%.3f", v):gsub("0+$", ""):gsub("%.$", ""))
231    end
232    return c(1) .. " " .. c(3) .. " " .. c(5) .. " rg"
233  end
234  local function define_font(name, size, id)
235    local f = native.define_font(name, size, id)
236    if type(f) == "number" and type(name) == "string" then
237      local value = name:match("[:;]color={?([^;}]+)}?")
238      local literal = value and not colors[f] and color_literal(value)
239      if literal then
240        colors[f] = literal
241        if not color_handler then
242          color_handler = luaotfload.color_handler
243          lb.add_to_callback("post_linebreak_filter", color_handler, "luaotfload.color_handler")
244          lb.add_to_callback("hpack_filter", color_handler, "luaotfload.color_handler")
245          lb.add_to_callback("post_mlist_to_hlist_filter", color_handler, "luaotfload.color_handler")
246        end
247      end
248    end
249    return f
250  end
251  local GLYPH, HLIST, VLIST, WHATSIT = node.id("glyph"), node.id("hlist"), node.id("vlist"), node.id("whatsit")
252  local LINE = 1
253  local function stack(command, data)
254    local w = node.new("whatsit", "pdf_colorstack")
255    w.stack = 0
256    w.command = command
257    if data then w.data = data end
258    return w
259  end
260  -- Colours the glyphs of `head` starting with colour `current`; returns
261  -- the new head and the colour at its end. Paragraph lines are coloured
262  -- on their own; other boxes and whatsits end a coloured run.
263  local function colorize(head, current)
264    local n = head
265    while n do
266      local id = n.id
267      if id == GLYPH then
268        local c = colors[n.font]
269        if c ~= current then
270          local w = not c and stack(2) or stack(current and 0 or 1, c)
271          head = node.insert_before(head, n, w)
272          current = c
273        end
274      elseif id == HLIST and n.subtype == LINE then
275        if current then
276          head = node.insert_before(head, n, stack(2))
277          current = nil
278        end
279        local list, last = colorize(n.list, nil)
280        if last then
281          list = node.insert_after(list, node.tail(list), stack(2))
282        end
283        n.list = list
284      elseif id == HLIST or id == VLIST or id == WHATSIT then
285        if current then
286          head = node.insert_before(head, n, stack(2))
287          current = nil
288        end
289      end
290      n = n.next
291    end
292    return head, current
293  end
294  function luaotfload.color_handler(head)
295    if not head then return head end
296    local h, last = colorize(head, nil)
297    if last then
298      h = node.insert_after(h, node.tail(h), stack(2))
299    end
300    return h
301  end
302  lb.add_to_callback("define_font", define_font, "luaotfload.define_font")
303  lb.new_attribute("luaotfload_color_attribute")
304  log("conf", 'Root cache directory is "' .. cache .. '".')
305  lb.add_to_callback("find_opentype_file", noop, "luaotfload.harf.strip_prefix")
306  lb.add_to_callback("find_truetype_file", noop, "luaotfload.harf.strip_prefix")
307  lb.remove_from_callback("glyph_stream_provider", "luaotfload.glyph_stream")
308  lb.add_to_callback("glyph_stream_provider", noop, "luaotfload.harf.glyphstream")
309  lb.add_to_callback("post_linebreak_filter", noop, "luaotfload.harf.finalize_vlist")
310  lb.add_to_callback("hpack_filter", noop, "luaotfload.harf.finalize_hlist")
311  lb.add_to_callback("wrapup_run", noop, "luaotfload.cleanup_files")
312  lb.add_to_callback("finish_pdffile", noop, "luaotfload.harf.finalize_unicode")
313  -- Box displays show the character as UTF-8 (observed: "^^@" for 0
314  -- and ^^^^^^ notation beyond Unicode).
315  lb.add_to_callback("glyph_info", function(n)
316    -- mode=harf glyphs show their text (a ligature's characters).
317    local text = native.harf_glyph_text(n.font, n.char)
318    if text then return text end
319    local c = n.char
320    if c == 0 then return "^^@" end
321    if c > 0x10FFFF then return string.format("^^^^^^%06x", c) end
322    return utf8.char(c)
323  end, "luaotfload.glyphinfo")
324  lb.new_attribute("luaotfload.letterspace_done")
325  lb.create_callback("luaotfload.patch_font", "simple", noop)
326  lb.create_callback("luaotfload.patch_font_unsafe", "simple", noop)
327  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_sscale_dimens")
328  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_font_index")
329  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.patch_cambria_domh")
330  lb.add_to_callback("luaotfload.patch_font_unsafe", noop, "luaotfload.aux.fixup_fontdata")
331  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_capheight")
332  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_xheight")
333  lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.rewrite_fontname")
334  -- The colour interface of luaotfload's colors module, set up by
335  -- main() (l3backend checks for set_transparent_colorstack before
336  -- hooking into parse_color).
337  function luaotfload.add_colorscheme() end
338  function luaotfload.set_colorhandler() end
339  function luaotfload.set_transparenthandler() end
340  function luaotfload.set_transparent_colorstack() end
341  lb.create_callback("luaotfload.split_color", "exclusive", noop)
342  lb.create_callback("luaotfload.parse_color", "exclusive", noop)
343  lb.create_callback("luaotfload.parse_transparent", "exclusive", noop)
344  return true
345end
346return luaotfload
347"#;
348
349/// lualibs' `utf` table: the functions packages call, with the behaviour
350/// observed in lualatex (`utf.byte` gives the first code point only and
351/// nothing for an empty string; `utf.sub` counts characters as
352/// `string.sub` counts bytes).
353const UTF_LUA: &str = r#"
354local u = {}
355u.char = utf8.char
356u.len = utf8.len
357function u.byte(s)
358  if s and #s > 0 then return utf8.codepoint(s, 1) end
359end
360function u.sub(s, i, j)
361  local n = utf8.len(s) or 0
362  i = i or 1
363  j = j or -1
364  if i < 0 then i = math.max(n + i + 1, 1) elseif i == 0 then i = 1 end
365  if j < 0 then j = n + j + 1 elseif j > n then j = n end
366  if i > j then return "" end
367  local b = utf8.offset(s, i)
368  local e = utf8.offset(s, j + 1)
369  return s:sub(b, (e or #s + 1) - 1)
370end
371function u.characters(s)
372  return s:gmatch(utf8.charpattern)
373end
374function u.values(s)
375  local f = s:gmatch(utf8.charpattern)
376  return function()
377    local c = f()
378    if c then return utf8.codepoint(c) end
379  end
380end
381return u
382"#;
383
384/// Installs the `luaotfload-main` module (in `package.preload`).
385pub fn install(lua: &Lua) -> mlua::Result<()> {
386    lua.set_app_data(Rc::new(RefCell::new(FontState::default())));
387    let native = lua.create_table()?;
388    native.set(
389        "define_font",
390        lua.create_function(|lua, (name, size, _id): (mlua::String, i64, Option<i64>)| {
391            let name = name.to_str()?.to_owned();
392            let t0 = std::time::Instant::now();
393            let before = with_engine(lua, |e| {
394                e.work_begin(retex_engine::profile::Work::Fonts);
395                e.fonts.fonts.len()
396            })?;
397            let r = define_font(lua, &name, size as i32);
398            with_engine(lua, Engine::work_end)?;
399            retex_engine::timing::FONTS.with(|a| a.add(t0));
400            // A new font holds Lua references, as in LuaTeX (observed in
401            // the numbers box displays show for Lua data).
402            with_engine(lua, |e| {
403                for _ in before..e.fonts.fonts.len() {
404                    e.nodes.lua_refs.font_defined(true);
405                }
406            })?;
407            r
408        })?,
409    )?;
410    native.set(
411        "reserve_lua_refs",
412        lua.create_function(|lua, n: i64| {
413            with_engine(lua, |e| {
414                for _ in 0..n {
415                    e.nodes.lua_refs.take();
416                }
417            })
418        })?,
419    )?;
420    native.set(
421        "node_processor",
422        lua.create_function(|lua, (head, _group): (Value, Value)| {
423            let h = match &head {
424                Value::UserData(u) => u
425                    .borrow::<crate::nodelib::LNode>()
426                    .ok()
427                    .map(|n| NodeRef(n.0)),
428                _ => None,
429            };
430            if let Some(h) = h {
431                let nh = with_engine(lua, |e| process_list(e, h))?;
432                if nh != h {
433                    return Ok(Value::UserData(
434                        lua.create_userdata(crate::nodelib::LNode(nh.0))?,
435                    ));
436                }
437            }
438            Ok(Value::Boolean(true))
439        })?,
440    )?;
441    native.set(
442        "harf_glyph_text",
443        lua.create_function(|lua, (font, c): (i64, i64)| {
444            let text = with_engine(lua, |e| {
445                let f = u32::try_from(font)
446                    .ok()
447                    .filter(|&f| e.font_exists(f as i32))?;
448                let lf = loaded_font(e, f)?;
449                if lf.request.mode != retex_font::request::Mode::Harf {
450                    return None;
451                }
452                let t = lf.tex.tounicode(u32::try_from(c).ok()?);
453                (t.len() > 1).then(|| t.into_iter().filter_map(char::from_u32).collect::<String>())
454            })?;
455            Ok(text)
456        })?,
457    )?;
458    native.set(
459        "font_parameters",
460        lua.create_function(|lua, id: i64| {
461            let Some(lf) = with_engine(lua, |e| {
462                u32::try_from(id)
463                    .ok()
464                    .filter(|&f| e.font_exists(f as i32))
465                    .and_then(|f| loaded_font(e, f))
466            })?
467            else {
468                return Ok(None);
469            };
470            let t = &lf.tex;
471            let p = lua.create_table()?;
472            for (k, v) in retex_font::texfont::LOADER_PARAMS
473                .iter()
474                .zip(t.loader_params)
475            {
476                let spacing = matches!(*k, "space_stretch" | "space_shrink" | "extra_space");
477                let v = if spacing && v == 0.0 {
478                    // Monospaced fonts: integer zeros (observed).
479                    Value::Integer(0)
480                } else {
481                    Value::Number(v)
482                };
483                match k.parse::<i64>() {
484                    Ok(n) => p.set(n, v)?,
485                    Err(_) => p.set(*k, v)?,
486                }
487            }
488            let factor = t.size_f64 / f64::from(t.units_per_em);
489            for k in ["factor", "hfactor", "vfactor"] {
490                p.set(k, factor)?;
491            }
492            for k in ["size", "scaledpoints", "textsize"] {
493                p.set(k, t.size)?;
494            }
495            p.set("units", t.units_per_em)?;
496            p.set("extrafactor", 1)?;
497            for k in ["mathsize", "mode", "width"] {
498                p.set(k, 0)?;
499            }
500            Ok(Some(p))
501        })?,
502    )?;
503    native.set(
504        "cache_dir",
505        lua.create_function(|lua, ()| with_engine(lua, names_cache_dir))?,
506    )?;
507    let utf = lua
508        .load(UTF_LUA)
509        .set_name("=lualibs-unicode")
510        .into_function()?;
511    native.set("lualibs_utf", utf)?;
512    crate::otfaux::install(lua, &native)?;
513    let loader = lua.create_function(move |lua, ()| {
514        // Like `require` of the real module: it fails where luaotfload
515        // cannot be found (l3build sandboxes without the search tree).
516        let kpse: mlua::Table = lua.globals().get("kpse")?;
517        let find: mlua::Function = kpse.get("find_file")?;
518        if find.call::<Value>(("luaotfload-main.lua", "lua"))?.is_nil() {
519            return Err(mlua::Error::runtime("module 'luaotfload-main' not found"));
520        }
521        let f = lua
522            .load(MAIN_LUA)
523            .set_name("=luaotfload-main")
524            .into_function()?;
525        let native: mlua::Table = lua.named_registry_value("retex.luaotfload.native")?;
526        f.call::<Value>(native)
527    })?;
528    lua.set_named_registry_value("retex.luaotfload.native", native)?;
529    let package: mlua::Table = lua.globals().get("package")?;
530    let preload: mlua::Table = package.get("preload")?;
531    preload.set("luaotfload-main", loader)?;
532    // retex stands in for luahbtex, whose `luaharfbuzz` module packages
533    // probe for (expl3's \c_sys_engine_exec_str is `luahbtex` when
534    // `require 'luaharfbuzz'` succeeds). Shaping is native, so the module
535    // only has the version information; its classes are empty.
536    let harfbuzz = lua
537        .load(
538            r#"local hb = { shapers = function() return "graphite2", "ot", "fallback" end,
539                            version = function() return "12.3.2" end }
540               for _, k in ipairs{"Blob", "Buffer", "Direction", "Face", "Feature", "Font",
541                                  "Language", "Script", "Tag", "Variation", "ot", "unicode"} do
542                 hb[k] = {}
543               end
544               return function() return hb end"#,
545        )
546        .set_name("=[luaharfbuzz]")
547        .call::<mlua::Function>(())?;
548    preload.set("luaharfbuzz", harfbuzz)?;
549    Ok(())
550}
551
552/// The directory of luaotfload's font names database: in the first
553/// directory of `TEXMFCACHE` that can be written (observed: with
554/// `TEXMFCACHE=$TEXMFSYSVAR:$TEXMFVAR`, the default, and a system tree
555/// that is not writable, `TEXMFVAR`), and that `openout_any` accepts.
556fn names_cache_dir(e: &mut retex_engine::Engine) -> String {
557    let Some(k) = e.kpse.as_ref() else {
558        return String::new();
559    };
560    let k = k.borrow();
561    let writable = |dir: &str| {
562        let probe =
563            std::path::Path::new(dir).join(format!(".retex-probe-{}", retex_kpath::process_id()));
564        let ok = std::fs::File::create(&probe).is_ok();
565        let _ = std::fs::remove_file(&probe);
566        ok
567    };
568    let root = k
569        .var_value("TEXMFCACHE")
570        .unwrap_or_default()
571        .split(retex_kpath::SEP)
572        .find(|d| {
573            !d.is_empty() && k.name_ok(d, retex_kpath::Access::Write, true).is_ok() && writable(d)
574        })
575        .map(str::to_owned)
576        .or_else(|| k.var_value("TEXMFVAR"))
577        .unwrap_or_default();
578    format!("{root}/luatex-cache/generic/names")
579}
580
581/// Whether the `luaotfload.sty` found is older than version 3.29 (its
582/// `\ProvidesPackage` line, `[2024/02/14 v3.28 …]`).
583fn legacy_luaotfload(e: &mut Engine) -> bool {
584    let Some(path) = e.kpse.as_ref().and_then(|k| {
585        k.borrow_mut()
586            .find_file("luaotfload.sty", retex_kpath::FileFormat::Tex)
587    }) else {
588        return false;
589    };
590    let Ok(text) = std::fs::read(path) else {
591        return false;
592    };
593    let text = String::from_utf8_lossy(&text);
594    let Some(at) = text.find("\\ProvidesPackage{luaotfload}") else {
595        return false;
596    };
597    let rest = &text[at..];
598    let Some(v) = rest.find(" v").map(|i| &rest[i + 2..]) else {
599        return false;
600    };
601    let ver: String = v
602        .chars()
603        .take_while(|c| c.is_ascii_digit() || *c == '.')
604        .collect();
605    let mut parts = ver.split('.').map(|p| p.parse::<u32>().unwrap_or(0));
606    let (major, minor) = (parts.next().unwrap_or(0), parts.next().unwrap_or(0));
607    (major, minor) < (3, 29) && major > 0
608}
609
610/// Loads a font for the `define_font` callback and registers it with the
611/// engine. Returns the engine font id, or nil if the font cannot be loaded.
612fn define_font(lua: &Lua, name: &str, size: i32) -> mlua::Result<Value> {
613    let state = lua
614        .app_data_ref::<Rc<RefCell<FontState>>>()
615        .map(|s| s.clone())
616        .ok_or_else(|| mlua::Error::runtime("font state missing"))?;
617    if let Some(&id) = state.borrow().tfm_ids.get(&(name.to_owned(), size)) {
618        return Ok(Value::Integer(i64::from(id)));
619    }
620    let remember = |id: u32| {
621        state
622            .borrow_mut()
623            .tfm_ids
624            .insert((name.to_owned(), size), id);
625    };
626    // An anonymous request is a TeX metric file if there is one (the
627    // first step of luaotfload's "tex,path,name" anonymous lookup).
628    if let Ok(req) = retex_font::FontRequest::parse(name)
629        && let retex_font::FontSource::Any(base) = &req.source
630    {
631        let id = with_engine(lua, |e| {
632            let f = e.load_tfm_font(base.as_bytes(), size, name.as_bytes());
633            if let Some(f) = f {
634                e.check_lua_loaded_font(f);
635            }
636            f
637        })?;
638        if let Some(i) = id {
639            remember(i);
640            return Ok(Value::Integer(i64::from(i)));
641        }
642    }
643    // luaotfload loads its font name database for the first font that is
644    // no TeX metric file (also for file lookups) and says so in the log.
645    {
646        if !state.borrow().db_announced {
647            state.borrow_mut().db_announced = true;
648            let dir = with_engine(lua, names_cache_dir)?;
649            let texio: mlua::Table = lua.globals().get("texio")?;
650            let write: mlua::Function = texio.get("write_nl")?;
651            write.call::<()>((
652                "log",
653                format!(
654                    "luaotfload | db : Font names database loaded from {dir}/luaotfload-names.luc.gz"
655                ),
656            ))?;
657        }
658        // A lookup by name: `name:`, or an anonymous request that is no
659        // file's path.
660        let by_name = match retex_font::FontRequest::parse(name).map(|r| r.source) {
661            Ok(retex_font::FontSource::Name(_)) => true,
662            Ok(retex_font::FontSource::Any(s)) => !std::path::Path::new(&s).is_file(),
663            _ => false,
664        };
665        if by_name && !state.borrow().lookup_announced {
666            state.borrow_mut().lookup_announced = true;
667            let cache = format!(
668                "{}/luaotfload-lookup-cache.luc",
669                with_engine(lua, names_cache_dir)?
670            );
671            if std::path::Path::new(&cache).is_file() {
672                let texio: mlua::Table = lua.globals().get("texio")?;
673                let write: mlua::Function = texio.get("write_nl")?;
674                write.call::<()>((
675                    "log",
676                    format!("luaotfload | cache : Lookup cache loaded from {cache}."),
677                ))?;
678            }
679        }
680    }
681    let loaded: Result<Arc<LoadedFont>, String> = with_engine(lua, |e| {
682        let mut st = state.borrow_mut();
683        if st.loader.is_none() {
684            let k = e
685                .kpse
686                .clone()
687                .ok_or_else(|| "kpathsea unavailable".to_owned())?;
688            st.loader = Some(retex_font::FontLoader::new(k));
689        }
690        st.loader
691            .as_mut()
692            .expect("loader initialized")
693            .load(name, size)
694            .map_err(|e| e.to_string())
695    })?;
696    let Ok(font) = loaded else {
697        // luaotfload falls back to TeX metric files (e.g. `cmr10`).
698        let plain = name.strip_prefix("file:").unwrap_or(name);
699        let plain = plain.split(':').next().unwrap_or(plain).to_owned();
700        let id = with_engine(lua, |e| {
701            let f = e.load_tfm_font(plain.as_bytes(), size, name.as_bytes());
702            if let Some(f) = f {
703                e.check_lua_loaded_font(f);
704            }
705            f
706        })?;
707        if let Some(i) = id {
708            remember(i);
709        }
710        return Ok(id.map_or(Value::Nil, |i| Value::Integer(i64::from(i))));
711    };
712    // The font file is one the job depends on, as luatex's -recorder
713    // lists it (observed).
714    with_engine(lua, |e| {
715        if !e.files.watched.contains(&font.tex.path) {
716            e.files.watched.push(font.tex.path.clone());
717        }
718    })?;
719    let key = (Arc::as_ptr(&font) as usize, size < 0);
720    if let Some(&id) = state.borrow().ids.get(&key) {
721        return Ok(Value::Integer(i64::from(id)));
722    }
723    let known = state.borrow().legacy_design_size;
724    let legacy = match known {
725        Some(l) => l,
726        None => with_engine(lua, legacy_luaotfload)?,
727    };
728    state.borrow_mut().legacy_design_size = Some(legacy);
729    let id = with_engine(lua, |e| {
730        let tex = &font.tex;
731        let design_size = if legacy {
732            retex_font::texfont::design_size(&font.file).round() as i32
733        } else {
734            tex.design_size
735        };
736        let mut params = vec![0; tex.params.len() + 1];
737        for (i, p) in tex.params.iter().enumerate() {
738            params[i + 1] = *p;
739        }
740        let mut chars = HashMap::with_capacity_and_hasher(
741            tex.num_chars(),
742            retex_engine::hash::FastHash::default(),
743        );
744        for (code, m) in tex.chars() {
745            chars.insert(
746                code,
747                CharMetrics {
748                    width: m.width,
749                    height: m.height,
750                    depth: m.depth,
751                    italic: m.italic,
752                    index: u32::from(m.glyph),
753                },
754            );
755        }
756        let info = FontInfo {
757            name: tex.name.clone().into_bytes(),
758            size: tex.size,
759            design_size,
760            checksum: 0,
761            params,
762            chars,
763            hyphen_char: i32::MIN,
764            skew_char: i32::MIN,
765            kind: FontKind::OpenType,
766            handle: 0,
767            char_codes: Default::default(),
768            ignore_ligatures: 0,
769            data: Some(FontData(Rc::new(font.clone()))),
770            lig_kern: Default::default(),
771            right_boundary: None,
772            expansion: None,
773            size_spec: i32::MIN,
774            vf: None,
775            math_chars: {
776                let mc = tex.math_chars(&font.file);
777                (!mc.is_empty()).then(|| Rc::new(convert_math_chars(mc)))
778            },
779            math_constants: tex.math.as_ref().map(|m| {
780                Rc::new(
781                    m.iter()
782                        .map(|(k, v)| ((*k).to_owned(), v.to_sp()))
783                        .collect(),
784                )
785            }),
786        };
787        e.register_font(info)
788    })?;
789    state.borrow_mut().ids.insert(key, id);
790    Ok(Value::Integer(i64::from(id)))
791}
792
793/// Converts the font crate's math data to the engine's, rounding the
794/// dimensions to scaled points.
795fn convert_math_chars(
796    mc: HashMap<u32, retex_font::texfont::MathChar>,
797) -> HashMap<u32, retex_engine::fonts::MathCharInfo> {
798    use retex_engine::fonts::{ExtPart, MathCharInfo};
799    let parts = |v: &Option<Vec<retex_font::texfont::MathPart>>| {
800        v.as_ref().map(|ps| {
801            ps.iter()
802                .map(|p| ExtPart {
803                    code: p.code,
804                    start: retex_font::texfont::round_sp(p.start),
805                    end: retex_font::texfont::round_sp(p.end),
806                    advance: retex_font::texfont::round_sp(p.advance),
807                    extender: p.extender,
808                })
809                .collect::<Rc<[ExtPart]>>()
810        })
811    };
812    mc.into_iter()
813        .map(|(c, m)| {
814            (
815                c,
816                MathCharInfo {
817                    next: m.next,
818                    top_accent: m.top_accent.map(retex_font::texfont::round_sp),
819                    vert: parts(&m.vert),
820                    horiz: parts(&m.horiz),
821                },
822            )
823        })
824        .collect()
825}
826
827/// The OpenType font behind engine font `f`, if any.
828pub fn loaded_font(e: &Engine, f: u32) -> Option<Arc<LoadedFont>> {
829    let data = e.font(f).data.as_ref()?;
830    data.0.downcast_ref::<Arc<LoadedFont>>().cloned()
831}
832
833pub use crate::otfnodes::process_list;
834
835#[cfg(test)]
836mod tests {
837    #[test]
838    fn lualibs_utf() {
839        let lua = mlua::Lua::new();
840        let utf: mlua::Table = lua.load(super::UTF_LUA).eval().expect("utf");
841        lua.globals().set("utf", utf).expect("set");
842        let r: String = lua
843            .load(
844                r##"local o = {}
845                   local function add(...) o[#o + 1] = table.concat({...}, ",") end
846                   add(utf.char(65, 0x3B1), utf.byte("\u{3B1}b"), select("#", utf.byte("")))
847                   add(utf.len("\u{3B1}b"), utf.sub("\u{3B1}\u{3B2}c", 2, 3),
848                       utf.sub("\u{3B1}\u{3B2}c", 2), utf.sub("\u{3B1}\u{3B2}c", -1))
849                   for c in utf.characters("a\u{3B1}") do add(c) end
850                   for c in utf.values("a\u{3B1}") do add(c) end
851                   return table.concat(o, "|")"##,
852            )
853            .eval()
854            .expect("run");
855        // Observed in lualatex.
856        assert_eq!(r, "A\u{3B1},945,0|2,\u{3B2}c,\u{3B2}c,c|a|\u{3B1}|97|945");
857    }
858}