diff options
Diffstat (limited to 'luaotfload-colors.lua')
-rw-r--r-- | luaotfload-colors.lua | 348 |
1 files changed, 234 insertions, 114 deletions
diff --git a/luaotfload-colors.lua b/luaotfload-colors.lua index 3d8bfab..ec076c2 100644 --- a/luaotfload-colors.lua +++ b/luaotfload-colors.lua @@ -1,44 +1,110 @@ if not modules then modules = { } end modules ['luaotfload-colors'] = { - version = 1.001, + version = 2.200, comment = "companion to luaotfload.lua (font color)", - author = "Khaled Hosny and Elie Roux", + author = "Khaled Hosny, Elie Roux, Philipp Gesang", copyright = "Luaotfload Development Team", - license = "GPL" + license = "GNU GPL v2" } -local newnode = node.new -local nodetype = node.id -local traverse_nodes = node.traverse -local insert_node_before = node.insert_before -local insert_node_after = node.insert_after +--[[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 +--doc]]-- -local stringformat = string.format -local stringgsub = string.gsub -local stringfind = string.find -local otffeatures = fonts.constructors.newfeatures("otf") -local ids = fonts.hashes.identifiers -local registerotffeature = otffeatures.register +local color_callback = config.luaotfload.color_callback +if not color_callback then + --- maybe this would be better as a method: "early" | "late" + color_callback = "pre_linebreak_filter" +-- color_callback = "pre_output_filter" --- old behavior, breaks expansion +end -local function setcolor(tfmdata,value) - local sanitized - local properties = tfmdata.properties - if value then - value = tostring(value) - if #value == 6 or #value == 8 then - sanitized = value - elseif #value == 7 then - _, _, sanitized = stringfind(value, "(......)") - elseif #value > 8 then - _, _, sanitized = stringfind(value, "(........)") - else - -- broken color code ignored, issue a warning? - end +local newnode = node.new +local nodetype = node.id +local traverse_nodes = node.traverse +local insert_node_before = node.insert_before +local insert_node_after = node.insert_after + +local stringformat = string.format +local stringgsub = string.gsub +local stringfind = string.find +local stringsub = string.sub + +local otffeatures = fonts.constructors.newfeatures("otf") +local identifiers = fonts.hashes.identifiers +local registerotffeature = otffeatures.register + +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 octet = C(digit16 * digit16) + +local p_rgb = octet * octet * octet +local p_rgba = p_rgb * octet +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 + luaotfload.warning( + "“%s” is not a valid rgb[a] color expression", digits) + return nil + end + return sanitized +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 - tfmdata.properties.color = sanitized + properties.color = sanitized add_color_callback() end end @@ -52,133 +118,187 @@ registerotffeature { } } -local function hex2dec(hex,one) - if one then - return stringformat("%.1g", tonumber(hex, 16)/255) - else - return stringformat("%.3g", tonumber(hex, 16)/255) - end -end -local res +--- something is carried around in ``res`` +--- for later use by color_handler() --- but what? + +local res --- <- state of what? -local function pageresources(a) +--- float -> unit +local function pageresources(alpha) local res2 if not res then res = "/TransGs1<</ca 1/CA 1>>" end - res2 = stringformat("/TransGs%s<</ca %s/CA %s>>", a, a, a) - res = stringformat("%s%s", res, stringfind(res, res2) and "" or res2) + res2 = stringformat("/TransGs%s<</ca %s/CA %s>>", + alpha, alpha, alpha) + res = stringformat("%s%s", + res, + stringfind(res, res2) and "" or res2) end -local function hex_to_rgba(hex) - local r, g, b, a, push, pop, res3 - if hex then - if #hex == 6 then - _, _, r, g, b = stringfind(hex, '(..)(..)(..)') - elseif #hex == 8 then - _, _, r, g, b, a = stringfind(hex, '(..)(..)(..)(..)') - a = hex2dec(a,true) - pageresources(a) - end - else - return nil +--- 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 - r = hex2dec(r) - g = hex2dec(g) - b = hex2dec(b) - if a then - push = stringformat('/TransGs%g gs %s %s %s rg', a, r, g, b) - pop = '0 g /TransGs1 gs' - else - push = stringformat('%s %s %s rg', r, g, b) - pop = '0 g' + + --- 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 push, pop + + return cached[1], cached[2] end -local glyph = nodetype('glyph') -local hlist = nodetype('hlist') -local vlist = nodetype('vlist') -local whatsit = nodetype('whatsit') -local pgi = nodetype('page_insert') -local sbox = nodetype('sub_box') +--- Luatex internal types + +local glyph_t = nodetype("glyph") +local hlist_t = nodetype("hlist") +local vlist_t = nodetype("vlist") +local whatsit_t = nodetype("whatsit") +local page_insert_t = nodetype("page_insert") +local sub_box_t = nodetype("sub_box") -local function lookup_next_color(head) +--- node -> nil | -1 | color‽ +local lookup_next_color +lookup_next_color = function (head) --- paragraph material for n in traverse_nodes(head) do - if n.id == glyph then - if ids[n.font] and ids[n.font].properties and ids[n.font].properties.color then - return ids[n.font].properties.color + local n_id = n.id + + if n_id == glyph_t then + local n_font + if identifiers[n_font] + and identifiers[n_font].properties + and identifiers[n_font].properties.color + then + return identifiers[n.font].properties.color else return -1 end - elseif n.id == vlist or n.id == hlist or n.id == sbox then + + elseif n_id == vlist_t or n_id == hlist_t or n_id == sub_box_t then local r = lookup_next_color(n.list) - if r == -1 then - return -1 - elseif r then + if r then return r end - elseif n.id == whatsit or n.id == pgi then + + elseif n_id == whatsit_t or n_id == page_insert_t then return -1 end end return nil end -local function node_colorize(head, current_color, next_color) +--[[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]]-- + +local cnt = 0 +--- node -> string -> int -> (node * string) +local node_colorize +node_colorize = function (head, current_color, next_color) for n in traverse_nodes(head) do - if n.id == hlist or n.id == vlist or n.id == sbox then - local next_color_in = lookup_next_color(n.next) or next_color + local n_id = n.id + local nextnode = n.next + + if n_id == hlist_t or n_id == vlist_t or n_id == sub_box_t then + local next_color_in = lookup_next_color(nextnode) or next_color n.list, current_color = node_colorize(n.list, current_color, next_color_in) - elseif n.id == glyph then - local tfmdata = ids[n.font] + + elseif n_id == glyph_t then + cnt = cnt + 1 + local tfmdata = identifiers[n.font] + + --- colorization is restricted to those fonts + --- that received the “color” property upon + --- loading (see ``setcolor()`` above) if tfmdata and tfmdata.properties and tfmdata.properties.color then - if tfmdata.properties.color ~= current_color then - local pushcolor = hex_to_rgba(tfmdata.properties.color) - local push = newnode(whatsit, 8) - push.mode = 1 - push.data = pushcolor - head = insert_node_before(head, n, push) - current_color = tfmdata.properties.color + local font_color = tfmdata.properties.color +-- luaotfload.info( +-- "n: %d; %s; %d %s, %s", +-- cnt, utf.char(n.char), n.font, "<TRUE>", font_color) + if font_color ~= current_color then + local pushcolor = hex_to_rgba(font_color) + local push = newnode(whatsit_t, 8) + push.mode = 1 + push.data = pushcolor + head = insert_node_before(head, n, push) + current_color = font_color end - local next_color_in = lookup_next_color (n.next) or next_color - if next_color_in ~= tfmdata.properties.color then - local _, popcolor = hex_to_rgba(tfmdata.properties.color) - local pop = newnode(whatsit, 8) - pop.mode = 1 - pop.data = popcolor - head = insert_node_after(head, n, pop) - current_color = nil + local next_color_in = lookup_next_color (nextnode) or next_color + if next_color_in ~= font_color then + local _, popcolor = hex_to_rgba(font_color) + local pop = newnode(whatsit_t, 8) + pop.mode = 1 + pop.data = popcolor + head = insert_node_after(head, n, pop) + current_color = nil end + +-- else +-- luaotfload.info( +-- "n: %d; %s; %d %s", +-- cnt, utf.char(n.char), n.font, "<FALSE>") end end end return head, current_color end -local function font_colorize(head) - -- check if our page resources existed in the previous run - -- and remove it to avoid duplicating it later - if res then - local r = "/ExtGState<<"..res..">>" - tex.pdfpageresources = stringgsub(tex.pdfpageresources, r, "") - end - local h = node_colorize(head, nil, nil) - -- now append our page resources - if res and stringfind(res, "%S") then -- test for non-empty string - local r = "/ExtGState<<"..res..">>" - tex.pdfpageresources = tex.pdfpageresources..r - end - return h +--- node -> node +local color_handler = function (head) + -- check if our page resources existed in the previous run + -- and remove it to avoid duplicating it later + if res then + local r = "/ExtGState<<" .. res .. ">>" + tex.pdfpageresources = stringgsub(tex.pdfpageresources, r, "") + end + local new_head = node_colorize(head, nil, nil) + -- now append our page resources + if res and stringfind(res, "%S") then -- test for non-empty string + local r = "/ExtGState<<" .. res .. ">>" + tex.pdfpageresources = tex.pdfpageresources..r + end + return new_head end local color_callback_activated = 0 -function add_color_callback() +--- unit -> unit +add_color_callback = function ( ) if color_callback_activated == 0 then - luatexbase.add_to_callback( - "pre_output_filter", font_colorize, "luaotfload.colorize") + luatexbase.add_to_callback(color_callback, + color_handler, + "luaotfload.color_handler") color_callback_activated = 1 end end |