diff options
Diffstat (limited to 'texmf-dist/tex/harftex/base/harf-node.lua')
-rw-r--r-- | texmf-dist/tex/harftex/base/harf-node.lua | 992 |
1 files changed, 0 insertions, 992 deletions
diff --git a/texmf-dist/tex/harftex/base/harf-node.lua b/texmf-dist/tex/harftex/base/harf-node.lua deleted file mode 100644 index 466db2ac..00000000 --- a/texmf-dist/tex/harftex/base/harf-node.lua +++ /dev/null @@ -1,992 +0,0 @@ -local module = { - name = "harf-node", - description = "Harf text shaping", - version = "0.4.2", - date = "2019-09-07", - license = "GPL v2.0" -} -luatexbase.provides_module(module) - -local hb = require("harf-base") - -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 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 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 pdfcolorstack_t = node.subtype("pdf_colorstack") - -local explicitdisc_t = 1 -local fontkern_t = 0 -local italiccorr_t = 3 -local regulardisc_t = 3 -local spaceskip_t = 13 - -local getscript = hb.unicode.script - -local common_s = hb.Script.new("Zyyy") -local inherited_s = hb.Script.new("Zinh") -local unknown_s = hb.Script.new("Zzzz") -local latn_s = hb.Script.new("Latn") - -local invalid_l = hb.Language.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 = "startactualtext" -local endactual_p = "endactualtext" -local color_p = "color" -local string_p = "string" - --- 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 - --- New kern node of amount `v`, inheriting the properties/attributes of `n`. -local function newkern(v, n) - local kern = newnode(kern_t) - local props = getproperty(n) - local attrs = getattrs(n) - if props then - setproperty(kern, copytable(props)) - end - if attrs then - setattrs(kern, copynodelist(attrs)) - end - - 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 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 trep = hb.texrep - -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 = common_s - local skip = false - - if id == glyph_t then - currfontid = getfont(n) - if getsubtype(n) > 255 then - skip = true - else - code = getchar(n) - script = getscript(code) - end - elseif id == glue_t and getsubtype(n) == spaceskip_t then - code = 0x0020 -- SPACE - elseif id == disc_t - and (getsubtype(n) == explicitdisc_t -- \- - or getsubtype(n) == regulardisc_t) -- \discretionary - then - code = 0x00AD -- SOFT HYPHEN - 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 - elseif id == localpar_t then - currdir = getdir(n) - end - - local fontdata = currfontid and font.getfont(currfontid) - local hbdata = fontdata and fontdata.hb - local options = hbdata and hbdata.options - local texlig = options and options.texlig - if texlig then - local replacement = trep[code] - if replacement then - code = replacement - end - end - - 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 == common_s or script == inherited_s) then - script = props[#props].script - -- Paired punctuation characters - if paired_open[code] then - tableinsert(pairstack, { code, script }) - elseif paired_close[code] then - while #pairstack > 0 do - local c = tableremove(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 == common_s or script == inherited_s) and hbdata then - local options = hbdata.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 == common_s or props[i].script == inherited_s 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 not safe to break before this glyph. -local function unsafetobreak(glyph, nodes) - return glyph - and glyph.flags - and glyph.flags & fl_unsafe - -- 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]) ~= glue_t -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]) ~= disc_t 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 features = hbdata.features - local options = hbdata.options - local palette = hbdata.palette - local hbshared = hbdata.shared - local hbfont = hbshared.font - local hbface = hbshared.face - - local lang = lang or options.language or invalid_l - 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() - - -- 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. - tableremove(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 - tableinsert(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 == glyph_t or id == glue_t 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 the engine 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 = ostmpname() - local file = 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, color_p, 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, color_p, color_to_rgba(glyph.color)) - end - - if id == glyph_t 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 = imgnode { - filename = path, - width = character.width, - height = character.height, - depth = character.depth, - } - head, current = insertafter(head, current, todirect(image)) - if fonttype then - -- Color bitmap font with glyph outlines. Insert negative kerning - -- as we will insert the glyph node below (to help with text - -- copying) and want the bitmap and the glyph to take the same - -- advance width. - local kern = newkern(-character.width, n) - head, current = insertkern(head, current, kern, rtl) - end - end - if pngblob and not fonttype then - -- Color bitmap font with no glyph outlines, and has a bitmap for - -- this glyph. No further work is needed. - elseif haspng and 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 insert the glyph node and move on, no further work is needed. - setfont(n, 0) - head, current = insertafter(head, current, n) - else - local oldcharacter = characters[getchar(n)] - -- 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 not oldcharacter or character.index ~= oldcharacter.index then - setchar(n, char) - 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 - -- 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, n) - head, current = insertkern(head, current, kern, rtl) - end - - fontglyph.used = 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. - setprop(n, string_p, 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, startactual_p, tounicode) - glyphs[i + nglyphs - 1].endactual = true - end - end - if glyph.endactual then - setprop(n, endactual_p, true) - end - end - elseif id == glue_t and getsubtype(n) == 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(n, width) - setfield(n, "stretch", width / 2) - setfield(n, "shrink", width / 3) - end - head, current = insertafter(head, current, n) - elseif id == kern_t and getsubtype(n) == 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. - -- 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 and 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 == disc_t 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) == kern_t and getsubtype(current) == fontkern_t 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.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(glyph_t, 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_t, pdfliteral_t) - setfield(n, "mode", 2) -- direct - setdata(n, data) - return n -end - -local function pdfcolor(color) - local c = newnode(whatsit_t, pdfcolorstack_t) - setfield(c, "stack", 0) - setfield(c, "command", color and 1 or 2) -- 1: push, 2: pop - setfield(c, "data", color) - return c -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[startactual_p] - endactual = harfprops[endactual_p] - color = harfprops[color_p] - end - - if currentcolor and currentcolor ~= color then - -- Pop current color. - currentcolor = nil - head = insertbefore(head, n, pdfcolor(currentcolor)) - end - - if currentcolor ~= color then - -- Push new color. - currentcolor = color - head = insertbefore(head, n, pdfcolor(currentcolor)) - 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 - 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 glyphs = hbdata.shared.glyphs - for gid = 0, #glyphs do - local glyph = glyphs[gid] - if glyph.used then - local tounicode = glyph.tounicode or "FFFD" - local character = characters[gid + hb.CH_GID_PREFIX] - newcharacters[gid + hb.CH_GID_PREFIX] = character - character.tounicode = tounicode - character.used = true - end - end - font.addcharacters(fontid, { characters = newcharacters }) - end - 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[string_p] or nil -end - -return { - process = process_nodes, - post_process = post_process_nodes, - cleanup = run_cleanup, - set_tounicode = set_tounicode, - get_glyph_string = get_glyph_string, -} |