diff options
Diffstat (limited to 'macros/luatex/generic/luaotfload/luaotfload-harf-plug.lua')
-rw-r--r-- | macros/luatex/generic/luaotfload/luaotfload-harf-plug.lua | 983 |
1 files changed, 983 insertions, 0 deletions
diff --git a/macros/luatex/generic/luaotfload/luaotfload-harf-plug.lua b/macros/luatex/generic/luaotfload/luaotfload-harf-plug.lua new file mode 100644 index 0000000000..123a1c4563 --- /dev/null +++ b/macros/luatex/generic/luaotfload/luaotfload-harf-plug.lua @@ -0,0 +1,983 @@ +----------------------------------------------------------------------- +-- 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.1", --TAGVERSION + date = "2019-11-04", --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 invalid_l = hb.Language.new() +local invalid_s = hb.Script.new() + +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 or invalid_l + local script = spec.script or invalid_s + 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<</ActualText<FEFF"..startactual..">>>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) |