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