diff options
Diffstat (limited to 'macros/luatex/generic/luaotfload/luaotfload-letterspace.lua')
-rw-r--r-- | macros/luatex/generic/luaotfload/luaotfload-letterspace.lua | 578 |
1 files changed, 578 insertions, 0 deletions
diff --git a/macros/luatex/generic/luaotfload/luaotfload-letterspace.lua b/macros/luatex/generic/luaotfload/luaotfload-letterspace.lua new file mode 100644 index 0000000000..1acf67df8b --- /dev/null +++ b/macros/luatex/generic/luaotfload/luaotfload-letterspace.lua @@ -0,0 +1,578 @@ +----------------------------------------------------------------------- +-- FILE: luaotfload-letterspace.lua +-- DESCRIPTION: part of luaotfload / letterspacing +----------------------------------------------------------------------- + +local ProvidesLuaModule = { + name = "luaotfload-letterspace", + version = "2.991", --TAGVERSION + date = "2019-08-11", --TAGDATE + description = "luaotfload submodule / color", + license = "GPL v2.0", + copyright = "PRAGMA ADE / ConTeXt Development Team", + author = "Hans Hagen, PRAGMA-ADE, Hasselt NL; adapted by Philipp Gesang, Ulrike Fischer, Marcel Krüger" +} + +if luatexbase and luatexbase.provides_module then + luatexbase.provides_module (ProvidesLuaModule) +end + +--- This code diverged quite a bit from its origin in Context. Please +--- do *not* report bugs on the Context list. + +local logreport = luaotfload.log.report + +local getmetatable = getmetatable +local setmetatable = setmetatable +local tonumber = tonumber +local unpack = table.unpack + +local next = next +local node, fonts = node, fonts + +local nodedirect = node.direct + +local getfield = nodedirect.getfield +local setfield = nodedirect.setfield + +local getfont = nodedirect.getfont +local getid = nodedirect.getid + +local getnext = nodedirect.getnext +local setnext = nodedirect.setnext + +local getprev = nodedirect.getprev +local setprev = nodedirect.setprev + +local getboth = nodedirect.getboth + +local setlink = nodedirect.setlink + +local getdisc = nodedirect.getdisc +local setdisc = nodedirect.setdisc + +local getsubtype = nodedirect.getsubtype +local setsubtype = nodedirect.setsubtype + +local getchar = nodedirect.getchar +local setchar = nodedirect.setchar + +local getkern = nodedirect.getkern +local setkern = nodedirect.setkern + +local getglue = nodedirect.getglue +local setglue = nodedirect.setglue + +local find_node_tail = nodedirect.tail +local todirect = nodedirect.todirect +local tonode = nodedirect.tonode + +local insert_node_before = nodedirect.insert_before +local free_node = nodedirect.free +local copy_node = nodedirect.copy +local new_node = nodedirect.new + +local glyph_code = node.id"glyph" +local kern_code = node.id"kern" +local disc_code = node.id"disc" +local glue_code = node.id"glue" +local whatsit_code = node.id"whatsit" + +local fonthashes = fonts.hashes +local identifiers = fonthashes.identifiers +local chardata = fonthashes.characters +local otffeatures = fonts.constructors.newfeatures "otf" + +local function getprevreal(n) + repeat + n = getprev(n) + until not n or getid(n) ~= whatsit_code + return n +end +local function getnextreal(n) + repeat + n = getnext(n) + until not n or getid(n) ~= whatsit_code + return n +end + +--[[doc-- + + Since the letterspacing method was derived initially from Context’s + typo-krn.lua we keep the sub-namespace “letterspace” inside the + “luaotfload” table. + +--doc]]-- + +luaotfload.letterspace = luaotfload.letterspace or { } +local letterspace = luaotfload.letterspace + +local lectured = false +letterspace.keepligature = true +letterspace.keeptogether = false +letterspace.keepwordspacing = false + +---=================================================================--- +--- preliminary definitions +---=================================================================--- +-- We set up a layer emulating some Context internals that are needed +-- for the letterspacing callback. +----------------------------------------------------------------------- +--- node-ini +----------------------------------------------------------------------- + +local kerning_code = 0 +local userkern_code = 1 +local userskip_code = 0 +local spaceskip_code = 13 +local xspaceskip_code = 14 + +if not chardata then + chardata = { } + table.setmetatableindex(chardata, function(t, k) + if k == true then + return chardata[currentfont()] + else + local tfmdata = font.getfont(k) or font.fonts[k] + if tfmdata then + local characters = tfmdata.characters + t[k] = characters + return characters + end + end + end) + fonthashes.characters = chardata +end + +---=================================================================--- +--- character kerning functionality +---=================================================================--- + +-- UF changed 2017-07-14 +local kern_injector = function (fillup, kern) + if fillup then + local g = new_node(glue_code) + setglue(g, 0, kern, 0, 1, 0) + return g + end + local g = new_node(kern_code) + setkern(g,kern) + return g +end +-- /UF + +local kernable_skip = function (n) + local st = getsubtype (n) + return st == userskip_code + or st == spaceskip_code + or st == xspaceskip_code +end + +--[[doc-- + + Caveat lector. + This is an adaptation of the Context character kerning mechanism + that emulates XeTeX-style fontwise letterspacing. Note that in its + present state it is far inferior to the original, which is + attribute-based and ignores font-boundaries. Nevertheless, due to + popular demand the following callback has been added. + +--doc]]-- + +local kernamounts = table.setmetatableindex(function (t, f) --- fontid -> {kern, fill} + local tfmdata = font.getfont(f) or font.fonts[f] + if tfmdata then + local fontproperties = tfmdata.properties + local fontparameters = tfmdata.parameters + if fontproperties and fontparameters then + local r + if fontproperties.kerncharacters == "max" then + r = {fontparameters.quad/4, true} + elseif fontproperties.kerncharacters then + r = {fontproperties.kerncharacters * fontparameters.quad, false} + else + r = {} + end + t[f] = r + return r + end + end + return {} +end) + +local kerncharacters +kerncharacters = function (head) + local start = head + local lastfont = nil + local keeptogether = letterspace.keeptogether --- function + local keepligature = letterspace.keepligature +-- if not lectured and keepligature ~= true then +-- logreport ("both", 0, "letterspace", +-- "Breaking ligatures through letterspacing is deprecated and " +-- .. "will be removed soon. Please disable unwanted ligatures through " +-- .. "font features instead and reset luaotfload.letterspace.keepligature " +-- .. "to true to maintain compatibility with future versions of luaotfload.") +-- lectured = true +-- end + if type(keepligature) ~= "function" then + local savedligature = keepligature + keepligature = function() return savedligature end + end + local keepwordspacing = letterspace.keepwordspacing + if type(keepwordspacing) ~= "function" then + local savedwordspacing = keepwordspacing + keepwordspacing = function() return savedwordspacing end + end + + local kernamounts = kernamounts + local firstkern = true + + while start do + local id = getid(start) + if id == glyph_code then + --- 1) look up kern factor (slow, but cached rudimentarily) + local fontid = getfont(start) + local krn, fillup = unpack(kernamounts[fontid]) + if not krn or krn == 0 then + firstkern = true + goto nextnode + elseif firstkern then + firstkern = false + if (id ~= disc_code) and (not getfield(start, "components")) then + --- not a ligature, skip node + goto nextnode + end + end + + lastfont = fontid + + --- 2) resolve ligatures + local c = getfield(start, "components") + + if c then + if keepligature(start) then + -- keep 'm + c = nil + else + while c do + local s = start + local p, n = getboth (s) + if p then + setlink (p, c) + else + head = c + end + if n then + local tail = find_node_tail(c) + setlink (tail, n) + end + start = c + setfield(s, "components", nil) + free_node(s) --> double free with multipart components + c = getfield (start, "components") + end + end + end -- kern ligature + + --- 3) apply the extra kerning + local prev = getprevreal(start) + if prev then + local pid = getid(prev) + + if not pid then + -- nothing + + elseif pid == glue_code and kernable_skip(prev) + and not keepwordspacing(prev, lastfont) then + local wd, stretch, shrink = getglue(prev) + if wd > 0 then + local newwd = wd + krn + local stretched = (stretch * newwd) / wd + local shrunk = (shrink * newwd) / wd + if fillup then + setglue(prev, newwd, 2 * stretched, 2 * shrunk, 1, 0) + else + setglue(prev, newwd, stretched, shrunk, 0, 0) + end + end + + elseif pid == kern_code then + local prev_subtype = getsubtype(prev) + if prev_subtype == kerning_code --- context does this by means of an + or prev_subtype == userkern_code --- attribute; we may need a test + then + + local pprev = getprevreal(prev) + local pprev_id = getid(pprev) + + if keeptogether + and pprev_id == glyph_code + and keeptogether(pprev, start) + then + -- keep + else + setsubtype (prev, userkern_code) + local prev_kern = getkern(prev) + prev_kern = prev_kern + krn + setkern (prev, prev_kern) + end + end + + elseif pid == glyph_code then + if getfont(prev) == lastfont then + local prevchar = getchar(prev) + local lastchar = getchar(start) + if keeptogether and keeptogether(prev, start) then + -- keep 'm + elseif identifiers[lastfont] then + local lastfontchars = chardata[lastfont] + if lastfontchars then + local prevchardata = lastfontchars[prevchar] + if not prevchardata then + --- font doesn’t contain the glyph + else + local kern = 0 + local kerns = prevchardata.kerns + if kerns then kern = kerns[lastchar] or kern end + krn = kern + krn -- here + insert_node_before(head,start,kern_injector(fillup,krn)) + end + end + end + else + insert_node_before(head,start,kern_injector(fillup,krn)) + end + + elseif pid == disc_code then + local disc = prev -- disc + local pre, post, replace = getdisc (disc) + local prv = getprevreal(disc) + local nxt = getnextreal(disc) + + if pre and prv then -- must pair with start.prev + -- this one happens in most cases + local before = copy_node(prv) + setprev(pre, before) + setnext(before, pre) + setprev(before, nil) + pre = kerncharacters (before) + pre = getnext(pre) + setprev(pre, nil) + setfield(disc, "pre", pre) + free_node(before) + end + + if post and nxt then -- must pair with start + local after = copy_node(nxt) + local tail = find_node_tail(post) + setnext(tail, after) + setprev(after, tail) + post = kerncharacters (post) + setnext(getprev(after), nil) + setfield(disc, "post", post) + free_node(after) + end + + if replace and prv and nxt then -- must pair with start and start.prev + local before = copy_node(prv) + local after = copy_node(nxt) + local tail = find_node_tail(replace) + setprev(replace, before) + setnext(before, replace) + setprev(before, nil) + setnext(tail, after) + setprev(after, tail) + setnext(after, nil) + replace = kerncharacters (before) + replace = getnext(replace) + setprev(replace, nil) + setnext(getprev(after), nil) + setfield(disc, "replace", replace) + free_node(after) + free_node(before) + + elseif identifiers[lastfont] then + if prv + and getid(prv) == glyph_code + and getfont(prv) == lastfont + then + local kern = 0 + local prevchar = getchar(prv) + local lastchar = getchar(start) + local lastfontchars = chardata[lastfont] + if lastfontchars then + local prevchardata = lastfontchars[prevchar] + if not prevchardata then + --- font doesn’t contain the glyph + else + local kerns = prevchardata.kerns + if kerns then kern = kerns[lastchar] or kern end + end + end + krn = kern + krn -- here + end + setfield(disc, "replace", kern_injector(false, krn)) + end --[[if replace and prv and nxt]] + end --[[if not pid]] + end --[[if prev]] + end --[[if id == glyph_code]] + + ::nextnode:: + if start then + start = getnext(start) + end + end + return head +end + +---=================================================================--- +--- integration +---=================================================================--- + +--- · callback: kerncharacters +--- · enabler: enablefontkerning + +--- callback wrappers + +--- (node_t -> node_t) -> string -> string list -> bool +local registered_as = { } --- procname -> callbacks +local add_processor = function (processor, name, ...) + local callbacks = { ... } + for i=1, #callbacks do + luatexbase.add_to_callback(callbacks[i], processor, name) + end + registered_as[name] = callbacks --- for removal + return true +end + +--- string -> bool +local remove_processor = function (name) + local callbacks = registered_as[name] + if callbacks then + for i=1, #callbacks do + luatexbase.remove_from_callback(callbacks[i], name) + end + return true + end + return false --> unregistered +end + +--- When font kerning is requested, usually by defining a font with the +--- ``letterspace`` parameter, we inject a wrapper for the +--- ``kerncharacters()`` node processor in the relevant callbacks. This +--- wrapper initially converts the received head node into its “direct” +--- counterpart. Likewise, the callback result is converted back to an +--- ordinary node prior to returning. Internally, ``kerncharacters()`` +--- performs all node operations on direct nodes. + +--- unit -> bool +local enablefontkerning = function ( ) + + local handler = function (hd) + local direct_hd = todirect (hd) + logreport ("term", 5, "letterspace", + "kerncharacters() invoked with node.direct interface \z + (``%s`` -> ``%s``)", tostring (hd), tostring (direct_hd)) + local direct_hd = kerncharacters (direct_hd) + if not direct_hd then --- bad + logreport ("both", 0, "letterspace", + "kerncharacters() failed to return a valid new head") + end + return tonode (direct_hd) + end + + return add_processor( handler + , "luaotfload.letterspace" + , "pre_linebreak_filter" + , "hpack_filter") +end + +--[[doc-- + + Fontwise kerning is enabled via the “kernfactor” option at font + definition time. Unlike the Context implementation which relies on + Luatex attributes, it uses a font property for passing along the + letterspacing factor of a node. + + The callback is activated the first time a letterspaced font is + requested and stays active until the end of the run. Since the font + is a property of individual glyphs, every glyph in the entire + document must be checked for the kern property. This is quite + inefficient compared to Context’s attribute based approach, but Xetex + compatibility reduces our options significantly. + +--doc]]-- + + +local fontkerning_enabled = false --- callback state + +--- fontobj -> float -> unit +local initializefontkerning = function (tfmdata, factor) + if factor ~= "max" then + factor = tonumber (factor) or 0 + end + if factor == "max" or factor ~= 0 then + local fontproperties = tfmdata.properties + if fontproperties then + --- hopefully this field stays unused otherwise + fontproperties.kerncharacters = factor + end + if not fontkerning_enabled then + fontkerning_enabled = enablefontkerning () + end + end +end + +--- like the font colorization, fontwise kerning is hooked into the +--- feature mechanism + +otffeatures.register { + name = "kernfactor", + description = "kernfactor", + initializers = { + base = initializefontkerning, + node = initializefontkerning, + } +} + +--[[doc-- + + The “letterspace” feature is essentially identical with the above + “kernfactor” method, but scales the factor to percentages to match + Xetex’s behavior. (See the Xetex reference, page 5, section 1.2.2.) + + Since Xetex doesn’t appear to have a (documented) “max” keyword, we + assume all input values are numeric. + +--doc]]-- + +local initializecompatfontkerning = function (tfmdata, percentage) + local factor = tonumber (percentage) + if not factor then + logreport ("both", 0, "letterspace", + "Invalid argument to letterspace: %s (type %q), " .. + "was expecting percentage as Lua number instead.", + percentage, type (percentage)) + return + end + return initializefontkerning (tfmdata, factor * 0.01) +end + +otffeatures.register { + name = "letterspace", + description = "letterspace", + initializers = { + base = initializecompatfontkerning, + node = initializecompatfontkerning, + } +} + +--[[example-- + +See https://bitbucket.org/phg/lua-la-tex-tests/src/tip/pln-letterspace-8-compare.tex +for an example. + +--example]]-- + +--- vim:sw=2:ts=2:expandtab:tw=71 + |