attr-col.lmt /size: 20 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['attr-col'] = {
2    version   = 1.001,
3    comment   = "companion to attr-col.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- this module is being reconstructed and code will move to other places
10-- we can also do the nsnone via a metatable and then also se index 0
11
12-- list could as well refer to the tables (instead of numbers that
13-- index into another table) .. depends on what we need
14
15local type, tonumber = type, tonumber
16local concat = table.concat
17local min, max, floor, mod = math.min, math.max, math.floor, math.mod
18
19local attributes            = attributes
20local nodes                 = nodes
21local utilities             = utilities
22local logs                  = logs
23local backends              = backends
24local storage               = storage
25local context               = context
26local tex                   = tex
27
28local variables             = interfaces.variables
29local v_yes                 = variables.yes
30local v_no                  = variables.no
31
32local p_split_comma         = lpeg.tsplitat(",")
33local p_split_colon         = lpeg.splitat(":")
34local lpegmatch             = lpeg.match
35
36local allocate              = utilities.storage.allocate
37local setmetatableindex     = table.setmetatableindex
38
39local report_attributes     = logs.reporter("attributes","colors")
40local report_colors         = logs.reporter("colors","support")
41local report_transparencies = logs.reporter("transparencies","support")
42
43-- todo: document this but first reimplement this as it reflects the early
44-- days of luatex / mkiv and we have better ways now
45
46-- nb: attributes: color etc is much slower than normal (marks + literals) but ...
47-- nb. too many "0 g"s
48
49local states          = attributes.states
50local registrations   = backends.registrations
51local nodeinjections  = backends.nodeinjections
52local unsetvalue      = attributes.unsetvalue
53
54local enableaction    = nodes.tasks.enableaction
55local setaction       = nodes.tasks.setaction
56
57local registerstorage = storage.register
58local formatters      = string.formatters
59
60local interfaces      = interfaces
61local implement       = interfaces.implement
62
63local texgetattribute = tex.getattribute
64
65-- We can distinguish between rules and glyphs but it's not worth the trouble. A
66-- first implementation did that and while it saves a bit for glyphs and rules, it
67-- costs more resourses for transparencies. So why bother.
68
69--
70-- colors
71--
72
73-- we can also collapse the two attributes: n, n+1, n+2 and then
74-- at the tex end add 0, 1, 2, but this is not faster and less
75-- flexible (since sometimes we freeze color attribute values at
76-- the lua end of the game)
77--
78-- we also need to store the colorvalues because we need then in mp
79--
80-- This is a compromis between speed and simplicity. We used to store the
81-- values and data in one array, which made in neccessary to store the
82-- converters that need node constructor into strings and evaluate them
83-- at runtime (after reading from storage). Think of:
84--
85-- colors.strings = colors.strings or { }
86--
87-- if environment.initex then
88--     colors.strings[color] = "return colors." .. colorspace .. "(" .. concat({...},",") .. ")"
89-- end
90--
91-- registerstorage("attributes/colors/data", colors.strings, "attributes.colors.data") -- evaluated
92--
93-- We assume that only processcolors are defined in the format.
94
95attributes.colors = attributes.colors or { }
96local colors      = attributes.colors
97
98local a_color     = attributes.private('color')
99local a_selector  = attributes.private('colormodel')
100
101colors.data       = allocate()
102colors.values     = colors.values or { }
103colors.registered = colors.registered or { }
104colors.weightgray = true
105colors.attribute  = a_color
106colors.selector   = a_selector
107colors.default    = 1
108colors.main       = nil
109colors.triggering = true
110colors.supported  = true
111colors.model      = "all"
112
113local data        = colors.data
114local values      = colors.values
115local registered  = colors.registered
116
117local cmykrgbmode = 0 -- only for testing, already defined colors are not affected
118
119local numbers     = attributes.numbers
120local list        = attributes.list
121
122registerstorage("attributes/colors/values",     values,     "attributes.colors.values")
123registerstorage("attributes/colors/registered", registered, "attributes.colors.registered")
124
125directives.register("colors.cmykrgbmode", function(v) cmykrgbmode = tonumber(v) or 0 end)
126
127local f_colors = {
128    rgb  = formatters["r:%s:%s:%s"],
129    cmyk = formatters["c:%s:%s:%s:%s"],
130    gray = formatters["s:%s"],
131    spot = formatters["p:%s:%s:%s:%s"],
132}
133
134local models = {
135    [interfaces.variables.none] = unsetvalue,
136    black = unsetvalue,
137    bw    = unsetvalue,
138    all   = 1,
139    gray  = 2,
140    rgb   = 3,
141    cmyk  = 4,
142}
143
144local function rgbtocmyk(r,g,b) -- we could reduce
145    if not r then
146        return 0, 0, 0
147    else
148        return 1-r, 1-g, 1-b, 0
149    end
150end
151
152local function cmyktorgb(c,m,y,k)
153    if not c then
154        return 0, 0, 0, 1
155    elseif cmykrgbmode == 1 then
156        local d = 1.0 - k
157        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)
158    else
159        return 1.0 - min(1.0,c  +k), 1.0 - min(1.0,m  +k), 1.0 - min(1.0,y  +k)
160    end
161end
162
163local function rgbtogray(r,g,b)
164    if not r then
165        return 0
166    end
167    local w = colors.weightgray
168    if w == true then
169        return .30*r + .59*g + .11*b
170    elseif not w then
171        return r/3 + g/3 + b/3
172    else
173        return w[1]*r + w[2]*g + w[3]*b
174    end
175end
176
177local function cmyktogray(c,m,y,k)
178    return rgbtogray(cmyktorgb(c,m,y,k))
179end
180
181-- http://en.wikipedia.org/wiki/HSI_color_space
182-- http://nl.wikipedia.org/wiki/HSV_(kleurruimte)
183
184-- 	h /= 60;        // sector 0 to 5
185-- 	i = floor( h );
186-- 	f = h - i;      // factorial part of h
187
188local function hsvtorgb(h,s,v)
189    if s > 1 then
190        s = 1
191    elseif s < 0 then
192        s = 0
193    elseif s == 0 then
194        return v, v, v
195    end
196    if v > 1 then
197        s = 1
198    elseif v < 0 then
199        v = 0
200    end
201    if h < 0 then
202        h = 0
203    elseif h >= 360 then
204        h = mod(h,360)
205    end
206    local hd = h / 60
207    local hi = floor(hd)
208    local f =  hd - hi
209    local p = v * (1 - s)
210    local q = v * (1 - f * s)
211    local t = v * (1 - (1 - f) * s)
212    if hi == 0 then
213        return v, t, p
214    elseif hi == 1 then
215        return q, v, p
216    elseif hi == 2 then
217        return p, v, t
218    elseif hi == 3 then
219        return p, q, v
220    elseif hi == 4 then
221        return t, p, v
222    elseif hi == 5 then
223        return v, p, q
224    else
225        print("error in hsv -> rgb",h,s,v)
226        return 0, 0, 0
227    end
228end
229
230local function rgbtohsv(r,g,b)
231    local offset, maximum, other_1, other_2
232    if r >= g and r >= b then
233        offset, maximum, other_1, other_2 = 0, r, g, b
234    elseif g >= r and g >= b then
235        offset, maximum, other_1, other_2 = 2, g, b, r
236    else
237        offset, maximum, other_1, other_2 = 4, b, r, g
238    end
239    if maximum == 0 then
240        return 0, 0, 0
241    end
242    local minimum = other_1 < other_2 and other_1 or other_2
243    if maximum == minimum then
244        return 0, 0, maximum
245    end
246    local delta = maximum - minimum
247    return (offset + (other_1-other_2)/delta)*60, delta/maximum, maximum
248end
249
250local function graytorgb(s) -- unweighted
251   return 1-s, 1-s, 1-s
252end
253
254local function hsvtogray(h,s,v)
255    return rgb_to_gray(hsv_to_rgb(h,s,v))
256end
257
258local function graytohsv(s)
259    return 0, 0, s
260end
261
262local function hwbtorgb(hue,black,white)
263    local r, g, b = hsvtorgb(hue,1,.5)
264    local f = 1 - white - black
265    return f * r + white, f * g + white , f * b + white
266end
267
268colors.rgbtocmyk  = rgbtocmyk
269colors.rgbtogray  = rgbtogray
270colors.cmyktorgb  = cmyktorgb
271colors.cmyktogray = cmyktogray
272colors.rgbtohsv   = rgbtohsv
273colors.hsvtorgb   = hsvtorgb
274colors.hwbtorgb   = hwbtorgb
275colors.hsvtogray  = hsvtogray
276colors.graytohsv  = graytohsv
277
278-- we can share some *data by using s, rgb and cmyk hashes, but
279-- normally the amount of colors is not that large; storing the
280-- components costs a bit of extra runtime, but we expect to gain
281-- some back because we have them at hand; the number indicates the
282-- default color space
283
284function colors.gray(s)
285    return { 2, s, s, s, s, 0, 0, 0, 1-s }
286end
287
288function colors.rgb(r,g,b)
289    local s = rgbtogray(r,g,b)
290    local c, m, y, k = rgbtocmyk(r,g,b)
291    return { 3, s, r, g, b, c, m, y, k }
292end
293
294function colors.cmyk(c,m,y,k)
295    local s = cmyktogray(c,m,y,k)
296    local r, g, b = cmyktorgb(c,m,y,k)
297    return { 4, s, r, g, b, c, m, y, k }
298end
299
300--~ function colors.spot(parent,f,d,p)
301--~     return { 5, .5, .5, .5, .5, 0, 0, 0, .5, parent, f, d, p }
302--~ end
303
304function colors.spot(parent,f,d,p)
305 -- inspect(parent) inspect(f) inspect(d) inspect(p)
306    if type(p) == "number" then
307        local n = list[numbers.color][parent] -- hard coded ref to color number
308        if n then
309            local v = values[n]
310            if v then
311                -- the via cmyk hack is dirty, but it scales better
312                local c, m, y, k = p*v[6], p*v[7], p*v[8], p*v[9]
313                local r, g, b = cmyktorgb(c,m,y,k)
314                local s = cmyktogray(c,m,y,k)
315                return { 5, s, r, g, b, c, m, y, k, parent, f, d, p }
316            end
317        end
318    else
319        -- todo, multitone (maybe p should be a table)
320        local ps = lpegmatch(p_split_comma,p)
321        local ds = lpegmatch(p_split_comma,d)
322        local c, m, y, k = 0, 0, 0, 0
323        local done = false
324        for i=1,#ps do
325            local p = tonumber(ps[i])
326            local d = ds[i]
327            if p and d then
328                local n = list[numbers.color][d] -- hard coded ref to color number
329                if n then
330                    local v = values[n]
331                    if v then
332                        c = c + p*v[6]
333                        m = m + p*v[7]
334                        y = y + p*v[8]
335                        k = k + p*v[9]
336                        done = true
337                    end
338                end
339            end
340        end
341        if done then
342            local r, g, b = cmyktorgb(c,m,y,k)
343            local s = cmyktogray(c,m,y,k)
344            local f = tonumber(f)
345            return { 5, s, r, g, b, c, m, y, k, parent, f, d, p }
346        end
347    end
348    return { 5, .5, .5, .5, .5, 0, 0, 0, .5, parent, f, d, p }
349end
350
351local graycolor = nodeinjections.graycolor
352local rgbcolor  = nodeinjections.rgbcolor
353local cmykcolor = nodeinjections.cmykcolor
354local spotcolor = nodeinjections.spotcolor
355
356updaters.register("backends.injections.latebindings",function()
357    local nodeinjections = backends.nodeinjections
358    graycolor = nodeinjections.graycolor
359    rgbcolor  = nodeinjections.rgbcolor
360    cmykcolor = nodeinjections.cmykcolor
361    spotcolor = nodeinjections.spotcolor
362end)
363
364local function extender(colors,key)
365    if colors.supported and key == "none" then
366        local d = graycolor(0)
367        colors.none = d
368        return d
369    end
370end
371
372local function reviver(data,n)
373    if colors.supported then
374        local v = values[n]
375        local d
376        if not v then
377            local gray = graycolor(0)
378            d = { gray, gray, gray, gray }
379            report_attributes("unable to revive color %a",n)
380        else
381            local model = colors.forcedmodel(v[1])
382            if model == 2 then
383                local gray = graycolor(v[2])
384                d = { gray, gray, gray, gray }
385            elseif model == 3 then
386                local gray = graycolor(v[2])
387                local rgb  = rgbcolor(v[3],v[4],v[5])
388                local cmyk = cmykcolor(v[6],v[7],v[8],v[9])
389                d = { rgb, gray, rgb, cmyk }
390            elseif model == 4 then
391                local gray = graycolor(v[2])
392                local rgb  = rgbcolor(v[3],v[4],v[5])
393                local cmyk = cmykcolor(v[6],v[7],v[8],v[9])
394                d = { cmyk, gray, rgb, cmyk }
395            elseif model == 5 then
396                local spot = spotcolor(v[10],v[11],v[12],v[13])
397            --  d = { spot, gray, rgb, cmyk }
398                d = { spot, spot, spot, spot }
399            end
400        end
401        data[n] = d
402        return d
403    end
404end
405
406setmetatableindex(colors, extender)
407setmetatableindex(colors.data, reviver)
408
409function colors.filter(n)
410    return concat(data[n],":",5)
411end
412
413-- unweighted (flat) gray could be another model but a bit work as we need to check:
414--
415--   attr-col colo-ini colo-run
416--   grph-inc grph-wnd
417--   lpdf-col lpdf-fmt lpdf-fld lpdf-grp
418--   meta-pdf meta-pdh mlib-pps
419--
420-- but as we never needed it we happily delay that.
421
422function colors.setmodel(name,weightgray)
423    if weightgray == true or weightgray == v_yes then
424        weightgray = true
425    elseif weightgray == false or weightgray == v_no then
426        weightgray = false
427    else
428        local r, g, b = lpegmatch(p_split_colon,weightgray)
429        if r and g and b then
430            weightgray = { r, g, b }
431        else
432            weightgray = true
433        end
434    end
435    local default = models[name] or 1
436
437    colors.model      = name       -- global, not useful that way
438    colors.default    = default    -- global
439    colors.weightgray = weightgray -- global
440
441    -- avoid selective checking is no need for it
442
443    local forced = colors.forced
444
445    if forced == nil then
446        -- unset
447        colors.forced = default
448    elseif forced == false then
449        -- assumed mixed
450    elseif forced ~= default then
451        -- probably mixed
452        colors.forced = false
453    else
454        -- stil the same
455    end
456    return default
457end
458
459function colors.register(name, colorspace, ...) -- passing 9 vars is faster (but not called that often)
460    local stamp = f_colors[colorspace](...)
461    local color = registered[stamp]
462    if not color then
463        color = #values + 1
464        values[color] = colors[colorspace](...)
465        registered[stamp] = color
466    -- colors.reviver(color)
467    end
468    if name then
469        list[a_color][name] = color -- not grouped, so only global colors
470    end
471    return registered[stamp]
472end
473
474function colors.value(id)
475    return values[id]
476end
477
478attributes.colors.handler = nodes.installattributehandler {
479    name        = "color",
480    namespace   = colors,
481    initializer = states.initialize,
482    finalizer   = states.finalize,
483    processor   = states.selective,
484    resolver    = function() return colors.main end,
485}
486
487function colors.enable(value)
488    setaction("shipouts","attributes.colors.handler",not (value == false or not colors.supported))
489end
490
491function colors.forcesupport(value) -- can move to attr-div
492    colors.supported = value
493    report_colors("color is %ssupported",value and "" or "not ")
494    colors.enable(value)
495end
496
497function colors.toattributes(name)
498    local mc = list[a_color][name]
499    local mm = texgetattribute(a_selector)
500    return (mm == unsetvalue and 1) or mm or 1, mc or list[a_color][1] or unsetvalue
501end
502
503-- transparencies
504
505local a_transparency      = attributes.private('transparency')
506
507attributes.transparencies = attributes.transparencies or { }
508local transparencies      = attributes.transparencies
509transparencies.registered = transparencies.registered or { }
510transparencies.data       = allocate()
511transparencies.values     = transparencies.values or { }
512transparencies.triggering = true
513transparencies.attribute  = a_transparency
514transparencies.supported  = true
515
516local registered          = transparencies.registered -- we could use a 2 dimensional table instead
517local data                = transparencies.data
518local values              = transparencies.values
519local f_transparency      = formatters["%s:%s"]
520
521registerstorage("attributes/transparencies/registered", registered, "attributes.transparencies.registered")
522registerstorage("attributes/transparencies/values",     values,     "attributes.transparencies.values")
523
524local register_transparency = registrations.transparency
525local inject_transparency   = nodeinjections.transparency
526
527updaters.register("backends.injections.latebindings",function()
528    register_transparency = backends.registrations.transparency
529    inject_transparency   = backends.nodeinjections.transparency
530end)
531
532function transparencies.register(name,a,t,force) -- name is irrelevant here (can even be nil)
533    -- Force needed here for metapost converter. We could always force
534    -- but then we'd end up with transparencies resources even if we
535    -- would not use transparencies (but define them only). This is
536    -- somewhat messy.
537    local stamp = f_transparency(a,t)
538    local n = registered[stamp]
539    if not n then
540        n = #values + 1
541        values[n] = { a, t }
542        registered[stamp] = n
543        if force then
544            register_transparency(n,a,t)
545        end
546    elseif force and not data[n] then
547        register_transparency(n,a,t)
548    end
549    if name then
550        list[a_transparency][name] = n -- not grouped, so only global transparencies
551    end
552    return registered[stamp]
553end
554
555local function extender(transparencies,key)
556    if colors.supported and key == "none" then
557        local d = inject_transparency(0)
558        transparencies.none = d
559        return d
560    end
561end
562
563local function reviver(data,n)
564    if n and transparencies.supported then
565        local v = values[n]
566        local d
567        if not v then
568            d = inject_transparency(0)
569        else
570            d = inject_transparency(n)
571            register_transparency(n,v[1],v[2])
572        end
573        data[n] = d
574        return d
575    else
576        return ""
577    end
578end
579
580setmetatableindex(transparencies,extender)
581setmetatableindex(transparencies.data,reviver) -- register if used
582
583-- check if there is an identity
584
585function transparencies.value(id)
586    return values[id]
587end
588
589attributes.transparencies.handler = nodes.installattributehandler {
590    name        = "transparency",
591    namespace   = transparencies,
592    initializer = states.initialize,
593    finalizer   = states.finalize,
594    processor   = states.process,
595}
596
597function transparencies.enable(value) -- nil is enable
598    setaction("shipouts","attributes.transparencies.handler",not (value == false or not transparencies.supported))
599end
600
601function transparencies.forcesupport(value) -- can move to attr-div
602    transparencies.supported = value
603    report_transparencies("transparency is %ssupported",value and "" or "not ")
604    transparencies.enable(value)
605end
606
607function transparencies.toattribute(name)
608    return list[a_transparency][name] or unsetvalue
609end
610
611--- colorintents: overprint / knockout
612
613attributes.colorintents = attributes.colorintents or  { }
614local colorintents      = attributes.colorintents
615colorintents.data       = allocate() -- colorintents.data or { }
616colorintents.attribute  = attributes.private('colorintent')
617
618colorintents.registered = allocate {
619    overprint = 1,
620    knockout  = 2,
621}
622
623local inject_overprint = nodeinjections.injectoverprint
624local inject_knockout  = nodeinjections.injectknockout
625
626updaters.register("backends.injections.latebindings",function()
627    local nodeinjections = backends.nodeinjections
628    inject_overprint = nodeinjections.injectoverprint
629    inject_knockout  = nodeinjections.injectknockout
630end)
631
632local data       = colorintents.data
633local registered = colorintents.registered
634
635local function extender(colorintents,key)
636    if key == "none" then
637        local d = data[2]
638        colorintents.none = d
639        return d
640    end
641end
642
643local function reviver(data,n)
644    if n == 1 then
645        local d = inject_overprint() -- called once
646        data[1] = d
647        return d
648    elseif n == 2 then
649        local d = inject_knockout() -- called once
650        data[2] = d
651        return d
652    end
653end
654
655setmetatableindex(colorintents, extender)
656setmetatableindex(colorintents.data, reviver)
657
658function colorintents.register(stamp)
659    return registered[stamp] or registered.overprint
660end
661
662colorintents.handler = nodes.installattributehandler {
663    name        = "colorintent",
664    namespace   = colorintents,
665    initializer = states.initialize,
666    finalizer   = states.finalize,
667    processor   = states.process,
668}
669
670function colorintents.enable()
671    enableaction("shipouts","attributes.colorintents.handler")
672end
673
674-- interface
675
676implement { name = "enablecolor",        onlyonce = true, actions = colors.enable }
677implement { name = "enabletransparency", onlyonce = true, actions = transparencies.enable }
678implement { name = "enablecolorintents", onlyonce = true, actions = colorintents.enable }
679
680--------- { name = "registercolor",        actions = { colors        .register, context }, arguments = "string" }
681--------- { name = "registertransparency", actions = { transparencies.register, context }, arguments = "string" }
682implement { name = "registercolorintent",  actions = { colorintents  .register, context }, arguments = "string" }
683