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, bool), u32>,
41 legacy_design_size: Option<bool>,
46 db_announced: bool,
48 lookup_announced: bool,
51}
52
53const 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
319const 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
354pub 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 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 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 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 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
518fn 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
546fn 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
575fn 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 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 {
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 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 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
738fn 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
772pub 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 assert_eq!(r, "A\u{3B1},945,0|2,\u{3B2}c,\u{3B2}c,c|a|\u{3B1}|97|945");
802 }
803}