----------------------------------------------------------------------- -- FILE: luaotfload-harf-plug.lua -- DESCRIPTION: part of luaotfload / HarfBuzz / fontloader plugin ----------------------------------------------------------------------- do -- block to avoid to many local variables error local ProvidesLuaModule = { name = "luaotfload-harf-plug", version = "3.11", --TAGVERSION date = "2019-11-10", --TAGDATE description = "luaotfload submodule / database", license = "GPL v2.0", author = "Khaled Hosny, Marcel Krüger", copyright = "Luaotfload Development Team", } if luatexbase and luatexbase.provides_module then luatexbase.provides_module (ProvidesLuaModule) end end local hb = luaotfload.harfbuzz local assert = assert local next = next local tonumber = tonumber local type = type local format = string.format local open = io.open local tableinsert = table.insert local tableremove = table.remove local ostmpname = os.tmpname local osremove = os.remove local direct = node.direct local tonode = direct.tonode local todirect = direct.todirect local traverse = direct.traverse local insertbefore = direct.insert_before local insertafter = direct.insert_after local protectglyph = direct.protect_glyph local newnode = direct.new local freenode = direct.free local copynode = direct.copy local removenode = direct.remove local copynodelist = direct.copy_list local ischar = direct.is_char local uses_font = direct.uses_font local getattrs = direct.getattributelist local setattrs = direct.setattributelist local getchar = direct.getchar local setchar = direct.setchar local getdir = direct.getdir local setdir = direct.setdir local getdisc = direct.getdisc local setdisc = direct.setdisc local getfont = direct.getfont local getdata = direct.getdata local setdata = direct.setdata local getfont = direct.getfont local setfont = direct.setfont local getfield = direct.getfield local setfield = direct.setfield local getid = direct.getid local getkern = direct.getkern local setkern = direct.setkern local getnext = direct.getnext local setnext = direct.setnext local getoffsets = direct.getoffsets local setoffsets = direct.setoffsets local getproperty = direct.getproperty local setproperty = direct.setproperty local getprev = direct.getprev local setprev = direct.setprev local getsubtype = direct.getsubtype local setsubtype = direct.setsubtype local getwidth = direct.getwidth local setwidth = direct.setwidth local is_char = direct.is_char local tail = direct.tail local properties = direct.get_properties_table() local imgnode = img.node local disc_t = node.id("disc") local glue_t = node.id("glue") local glyph_t = node.id("glyph") local dir_t = node.id("dir") local kern_t = node.id("kern") local localpar_t = node.id("local_par") local whatsit_t = node.id("whatsit") local pdfliteral_t = node.subtype("pdf_literal") local explicitdisc_t = 1 local fontkern_t = 0 local italiccorr_t = 3 local regulardisc_t = 3 local spaceskip_t = 13 local dir_ltr = hb.Direction.new("ltr") local dir_rtl = hb.Direction.new("rtl") local fl_unsafe = hb.Buffer.GLYPH_FLAG_UNSAFE_TO_BREAK local startactual_p = "luaotfload_startactualtext" local endactual_p = "luaotfload_endactualtext" -- "Copy" properties as done by LuaTeX: Make old properties metatable local function copytable(old) local new = {} for k, v in next, old do if type(v) == "table" then v = copytable(v) end new[k] = v end setmetatable(new, getmetatable(old)) return new end -- Set and get properties. local function setprop(n, prop, value) local props = properties[n] if not props then props = {} properties[n] = props end props[prop] = value end local function inherit(t, base, properties) local n = newnode(t) setattrs(n, getattrs(base)) if properties then setproperty(n, setmetatable({}, {__index = properties})) end return n end -- New kern node of amount `v`, inheriting the properties/attributes of `n`. local function newkern(v, n) local kern = inherit(kern_t, n, getproperty(n)) setkern(kern, v) return kern end local function insertkern(head, current, kern, rtl) if rtl then head = insertbefore(head, current, kern) else head, current = insertafter(head, current, kern) end return head, current end -- Convert list of integers to UTF-16 hex string used in PDF. local function to_utf16_hex(uni) if uni < 0x10000 then return format("%04X", uni) else uni = uni - 0x10000 local hi = 0xD800 + (uni // 0x400) local lo = 0xDC00 + (uni % 0x400) return format("%04X%04X", hi, lo) end end local process -- Legacy TeX Input Method Disguised as Font Ligatures hack. -- -- Single replacements, keyed by character to replace. Handled separately -- because TeX ligaturing mechanism does not support one-to-one replacements. local trep = { [0x0022] = 0x201D, -- ["] [0x0027] = 0x2019, -- ['] [0x0060] = 0x2018, -- [`] } local function itemize(head, fontid, direction) local fontdata = font.getfont(fontid) local hbdata = fontdata and fontdata.hb local spec = fontdata and fontdata.specification local options = spec and spec.features.raw local texlig = options and options.tlig local runs, codes = {}, {} local dirstack = {} local currdir = direction or "TLT" local lastskip, lastdir = true local lastrun = {} for n, id, subtype in direct.traverse(head) do local code = 0xFFFC -- OBJECT REPLACEMENT CHARACTER local skip = false if id == glyph_t then if is_char(n) and getfont(n) == fontid then code = getchar(n) else skip = true end elseif id == glue_t and subtype == spaceskip_t then code = 0x0020 -- SPACE skip = lastskip elseif id == disc_t then if uses_font(n, fontid) then code = 0x00AD -- SOFT HYPHEN else skip = true end elseif id == dir_t then local dir = getdir(n) if dir:sub(1, 1) == "+" then -- Push the current direction to the stack. tableinsert(dirstack, currdir) currdir = dir:sub(2) else assert(currdir == dir:sub(2)) -- Pop the last direction from the stack. currdir = tableremove(dirstack) end skip = lastskip elseif id == localpar_t then currdir = getdir(n) skip = lastskip end if not skip and texlig then local replacement = trep[code] if replacement then code = replacement end end codes[#codes + 1] = code if lastdir ~= currdir or lastskip ~= skip then lastrun.after = n lastrun = { start = #codes, len = 1, font = fontid, dir = currdir == "TRT" and dir_rtl or dir_ltr, skip = skip, codes = codes, } runs[#runs + 1] = lastrun lastdir, lastskip = currdir, skip else lastrun.len = lastrun.len + 1 end end return runs end -- Check if it is not safe to break before this glyph. local function unsafetobreak(glyph) return glyph and glyph.flags and glyph.flags & fl_unsafe end local shape -- Make s a sub run, used by discretionary nodes. local function makesub(run, codes, nodelist) local subrun = { start = 1, len = #codes, font = run.font, dir = run.dir, fordisc = true, node = nodelist, codes = codes, } local glyphs if nodelist == 0 then -- FIXME: This shouldn't happen nodelist = nil end nodelist, glyphs = shape(nodelist, nodelist, subrun) return { glyphs = glyphs, run = subrun, head = nodelist } end local function printnodes(label, head, after) after = tonode(after) for n in node.traverse(tonode(head)) do if n == after then return end print(label, n, n.font or '', n.char or '') local pre, post, rep = getdisc(todirect(n)) if pre then printnodes(label .. '<', pre) end if post then printnodes(label .. '>', post) end if rep then printnodes(label .. '=', rep) end end end -- Main shaping function that calls HarfBuzz, and does some post-processing of -- the output. function shape(head, node, run) local codes = run.codes local offset = run.start local nodeindex = offset run.start = offset local len = run.len local fontid = run.font local dir = run.dir local fordisc = run.fordisc local cluster = offset - 2 local fontdata = font.getfont(fontid) local hbdata = fontdata.hb local spec = hbdata.spec local features = spec.hb_features local options = spec.features.raw local hbshared = hbdata.shared local hbfont = hbshared.font local lang = spec.language local script = spec.script local shapers = options.shaper and { options.shaper } or {} local buf = hb.Buffer.new() buf:set_direction(dir) buf:set_script(script) buf:set_language(lang) buf:set_cluster_level(buf.CLUSTER_LEVEL_MONOTONE_CHARACTERS) buf:add_codepoints(codes, offset - 1, len) local hscale = hbdata.hscale local vscale = hbdata.vscale hbfont:set_scale(hscale, vscale) if hb.shape_full(hbfont, buf, features, shapers) then -- The engine wants the glyphs in logical order, but HarfBuzz outputs them -- in visual order, so we reverse RTL buffers. if dir:is_backward() then buf:reverse() end local glyphs = buf:get_glyphs() local i = 0 while i < #glyphs do i = i + 1 local glyph = glyphs[i] -- Calculate the Unicode code points of this glyph. If cluster did not -- change then this is a glyph inside a complex cluster and will be -- handled with the start of its cluster. if cluster ~= glyph.cluster then cluster = glyph.cluster for i = nodeindex, cluster do node = getnext(node) end nodeindex = cluster + 1 local hex = "" local str = "" local nextcluster for j = i+1, #glyphs do nextcluster = glyphs[j].cluster if cluster ~= nextcluster then glyph.nglyphs = j - i goto NEXTCLUSTERFOUND -- break end end -- else -- only executed if the loop reached the end without -- finding another cluster nextcluster = offset + len - 1 glyph.nglyphs = #glyphs + 1 - i ::NEXTCLUSTERFOUND:: -- end glyph.nextcluster = nextcluster do local node = node for j = cluster,nextcluster-1 do local id = getid(node) if id == glyph_t or id == glue_t then local code = codes[j + 1] hex = hex..to_utf16_hex(code) str = str..utf8.char(code) end end glyph.tounicode = hex glyph.string = str end if not fordisc then local discindex = nil local disc = node for j = cluster + 1, nextcluster do if codes[j] == 0x00AD then discindex = j break end disc = getnext(disc) end if discindex then -- Discretionary found. local startindex, stopindex = nil, nil local startglyph, stopglyph = nil, nil -- Find the previous glyph that is safe to break at. local startglyph = i while startglyph > 1 and codes[glyphs[startglyph - 1].cluster + 1] ~= 0x20 and codes[glyphs[startglyph - 1].cluster + 1] ~= 0xFFFC and unsafetobreak(glyphs[startglyph]) do startglyph = startglyph - 1 end -- Get the corresponding character index. startindex = glyphs[startglyph].cluster + 1 -- Find the next glyph that is safe to break at. stopglyph = i + 1 local lastcluster = glyphs[i].cluster while stopglyph <= #glyphs and codes[glyphs[stopglyph].cluster + 1] ~= 0x20 and codes[glyphs[stopglyph].cluster + 1] ~= 0xFFFC and (unsafetobreak(glyphs[stopglyph]) or lastcluster == glyphs[stopglyph].cluster) do lastcluster = glyphs[stopglyph].cluster stopglyph = stopglyph + 1 end stopindex = stopglyph <= #glyphs and glyphs[stopglyph].cluster + 1 or offset + len local startnode, stopnode = node, node for j=nodeindex - 1, startindex, -1 do startnode = getprev(startnode) end for j=nodeindex + 1, stopindex do stopnode = getnext(stopnode) end glyphs[startglyph] = glyph glyph.cluster = startindex - 1 glyph.nextcluster = startindex for j = stopglyph, #glyphs do local glyph = glyphs[j] glyph.cluster = glyph.cluster - (stopindex - startindex) + 1 end len = len - (stopindex - startindex) + 1 table.move(glyphs, stopglyph, #glyphs + stopglyph - startglyph - 1, startglyph + 1) local subcodes, subindex = {} do local node = startnode while node ~= stopnode do if node == disc then subindex = #subcodes startindex = startindex + 1 node = getnext(node) elseif getid(node) == disc_t then local oldnode = node startnode, node = removenode(startnode, node) freenode(oldnode) tableremove(codes, startindex) else subcodes[#subcodes + 1] = tableremove(codes, startindex) node = getnext(node) end end end local pre, post, rep, lastpre, lastpost, lastrep = getdisc(disc, true) local precodes, postcodes, repcodes = {}, {}, {} table.move(subcodes, 1, subindex, 1, repcodes) for n, id, subtype in traverse(rep) do repcodes[#repcodes + 1] = getfont(n) == fontid and getchar(n) or 0xFFFC end table.move(subcodes, subindex + 1, #subcodes, #repcodes + 1, repcodes) table.move(subcodes, 1, subindex, 1, precodes) for n, id, subtype in traverse(pre) do precodes[#precodes + 1] = getfont(n) == fontid and getchar(n) or 0xFFFC end for n, id, subtype in traverse(post) do postcodes[#postcodes + 1] = getfont(n) == fontid and getchar(n) or 0xFFFC end table.move(subcodes, subindex + 1, #subcodes, #postcodes + 1, postcodes) if startnode ~= disc then local newpre = copynodelist(startnode, disc) setnext(tail(newpre), pre) pre = newpre end if post then setnext(lastpost, copynodelist(getnext(disc), stopnode)) else post = copynodelist(getnext(disc), stopnode) end if startnode ~= disc then local predisc = getprev(disc) setnext(predisc, rep) setprev(rep, predisc) if startnode == head then head = disc else local before = getprev(startnode) setnext(before, disc) setprev(disc, before) end setprev(startnode, nil) rep = startnode lastrep = lastrep or predisc end if getnext(disc) ~= stopnode then setnext(getprev(stopnode), nil) setprev(stopnode, disc) setprev(getnext(disc), lastrep) setnext(lastrep, getnext(disc)) rep = rep or getnext(disc) setnext(disc, stopnode) end glyph.replace = makesub(run, repcodes, rep) glyph.pre = makesub(run, precodes, pre) glyph.post = makesub(run, postcodes, post) i = startglyph node = disc cluster = glyph.cluster nodeindex = cluster + 1 end end end end return head, glyphs, run.len - len end return head, {}, 0 end local function color_to_rgba(color) local r = color.red / 255 local g = color.green / 255 local b = color.blue / 255 local a = color.alpha / 255 if a ~= 1 then -- XXX: alpha return format('%s %s %s rg', r, g, b) else return format('%s %s %s rg', r, g, b) end end -- Cache of color glyph PNG data for bookkeeping, only because I couldn’t -- figure how to make the engine load the image from the binary data directly. local pngcache = {} local pngcachefiles = {} local function cachedpng(data) local hash = md5.sumhexa(data) local i = pngcache[hash] if not i then local path = ostmpname() pngcachefiles[#pngcachefiles + 1] = path open(path, "wb"):write(data):close() -- local file = open(path, "wb"):write():close() -- file:write(data) -- file:close() i = img.scan{filename = path} pngcache[hash] = i end return i end local function get_png_glyph(gid, fontid, characters, haspng) return gid end local push_cmd = { "push" } local pop_cmd = { "pop" } local nop_cmd = { "nop" } local save_cmd = { "pdf", "page", "q" } local restore_cmd = { "pdf", "page", "Q" } -- Convert glyphs to nodes and collect font characters. local function tonodes(head, node, run, glyphs) local nodeindex = run.start local dir = run.dir local fontid = run.font local fontdata = font.getfont(fontid) local characters = fontdata.characters local hbdata = fontdata.hb local palette = hbdata.palette local hbshared = hbdata.shared local hbface = hbshared.face local nominals = hbshared.nominals local hbfont = hbshared.font local fontglyphs = hbshared.glyphs local gid_offset = hbshared.gid_offset local rtl = dir:is_backward() local lastprops local scale = hbdata.scale local letterspace = hbdata.letterspace local haspng = hbshared.haspng local fonttype = hbshared.fonttype local nextcluster for i, glyph in ipairs(glyphs) do if glyph.cluster + 1 >= nodeindex then -- Reached a new cluster nextcluster = glyph.nextcluster assert(nextcluster) for j = nodeindex, glyph.cluster do local oldnode = node head, node = removenode(head, node) freenode(oldnode) end lastprops = getproperty(node) nodeindex = glyph.cluster + 1 elseif nextcluster + 1 == nodeindex then -- Oops, we went too far nodeindex = nodeindex - 1 local new = inherit(glyph_t, getprev(node), lastprops) setfont(new, fontid) head, node = insertbefore(head, node, new) end local gid = glyph.codepoint local char = nominals[gid] or gid_offset + gid local id = getid(node) if glyph.replace then -- For discretionary the glyph itself is skipped and a discretionary node -- is output in place of it. local rep, pre, post = glyph.replace, glyph.pre, glyph.post setdisc(node, tonodes(pre.head, pre.head, pre.run, pre.glyphs), tonodes(post.head, post.head, post.run, post.glyphs), tonodes(rep.head, rep.head, rep.run, rep.glyphs)) node = getnext(node) nodeindex = nodeindex + 1 elseif glyph.skip then local oldnode = node head, node = removenode(head, node) freenode(oldnode) nodeindex = nodeindex + 1 else if id == glyph_t then local done local fontglyph = fontglyphs[gid] local character = characters[char] if not character.commands then if palette then local layers = fontglyph.layers if layers == nil then layers = hbface:ot_color_glyph_get_layers(gid) or false fontglyph.layers = layers end if layers then local cmds = {} -- Every layer will add 5 cmds local prev_color = nil for j = 1, #layers do local layer = layers[j] local layerchar = characters[gid_offset + layer.glyph] if layerchar.height > character.height then character.height = layerchar.height end if layerchar.depth > character.depth then character.depth = layerchar.depth end -- color_index has a special value, 0x10000, that mean use text -- color, we don’t check for it here explicitly since we will -- get nil anyway. local color = palette[layer.color_index] cmds[5*j - 4] = (color and not prev_color) and save_cmd or nop_cmd cmds[5*j - 3] = prev_color == color and nop_cmd or (color and {"pdf", "page", color_to_rgba(color)} or restore_cmd) cmds[5*j - 2] = push_cmd cmds[5*j - 1] = {"char", layer.glyph + gid_offset} cmds[5*j] = pop_cmd fontglyphs[layer.glyph].used = true prev_color = color end cmds[#cmds + 1] = prev_color and restore_cmd if not character.colored then local coloredcharacter = {} for k,v in next, character do coloredcharacter[k] = v end coloredcharacter.commands = cmds local newcharacters = {[gid + 0x130000] = coloredcharacter} characters[gid + 0x130000] = coloredcharacter if char ~= gid + gid_offset then newcharacters[char] = coloredcharacter characters[char] = coloredcharacter character.colored = char else character.colored = gid + 0x130000 end font.addcharacters(fontid, {characters = newcharacters}) end char = character.colored character = characters[char] end end if haspng then local pngglyph = character.pngglyph if pngglyph == nil then local pngblob = hbfont:ot_color_glyph_get_png(gid) if pngblob then local glyphimg = cachedpng(pngblob:get_data()) local pngchar = { } for k,v in next, character do pngchar[k] = v end local i = img.copy(glyphimg) i.width = character.width i.depth = 0 i.height = character.height + character.depth pngchar.commands = fonttype and { {"push"}, {"char", gid_offset + gid}, {"pop"}, {"down", character.depth}, {"image", i} } or { {"down", character.depth}, {"image", i} } if not nominals[gid] then char = 0x130000 + gid end characters[char] = pngchar pngglyph = char font.addcharacters(fontid, {characters = {[char] = pngchar}}) end character.pngglyph = pngglyph end if pngglyph then char = pngglyph elseif not fonttype then -- Color bitmap font with no glyph outlines (like Noto -- Color Emoji) but has no bitmap for current glyph (most likely -- `.notdef` glyph). The engine does not know how to embed such -- fonts, so we don’t want them to reach the backend as it will cause -- a fatal error. We use `nullfont` instead. That is a hack, but I -- think it is good enough for now. We could make the glyph virtual -- with empty commands suh that LuaTeX ignores it, but we still want -- a missing glyph warning. -- We insert the glyph node and move on, no further work is needed. setfont(node, 0) done = true end end end if not done then local oldcharacter = characters[getchar(node)] -- If the glyph index of current font character is the same as shaped -- glyph, keep the node char unchanged. Helps with primitives that -- take characters as input but actually work on glyphs, like -- `\rpcode`. if character.commands or not oldcharacter or character.index ~= oldcharacter.index then setchar(node, char) end local xoffset = (rtl and -glyph.x_offset or glyph.x_offset) * scale local yoffset = glyph.y_offset * scale setoffsets(node, xoffset, yoffset) fontglyph.used = fonttype and true -- The engine will use this string when printing a glyph node e.g. in -- overfull messages, otherwise it will be trying to print our -- invalid pseudo Unicode code points. -- If the string is empty it means this glyph is part of a larger -- cluster and we don’t to print anything for it as the first glyph -- in the cluster will have the string of the whole cluster. local props = properties[node] if not props then props = {} properties[node] = props end props.glyph_info = glyph.string or "" -- Handle PDF text extraction: -- * Find how many characters in this cluster and how many glyphs, -- * If there is more than 0 characters -- * One glyph: one to one or one to many mapping, can be -- represented by font’s /ToUnicode -- * More than one: many to one or many to many mapping, can be -- represented by /ActualText spans. -- * If there are zero characters, then this glyph is part of complex -- cluster that will be covered by an /ActualText span. local tounicode = glyph.tounicode if tounicode then if glyph.nglyphs == 1 and not character.commands and not fontglyph.tounicode then fontglyph.tounicode = tounicode elseif character.commands or tounicode ~= fontglyph.tounicode then setprop(node, startactual_p, tounicode) glyphs[i + glyph.nglyphs - 1].endactual = true end end if glyph.endactual then setprop(node, endactual_p, true) end local x_advance = glyph.x_advance + letterspace local width = fontglyph.width if width ~= x_advance then -- The engine always uses the glyph width from the font, so we need -- to insert a kern node if the x advance is different. local kern = newkern((x_advance - width) * scale, node) head, node = insertkern(head, node, kern, rtl) end end elseif id == glue_t and getsubtype(node) == spaceskip_t then -- If the glyph advance is different from the font space, then a -- substitution or positioning was applied to the space glyph changing -- it from the default, so reset the glue using the new advance. -- We are intentionally not comparing with the existing glue width as -- spacing after the period is larger by default in TeX. local width = (glyph.x_advance + letterspace) * scale if fontdata.parameters.space ~= width then setwidth(node, width) setfield(node, "stretch", width / 2) setfield(node, "shrink", width / 3) end elseif id == kern_t and getsubtype(node) == italiccorr_t then -- If this is an italic correction node and the previous node is a -- glyph, update its kern value with the glyph’s italic correction. local prevchar, prevfontid = ischar(getprev(node)) if prevfontid == fontid and prevchar and prevchar > 0 then local italic = characters[prevchar].italic if italic then setkern(node, italic) end end end node = getnext(node) nodeindex = nodeindex + 1 end end while node ~= run.after do local oldnode = node head, node = removenode(head, node) freenode(oldnode) end return head, node end local function shape_run(head, current, run) if not run.skip then -- Font loaded with our loader and an HarfBuzz face is present, do our -- shaping. local fontid = run.font local fontdata = font.getfont(fontid) local options = fontdata.specification.features.raw local glyphs, offset head, glyphs, offset = shape(head, current, run) return offset, tonodes(head, current, run, glyphs) else return 0, head, run.after end end function process(head, font, direction) local newhead, current = head, head local runs = itemize(head, font, direction) local offset = 0 for i = 1,#runs do local run = runs[i] run.start = run.start - offset local new_offset new_offset, newhead, current = shape_run(newhead, current, run) offset = offset + new_offset end return newhead or head end local function pageliteral(data) local n = newnode(whatsit_t, pdfliteral_t) setfield(n, "mode", 1) -- page setdata(n, data) return n end local function post_process(head) for n in traverse(head) do local props = properties[n] local startactual, endactual if props then startactual = rawget(props, startactual_p) endactual = rawget(props, endactual_p) end if startactual then local actualtext = "/Span<>>BDC" head = insertbefore(head, n, pageliteral(actualtext)) end if endactual then head = insertafter(head, n, pageliteral("EMC")) end local replace = getfield(n, "replace") if replace then setfield(n, "replace", post_process(replace)) end local subhead = getfield(n, "head") if subhead then setfield(n, "head", post_process(subhead)) end end return head end local function post_process_nodes(head, groupcode) return tonode(post_process(todirect(head))) end local function run_cleanup() -- Remove temporary PNG files that we created, if any. -- FIXME: It would be nice if we wouldn't need this for _, path in next, pngcachefiles do osremove(path) end end local function set_tounicode() for fontid, fontdata in font.each() do local hbdata = fontdata.hb if hbdata and fontid == pdf.getfontname(fontid) then local characters = fontdata.characters local newcharacters = {} local hbshared = hbdata.shared local glyphs = hbshared.glyphs local nominals = hbshared.nominals local gid_offset = hbshared.gid_offset for gid = 0, #glyphs do local glyph = glyphs[gid] if glyph.used then local character = characters[gid + gid_offset] newcharacters[gid + gid_offset] = character local unicode = nominals[gid] if unicode then newcharacters[unicode] = character end character.tounicode = glyph.tounicode or "FFFD" character.used = true end end font.addcharacters(fontid, { characters = newcharacters }) end end end -- FIXME: Move this into generic parts of luaotfload local utfchar = utf8.char local function get_glyph_info(n) n = todirect(n) local props = properties[n] return props and props.glyph_info or utfchar(getchar(n)):gsub('\0', '^^@') end fonts.handlers.otf.registerplugin('harf', process) -- luatexbase does not know how to handle `wrapup_run` callback, teach it. -- TODO: Move these into ltluatex luatexbase.callbacktypes.wrapup_run = 1 -- simple luatexbase.callbacktypes.glyph_info = 1 -- simple local base_callback_descriptions = luatexbase.callback_descriptions local base_add_to_callback = luatexbase.add_to_callback local base_remove_from_callback = luatexbase.remove_from_callback -- Remove all existing functions from given callback, insert ours, then -- reinsert the removed ones, so ours takes a priority. local function add_to_callback(name, func) local saved_callbacks = {}, ff, dd for k, v in next, base_callback_descriptions(name) do saved_callbacks[k] = { base_remove_from_callback(name, v) } end base_add_to_callback(name, func, "Harf "..name.." callback") for _, v in next, saved_callbacks do base_add_to_callback(name, v[1], v[2]) end end add_to_callback('pre_output_filter', post_process_nodes) -- FIXME: Wrong callback, but I want to get rid of the whole function anyway add_to_callback('wrapup_run', run_cleanup) add_to_callback('finish_pdffile', set_tounicode) add_to_callback('glyph_info', get_glyph_info)