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