if not modules then modules = { } end modules ['attr-col'] = { version = 1.001, comment = "companion to attr-col.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- this module is being reconstructed and code will move to other places -- we can also do the nsnone via a metatable and then also se index 0 -- list could as well refer to the tables (instead of numbers that -- index into another table) .. depends on what we need local type, tonumber = type, tonumber local concat = table.concat local min, max, floor, mod = math.min, math.max, math.floor, math.mod local attributes = attributes local nodes = nodes local utilities = utilities local logs = logs local backends = backends local storage = storage local context = context local tex = tex local variables = interfaces.variables local v_yes = variables.yes local v_no = variables.no local p_split_comma = lpeg.tsplitat(",") local p_split_colon = lpeg.splitat(":") local lpegmatch = lpeg.match local allocate = utilities.storage.allocate local setmetatableindex = table.setmetatableindex local report_attributes = logs.reporter("attributes","colors") local report_colors = logs.reporter("colors","support") local report_transparencies = logs.reporter("transparencies","support") -- todo: document this but first reimplement this as it reflects the early -- days of luatex / mkiv and we have better ways now -- nb: attributes: color etc is much slower than normal (marks + literals) but ... -- nb. too many "0 g"s local states = attributes.states local nodeinjections = backends.nodeinjections local registrations = backends.registrations local unsetvalue = attributes.unsetvalue local enableaction = nodes.tasks.enableaction local setaction = nodes.tasks.setaction local registerstorage = storage.register local formatters = string.formatters local interfaces = interfaces local implement = interfaces.implement local texgetattribute = tex.getattribute -- We can distinguish between rules and glyphs but it's not worth the trouble. A -- first implementation did that and while it saves a bit for glyphs and rules, it -- costs more resourses for transparencies. So why bother. -- -- colors -- -- we can also collapse the two attributes: n, n+1, n+2 and then -- at the tex end add 0, 1, 2, but this is not faster and less -- flexible (since sometimes we freeze color attribute values at -- the lua end of the game) -- -- we also need to store the colorvalues because we need then in mp -- -- This is a compromis between speed and simplicity. We used to store the -- values and data in one array, which made in neccessary to store the -- converters that need node constructor into strings and evaluate them -- at runtime (after reading from storage). Think of: -- -- colors.strings = colors.strings or { } -- -- if environment.initex then -- colors.strings[color] = "return colors." .. colorspace .. "(" .. concat({...},",") .. ")" -- end -- -- registerstorage("attributes/colors/data", colors.strings, "attributes.colors.data") -- evaluated -- -- We assume that only processcolors are defined in the format. attributes.colors = attributes.colors or { } local colors = attributes.colors local a_color = attributes.private('color') local a_selector = attributes.private('colormodel') colors.data = allocate() colors.values = colors.values or { } colors.registered = colors.registered or { } colors.weightgray = true colors.attribute = a_color colors.selector = a_selector colors.default = 1 colors.main = nil colors.triggering = true colors.supported = true colors.model = "all" local data = colors.data local values = colors.values local registered = colors.registered local cmykrgbmode = 0 -- only for testing, already defined colors are not affected local numbers = attributes.numbers local list = attributes.list registerstorage("attributes/colors/values", values, "attributes.colors.values") registerstorage("attributes/colors/registered", registered, "attributes.colors.registered") directives.register("colors.cmykrgbmode", function(v) cmykrgbmode = tonumber(v) or 0 end) local f_colors = { rgb = formatters["r:%s:%s:%s"], cmyk = formatters["c:%s:%s:%s:%s"], gray = formatters["s:%s"], spot = formatters["p:%s:%s:%s:%s"], } local models = { [interfaces.variables.none] = unsetvalue, black = unsetvalue, bw = unsetvalue, all = 1, gray = 2, rgb = 3, cmyk = 4, } local function rgbtocmyk(r,g,b) -- we could reduce if not r then return 0, 0, 0 else return 1-r, 1-g, 1-b, 0 end end local function cmyktorgb(c,m,y,k) if not c then return 0, 0, 0, 1 elseif cmykrgbmode == 1 then local d = 1.0 - k return 1.0 - min(1.0,c*d+k), 1.0 - min(1.0,m*d+k), 1.0 - min(1.0,y*d+k) else return 1.0 - min(1.0,c +k), 1.0 - min(1.0,m +k), 1.0 - min(1.0,y +k) end end local function rgbtogray(r,g,b) if not r then return 0 end local w = colors.weightgray if w == true then return .30*r + .59*g + .11*b elseif not w then return r/3 + g/3 + b/3 else return w[1]*r + w[2]*g + w[3]*b end end local function cmyktogray(c,m,y,k) return rgbtogray(cmyktorgb(c,m,y,k)) end -- http://en.wikipedia.org/wiki/HSI_color_space -- http://nl.wikipedia.org/wiki/HSV_(kleurruimte) -- h /= 60; // sector 0 to 5 -- i = floor( h ); -- f = h - i; // factorial part of h local function hsvtorgb(h,s,v) if s > 1 then s = 1 elseif s < 0 then s = 0 elseif s == 0 then return v, v, v end if v > 1 then s = 1 elseif v < 0 then v = 0 end if h < 0 then h = 0 elseif h >= 360 then h = mod(h,360) end local hd = h / 60 local hi = floor(hd) local f = hd - hi local p = v * (1 - s) local q = v * (1 - f * s) local t = v * (1 - (1 - f) * s) if hi == 0 then return v, t, p elseif hi == 1 then return q, v, p elseif hi == 2 then return p, v, t elseif hi == 3 then return p, q, v elseif hi == 4 then return t, p, v elseif hi == 5 then return v, p, q else print("error in hsv -> rgb",h,s,v) return 0, 0, 0 end end local function rgbtohsv(r,g,b) local offset, maximum, other_1, other_2 if r >= g and r >= b then offset, maximum, other_1, other_2 = 0, r, g, b elseif g >= r and g >= b then offset, maximum, other_1, other_2 = 2, g, b, r else offset, maximum, other_1, other_2 = 4, b, r, g end if maximum == 0 then return 0, 0, 0 end local minimum = other_1 < other_2 and other_1 or other_2 if maximum == minimum then return 0, 0, maximum end local delta = maximum - minimum return (offset + (other_1-other_2)/delta)*60, delta/maximum, maximum end local function graytorgb(s) -- unweighted return 1-s, 1-s, 1-s end local function hsvtogray(h,s,v) return rgb_to_gray(hsv_to_rgb(h,s,v)) end local function graytohsv(s) return 0, 0, s end local function hwbtorgb(hue,black,white) local r, g, b = hsvtorgb(hue,1,.5) local f = 1 - white - black return f * r + white, f * g + white , f * b + white end colors.rgbtocmyk = rgbtocmyk colors.rgbtogray = rgbtogray colors.cmyktorgb = cmyktorgb colors.cmyktogray = cmyktogray colors.rgbtohsv = rgbtohsv colors.hsvtorgb = hsvtorgb colors.hwbtorgb = hwbtorgb colors.hsvtogray = hsvtogray colors.graytohsv = graytohsv -- we can share some *data by using s, rgb and cmyk hashes, but -- normally the amount of colors is not that large; storing the -- components costs a bit of extra runtime, but we expect to gain -- some back because we have them at hand; the number indicates the -- default color space function colors.gray(s) return { 2, s, s, s, s, 0, 0, 0, 1-s } end function colors.rgb(r,g,b) local s = rgbtogray(r,g,b) local c, m, y, k = rgbtocmyk(r,g,b) return { 3, s, r, g, b, c, m, y, k } end function colors.cmyk(c,m,y,k) local s = cmyktogray(c,m,y,k) local r, g, b = cmyktorgb(c,m,y,k) return { 4, s, r, g, b, c, m, y, k } end --~ function colors.spot(parent,f,d,p) --~ return { 5, .5, .5, .5, .5, 0, 0, 0, .5, parent, f, d, p } --~ end function colors.spot(parent,f,d,p) -- inspect(parent) inspect(f) inspect(d) inspect(p) if type(p) == "number" then local n = list[numbers.color][parent] -- hard coded ref to color number if n then local v = values[n] if v then -- the via cmyk hack is dirty, but it scales better local c, m, y, k = p*v[6], p*v[7], p*v[8], p*v[9] local r, g, b = cmyktorgb(c,m,y,k) local s = cmyktogray(c,m,y,k) return { 5, s, r, g, b, c, m, y, k, parent, f, d, p } end end else -- todo, multitone (maybe p should be a table) local ps = lpegmatch(p_split_comma,p) local ds = lpegmatch(p_split_comma,d) local c, m, y, k = 0, 0, 0, 0 local done = false for i=1,#ps do local p = tonumber(ps[i]) local d = ds[i] if p and d then local n = list[numbers.color][d] -- hard coded ref to color number if n then local v = values[n] if v then c = c + p*v[6] m = m + p*v[7] y = y + p*v[8] k = k + p*v[9] done = true end end end end if done then local r, g, b = cmyktorgb(c,m,y,k) local s = cmyktogray(c,m,y,k) local f = tonumber(f) return { 5, s, r, g, b, c, m, y, k, parent, f, d, p } end end return { 5, .5, .5, .5, .5, 0, 0, 0, .5, parent, f, d, p } end local function graycolor(...) graycolor = nodeinjections.graycolor return graycolor(...) end local function rgbcolor (...) rgbcolor = nodeinjections.rgbcolor return rgbcolor (...) end local function cmykcolor(...) cmykcolor = nodeinjections.cmykcolor return cmykcolor(...) end local function spotcolor(...) spotcolor = nodeinjections.spotcolor return spotcolor(...) end local function extender(colors,key) if colors.supported and key == "none" then local d = graycolor(0) colors.none = d return d end end local function reviver(data,n) if colors.supported then local v = values[n] local d if not v then local gray = graycolor(0) d = { gray, gray, gray, gray } report_attributes("unable to revive color %a",n) else local model = colors.forcedmodel(v[1]) if model == 2 then local gray = graycolor(v[2]) d = { gray, gray, gray, gray } elseif model == 3 then local gray = graycolor(v[2]) local rgb = rgbcolor(v[3],v[4],v[5]) local cmyk = cmykcolor(v[6],v[7],v[8],v[9]) d = { rgb, gray, rgb, cmyk } elseif model == 4 then local gray = graycolor(v[2]) local rgb = rgbcolor(v[3],v[4],v[5]) local cmyk = cmykcolor(v[6],v[7],v[8],v[9]) d = { cmyk, gray, rgb, cmyk } elseif model == 5 then local spot = spotcolor(v[10],v[11],v[12],v[13]) -- d = { spot, gray, rgb, cmyk } d = { spot, spot, spot, spot } end end data[n] = d return d end end setmetatableindex(colors, extender) setmetatableindex(colors.data, reviver) function colors.filter(n) return concat(data[n],":",5) end -- unweighted (flat) gray could be another model but a bit work as we need to check: -- -- attr-col colo-ini colo-run -- grph-inc grph-wnd -- lpdf-col lpdf-fmt lpdf-fld lpdf-grp -- meta-pdf meta-pdh mlib-pps -- -- but as we never needed it we happily delay that. function colors.setmodel(name,weightgray) if weightgray == true or weightgray == v_yes then weightgray = true elseif weightgray == false or weightgray == v_no then weightgray = false else local r, g, b = lpegmatch(p_split_colon,weightgray) if r and g and b then weightgray = { r, g, b } else weightgray = true end end local default = models[name] or 1 colors.model = name -- global, not useful that way colors.default = default -- global colors.weightgray = weightgray -- global -- avoid selective checking is no need for it local forced = colors.forced if forced == nil then -- unset colors.forced = default elseif forced == false then -- assumed mixed elseif forced ~= default then -- probably mixed colors.forced = false else -- stil the same end return default end function colors.register(name, colorspace, ...) -- passing 9 vars is faster (but not called that often) local stamp = f_colors[colorspace](...) local color = registered[stamp] if not color then color = #values + 1 values[color] = colors[colorspace](...) registered[stamp] = color -- colors.reviver(color) end if name then list[a_color][name] = color -- not grouped, so only global colors end return registered[stamp] end function colors.value(id) return values[id] end attributes.colors.handler = nodes.installattributehandler { name = "color", namespace = colors, initializer = states.initialize, finalizer = states.finalize, processor = states.selective, resolver = function() return colors.main end, } function colors.enable(value) setaction("shipouts","attributes.colors.handler",not (value == false or not colors.supported)) end function colors.forcesupport(value) -- can move to attr-div colors.supported = value report_colors("color is %ssupported",value and "" or "not ") colors.enable(value) end function colors.toattributes(name) local mc = list[a_color][name] local mm = texgetattribute(a_selector) return (mm == unsetvalue and 1) or mm or 1, mc or list[a_color][1] or unsetvalue end -- transparencies local a_transparency = attributes.private('transparency') attributes.transparencies = attributes.transparencies or { } local transparencies = attributes.transparencies transparencies.registered = transparencies.registered or { } transparencies.data = allocate() transparencies.values = transparencies.values or { } transparencies.triggering = true transparencies.attribute = a_transparency transparencies.supported = true local registered = transparencies.registered -- we could use a 2 dimensional table instead local data = transparencies.data local values = transparencies.values local f_transparency = formatters["%s:%s"] registerstorage("attributes/transparencies/registered", registered, "attributes.transparencies.registered") registerstorage("attributes/transparencies/values", values, "attributes.transparencies.values") local function inject_transparency(...) inject_transparency = nodeinjections.transparency return inject_transparency(...) end local function register_transparency(...) register_transparency = registrations.transparency return register_transparency(...) end function transparencies.register(name,a,t,force) -- name is irrelevant here (can even be nil) -- Force needed here for metapost converter. We could always force -- but then we'd end up with transparencies resources even if we -- would not use transparencies (but define them only). This is -- somewhat messy. local stamp = f_transparency(a,t) local n = registered[stamp] if not n then n = #values + 1 values[n] = { a, t } registered[stamp] = n if force then register_transparency(n,a,t) end elseif force and not data[n] then register_transparency(n,a,t) end if name then list[a_transparency][name] = n -- not grouped, so only global transparencies end return registered[stamp] end local function extender(transparencies,key) if colors.supported and key == "none" then local d = inject_transparency(0) transparencies.none = d return d end end local function reviver(data,n) if n and transparencies.supported then local v = values[n] local d if not v then d = inject_transparency(0) else d = inject_transparency(n) register_transparency(n,v[1],v[2]) end data[n] = d return d else return "" end end setmetatableindex(transparencies,extender) setmetatableindex(transparencies.data,reviver) -- register if used -- check if there is an identity function transparencies.value(id) return values[id] end attributes.transparencies.handler = nodes.installattributehandler { name = "transparency", namespace = transparencies, initializer = states.initialize, finalizer = states.finalize, processor = states.process, } function transparencies.enable(value) -- nil is enable setaction("shipouts","attributes.transparencies.handler",not (value == false or not transparencies.supported)) end function transparencies.forcesupport(value) -- can move to attr-div transparencies.supported = value report_transparencies("transparency is %ssupported",value and "" or "not ") transparencies.enable(value) end function transparencies.toattribute(name) return list[a_transparency][name] or unsetvalue end --- colorintents: overprint / knockout attributes.colorintents = attributes.colorintents or { } local colorintents = attributes.colorintents colorintents.data = allocate() -- colorintents.data or { } colorintents.attribute = attributes.private('colorintent') colorintents.registered = allocate { overprint = 1, knockout = 2, } local data, registered = colorintents.data, colorintents.registered local function extender(colorintents,key) if key == "none" then local d = data[2] colorintents.none = d return d end end local function reviver(data,n) if n == 1 then local d = nodeinjections.overprint() -- called once data[1] = d return d elseif n == 2 then local d = nodeinjections.knockout() -- called once data[2] = d return d end end setmetatableindex(colorintents, extender) setmetatableindex(colorintents.data, reviver) function colorintents.register(stamp) return registered[stamp] or registered.overprint end colorintents.handler = nodes.installattributehandler { name = "colorintent", namespace = colorintents, initializer = states.initialize, finalizer = states.finalize, processor = states.process, } function colorintents.enable() enableaction("shipouts","attributes.colorintents.handler") end -- interface implement { name = "enablecolor", onlyonce = true, actions = colors.enable } implement { name = "enabletransparency", onlyonce = true, actions = transparencies.enable } implement { name = "enablecolorintents", onlyonce = true, actions = colorintents.enable } --------- { name = "registercolor", actions = { colors .register, context }, arguments = "string" } --------- { name = "registertransparency", actions = { transparencies.register, context }, arguments = "string" } implement { name = "registercolorintent", actions = { colorintents .register, context }, arguments = "string" }