summaryrefslogtreecommitdiff
path: root/luaotfload-colors.lua
diff options
context:
space:
mode:
Diffstat (limited to 'luaotfload-colors.lua')
-rw-r--r--luaotfload-colors.lua348
1 files changed, 234 insertions, 114 deletions
diff --git a/luaotfload-colors.lua b/luaotfload-colors.lua
index 1525214..ec076c2 100644
--- a/luaotfload-colors.lua
+++ b/luaotfload-colors.lua
@@ -6,39 +6,105 @@ if not modules then modules = { } end modules ['luaotfload-colors'] = {
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
-
-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 function setcolor(tfmdata,value)
- local sanitized
- local properties = tfmdata.properties
+--[[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]]--
- 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 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 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 function lookup_next_color(head)
+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")
+
+--- 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