diff options
Diffstat (limited to 'macros/luatex/generic/luaotfload/luaotfload-colors.lua')
-rw-r--r-- | macros/luatex/generic/luaotfload/luaotfload-colors.lua | 414 |
1 files changed, 414 insertions, 0 deletions
diff --git a/macros/luatex/generic/luaotfload/luaotfload-colors.lua b/macros/luatex/generic/luaotfload/luaotfload-colors.lua new file mode 100644 index 0000000000..d9dabd1c6c --- /dev/null +++ b/macros/luatex/generic/luaotfload/luaotfload-colors.lua @@ -0,0 +1,414 @@ +----------------------------------------------------------------------- +-- FILE: luaotfload-colors.lua +-- DESCRIPTION: part of luaotfload / font colors +----------------------------------------------------------------------- + +local ProvidesLuaModule = { + name = "luaotfload-colors", + version = "2.991", --TAGVERSION + date = "2019-08-11", --TAGDATE + description = "luaotfload submodule / color", + license = "GPL v2.0", + author = "Khaled Hosny, Elie Roux, Philipp Gesang, Dohyun Kim, David Carlisle", + copyright = "Luaotfload Development Team" + } + +if luatexbase and luatexbase.provides_module then + luatexbase.provides_module (ProvidesLuaModule) +end + + +--[[doc-- + +buggy coloring with the pre_output_filter when expansion is enabled + · tfmdata for different expansion values is split over different objects + · in ``initializeexpansion()``, chr.expansion_factor is set, and only + those characters that have it are affected + · in constructors.scale: chr.expansion_factor = ve*1000 if commented out + makes the bug vanish + +explanation: http://tug.org/pipermail/luatex/2013-May/004305.html + +--doc]]-- + +local logreport = luaotfload and luaotfload.log.report or print + +local nodedirect = node.direct +local newnode = nodedirect.new +local insert_node_before = nodedirect.insert_before +local insert_node_after = nodedirect.insert_after +local todirect = nodedirect.todirect +local tonode = nodedirect.tonode +local setfield = nodedirect.setfield +local getid = nodedirect.getid +local getfont = nodedirect.getfont +local getlist = nodedirect.getlist +local getsubtype = nodedirect.getsubtype +local getnext = nodedirect.getnext +local nodetail = nodedirect.tail +local getattribute = nodedirect.has_attribute +local setattribute = nodedirect.set_attribute + +local stringformat = string.format +local identifiers = fonts.hashes.identifiers + +local add_color_callback --[[ this used to be a global‽ ]] + +--[[doc-- +This converts a single octet into a decimal with three digits of +precision. The optional second argument limits precision to a single +digit. +--doc]]-- + +--- string -> bool? -> string +local hex_to_dec = function (hex,one) --- one isn’t actually used anywhere ... + if one then + return stringformat("%.1g", tonumber(hex, 16)/255) + else + return stringformat("%.3g", tonumber(hex, 16)/255) + end +end + +--[[doc-- +Color string validator / parser. +--doc]]-- + +local lpeg = require"lpeg" +local lpegmatch = lpeg.match +local C, Cg, Ct, P, R, S = lpeg.C, lpeg.Cg, lpeg.Ct, lpeg.P, lpeg.R, lpeg.S + +local digit16 = R("09", "af", "AF") +local opaque = S("fF") * S("fF") +local octet = C(digit16 * digit16) + +local p_rgb = octet * octet * octet +local p_rgba = p_rgb * (octet - opaque) +local valid_digits = C(p_rgba + p_rgb) -- matches eight or six hex digits + +local p_Crgb = Cg(octet/hex_to_dec, "red") --- for captures + * Cg(octet/hex_to_dec, "green") + * Cg(octet/hex_to_dec, "blue") +local p_Crgba = p_Crgb * Cg(octet/hex_to_dec, "alpha") +local extract_color = Ct(p_Crgba + p_Crgb) + +--- string -> (string | nil) +local sanitize_color_expression = function (digits) + digits = tostring(digits) + local sanitized = lpegmatch(valid_digits, digits) + if not sanitized then + logreport("both", 0, "color", + "%q is not a valid rgb[a] color expression", + digits) + return nil + end + return sanitized +end + +--- something is carried around in ``res`` +--- for later use by color_handler() --- but what? + +local res = nil + +--- float -> unit +local function pageresources(alpha) + res = res or {} + res[alpha] = true +end + +--- we store results of below color handler as tuples of +--- push/pop strings +local color_cache = { } --- (string, (string * string)) hash_t + +--- string -> (string * string) +local hex_to_rgba = function (digits) + if not digits then + return + end + + --- this is called like a thousand times, so some + --- memoizing is in order. + local cached = color_cache[digits] + if not cached then + local push, pop + local rgb = lpegmatch(extract_color, digits) + if rgb.alpha then + pageresources(rgb.alpha) + push = stringformat( + "/TransGs%g gs %s %s %s rg", + rgb.alpha, + rgb.red, + rgb.green, + rgb.blue) + pop = "0 g /TransGs1 gs" + else + push = stringformat( + "%s %s %s rg", + rgb.red, + rgb.green, + rgb.blue) + pop = "0 g" + end + color_cache[digits] = { push, pop } + return push, pop + end + + return cached[1], cached[2] +end + +--- Luatex internal types + +local nodetype = node.id +local glyph_t = nodetype("glyph") +local hlist_t = nodetype("hlist") +local vlist_t = nodetype("vlist") +local whatsit_t = nodetype("whatsit") +local disc_t = nodetype("disc") +local pdfliteral_t = node.subtype("pdf_literal") +local colorstack_t = node.subtype("pdf_colorstack") +local mlist_to_hlist = node.mlist_to_hlist + +local color_callback +local color_attr = luatexbase.new_attribute("luaotfload_color_attribute") + +-- (node * node * string * bool * (bool | nil)) -> (node * node * (string | nil)) +local color_whatsit +color_whatsit = function (head, curr, color, push, tail) + local pushdata = hex_to_rgba(color) + local colornode = newnode(whatsit_t, colorstack_t) + setfield(colornode, "stack", 0) + setfield(colornode, "command", push and 1 or 2) -- 1: push, 2: pop + setfield(colornode, "data", push and pushdata or nil) + if tail then + head, curr = insert_node_after (head, curr, colornode) + else + head = insert_node_before(head, curr, colornode) + end + if not push and color:len() > 6 then + local colornode = newnode(whatsit_t, pdfliteral_t) + setfield(colornode, "mode", 2) + setfield(colornode, "data", "/TransGs1 gs") + if tail then + head, curr = insert_node_after (head, curr, colornode) + else + head = insert_node_before(head, curr, colornode) + end + end + color = push and color or nil + return head, curr, color +end + +-- number -> string | nil +local get_font_color = function (font_id) + local tfmdata = identifiers[font_id] + local font_color = tfmdata and tfmdata.properties and tfmdata.properties.color + return font_color +end + +--[[doc-- +While the second argument and second returned value are apparently +always nil when the function is called, they temporarily take string +values during the node list traversal. +--doc]]-- + +--- (node * (string | nil)) -> (node * (string | nil)) +local node_colorize +node_colorize = function (head, toplevel, current_color) + local n = head + while n do + local n_id = getid(n) + + if n_id == hlist_t or n_id == vlist_t then + local n_list = getlist(n) + if getattribute(n_list, color_attr) then + if current_color then + head, n, current_color = color_whatsit(head, n, current_color, false) + end + else + n_list, current_color = node_colorize(n_list, false, current_color) + if current_color and getsubtype(n) == 1 then -- created by linebreak + n_list, _, current_color = color_whatsit(n_list, nodetail(n_list), current_color, false, true) + end + setfield(n, "head", n_list) + end + + elseif n_id == glyph_t then + --- colorization is restricted to those fonts + --- that received the “color” property upon + --- loading (see ``setcolor()`` above) + local font_color = get_font_color(getfont(n)) + if font_color ~= current_color then + if current_color then + head, n, current_color = color_whatsit(head, n, current_color, false) + end + if font_color then + head, n, current_color = color_whatsit(head, n, font_color, true) + end + end + + if current_color and color_callback == "pre_linebreak_filter" then + local nn = getnext(n) + while nn and getid(nn) == glyph_t do + local font_color = get_font_color(getfont(nn)) + if font_color == current_color then + n = nn + else + break + end + nn = getnext(nn) + end + if getid(nn) == disc_t then + head, n, current_color = color_whatsit(head, nn, current_color, false, true) + else + head, n, current_color = color_whatsit(head, n, current_color, false, true) + end + end + + elseif n_id == whatsit_t then + if current_color then + head, n, current_color = color_whatsit(head, n, current_color, false) + end + + end + + n = getnext(n) + end + + if toplevel and current_color then + head, _, current_color = color_whatsit(head, nodetail(head), current_color, false, true) + end + + setattribute(head, color_attr, 1) + return head, current_color +end + +local getpageres = pdf.getpageresources or function() return pdf.pageresources end +local setpageres = pdf.setpageresources or function(s) pdf.pageresources = s end +local catat11 = luatexbase.registernumber("catcodetable@atletter") +local gettoks, scantoks = tex.gettoks, tex.scantoks +local pgf = { bye = "pgfutil@everybye", extgs = "\\pgf@sys@addpdfresource@extgs@plain" } + +--- node -> node +local color_handler = function (head) + head = todirect(head) + head = node_colorize(head, true) + head = tonode(head) + + -- now append our page resources + if res then + res["1"] = true + if scantoks and pgf.bye and not pgf.loaded then + pgf.loaded = token.create(pgf.bye).cmdname == "assign_toks" + pgf.bye = pgf.loaded and pgf.bye + end + local tpr = pgf.loaded and gettoks(pgf.bye) or getpageres() or "" + + local t = "" + for k in pairs(res) do + local str = stringformat("/TransGs%s<</ca %s>>", k, k) -- don't touch stroking elements + if not tpr:find(str) then + t = t .. str + end + end + if t ~= "" then + if pgf.loaded then + scantoks("global", pgf.bye, catat11, stringformat("%s{%s}%s", pgf.extgs, t, tpr)) + else + local tpr, n = tpr:gsub("/ExtGState<<", "%1"..t) + if n == 0 then + tpr = stringformat("%s/ExtGState<<%s>>", tpr, t) + end + setpageres(tpr) + end + end + res = nil -- reset res + end + return head +end + +local color_callback_name = "luaotfload.color_handler" +local color_callback_activated = 0 +local add_to_callback = luatexbase.add_to_callback + +--- unit -> bool +local mlist_to_hlist_initial = function () + local cdesc = luatexbase.callback_descriptions "mlist_to_hlist" + return cdesc and cdesc[1] == color_callback_name +end + +--- unit -> unit +add_color_callback = function ( ) + color_callback = config.luaotfload.run.color_callback + if not color_callback then + color_callback = "post_linebreak_filter" + end + + if color_callback_activated == 0 then + add_to_callback(color_callback, + color_handler, + color_callback_name) + add_to_callback("hpack_filter", + function (head, groupcode) + if groupcode == "hbox" or + groupcode == "adjusted_hbox" or + groupcode == "align_set" then + head = color_handler(head) + end + return head + end, + color_callback_name) + add_to_callback("mlist_to_hlist", + function (head, display_type, need_penalties) + if mlist_to_hlist_initial () then + head = mlist_to_hlist(head, display_type, need_penalties) + end + if display_type == "text" then + return head + end + return color_handler(head) + end, + color_callback_name) + color_callback_activated = 1 + end +end + +--[[doc-- +``setcolor`` modifies tfmdata.properties.color in place +--doc]]-- + +--- fontobj -> string -> unit +--- +--- (where “string” is a rgb value as three octet +--- hexadecimal, with an optional fourth transparency +--- value) +--- +local setcolor = function (tfmdata, value) + local sanitized = sanitize_color_expression(value) + local properties = tfmdata.properties + + if sanitized then + properties.color = sanitized + add_color_callback() + end +end + +return function () + logreport = luaotfload.log.report + if not fonts then + logreport ("log", 0, "color", + "OTF mechanisms missing -- did you forget to \z + load a font loader?") + return false + end + fonts.handlers.otf.features.register { + name = "color", + description = "color", + initializers = { + base = setcolor, + node = setcolor, + } + } + return true +end + +-- vim:tw=71:sw=4:ts=4:expandtab + |