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 tfm_ids: HashMap<(String, i32), u32>,
46 legacy_design_size: Option<bool>,
51 db_announced: bool,
53 lookup_announced: bool,
56}
57
58const 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 -- Letterspacing (`kernfactor=f` or `letterspace=p`, p = 100 f, as
171 -- microtype's \textls asks for): after shaping, a glyph of such a font
172 -- that follows a glyph of one gets the font's quad times f before it.
173 -- Observed with luaotfload 3.29: an adjacent glyph or whatsit gets a
174 -- new font kern; a kern (not an italic correction) is enlarged; glue of
175 -- positive width too, its stretch and shrink in proportion (not with
176 -- `keepwordspacing`); penalties, rules, boxes and math get nothing.
177 -- Discretionaries get the kerns inside their lists: the pre-break text
178 -- after the glyph before, the post-break text before the glyph after,
179 -- the replacement on both sides. Ligatures are split into their
180 -- characters unless `keepligature` (true or a function of the direct
181 -- node) keeps them. Processed glyphs get the attribute
182 -- `luaotfload.letterspace_done`, so that a list packed again is not
183 -- spaced twice.
184 local letterspaced = {}
185 local any_letterspaced = false
186 local GLYPH_ID, KERN_ID, GLUE_ID, DISC_ID, WHATSIT_ID =
187 node.id("glyph"), node.id("kern"), node.id("glue"), node.id("disc"), node.id("whatsit")
188 local ITALIC = 3
189 local ls_done
190 local function ls_kern(amount, model)
191 local k = node.new(KERN_ID, 0)
192 k.kern = amount
193 k.attr = model.attr
194 return k
195 end
196 local function keep_ligature(n)
197 local keep = luaotfload.letterspace.keepligature
198 if type(keep) == "function" then
199 return keep(node.direct.todirect(n))
200 end
201 return keep
202 end
203 -- The characters of ligature `n`, ligatures among them split too.
204 local function split_ligature(n)
205 local first, last
206 local c = n.components
207 while c do
208 local nxt = c.next
209 local parts = c.components and split_ligature(c) or node.copy(c)
210 if c.components then
211 parts.prev = nil
212 else
213 parts.next = nil
214 parts.prev = nil
215 end
216 if last then
217 last.next = parts
218 parts.prev = last
219 else
220 first = parts
221 end
222 last = node.tail(parts)
223 c = nxt
224 end
225 return first
226 end
227 local letterspace_list
228 -- Adds the space before the glyph `after` that follows the list `head`
229 -- (a discretionary's): the list's last node is to it what a glyph's
230 -- predecessor is.
231 local function space_after(head, after)
232 local amount = letterspaced[after.font]
233 local t = node.tail(head)
234 local id = t.id
235 if id == KERN_ID and t.subtype ~= ITALIC then
236 t.kern = t.kern + amount
237 elseif id == GLUE_ID then
238 local w = t.width
239 if w > 0 and not luaotfload.letterspace.keepwordspacing then
240 local nw = w + amount
241 t.stretch = math.floor(t.stretch * nw / w + 0.5)
242 t.shrink = math.floor(t.shrink * nw / w + 0.5)
243 t.width = nw
244 end
245 elseif id == GLYPH_ID or id == WHATSIT_ID then
246 node.insert_after(head, t, ls_kern(amount, after))
247 end
248 end
249 -- Spaces the glyph `n` after `last` (the glyph before it, if any).
250 local function space_glyph(head, n, last)
251 local amount = letterspaced[n.font]
252 if not amount or not last or not letterspaced[last.font]
253 or node.has_attribute(n, ls_done) then
254 return head
255 end
256 local p = n.prev
257 local id = p and p.id
258 -- The first glyph of a discretionary's list follows `last` outside.
259 if not p or id == GLYPH_ID or id == WHATSIT_ID then
260 head = node.insert_before(head, n, ls_kern(amount, n))
261 elseif id == KERN_ID and p.subtype ~= ITALIC then
262 p.kern = p.kern + amount
263 elseif id == GLUE_ID then
264 local w = p.width
265 if w <= 0 or luaotfload.letterspace.keepwordspacing then return head end
266 local nw = w + amount
267 p.stretch = math.floor(p.stretch * nw / w + 0.5)
268 p.shrink = math.floor(p.shrink * nw / w + 0.5)
269 p.width = nw
270 else
271 return head
272 end
273 node.set_attribute(n, ls_done, 1)
274 return head
275 end
276 -- Letterspaces the list `head` whose glyph before is `last`; returns
277 -- the new head and the last glyph.
278 letterspace_list = function(head, last)
279 local n = head
280 while n do
281 local id = n.id
282 if id == GLYPH_ID then
283 if n.components and letterspaced[n.font] and not keep_ligature(n) then
284 local parts = split_ligature(n)
285 local first = parts
286 head = node.insert_before(head, n, parts)
287 head = node.remove(head, n)
288 node.free(n)
289 n = first
290 end
291 head = space_glyph(head, n, last)
292 last = n
293 elseif id == DISC_ID then
294 -- A discretionary before any glyph is left alone (observed).
295 if last and not node.has_attribute(n, ls_done) then
296 local nxt = n.next
297 local after = nxt and nxt.id == GLYPH_ID and letterspaced[nxt.font] and nxt
298 local pre, post, replace = n.pre, n.post, n.replace
299 local tail
300 pre = letterspace_list(pre, last)
301 post, tail = letterspace_list(post, nil)
302 if post and tail and after and letterspaced[tail.font] then
303 space_after(post, after)
304 end
305 if replace then
306 replace, tail = letterspace_list(replace, last)
307 if tail and after and letterspaced[tail.font] then
308 space_after(replace, after)
309 end
310 elseif after and letterspaced[last.font] then
311 replace = ls_kern(letterspaced[after.font], after)
312 end
313 n.pre, n.post, n.replace = pre, post, replace
314 node.set_attribute(n, ls_done, 1)
315 end
316 end
317 n = n.next
318 end
319 return head, last
320 end
321 local native_shape = native.node_processor
322 local function shape(head, groupcode)
323 local result = native_shape(head, groupcode)
324 if not any_letterspaced or result == false then
325 return result
326 end
327 local h = result == true and head or result
328 if not h then return result end
329 return (letterspace_list(h, nil))
330 end
331 local dispatch = shape
332 local call_callback = lb.call_callback
333 local function with_shaping_filters(head, groupcode, _, _, direction)
334 if not direction then
335 direction = tex.get("textdirection")
336 else
337 direction = direction == "TRT" and 1 or 0
338 end
339 local result = call_callback("pre_shaping_filter", head, groupcode, direction)
340 if result == false then
341 return false
342 elseif result ~= true then
343 head = result
344 end
345 result = shape(head, groupcode)
346 if result == false then
347 node.flush_list(head)
348 return nil
349 elseif result ~= true then
350 head = result
351 end
352 result = call_callback("post_shaping_filter", head, groupcode, direction)
353 if result == false then
354 node.flush_list(head)
355 return nil
356 elseif result == true then
357 return head
358 else
359 return result
360 end
361 end
362 local add_to_callback = lb.add_to_callback
363 function lb.add_to_callback(name, ...)
364 if name == "pre_shaping_filter" or name == "post_shaping_filter" then
365 dispatch = with_shaping_filters
366 end
367 return add_to_callback(name, ...)
368 end
369 local function node_processor(head, groupcode, size, packtype, direction)
370 return dispatch(head, groupcode, size, packtype, direction)
371 end
372 lb.add_to_callback("pre_linebreak_filter", node_processor, "luaotfload.node_processor")
373 lb.add_to_callback("hpack_filter", node_processor, "luaotfload.node_processor")
374 lb.add_to_callback("glyph_stream_provider", noop, "luaotfload.glyph_stream")
375 -- Font colours (`color=RRGGBB[AA]`): a font with one adds the colour
376 -- handler to the callbacks below, which brackets its glyphs with
377 -- colour stack 0 whatsits (observed; the alpha part is not supported).
378 local colors = {}
379 local color_handler
380 -- The colour literal for `value`, a colour in hex (RRGGBB, the alpha
381 -- of RRGGBBAA left out) or a name: the `luaotfload.parse_color`
382 -- callback (LaTeX's l3color) makes it if set, else hex is converted.
383 local function color_literal(value)
384 local hex = value:match("^%x%x%x%x%x%x$") or value:match("^(%x%x%x%x%x%x)%x%x$")
385 local ok, s = pcall(lb.call_callback, "luaotfload.parse_color", hex or value)
386 if ok and type(s) == "string" then return s end
387 if not hex then return nil end
388 local function c(i)
389 local v = tonumber(hex:sub(i, i + 1), 16) / 255
390 return (string.format("%.3f", v):gsub("0+$", ""):gsub("%.$", ""))
391 end
392 return c(1) .. " " .. c(3) .. " " .. c(5) .. " rg"
393 end
394 local function define_font(name, size, id)
395 local f = native.define_font(name, size, id)
396 if type(f) == "number" and type(name) == "string" then
397 local factor = tonumber(name:match("[:;]kernfactor=([-%d.]+)") or "")
398 local percent = tonumber(name:match("[:;]letterspace=([-%d.]+)") or "")
399 factor = factor or (percent and percent / 100)
400 if factor and factor ~= 0 then
401 local quad = font.getfont(f).parameters.quad
402 letterspaced[f] = math.floor(factor * quad + 0.5)
403 ls_done = ls_done or luatexbase.attributes["luaotfload.letterspace_done"]
404 any_letterspaced = true
405 end
406 local value = name:match("[:;]color={?([^;}]+)}?")
407 local literal = value and not colors[f] and color_literal(value)
408 if literal then
409 colors[f] = literal
410 if not color_handler then
411 color_handler = luaotfload.color_handler
412 lb.add_to_callback("post_linebreak_filter", color_handler, "luaotfload.color_handler")
413 lb.add_to_callback("hpack_filter", color_handler, "luaotfload.color_handler")
414 lb.add_to_callback("post_mlist_to_hlist_filter", color_handler, "luaotfload.color_handler")
415 end
416 end
417 end
418 return f
419 end
420 local GLYPH, HLIST, VLIST, WHATSIT = node.id("glyph"), node.id("hlist"), node.id("vlist"), node.id("whatsit")
421 local LINE = 1
422 local function stack(command, data)
423 local w = node.new("whatsit", "pdf_colorstack")
424 w.stack = 0
425 w.command = command
426 if data then w.data = data end
427 return w
428 end
429 -- Colours the glyphs of `head` starting with colour `current`; returns
430 -- the new head and the colour at its end. Paragraph lines are coloured
431 -- on their own; other boxes and whatsits end a coloured run.
432 local function colorize(head, current)
433 local n = head
434 while n do
435 local id = n.id
436 if id == GLYPH then
437 local c = colors[n.font]
438 if c ~= current then
439 local w = not c and stack(2) or stack(current and 0 or 1, c)
440 head = node.insert_before(head, n, w)
441 current = c
442 end
443 elseif id == HLIST and n.subtype == LINE then
444 if current then
445 head = node.insert_before(head, n, stack(2))
446 current = nil
447 end
448 local list, last = colorize(n.list, nil)
449 if last then
450 list = node.insert_after(list, node.tail(list), stack(2))
451 end
452 n.list = list
453 elseif id == HLIST or id == VLIST or id == WHATSIT then
454 if current then
455 head = node.insert_before(head, n, stack(2))
456 current = nil
457 end
458 end
459 n = n.next
460 end
461 return head, current
462 end
463 function luaotfload.color_handler(head)
464 if not head then return head end
465 local h, last = colorize(head, nil)
466 if last then
467 h = node.insert_after(h, node.tail(h), stack(2))
468 end
469 return h
470 end
471 lb.add_to_callback("define_font", define_font, "luaotfload.define_font")
472 lb.new_attribute("luaotfload_color_attribute")
473 log("conf", 'Root cache directory is "' .. cache .. '".')
474 lb.add_to_callback("find_opentype_file", noop, "luaotfload.harf.strip_prefix")
475 lb.add_to_callback("find_truetype_file", noop, "luaotfload.harf.strip_prefix")
476 lb.remove_from_callback("glyph_stream_provider", "luaotfload.glyph_stream")
477 lb.add_to_callback("glyph_stream_provider", noop, "luaotfload.harf.glyphstream")
478 lb.add_to_callback("post_linebreak_filter", noop, "luaotfload.harf.finalize_vlist")
479 lb.add_to_callback("hpack_filter", noop, "luaotfload.harf.finalize_hlist")
480 lb.add_to_callback("wrapup_run", noop, "luaotfload.cleanup_files")
481 lb.add_to_callback("finish_pdffile", noop, "luaotfload.harf.finalize_unicode")
482 -- Box displays show the character as UTF-8 (observed: "^^@" for 0
483 -- and ^^^^^^ notation beyond Unicode).
484 lb.add_to_callback("glyph_info", function(n)
485 -- mode=harf glyphs show their text (a ligature's characters).
486 local text = native.harf_glyph_text(n.font, n.char)
487 if text then return text end
488 local c = n.char
489 if c == 0 then return "^^@" end
490 if c > 0x10FFFF then return string.format("^^^^^^%06x", c) end
491 return utf8.char(c)
492 end, "luaotfload.glyphinfo")
493 lb.new_attribute("luaotfload.letterspace_done")
494 lb.create_callback("luaotfload.patch_font", "simple", noop)
495 lb.create_callback("luaotfload.patch_font_unsafe", "simple", noop)
496 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_sscale_dimens")
497 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_font_index")
498 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.patch_cambria_domh")
499 lb.add_to_callback("luaotfload.patch_font_unsafe", noop, "luaotfload.aux.fixup_fontdata")
500 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_capheight")
501 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.aux.set_xheight")
502 lb.add_to_callback("luaotfload.patch_font", noop, "luaotfload.rewrite_fontname")
503 -- The colour interface of luaotfload's colors module, set up by
504 -- main() (l3backend checks for set_transparent_colorstack before
505 -- hooking into parse_color).
506 function luaotfload.add_colorscheme() end
507 function luaotfload.set_colorhandler() end
508 function luaotfload.set_transparenthandler() end
509 function luaotfload.set_transparent_colorstack() end
510 lb.create_callback("luaotfload.split_color", "exclusive", noop)
511 lb.create_callback("luaotfload.parse_color", "exclusive", noop)
512 lb.create_callback("luaotfload.parse_transparent", "exclusive", noop)
513 return true
514end
515return luaotfload
516"#;
517
518const UTF_LUA: &str = r#"
523local u = {}
524u.char = utf8.char
525u.len = utf8.len
526function u.byte(s)
527 if s and #s > 0 then return utf8.codepoint(s, 1) end
528end
529function u.sub(s, i, j)
530 local n = utf8.len(s) or 0
531 i = i or 1
532 j = j or -1
533 if i < 0 then i = math.max(n + i + 1, 1) elseif i == 0 then i = 1 end
534 if j < 0 then j = n + j + 1 elseif j > n then j = n end
535 if i > j then return "" end
536 local b = utf8.offset(s, i)
537 local e = utf8.offset(s, j + 1)
538 return s:sub(b, (e or #s + 1) - 1)
539end
540function u.characters(s)
541 return s:gmatch(utf8.charpattern)
542end
543function u.values(s)
544 local f = s:gmatch(utf8.charpattern)
545 return function()
546 local c = f()
547 if c then return utf8.codepoint(c) end
548 end
549end
550return u
551"#;
552
553pub fn install(lua: &Lua) -> mlua::Result<()> {
555 lua.set_app_data(Rc::new(RefCell::new(FontState::default())));
556 let native = lua.create_table()?;
557 native.set(
558 "define_font",
559 lua.create_function(|lua, (name, size, _id): (mlua::String, i64, Option<i64>)| {
560 let name = name.to_str()?.to_owned();
561 let t0 = std::time::Instant::now();
562 let before = with_engine(lua, |e| {
563 e.work_begin(retex_engine::profile::Work::Fonts);
564 e.fonts.fonts.len()
565 })?;
566 let r = define_font(lua, &name, size as i32);
567 with_engine(lua, Engine::work_end)?;
568 retex_engine::timing::FONTS.with(|a| a.add(t0));
569 with_engine(lua, |e| {
572 for _ in before..e.fonts.fonts.len() {
573 e.nodes.lua_refs.font_defined(true);
574 }
575 })?;
576 r
577 })?,
578 )?;
579 native.set(
580 "reserve_lua_refs",
581 lua.create_function(|lua, n: i64| {
582 with_engine(lua, |e| {
583 for _ in 0..n {
584 e.nodes.lua_refs.take();
585 }
586 })
587 })?,
588 )?;
589 native.set(
590 "node_processor",
591 lua.create_function(|lua, (head, _group): (Value, Value)| {
592 let h = match &head {
593 Value::UserData(u) => u
594 .borrow::<crate::nodelib::LNode>()
595 .ok()
596 .map(|n| NodeRef(n.0)),
597 _ => None,
598 };
599 if let Some(h) = h {
600 let nh = with_engine(lua, |e| process_list(e, h))?;
601 if nh != h {
602 return Ok(Value::UserData(
603 lua.create_userdata(crate::nodelib::LNode(nh.0))?,
604 ));
605 }
606 }
607 Ok(Value::Boolean(true))
608 })?,
609 )?;
610 native.set(
611 "harf_glyph_text",
612 lua.create_function(|lua, (font, c): (i64, i64)| {
613 let text = with_engine(lua, |e| {
614 let f = u32::try_from(font)
615 .ok()
616 .filter(|&f| e.font_exists(f as i32))?;
617 let lf = loaded_font(e, f)?;
618 if lf.request.mode != retex_font::request::Mode::Harf {
619 return None;
620 }
621 let t = lf.tex.tounicode(u32::try_from(c).ok()?);
622 (t.len() > 1).then(|| t.into_iter().filter_map(char::from_u32).collect::<String>())
623 })?;
624 Ok(text)
625 })?,
626 )?;
627 native.set(
628 "font_parameters",
629 lua.create_function(|lua, id: i64| {
630 let Some(lf) = with_engine(lua, |e| {
631 u32::try_from(id)
632 .ok()
633 .filter(|&f| e.font_exists(f as i32))
634 .and_then(|f| loaded_font(e, f))
635 })?
636 else {
637 return Ok(None);
638 };
639 let t = &lf.tex;
640 let p = lua.create_table()?;
641 for (k, v) in retex_font::texfont::LOADER_PARAMS
642 .iter()
643 .zip(t.loader_params)
644 {
645 let spacing = matches!(*k, "space_stretch" | "space_shrink" | "extra_space");
646 let v = if spacing && v == 0.0 {
647 Value::Integer(0)
649 } else {
650 Value::Number(v)
651 };
652 match k.parse::<i64>() {
653 Ok(n) => p.set(n, v)?,
654 Err(_) => p.set(*k, v)?,
655 }
656 }
657 let factor = t.size_f64 / f64::from(t.units_per_em);
658 for k in ["factor", "hfactor", "vfactor"] {
659 p.set(k, factor)?;
660 }
661 for k in ["size", "scaledpoints", "textsize"] {
662 p.set(k, t.size)?;
663 }
664 p.set("units", t.units_per_em)?;
665 p.set("extrafactor", 1)?;
666 for k in ["mathsize", "mode", "width"] {
667 p.set(k, 0)?;
668 }
669 Ok(Some(p))
670 })?,
671 )?;
672 native.set(
673 "cache_dir",
674 lua.create_function(|lua, ()| with_engine(lua, names_cache_dir))?,
675 )?;
676 let utf = lua
677 .load(UTF_LUA)
678 .set_name("=lualibs-unicode")
679 .into_function()?;
680 native.set("lualibs_utf", utf)?;
681 crate::otfaux::install(lua, &native)?;
682 let loader = lua.create_function(move |lua, ()| {
683 let kpse: mlua::Table = lua.globals().get("kpse")?;
686 let find: mlua::Function = kpse.get("find_file")?;
687 if find.call::<Value>(("luaotfload-main.lua", "lua"))?.is_nil() {
688 return Err(mlua::Error::runtime("module 'luaotfload-main' not found"));
689 }
690 let f = lua
691 .load(MAIN_LUA)
692 .set_name("=luaotfload-main")
693 .into_function()?;
694 let native: mlua::Table = lua.named_registry_value("retex.luaotfload.native")?;
695 f.call::<Value>(native)
696 })?;
697 lua.set_named_registry_value("retex.luaotfload.native", native)?;
698 let package: mlua::Table = lua.globals().get("package")?;
699 let preload: mlua::Table = package.get("preload")?;
700 preload.set("luaotfload-main", loader)?;
701 let harfbuzz = lua
706 .load(
707 r#"local hb = { shapers = function() return "graphite2", "ot", "fallback" end,
708 version = function() return "12.3.2" end }
709 for _, k in ipairs{"Blob", "Buffer", "Direction", "Face", "Feature", "Font",
710 "Language", "Script", "Tag", "Variation", "ot", "unicode"} do
711 hb[k] = {}
712 end
713 return function() return hb end"#,
714 )
715 .set_name("=[luaharfbuzz]")
716 .call::<mlua::Function>(())?;
717 preload.set("luaharfbuzz", harfbuzz)?;
718 Ok(())
719}
720
721fn names_cache_dir(e: &mut retex_engine::Engine) -> String {
726 let Some(k) = e.kpse.as_ref() else {
727 return String::new();
728 };
729 let k = k.borrow();
730 let writable = |dir: &str| {
731 let probe =
732 std::path::Path::new(dir).join(format!(".retex-probe-{}", retex_kpath::process_id()));
733 let ok = std::fs::File::create(&probe).is_ok();
734 let _ = std::fs::remove_file(&probe);
735 ok
736 };
737 let root = k
738 .var_value("TEXMFCACHE")
739 .unwrap_or_default()
740 .split(retex_kpath::SEP)
741 .find(|d| {
742 !d.is_empty() && k.name_ok(d, retex_kpath::Access::Write, true).is_ok() && writable(d)
743 })
744 .map(str::to_owned)
745 .or_else(|| k.var_value("TEXMFVAR"))
746 .unwrap_or_default();
747 format!("{root}/luatex-cache/generic/names")
748}
749
750fn legacy_luaotfload(e: &mut Engine) -> bool {
753 let Some(path) = e.kpse.as_ref().and_then(|k| {
754 k.borrow_mut()
755 .find_file("luaotfload.sty", retex_kpath::FileFormat::Tex)
756 }) else {
757 return false;
758 };
759 let Ok(text) = std::fs::read(path) else {
760 return false;
761 };
762 let text = String::from_utf8_lossy(&text);
763 let Some(at) = text.find("\\ProvidesPackage{luaotfload}") else {
764 return false;
765 };
766 let rest = &text[at..];
767 let Some(v) = rest.find(" v").map(|i| &rest[i + 2..]) else {
768 return false;
769 };
770 let ver: String = v
771 .chars()
772 .take_while(|c| c.is_ascii_digit() || *c == '.')
773 .collect();
774 let mut parts = ver.split('.').map(|p| p.parse::<u32>().unwrap_or(0));
775 let (major, minor) = (parts.next().unwrap_or(0), parts.next().unwrap_or(0));
776 (major, minor) < (3, 29) && major > 0
777}
778
779fn define_font(lua: &Lua, name: &str, size: i32) -> mlua::Result<Value> {
782 let state = lua
783 .app_data_ref::<Rc<RefCell<FontState>>>()
784 .map(|s| s.clone())
785 .ok_or_else(|| mlua::Error::runtime("font state missing"))?;
786 if let Some(&id) = state.borrow().tfm_ids.get(&(name.to_owned(), size)) {
787 return Ok(Value::Integer(i64::from(id)));
788 }
789 let remember = |id: u32| {
790 state
791 .borrow_mut()
792 .tfm_ids
793 .insert((name.to_owned(), size), id);
794 };
795 if let Ok(req) = retex_font::FontRequest::parse(name)
798 && let retex_font::FontSource::Any(base) = &req.source
799 {
800 let (first_new, id) = with_engine(lua, |e| {
801 let first_new = e.fonts.fonts.len() as u32;
802 let f = e.load_tfm_font(base.as_bytes(), size, name.as_bytes());
803 if let Some(f) = f {
804 e.check_lua_loaded_font(f);
805 }
806 (first_new, f)
807 })?;
808 if let Some(i) = id {
809 remember(i);
810 let locals = with_engine(lua, |e| e.virtual_font_locals(i))?;
816 let mut st = state.borrow_mut();
817 for (n, s, l) in locals.into_iter().filter(|l| l.2 >= first_new) {
818 st.tfm_ids
819 .entry((String::from_utf8_lossy(&n).into_owned(), s))
820 .or_insert(l);
821 }
822 return Ok(Value::Integer(i64::from(i)));
823 }
824 }
825 {
828 if !state.borrow().db_announced {
829 state.borrow_mut().db_announced = true;
830 let dir = with_engine(lua, names_cache_dir)?;
831 let texio: mlua::Table = lua.globals().get("texio")?;
832 let write: mlua::Function = texio.get("write_nl")?;
833 write.call::<()>((
834 "log",
835 format!(
836 "luaotfload | db : Font names database loaded from {dir}/luaotfload-names.luc.gz"
837 ),
838 ))?;
839 }
840 let by_name = match retex_font::FontRequest::parse(name).map(|r| r.source) {
843 Ok(retex_font::FontSource::Name(_)) => true,
844 Ok(retex_font::FontSource::Any(s)) => !std::path::Path::new(&s).is_file(),
845 _ => false,
846 };
847 if by_name && !state.borrow().lookup_announced {
848 state.borrow_mut().lookup_announced = true;
849 let cache = format!(
850 "{}/luaotfload-lookup-cache.luc",
851 with_engine(lua, names_cache_dir)?
852 );
853 if std::path::Path::new(&cache).is_file() {
854 let texio: mlua::Table = lua.globals().get("texio")?;
855 let write: mlua::Function = texio.get("write_nl")?;
856 write.call::<()>((
857 "log",
858 format!("luaotfload | cache : Lookup cache loaded from {cache}."),
859 ))?;
860 }
861 }
862 }
863 let loaded: Result<Arc<LoadedFont>, String> = with_engine(lua, |e| {
864 let mut st = state.borrow_mut();
865 if st.loader.is_none() {
866 let k = e
867 .kpse
868 .clone()
869 .ok_or_else(|| "kpathsea unavailable".to_owned())?;
870 st.loader = Some(retex_font::FontLoader::new(k));
871 }
872 st.loader
873 .as_mut()
874 .expect("loader initialized")
875 .load(name, size)
876 .map_err(|e| e.to_string())
877 })?;
878 let Ok(font) = loaded else {
879 let plain = name.strip_prefix("file:").unwrap_or(name);
881 let plain = plain.split(':').next().unwrap_or(plain).to_owned();
882 let id = with_engine(lua, |e| {
883 let f = e.load_tfm_font(plain.as_bytes(), size, name.as_bytes());
884 if let Some(f) = f {
885 e.check_lua_loaded_font(f);
886 }
887 f
888 })?;
889 if let Some(i) = id {
890 remember(i);
891 }
892 return Ok(id.map_or(Value::Nil, |i| Value::Integer(i64::from(i))));
893 };
894 with_engine(lua, |e| {
897 if !e.files.watched.contains(&font.tex.path) {
898 e.files.watched.push(font.tex.path.clone());
899 }
900 })?;
901 let key = (Arc::as_ptr(&font) as usize, size < 0);
902 if let Some(&id) = state.borrow().ids.get(&key) {
903 return Ok(Value::Integer(i64::from(id)));
904 }
905 let known = state.borrow().legacy_design_size;
906 let legacy = match known {
907 Some(l) => l,
908 None => with_engine(lua, legacy_luaotfload)?,
909 };
910 state.borrow_mut().legacy_design_size = Some(legacy);
911 let id = with_engine(lua, |e| {
912 let tex = &font.tex;
913 let design_size = if legacy {
914 retex_font::texfont::design_size(&font.file).round() as i32
915 } else {
916 tex.design_size
917 };
918 let mut params = vec![0; tex.params.len() + 1];
919 for (i, p) in tex.params.iter().enumerate() {
920 params[i + 1] = *p;
921 }
922 let mut chars = HashMap::with_capacity_and_hasher(
923 tex.num_chars(),
924 retex_engine::hash::FastHash::default(),
925 );
926 for (code, m) in tex.chars() {
927 chars.insert(
928 code,
929 CharMetrics {
930 width: m.width,
931 height: m.height,
932 depth: m.depth,
933 italic: m.italic,
934 index: u32::from(m.glyph),
935 },
936 );
937 }
938 let name = if tex.name.contains(' ') {
942 format!("\"{}\"", tex.name).into_bytes()
943 } else {
944 tex.name.clone().into_bytes()
945 };
946 let info = FontInfo {
947 name,
948 size: tex.size,
949 design_size,
950 checksum: 0,
951 params,
952 chars,
953 hyphen_char: i32::MIN,
954 skew_char: i32::MIN,
955 kind: FontKind::OpenType,
956 handle: 0,
957 char_codes: Default::default(),
958 ignore_ligatures: 0,
959 data: Some(FontData(Rc::new(font.clone()))),
960 lig_kern: Default::default(),
961 right_boundary: None,
962 expansion: None,
963 size_spec: i32::MIN,
964 vf: None,
965 math_chars: {
966 let mc = tex.math_chars(&font.file);
967 (!mc.is_empty()).then(|| Rc::new(convert_math_chars(mc)))
968 },
969 math_constants: tex.math.as_ref().map(|m| {
970 Rc::new(
971 m.iter()
972 .map(|(k, v)| ((*k).to_owned(), v.to_sp()))
973 .collect(),
974 )
975 }),
976 };
977 e.register_font(info)
978 })?;
979 state.borrow_mut().ids.insert(key, id);
980 Ok(Value::Integer(i64::from(id)))
981}
982
983fn convert_math_chars(
986 mc: HashMap<u32, retex_font::texfont::MathChar>,
987) -> HashMap<u32, retex_engine::fonts::MathCharInfo> {
988 use retex_engine::fonts::{ExtPart, MathCharInfo};
989 let parts = |v: &Option<Vec<retex_font::texfont::MathPart>>| {
990 v.as_ref().map(|ps| {
991 ps.iter()
992 .map(|p| ExtPart {
993 code: p.code,
994 start: retex_font::texfont::round_sp(p.start),
995 end: retex_font::texfont::round_sp(p.end),
996 advance: retex_font::texfont::round_sp(p.advance),
997 extender: p.extender,
998 })
999 .collect::<Rc<[ExtPart]>>()
1000 })
1001 };
1002 mc.into_iter()
1003 .map(|(c, m)| {
1004 (
1005 c,
1006 MathCharInfo {
1007 next: m.next,
1008 top_accent: m.top_accent.map(retex_font::texfont::round_sp),
1009 vert: parts(&m.vert),
1010 horiz: parts(&m.horiz),
1011 },
1012 )
1013 })
1014 .collect()
1015}
1016
1017pub fn loaded_font(e: &Engine, f: u32) -> Option<Arc<LoadedFont>> {
1019 let data = e.font(f).data.as_ref()?;
1020 data.0.downcast_ref::<Arc<LoadedFont>>().cloned()
1021}
1022
1023pub use crate::otfnodes::process_list;
1024
1025#[cfg(test)]
1026mod tests {
1027 #[test]
1028 fn lualibs_utf() {
1029 let lua = mlua::Lua::new();
1030 let utf: mlua::Table = lua.load(super::UTF_LUA).eval().expect("utf");
1031 lua.globals().set("utf", utf).expect("set");
1032 let r: String = lua
1033 .load(
1034 r##"local o = {}
1035 local function add(...) o[#o + 1] = table.concat({...}, ",") end
1036 add(utf.char(65, 0x3B1), utf.byte("\u{3B1}b"), select("#", utf.byte("")))
1037 add(utf.len("\u{3B1}b"), utf.sub("\u{3B1}\u{3B2}c", 2, 3),
1038 utf.sub("\u{3B1}\u{3B2}c", 2), utf.sub("\u{3B1}\u{3B2}c", -1))
1039 for c in utf.characters("a\u{3B1}") do add(c) end
1040 for c in utf.values("a\u{3B1}") do add(c) end
1041 return table.concat(o, "|")"##,
1042 )
1043 .eval()
1044 .expect("run");
1045 assert_eq!(r, "A\u{3B1},945,0|2,\u{3B2}c,\u{3B2}c,c|a|\u{3B1}|97|945");
1047 }
1048}