1use 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#[derive(Default)]
32pub struct FontState {
33 loader: Option<retex_font::FontLoader>,
34 ids: HashMap<usize, u32>,
39 db_announced: bool,
41}
42
43const MAIN_LUA: &str = r#"
46local native = ...
47local lb = luatexbase
48local function noop(...) return ... end
49local function log(cat, msg)
50 texio.write_nl("log", "luaotfload | " .. cat .. " : " .. msg)
51end
52luaotfload = luaotfload or {}
53luaotfload.define_font = native.define_font
54luaotfload.node_processor = native.node_processor
55luaotfload.version = luaotfload.version or "3.29"
56-- Letterspacing options (microtype replaces keepligature).
57luaotfload.letterspace = luaotfload.letterspace or
58 { keepligature = true, keeptogether = false, keepwordspacing = false }
59luaotfload.features = luaotfload.features or { defaults = {}, global = {} }
60local aux = luaotfload.aux or {}
61luaotfload.aux = aux
62for _, k in ipairs{"provides_script", "provides_language", "provides_feature",
63 "get_math_dimension", "font_has_glyph", "slot_of_name", "gid_of_name"} do
64 aux[k] = native["aux_" .. k]
65end
66aux.sprint_math_dimension = function(f, name)
67 tex.sprint(-2, tostring(aux.get_math_dimension(f, name)))
68end
69-- The parts of the fontloader's `fonts` table that packages touch.
70fonts = fonts or {}
71fonts.names = fonts.names or {}
72fonts.names.set_location_precedence = fonts.names.set_location_precedence or function() end
73fonts.hashes = fonts.hashes or {}
74fonts.hashes.identifiers = fonts.hashes.identifiers or setmetatable({}, {
75 __index = function(_, id) if type(id) == "number" then return font.getfont(id) end end })
76local initialized = false
77luaotfload.main = function()
78 if initialized then
79 log("load", "Luaotfload initialization requested but is already loaded, ignoring.")
80 return
81 end
82 initialized = true
83 lb.provides_module({ name = "luaotfload", date = "2024-12-03", version = "3.29",
84 description = "Lua based OpenType font support" })
85 lb.provides_module({ name = "lualibs", date = "2023-07-13", version = "2.76",
86 description = "ConTeXt Lua standard libraries." })
87 lb.provides_module({ name = "lualibs-extended", date = "2023-07-13", version = "2.76",
88 description = "ConTeXt Lua libraries -- extended collection." })
89 -- lualibs' global `utf` (luamml uses it).
90 if not utf then utf = native.lualibs_utf() end
91 local cache = native.cache_dir()
92 log("conf", 'Root cache directory is "' .. cache .. '".')
93 -- As luaotfload does before loading the fontloader, which wants the
94 -- attribute with index zero to be zero.
95 tex.attribute[0] = 0
96 -- luaotfload's set-up holds six Lua references (observed in the
97 -- numbers box displays show for Lua data).
98 native.reserve_lua_refs(6)
99 local loader = kpse.find_file("fontloader-2023-12-28.lua", "lua") or "fontloader-2023-12-28.lua"
100 log("init", 'Loading fontloader "fontloader-2023-12-28.lua" from kpse-resolved path "' .. loader .. '".')
101 -- As the fontloader does: node properties are flushed with their
102 -- nodes and inherited by copies, for good.
103 node.direct.set_properties_mode(true, true)
104 function node.direct.set_properties_mode() end
105 lb.new_attribute("luaotfload@noligature")
106 log("init", "Context OpenType loader version 3.134")
107 -- luaotfload's node processor runs the pre_shaping_filter and
108 -- post_shaping_filter callbacks around the shaping. They are only
109 -- called once something is added to them.
110 if not lb.callbacktypes.pre_shaping_filter then
111 lb.create_callback("pre_shaping_filter", "list")
112 lb.create_callback("post_shaping_filter", "reverselist")
113 end
114 local shape = native.node_processor
115 local dispatch = shape
116 local call_callback = lb.call_callback
117 local function with_shaping_filters(head, groupcode, _, _, direction)
118 if not direction then
119 direction = tex.get("textdirection")
120 else
121 direction = direction == "TRT" and 1 or 0
122 end
123 local result = call_callback("pre_shaping_filter", head, groupcode, direction)
124 if result == false then
125 return false
126 elseif result ~= true then
127 head = result
128 end
129 result = shape(head, groupcode)
130 if result == false then
131 node.flush_list(head)
132 return nil
133 elseif result ~= true then
134 head = result
135 end
136 result = call_callback("post_shaping_filter", head, groupcode, direction)
137 if result == false then
138 node.flush_list(head)
139 return nil
140 elseif result == true then
141 return head
142 else
143 return result
144 end
145 end
146 local add_to_callback = lb.add_to_callback
147 function lb.add_to_callback(name, ...)
148 if name == "pre_shaping_filter" or name == "post_shaping_filter" then
149 dispatch = with_shaping_filters
150 end
151 return add_to_callback(name, ...)
152 end
153 local function node_processor(head, groupcode, size, packtype, direction)
154 return dispatch(head, groupcode, size, packtype, direction)
155 end
156 lb.add_to_callback("pre_linebreak_filter", node_processor, "luaotfload.node_processor")
157 lb.add_to_callback("hpack_filter", node_processor, "luaotfload.node_processor")
158 lb.add_to_callback("glyph_stream_provider", noop, "luaotfload.glyph_stream")
159 lb.add_to_callback("define_font", native.define_font, "luaotfload.define_font")
160 lb.new_attribute("luaotfload_color_attribute")
161 log("conf", 'Root cache directory is "' .. cache .. '".')
162 lb.add_to_callback("find_opentype_file", noop, "luaotfload.harf.strip_prefix")
163 lb.add_to_callback("find_truetype_file", noop, "luaotfload.harf.strip_prefix")
164 lb.remove_from_callback("glyph_stream_provider", "luaotfload.glyph_stream")
165 lb.add_to_callback("glyph_stream_provider", noop, "luaotfload.harf.glyphstream")
166 lb.add_to_callback("post_linebreak_filter", noop, "luaotfload.harf.finalize_vlist")
167 lb.add_to_callback("hpack_filter", noop, "luaotfload.harf.finalize_hlist")
168 lb.add_to_callback("wrapup_run", noop, "luaotfload.cleanup_files")
169 lb.add_to_callback("finish_pdffile", noop, "luaotfload.harf.finalize_unicode")
170 -- Box displays show the character as UTF-8 (observed: "^^@" for 0
171 -- and ^^^^^^ notation beyond Unicode).
172 lb.add_to_callback("glyph_info", function(n)
173 local c = n.char
174 if c == 0 then return "^^@" end
175 if c > 0x10FFFF then return string.format("^^^^^^%06x", c) end
176 return utf8.char(c)
177 end, "luaotfload.glyphinfo")
178 lb.new_attribute("luaotfload.letterspace_done")
179 lb.create_callback("luaotfload.patch_font", "simple", noop)
180 lb.create_callback("luaotfload.patch_font_unsafe", "simple", noop)
181 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_sscale_dimens")
182 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_font_index")
183 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.patch_cambria_domh")
184 lb.add_to_callback("luaotfload.patch_font_unsafe", noop, "luaotfload.aux.fixup_fontdata")
185 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_capheight")
186 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_xheight")
187 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.rewrite_fontname")
188 -- The colour interface of luaotfload's colors module, set up by
189 -- main() (l3backend checks for set_transparent_colorstack before
190 -- hooking into parse_color).
191 function luaotfload.add_colorscheme() end
192 function luaotfload.set_colorhandler() end
193 function luaotfload.set_transparenthandler() end
194 function luaotfload.set_transparent_colorstack() end
195 lb.create_callback("luaotfload.split_color", "exclusive", noop)
196 lb.create_callback("luaotfload.parse_color", "exclusive", noop)
197 lb.create_callback("luaotfload.parse_transparent", "exclusive", noop)
198 return true
199end
200return luaotfload
201"#;
202
203const UTF_LUA: &str = r#"
208local u = {}
209u.char = utf8.char
210u.len = utf8.len
211function u.byte(s)
212 if s and #s > 0 then return utf8.codepoint(s, 1) end
213end
214function u.sub(s, i, j)
215 local n = utf8.len(s) or 0
216 i = i or 1
217 j = j or -1
218 if i < 0 then i = math.max(n + i + 1, 1) elseif i == 0 then i = 1 end
219 if j < 0 then j = n + j + 1 elseif j > n then j = n end
220 if i > j then return "" end
221 local b = utf8.offset(s, i)
222 local e = utf8.offset(s, j + 1)
223 return s:sub(b, (e or #s + 1) - 1)
224end
225function u.characters(s)
226 return s:gmatch(utf8.charpattern)
227end
228function u.values(s)
229 local f = s:gmatch(utf8.charpattern)
230 return function()
231 local c = f()
232 if c then return utf8.codepoint(c) end
233 end
234end
235return u
236"#;
237
238pub fn install(lua: &Lua) -> mlua::Result<()> {
240 lua.set_app_data(Rc::new(RefCell::new(FontState::default())));
241 let native = lua.create_table()?;
242 native.set(
243 "define_font",
244 lua.create_function(|lua, (name, size, _id): (mlua::String, i64, Option<i64>)| {
245 let name = name.to_str()?.to_owned();
246 let t0 = std::time::Instant::now();
247 let before = with_engine(lua, |e| e.fonts.fonts.len())?;
248 let r = define_font(lua, &name, size as i32);
249 retex_engine::timing::FONTS.with(|a| a.add(t0));
250 with_engine(lua, |e| {
253 for _ in before..e.fonts.fonts.len() {
254 e.nodes.lua_refs.font_defined(true);
255 }
256 })?;
257 r
258 })?,
259 )?;
260 native.set(
261 "reserve_lua_refs",
262 lua.create_function(|lua, n: i64| {
263 with_engine(lua, |e| {
264 for _ in 0..n {
265 e.nodes.lua_refs.take();
266 }
267 })
268 })?,
269 )?;
270 native.set(
271 "node_processor",
272 lua.create_function(|lua, (head, _group): (Value, Value)| {
273 let h = match &head {
274 Value::UserData(u) => u
275 .borrow::<crate::nodelib::LNode>()
276 .ok()
277 .map(|n| NodeRef(n.0)),
278 _ => None,
279 };
280 if let Some(h) = h {
281 let nh = with_engine(lua, |e| process_list(e, h))?;
282 if nh != h {
283 return Ok(Value::UserData(
284 lua.create_userdata(crate::nodelib::LNode(nh.0))?,
285 ));
286 }
287 }
288 Ok(Value::Boolean(true))
289 })?,
290 )?;
291 native.set(
292 "cache_dir",
293 lua.create_function(|lua, ()| with_engine(lua, names_cache_dir))?,
294 )?;
295 let utf = lua
296 .load(UTF_LUA)
297 .set_name("=lualibs-unicode")
298 .into_function()?;
299 native.set("lualibs_utf", utf)?;
300 crate::otfaux::install(lua, &native)?;
301 let loader = lua.create_function(move |lua, ()| {
302 let kpse: mlua::Table = lua.globals().get("kpse")?;
305 let find: mlua::Function = kpse.get("find_file")?;
306 if find.call::<Value>(("luaotfload-main.lua", "lua"))?.is_nil() {
307 return Err(mlua::Error::runtime("module 'luaotfload-main' not found"));
308 }
309 let f = lua
310 .load(MAIN_LUA)
311 .set_name("=luaotfload-main")
312 .into_function()?;
313 let native: mlua::Table = lua.named_registry_value("retex.luaotfload.native")?;
314 f.call::<Value>(native)
315 })?;
316 lua.set_named_registry_value("retex.luaotfload.native", native)?;
317 let package: mlua::Table = lua.globals().get("package")?;
318 let preload: mlua::Table = package.get("preload")?;
319 preload.set("luaotfload-main", loader)?;
320 let harfbuzz = lua
325 .load(
326 r#"local hb = { shapers = function() return "graphite2", "ot", "fallback" end,
327 version = function() return "12.3.2" end }
328 for _, k in ipairs{"Blob", "Buffer", "Direction", "Face", "Feature", "Font",
329 "Language", "Script", "Tag", "Variation", "ot", "unicode"} do
330 hb[k] = {}
331 end
332 return function() return hb end"#,
333 )
334 .set_name("=[luaharfbuzz]")
335 .call::<mlua::Function>(())?;
336 preload.set("luaharfbuzz", harfbuzz)?;
337 Ok(())
338}
339
340fn names_cache_dir(e: &mut retex_engine::Engine) -> String {
345 let Some(k) = e.kpse.as_ref() else {
346 return String::new();
347 };
348 let k = k.borrow();
349 let writable = |dir: &str| {
350 let probe = std::path::Path::new(dir).join(format!(".retex-probe-{}", std::process::id()));
351 let ok = std::fs::File::create(&probe).is_ok();
352 let _ = std::fs::remove_file(&probe);
353 ok
354 };
355 let root = k
356 .var_value("TEXMFCACHE")
357 .unwrap_or_default()
358 .split(':')
359 .find(|d| {
360 !d.is_empty() && k.name_ok(d, retex_kpath::Access::Write, true).is_ok() && writable(d)
361 })
362 .map(str::to_owned)
363 .or_else(|| k.var_value("TEXMFVAR"))
364 .unwrap_or_default();
365 format!("{root}/luatex-cache/generic/names")
366}
367
368fn define_font(lua: &Lua, name: &str, size: i32) -> mlua::Result<Value> {
371 let state = lua
372 .app_data_ref::<Rc<RefCell<FontState>>>()
373 .map(|s| s.clone())
374 .ok_or_else(|| mlua::Error::runtime("font state missing"))?;
375 if let Ok(req) = retex_font::FontRequest::parse(name)
378 && let retex_font::FontSource::Any(base) = &req.source
379 {
380 let id = with_engine(lua, |e| {
381 e.load_tfm_font(base.as_bytes(), size, name.as_bytes())
382 })?;
383 if let Some(i) = id {
384 return Ok(Value::Integer(i64::from(i)));
385 }
386 }
387 {
390 if !state.borrow().db_announced {
391 state.borrow_mut().db_announced = true;
392 let dir = with_engine(lua, names_cache_dir)?;
393 let texio: mlua::Table = lua.globals().get("texio")?;
394 let write: mlua::Function = texio.get("write_nl")?;
395 write.call::<()>((
396 "log",
397 format!(
398 "luaotfload | db : Font names database loaded from {dir}/luaotfload-names.luc.gz"
399 ),
400 ))?;
401 }
402 }
403 let loaded: Result<Arc<LoadedFont>, String> = with_engine(lua, |e| {
404 let mut st = state.borrow_mut();
405 if st.loader.is_none() {
406 let k = e
407 .kpse
408 .clone()
409 .ok_or_else(|| "kpathsea unavailable".to_owned())?;
410 st.loader = Some(retex_font::FontLoader::new(k));
411 }
412 st.loader
413 .as_mut()
414 .expect("loader initialized")
415 .load(name, size)
416 .map_err(|e| e.to_string())
417 })?;
418 let Ok(font) = loaded else {
419 let plain = name.strip_prefix("file:").unwrap_or(name);
421 let plain = plain.split(':').next().unwrap_or(plain).to_owned();
422 let id = with_engine(lua, |e| {
423 e.load_tfm_font(plain.as_bytes(), size, name.as_bytes())
424 })?;
425 return Ok(id.map_or(Value::Nil, |i| Value::Integer(i64::from(i))));
426 };
427 let key = Arc::as_ptr(&font) as usize;
428 if let Some(&id) = state.borrow().ids.get(&key) {
429 return Ok(Value::Integer(i64::from(id)));
430 }
431 let id = with_engine(lua, |e| {
432 let tex = &font.tex;
433 let mut params = vec![0; tex.params.len() + 1];
434 for (i, p) in tex.params.iter().enumerate() {
435 params[i + 1] = *p;
436 }
437 let mut chars = HashMap::with_capacity_and_hasher(
438 tex.num_chars(),
439 retex_engine::hash::FastHash::default(),
440 );
441 for (code, m) in tex.chars() {
442 chars.insert(
443 code,
444 CharMetrics {
445 width: m.width,
446 height: m.height,
447 depth: m.depth,
448 italic: m.italic,
449 index: u32::from(m.glyph),
450 },
451 );
452 }
453 let info = FontInfo {
454 name: tex.name.clone().into_bytes(),
455 size: tex.size,
456 design_size: tex.design_size,
457 checksum: 0,
458 params,
459 chars,
460 hyphen_char: i32::MIN,
461 skew_char: i32::MIN,
462 kind: FontKind::OpenType,
463 handle: 0,
464 char_codes: Default::default(),
465 ignore_ligatures: 0,
466 data: Some(FontData(Rc::new(font.clone()))),
467 lig_kern: Default::default(),
468 right_boundary: None,
469 expansion: None,
470 size_spec: i32::MIN,
471 vf: None,
472 math_chars: {
473 let mc = tex.math_chars(&font.file);
474 (!mc.is_empty()).then(|| Rc::new(convert_math_chars(mc)))
475 },
476 math_constants: tex.math.as_ref().map(|m| {
477 Rc::new(
478 m.iter()
479 .map(|(k, v)| ((*k).to_owned(), v.to_sp()))
480 .collect(),
481 )
482 }),
483 };
484 e.register_font(info)
485 })?;
486 state.borrow_mut().ids.insert(key, id);
487 Ok(Value::Integer(i64::from(id)))
488}
489
490fn convert_math_chars(
493 mc: HashMap<u32, retex_font::texfont::MathChar>,
494) -> HashMap<u32, retex_engine::fonts::MathCharInfo> {
495 use retex_engine::fonts::{ExtPart, MathCharInfo};
496 let parts = |v: &Option<Vec<retex_font::texfont::MathPart>>| {
497 v.as_ref().map(|ps| {
498 ps.iter()
499 .map(|p| ExtPart {
500 code: p.code,
501 start: retex_font::texfont::round_sp(p.start),
502 end: retex_font::texfont::round_sp(p.end),
503 advance: retex_font::texfont::round_sp(p.advance),
504 extender: p.extender,
505 })
506 .collect::<Rc<[ExtPart]>>()
507 })
508 };
509 mc.into_iter()
510 .map(|(c, m)| {
511 (
512 c,
513 MathCharInfo {
514 next: m.next,
515 top_accent: m.top_accent.map(retex_font::texfont::round_sp),
516 vert: parts(&m.vert),
517 horiz: parts(&m.horiz),
518 },
519 )
520 })
521 .collect()
522}
523
524pub fn loaded_font(e: &Engine, f: u32) -> Option<Arc<LoadedFont>> {
526 let data = e.font(f).data.as_ref()?;
527 data.0.downcast_ref::<Arc<LoadedFont>>().cloned()
528}
529
530pub use crate::otfnodes::process_list;
531
532#[cfg(test)]
533mod tests {
534 #[test]
535 fn lualibs_utf() {
536 let lua = mlua::Lua::new();
537 let utf: mlua::Table = lua.load(super::UTF_LUA).eval().expect("utf");
538 lua.globals().set("utf", utf).expect("set");
539 let r: String = lua
540 .load(
541 r##"local o = {}
542 local function add(...) o[#o + 1] = table.concat({...}, ",") end
543 add(utf.char(65, 0x3B1), utf.byte("\u{3B1}b"), select("#", utf.byte("")))
544 add(utf.len("\u{3B1}b"), utf.sub("\u{3B1}\u{3B2}c", 2, 3),
545 utf.sub("\u{3B1}\u{3B2}c", 2), utf.sub("\u{3B1}\u{3B2}c", -1))
546 for c in utf.characters("a\u{3B1}") do add(c) end
547 for c in utf.values("a\u{3B1}") do add(c) end
548 return table.concat(o, "|")"##,
549 )
550 .eval()
551 .expect("run");
552 assert_eq!(r, "A\u{3B1},945,0|2,\u{3B2}c,\u{3B2}c,c|a|\u{3B1}|97|945");
554 }
555}