summaryrefslogtreecommitdiff
path: root/texmf-dist/tex/generic/harftex/harf-node.lua
diff options
context:
space:
mode:
Diffstat (limited to 'texmf-dist/tex/generic/harftex/harf-node.lua')
-rw-r--r--texmf-dist/tex/generic/harftex/harf-node.lua914
1 files changed, 914 insertions, 0 deletions
diff --git a/texmf-dist/tex/generic/harftex/harf-node.lua b/texmf-dist/tex/generic/harftex/harf-node.lua
new file mode 100644
index 00000000..fb4bfabe
--- /dev/null
+++ b/texmf-dist/tex/generic/harftex/harf-node.lua
@@ -0,0 +1,914 @@
+local hb = require("harf-base")
+
+local direct = node.direct
+local tonode = direct.tonode
+local todirect = direct.todirect
+local traverse = direct.traverse
+local traverseid = direct.traverse_id
+local insertbefore = direct.insert_before
+local insertafter = direct.insert_after
+local protectglyph = direct.protect_glyph
+local newnode = direct.new
+local copynode = direct.copy
+local removenode = direct.remove
+local copynodelist = direct.copy_list
+local isglyph = direct.is_glyph
+
+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 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 getpre = function (n) return getfield(n, "pre") end
+local setpre = function (n, v) setfield(n, "pre", v) end
+local getpost = function (n) return getfield(n, "post") end
+local setpost = function (n, v) setfield(n, "post", v) end
+local getrep = function (n) return getfield(n, "replace") end
+local setrep = function (n, v) setfield(n, "replace", v) end
+
+local discid = node.id("disc")
+local glueid = node.id("glue")
+local glyphid = node.id("glyph")
+local dirid = node.id("dir")
+local kernid = node.id("kern")
+local localparid = node.id("local_par")
+
+local spaceskip = 13
+local directmode = 2
+local fontkern = 0
+local italiccorrection = 3
+
+local getscript = hb.unicode.script
+local sc_common = hb.Script.new("Zyyy")
+local sc_inherited = hb.Script.new("Zinh")
+local sc_unknown = hb.Script.new("Zzzz")
+local sc_latn = hb.Script.new("Latn")
+local dir_ltr = hb.Direction.new("ltr")
+local dir_rtl = hb.Direction.new("rtl")
+local lang_invalid = hb.Language.new()
+local fl_unsafe = hb.Buffer.GLYPH_FLAG_UNSAFE_TO_BREAK
+
+local p_startactual = "startactualtext"
+local p_endactual = "endactualtext"
+local p_color = "color"
+local p_string = "string"
+
+local format = string.format
+
+-- Simple table copying function.
+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
+ return new
+end
+
+-- Set and get properties from our private `harf` subtable.
+local function setprop(n, prop, value)
+ local props = getproperty(n)
+ if not props then
+ props = {}
+ setproperty(n, props)
+ end
+ props.harf = props.harf or {}
+ props.harf[prop] = value
+end
+
+-- Copy node properties and attributes.
+local function copyprops(src, dst)
+ local props = getproperty(src)
+ local attrs = getattrs(src)
+ if props then
+ setproperty(dst, copytable(props))
+ end
+ if attrs then
+ setattrs(dst, copynodelist(attrs))
+ end
+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 paired_open = {
+ [0x0028] = 0x0029, [0x003c] = 0x003e, [0x005b] = 0x005d, [0x007b] = 0x007d,
+ [0x00ab] = 0x00bb, [0x2018] = 0x2019, [0x201c] = 0x201d, [0x2039] = 0x203a,
+ [0x3008] = 0x3009, [0x300a] = 0x300b, [0x300c] = 0x300d, [0x300e] = 0x300f,
+ [0x3010] = 0x3011, [0x3014] = 0x3015, [0x3016] = 0x3017, [0x3018] = 0x3019,
+ [0x301a] = 0x301b,
+}
+
+local paired_close = {
+ [0x0029] = 0x0028, [0x003e] = 0x003c, [0x005d] = 0x005b, [0x007d] = 0x007b,
+ [0x00bb] = 0x00ab, [0x2019] = 0x2018, [0x201d] = 0x201c, [0x203a] = 0x2039,
+ [0x3009] = 0x3008, [0x300b] = 0x300a, [0x300d] = 0x300c, [0x300f] = 0x300e,
+ [0x3011] = 0x3010, [0x3015] = 0x3014, [0x3017] = 0x3016, [0x3019] = 0x3018,
+ [0x301b] = 0x301a,
+}
+
+local process
+
+local function itemize(head, direction)
+ -- Collect character properties (font, direction, script) and resolve common
+ -- and inherited scripts. Pre-requisite for itemization into smaller runs.
+ local props, nodes, codes = {}, {}, {}
+ local dirstack, pairstack = {}, {}
+ local currdir = direction or "TLT"
+ local currfontid = nil
+
+ for n in direct.traverse(head) do
+ local id = getid(n)
+ local code = 0xFFFC -- OBJECT REPLACEMENT CHARACTER
+ local script = sc_common
+ local skip = false
+
+ if id == glyphid then
+ currfontid = getfont(n)
+ if getsubtype(n) > 255 then
+ skip = true
+ else
+ code = getchar(n)
+ script = getscript(code)
+ end
+ elseif id == glueid and getsubtype(n) == spaceskip then
+ code = 0x0020 -- SPACE
+ elseif id == discid then
+ code = 0x00AD -- SOFT HYPHEN
+ elseif id == dirid then
+ local dir = getdir(n)
+ if dir:sub(1, 1) == "+" then
+ -- Push the current direction to the stack.
+ table.insert(dirstack, currdir)
+ currdir = dir:sub(2)
+ else
+ assert(currdir == dir:sub(2))
+ -- Pop the last direction from the stack.
+ currdir = table.remove(dirstack)
+ end
+ elseif id == localparid then
+ currdir = getdir(n)
+ end
+
+ local fontdata = currfontid and font.getfont(currfontid)
+ local hbdata = fontdata and fontdata.hb
+ if not hbdata then skip = true end
+
+ -- Resolve common and inherited scripts. Inherited takes the script of the
+ -- previous character. Common almost the same, but we tray to make paired
+ -- characters (e.g. parentheses) to take the same script.
+ if #props > 0 and (script == sc_common or script == sc_inherited) then
+ script = props[#props].script
+ -- Paired punctuation characters
+ if paired_open[code] then
+ table.insert(pairstack, { code, script })
+ elseif paired_close[code] then
+ while #pairstack > 0 do
+ local c = table.remove(pairstack)
+ if c[1] == paired_close[code] then
+ script = c[2]
+ break
+ end
+ end
+ end
+ end
+
+ -- If script is not resolved yet, and the font has a "script" option, use
+ -- it.
+ if (script == sc_common or script == sc_inherited) and hbdata then
+ local spec = hbdata.spec
+ local features = spec.features
+ local options = spec.options
+ script = options.script and hb.Script.new(options.script) or script
+ end
+
+ codes[#codes + 1] = code
+ nodes[#nodes + 1] = n
+ props[#props + 1] = {
+ font = currfontid,
+ -- XXX handle RTT and LTL.
+ dir = currdir == "TRT" and dir_rtl or dir_ltr,
+ script = script,
+ skip = skip,
+ }
+ end
+
+ for i = #props - 1, 1, -1 do
+ -- If script is not resolved yet, use that of the next character.
+ if props[i].script == sc_common or props[i].script == sc_inherited then
+ props[i].script = props[i + 1].script
+ end
+ end
+
+ -- Split into a list of runs, each has the same font, direction and script.
+ -- TODO: itemize by language as well.
+ local runs = {}
+ local currfontid, currdir, currscript, currskip = nil, nil, nil, nil
+ for i, prop in next, props do
+ local fontid = prop.font
+ local dir = prop.dir
+ local script = prop.script
+ local skip = prop.skip
+
+ -- Start a new run if there is a change in properties.
+ if fontid ~= currfontid or
+ dir ~= currdir or
+ script ~= currscript or
+ skip ~= currskip then
+ runs[#runs + 1] = {
+ start = i,
+ len = 0,
+ font = fontid,
+ dir = dir,
+ script = script,
+ skip = skip,
+ nodes = nodes,
+ codes = codes,
+ }
+ end
+
+ runs[#runs].len = runs[#runs].len + 1
+
+ currfontid = fontid
+ currdir = dir
+ currscript = script
+ currskip = skip
+ end
+
+ return runs
+end
+
+-- Find how many characters are part of this glyph.
+--
+-- The first return value is the number of characters, with 0 meaning it is
+-- inside a multi-glyph cluster
+--
+-- The second return value is the number of glyph in this cluster.
+--
+local function chars_in_glyph(i, glyphs, stop)
+ local nchars, nglyphs = 0, 0
+ local cluster = glyphs[i].cluster
+
+ -- Glyph is not the first in cluster
+ if glyphs[i - 1] and glyphs[i - 1].cluster == cluster then
+ return 0, 0
+ end
+
+ -- Find the last glyph in this cluster.
+ while glyphs[i + nglyphs] and glyphs[i + nglyphs].cluster == cluster do
+ nglyphs = nglyphs + 1
+ end
+
+ -- The number of characters is the diff between the next cluster in this one.
+ if glyphs[i + nglyphs] then
+ nchars = glyphs[i + nglyphs].cluster - cluster
+ else
+ -- This glyph cluster in the last in the run.
+ nchars = stop - cluster - 1
+ end
+
+ return nchars, nglyphs
+end
+
+-- Check if it is safe to break before this glyph.
+local function unsafetobreak(glyph, nodes)
+ return glyph
+ and glyph.flags
+ and glyph.flags & fl_unsafe
+ -- LuaTeX’s discretionary nodes can’t contain glue, so stop at first glue
+ -- as well. This is incorrect, but I don’t have a better idea.
+ and getid(nodes[glyph.cluster + 1]) ~= glueid
+end
+
+local shape
+
+-- Make s a sub run, used by discretionary nodes.
+local function makesub(run, start, stop, nodelist)
+ local nodes = run.nodes
+ local codes = run.codes
+ local start = start
+ local stop = stop
+ local subnodes, subcodes = {}, {}
+ for i = start, stop do
+ if getid(nodes[i]) ~= discid then
+ subnodes[#subnodes + 1] = copynode(nodes[i])
+ subcodes[#subcodes + 1] = codes[i]
+ end
+ end
+ -- Prepend any existing nodes to the list.
+ for n in traverse(nodelist) do
+ subnodes[#subnodes + 1] = n
+ subcodes[#subcodes + 1] = getchar(n)
+ end
+ local subrun = {
+ start = 1,
+ len = #subnodes,
+ font = run.font,
+ script = run.script,
+ dir = run.dir,
+ fordisc = true,
+ nodes = subnodes,
+ codes = subcodes,
+ }
+ return { glyphs = shape(subrun), run = subrun }
+end
+
+-- Main shaping function that calls HarfBuzz, and does some post-processing of
+-- the output.
+shape = function(run)
+ local nodes = run.nodes
+ local codes = run.codes
+ local offset = run.start
+ local len = run.len
+ local fontid = run.font
+ local dir = run.dir
+ local script = run.script
+ local lang = run.lang
+ local fordisc = run.fordisc
+
+ local fontdata = font.getfont(fontid)
+ local hbdata = fontdata.hb
+ local palette = hbdata.palette
+ local spec = hbdata.spec
+ local features = spec.features
+ local options = spec.options
+ local hbshared = hbdata.shared
+ local hbfont = hbshared.font
+ local hbface = hbshared.face
+
+ local lang = lang or options.language or lang_invalid
+ 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)
+
+ if hb.shape_full(hbfont, buf, features, shapers) then
+ -- LuaTeX wants the glyphs in logical order, so reverse RTL buffers.
+ if dir:is_backward() then buf:reverse() end
+
+ local glyphs = buf:get_glyphs()
+
+ -- If the font has COLR/CPAL tables, decompose each glyph to its color
+ -- layers and set the color from the palette.
+ if palette then
+ for i, glyph in next, glyphs do
+ local gid = glyph.codepoint
+ local layers = hbface:ot_color_glyph_get_layers(gid)
+ if layers then
+ -- Remove this glyph, we will use its layers.
+ table.remove(glyphs, i)
+ for j, layer in next, layers do
+ -- All glyphs but the last use 0 advance so that the layers
+ -- overlap.
+ local xadavance, yadvance = nil, nil
+ if dir:is_backward() then
+ x_advance = j == 1 and glyph.x_advance or 0
+ y_advance = j == 1 and glyph.y_advance or 0
+ else
+ x_advance = j == #layers and glyph.x_advance or 0
+ y_advance = j == #layers and glyph.y_advance or 0
+ end
+ table.insert(glyphs, i + j - 1, {
+ codepoint = layer.glyph,
+ cluster = glyph.cluster,
+ x_advance = x_advance,
+ y_advance = y_advance,
+ x_offset = glyph.x_offset,
+ y_offset = glyph.y_offset,
+ flags = glyph.flags,
+ -- 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.
+ color = palette[layer.color_index],
+ })
+ end
+ end
+ end
+ end
+
+ for i, glyph in next, glyphs do
+ local nodeindex = glyph.cluster + 1
+ local nchars, nglyphs = chars_in_glyph(i, glyphs, offset + len)
+ glyph.nchars, glyph.nglyphs = nchars, nglyphs
+
+ -- Calculate the Unicode code points of this glyph. If nchars is zero
+ -- then this is a glyph inside a complex cluster and will be handled with
+ -- the start of its cluster.
+ if nchars > 0 then
+ local hex = ""
+ local str = ""
+ for j = 0, nchars - 1 do
+ local id = getid(nodes[nodeindex + j])
+ if id == glyphid or id == glueid then
+ local code = codes[nodeindex + j]
+ hex = hex..to_utf16_hex(code)
+ str = str..utf8.char(code)
+ end
+ end
+ glyph.tounicode = hex
+ glyph.string = str
+ end
+
+ -- Find if we have a discretionary inside a ligature, if nchars less than
+ -- two then either this is not a ligature or there is no discretionary
+ -- involved.
+ if nchars > 2 and not fordisc then
+ local discindex = nil
+ for j = nodeindex, nodeindex + nchars - 1 do
+ if codes[j] == 0x00AD then
+ discindex = j
+ break
+ end
+ end
+ if discindex then
+ -- Discretionary found.
+ local disc = nodes[discindex]
+ local startindex, stopindex = nil, nil
+ local startglyph, stopglyph = nil, nil
+
+ -- Find the previous glyph that is safe to break at.
+ startglyph = i
+ while unsafetobreak(glyphs[startglyph], nodes) 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 + nglyphs
+ while unsafetobreak(glyphs[stopglyph], nodes) do
+ stopglyph = stopglyph + 1
+ end
+ -- We also want the last char in the previous glyph, so no +1 below.
+ stopindex = glyphs[stopglyph].cluster
+ -- We break up to stop glyph but not including it, so the -1 below.
+ stopglyph = stopglyph - 1
+
+ -- Mark these glyph for skipping since they will be replaced by the
+ -- discretionary fields.
+ for j = startglyph, stopglyph do
+ glyphs[j].skip = true
+ end
+
+ local pre, post, rep = getpre(disc), getpost(disc), getrep(disc)
+ glyph.disc = disc
+ glyph.replace = makesub(run, startindex, stopindex, rep)
+ glyph.pre = makesub(run, startindex, discindex - 1, pre)
+ glyph.post = makesub(run, discindex + 1, stopindex, post)
+ end
+ end
+ end
+ return glyphs
+ end
+
+ return {}
+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 LuaTeX load the image from the binary data directly.
+local pngcache = {}
+local function cachedpng(data)
+ local hash = md5.sumhexa(data)
+ local path = pngcache[hash]
+ if not path then
+ path = os.tmpname()
+ local file = io.open(path, "wb")
+ file:write(data)
+ file:close()
+ pngcache[hash] = path
+ end
+ return path
+end
+
+-- Convert glyphs to nodes and collect font characters.
+local function tonodes(head, current, run, glyphs, color)
+ local nodes = run.nodes
+ local dir = run.dir
+ local fontid = run.font
+ local fontdata = font.getfont(fontid)
+ local characters = fontdata.characters
+ local hbdata = fontdata.hb
+ local hbshared = hbdata.shared
+ local hbfont = hbshared.font
+ local fontglyphs = hbshared.glyphs
+ local rtl = dir:is_backward()
+
+ local tracinglostchars = tex.tracinglostchars
+ local tracingonline = tex.tracingonline
+
+ local scale = hbdata.scale
+ local letterspace = hbdata.letterspace
+
+ local haspng = hbshared.haspng
+ local fonttype = hbshared.fonttype
+
+ for i, glyph in next, glyphs do
+ local index = glyph.cluster + 1
+ local gid = glyph.codepoint
+ local char = hb.CH_GID_PREFIX + gid
+ local n = nodes[index]
+ local id = getid(n)
+ local nchars, nglyphs = glyph.nchars, glyph.nglyphs
+
+ -- If this glyph is part of a complex cluster, then copy the node as
+ -- more than one glyph will use it.
+ if nglyphs < 1 or nglyphs > 1 then
+ n = copynode(nodes[index])
+ end
+
+ if color then
+ setprop(n, p_color, color)
+ end
+
+ if glyph.disc then
+ -- For discretionary the glyph itself is skipped and a discretionary node
+ -- is output in place of it.
+ local disc = glyph.disc
+ local rep, pre, post = glyph.replace, glyph.pre, glyph.post
+
+ setrep(disc, tonodes(nil, nil, rep.run, rep.glyphs, color))
+ setpre(disc, tonodes(nil, nil, pre.run, pre.glyphs, color))
+ setpost(disc, tonodes(nil, nil, post.run, post.glyphs, color))
+
+ head, current = insertafter(head, current, disc)
+ elseif not glyph.skip then
+ if glyph.color then
+ setprop(n, p_color, color_to_rgba(glyph.color))
+ end
+
+ if id == glyphid then
+ local fontglyph = fontglyphs[gid]
+
+ local pngblob = fontglyph.png
+ if haspng and not pngblob then
+ pngblob = hbfont:ot_color_glyph_get_png(gid)
+ fontglyph.png = pngblob
+ end
+ local character = characters[char]
+ if pngblob then
+ -- Color bitmap font, extract the PNG data and insert it in the node
+ -- list.
+ local data = pngblob:get_data()
+ local path = cachedpng(data)
+
+ local image = img.node {
+ filename = path,
+ width = character.width,
+ height = character.height,
+ depth = character.depth,
+ }
+ head, current = insertafter(head, current, todirect(image))
+ else
+ if haspng and not fonttype then
+ -- If this is a color bitmap font with no glyph outlines (like Noto
+ -- Color Emoji) and we end up here then the glyph is not supported
+ -- by the font. LuaTeX does not now 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.
+ setfont(n, 0)
+ else
+ local oldcharacter = characters[getchar(n)]
+ -- If the glyph index of current font chars 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 not oldcharacter or character.index ~= oldcharacter.index then
+ setchar(n, char)
+ end
+ end
+ local xoffset = (rtl and -glyph.x_offset or glyph.x_offset) * scale
+ local yoffset = glyph.y_offset * scale
+ setoffsets(n, xoffset, yoffset)
+ protectglyph(n)
+ head, current = insertafter(head, current, n)
+
+ local x_advance = glyph.x_advance + letterspace
+ local width = fontglyph.width
+ if width ~= x_advance then
+ -- LuaTeX always uses the glyph width from the font, so we need to
+ -- insert a kern node if the x advance is different.
+ local kern = newnode(kernid)
+ setkern(kern, (x_advance - width) * scale)
+ copyprops(n, kern)
+ if rtl then
+ head = insertbefore(head, current, kern)
+ else
+ head, current = insertafter(head, current, kern)
+ end
+ end
+
+ -- HarfTeX 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.
+ setprop(n, p_string, 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 nglyphs == 1 and not fontglyph.tounicode then
+ fontglyph.tounicode = tounicode
+ elseif tounicode ~= fontglyph.tounicode then
+ setprop(n, p_startactual, tounicode)
+ glyphs[i + nglyphs - 1].endactual = true
+ end
+ end
+ if glyph.endactual then
+ setprop(n, p_endactual, true)
+ end
+ end
+ elseif id == glueid and getsubtype(n) == spaceskip 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(n, width)
+ setfield(n, "stretch", width / 2)
+ setfield(n, "shrink", width / 3)
+ end
+ head, current = insertafter(head, current, n)
+ elseif id == kernid and getsubtype(n) == italiccorrection 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.
+ -- I’d have expected the engine to do this, but apparently it doesn’t.
+ -- May be it is checking for the italic correction before we have had
+ -- loaded the glyph?
+ local prevchar, prevfontid = isglyph(current)
+ if prevchar > 0 then
+ local prevfontdata = font.getfont(prevfontid)
+ local prevcharacters = prevfontdata and prevfontdata.characters
+ local italic = prevcharacters and prevcharacters[prevchar].italic
+ if italic then
+ setkern(n, italic)
+ end
+ end
+ head, current = insertafter(head, current, n)
+ elseif id == discid then
+ assert(nglyphs == 1)
+ -- The simple case of a discretionary that is not part of a complex
+ -- cluster. We only need to make sure kerning before the hyphenation
+ -- point is dropped when a line break is inserted here.
+ --
+ -- TODO: nothing as simple as it sounds, we need to handle this like
+ -- the other discretionary handling, otherwise the discretionary
+ -- contents do not interact with the surrounding (e.g. no ligatures or
+ -- kerning) as it should.
+ if current and getid(current) == kernid and getsubtype(current) == fontkern then
+ setprev(current, nil)
+ setnext(current, nil)
+ setfield(n, "replace", current)
+ head, current = removenode(head, current)
+ end
+ local pre, post, rep = getpre(n), getpost(n), getrep(n)
+ setfield(n, "pre", process(pre, direction))
+ setfield(n, "post", process(post, direction))
+ setfield(n, "replace", process(rep, direction))
+
+ head, current = insertafter(head, current, n)
+ else
+ head, current = insertafter(head, current, n)
+ end
+ end
+ end
+
+ return head, current
+end
+
+local function validate_color(s)
+ local r = tonumber(s:sub(1, 2), 16)
+ local g = tonumber(s:sub(3, 4), 16)
+ local b = tonumber(s:sub(5, 6), 16)
+ if not (r and g and b) then return end
+ if #s == 8 then
+ local a = tonumber(s:sub(7, 8), 16)
+ if not a then return end
+ end
+ return s
+end
+
+local function hex_to_rgba(s)
+ if not validate_color(s) then return end
+ local r = tonumber(s:sub(1, 2), 16) / 255
+ local g = tonumber(s:sub(3, 4), 16) / 255
+ local b = tonumber(s:sub(5, 6), 16) / 255
+ if #s == 8 then
+ local a = tonumber(s:sub(7, 8), 16) / 255
+ -- XXX: alpha
+ return format('%s %s %s rg', r, g, b)
+ else
+ return format('%s %s %s rg', r, g, b)
+ end
+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.hb.spec.options
+ local color = options and options.color and hex_to_rgba(options.color)
+
+ local glyphs = shape(run)
+ head, current = tonodes(head, current, run, glyphs, color)
+ else
+ -- Not shaping, insert the original node list of of this run.
+ local nodes = run.nodes
+ local offset = run.start
+ local len = run.len
+ for i = offset, offset + len - 1 do
+ head, current = insertafter(head, current, nodes[i])
+ end
+ end
+
+ return head, current
+end
+
+process = function(head, direction)
+ local newhead, current = nil, nil
+ local runs = itemize(head, direction)
+
+ for _, run in next, runs do
+ newhead, current = shape_run(newhead, current, run)
+ end
+
+ return newhead or head
+end
+
+local function process_nodes(head, groupcode, size, packtype, direction)
+ local head = todirect(head)
+
+ -- Check if any fonts are loaded by us and then process the whole node list,
+ -- we will take care of skipping fonts we did not load later, otherwise
+ -- return unmodified head.
+ for n in traverseid(glyphid, head) do
+ local fontid = getfont(n)
+ local fontdata = font.getfont(fontid)
+ local hbdata = fontdata and fontdata.hb
+ if hbdata then
+ head = process(head, direction)
+ break
+ end
+ end
+
+ -- Nothing to do; no glyphs or no HarfBuzz fonts.
+ return tonode(head)
+end
+
+local function pdfdirect(data)
+ local n = newnode("whatsit", "pdf_literal")
+ setfield(n, "mode", directmode)
+ setdata(n, data)
+ return n
+end
+
+local function post_process(head, currentcolor)
+ for n in traverse(head) do
+ local props = getproperty(n)
+ local harfprops = props and props.harf
+
+ local startactual, endactual, color
+ if harfprops then
+ startactual = harfprops[p_startactual]
+ endactual = harfprops[p_endactual]
+ color = harfprops[p_color]
+ end
+
+ if currentcolor and currentcolor ~= color then
+ -- Pop current color.
+ head = insertbefore(head, n, pdfdirect("0 g"))
+ end
+
+ if currentcolor ~= color then
+ -- Push new color.
+ head = insertbefore(head, n, pdfdirect(color))
+ currentcolor = color
+ end
+
+ if startactual then
+ local actualtext = "/Span<</ActualText<FEFF"..startactual..">>>BDC"
+ head = insertbefore(head, n, pdfdirect(actualtext))
+ end
+
+ if endactual then
+ head = insertafter(head, n, pdfdirect("EMC"))
+ end
+
+ local replace = getfield(n, "replace")
+ if replace then
+ setfield(n, "replace", post_process(replace, currentcolor))
+ end
+
+ local subhead = getfield(n, "head")
+ if subhead then
+ setfield(n, "head", post_process(subhead, currentcolor))
+ 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.
+ for _, path in next, pngcache do
+ os.remove(path)
+ end
+end
+
+local function get_tounicode(fontid, c)
+ local fontdata = font.getfont(fontid)
+ local hbdata = fontdata.hb
+ if hbdata then
+ local hbshared = hbdata.shared
+ local fontglyphs = hbshared.glyphs
+ local hbfont = hbshared.font
+
+ local gid = c - hb.CH_GID_PREFIX
+ if c < hb.CH_GID_PREFIX then
+ gid = hbfont:get_nominal_glyph(c)
+ end
+
+ return fontglyphs[gid].tounicode
+ end
+end
+
+local function get_glyph_string(n)
+ local n = todirect(n)
+ local props = getproperty(n)
+ props = props and props.harf
+ return props and props[p_string] or nil
+end
+
+return {
+ process = process_nodes,
+ post_process = post_process_nodes,
+ cleanup = run_cleanup,
+ get_tounicode = get_tounicode,
+ get_glyph_string = get_glyph_string,
+}