attr-col.lua /size: 19 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 nodeinjections  = backends.nodeinjections
51local registrations   = backends.registrations
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 function graycolor(...) graycolor = nodeinjections.graycolor return graycolor(...) end
352local function rgbcolor (...) rgbcolor  = nodeinjections.rgbcolor  return rgbcolor (...) end
353local function cmykcolor(...) cmykcolor = nodeinjections.cmykcolor return cmykcolor(...) end
354local function spotcolor(...) spotcolor = nodeinjections.spotcolor return spotcolor(...) end
355
356local function extender(colors,key)
357    if colors.supported and key == "none" then
358        local d = graycolor(0)
359        colors.none = d
360        return d
361    end
362end
363
364local function reviver(data,n)
365    if colors.supported then
366        local v = values[n]
367        local d
368        if not v then
369            local gray = graycolor(0)
370            d = { gray, gray, gray, gray }
371            report_attributes("unable to revive color %a",n)
372        else
373            local model = colors.forcedmodel(v[1])
374            if model == 2 then
375                local gray = graycolor(v[2])
376                d = { gray, gray, gray, gray }
377            elseif model == 3 then
378                local gray = graycolor(v[2])
379                local rgb  = rgbcolor(v[3],v[4],v[5])
380                local cmyk = cmykcolor(v[6],v[7],v[8],v[9])
381                d = { rgb, gray, rgb, cmyk }
382            elseif model == 4 then
383                local gray = graycolor(v[2])
384                local rgb  = rgbcolor(v[3],v[4],v[5])
385                local cmyk = cmykcolor(v[6],v[7],v[8],v[9])
386                d = { cmyk, gray, rgb, cmyk }
387            elseif model == 5 then
388                local spot = spotcolor(v[10],v[11],v[12],v[13])
389            --  d = { spot, gray, rgb, cmyk }
390                d = { spot, spot, spot, spot }
391            end
392        end
393        data[n] = d
394        return d
395    end
396end
397
398setmetatableindex(colors, extender)
399setmetatableindex(colors.data, reviver)
400
401function colors.filter(n)
402    return concat(data[n],":",5)
403end
404
405-- unweighted (flat) gray could be another model but a bit work as we need to check:
406--
407--   attr-col colo-ini colo-run
408--   grph-inc grph-wnd
409--   lpdf-col lpdf-fmt lpdf-fld lpdf-grp
410--   meta-pdf meta-pdh mlib-pps
411--
412-- but as we never needed it we happily delay that.
413
414function colors.setmodel(name,weightgray)
415    if weightgray == true or weightgray == v_yes then
416        weightgray = true
417    elseif weightgray == false or weightgray == v_no then
418        weightgray = false
419    else
420        local r, g, b = lpegmatch(p_split_colon,weightgray)
421        if r and g and b then
422            weightgray = { r, g, b }
423        else
424            weightgray = true
425        end
426    end
427    local default = models[name] or 1
428
429    colors.model      = name       -- global, not useful that way
430    colors.default    = default    -- global
431    colors.weightgray = weightgray -- global
432
433    -- avoid selective checking is no need for it
434
435    local forced = colors.forced
436
437    if forced == nil then
438        -- unset
439        colors.forced = default
440    elseif forced == false then
441        -- assumed mixed
442    elseif forced ~= default then
443        -- probably mixed
444        colors.forced = false
445    else
446        -- stil the same
447    end
448    return default
449end
450
451function colors.register(name, colorspace, ...) -- passing 9 vars is faster (but not called that often)
452    local stamp = f_colors[colorspace](...)
453    local color = registered[stamp]
454    if not color then
455        color = #values + 1
456        values[color] = colors[colorspace](...)
457        registered[stamp] = color
458    -- colors.reviver(color)
459    end
460    if name then
461        list[a_color][name] = color -- not grouped, so only global colors
462    end
463    return registered[stamp]
464end
465
466function colors.value(id)
467    return values[id]
468end
469
470attributes.colors.handler = nodes.installattributehandler {
471    name        = "color",
472    namespace   = colors,
473    initializer = states.initialize,
474    finalizer   = states.finalize,
475    processor   = states.selective,
476    resolver    = function() return colors.main end,
477}
478
479function colors.enable(value)
480    setaction("shipouts","attributes.colors.handler",not (value == false or not colors.supported))
481end
482
483function colors.forcesupport(value) -- can move to attr-div
484    colors.supported = value
485    report_colors("color is %ssupported",value and "" or "not ")
486    colors.enable(value)
487end
488
489function colors.toattributes(name)
490    local mc = list[a_color][name]
491    local mm = texgetattribute(a_selector)
492    return (mm == unsetvalue and 1) or mm or 1, mc or list[a_color][1] or unsetvalue
493end
494
495-- transparencies
496
497local a_transparency      = attributes.private('transparency')
498
499attributes.transparencies = attributes.transparencies or { }
500local transparencies      = attributes.transparencies
501transparencies.registered = transparencies.registered or { }
502transparencies.data       = allocate()
503transparencies.values     = transparencies.values or { }
504transparencies.triggering = true
505transparencies.attribute  = a_transparency
506transparencies.supported  = true
507
508local registered          = transparencies.registered -- we could use a 2 dimensional table instead
509local data                = transparencies.data
510local values              = transparencies.values
511local f_transparency      = formatters["%s:%s"]
512
513registerstorage("attributes/transparencies/registered", registered, "attributes.transparencies.registered")
514registerstorage("attributes/transparencies/values",     values,     "attributes.transparencies.values")
515
516local function inject_transparency(...)
517    inject_transparency = nodeinjections.transparency
518    return inject_transparency(...)
519end
520
521local function register_transparency(...)
522    register_transparency = registrations.transparency
523    return register_transparency(...)
524end
525
526function transparencies.register(name,a,t,force) -- name is irrelevant here (can even be nil)
527    -- Force needed here for metapost converter. We could always force
528    -- but then we'd end up with transparencies resources even if we
529    -- would not use transparencies (but define them only). This is
530    -- somewhat messy.
531    local stamp = f_transparency(a,t)
532    local n = registered[stamp]
533    if not n then
534        n = #values + 1
535        values[n] = { a, t }
536        registered[stamp] = n
537        if force then
538            register_transparency(n,a,t)
539        end
540    elseif force and not data[n] then
541        register_transparency(n,a,t)
542    end
543    if name then
544        list[a_transparency][name] = n -- not grouped, so only global transparencies
545    end
546    return registered[stamp]
547end
548
549local function extender(transparencies,key)
550    if colors.supported and key == "none" then
551        local d = inject_transparency(0)
552        transparencies.none = d
553        return d
554    end
555end
556
557local function reviver(data,n)
558    if n and transparencies.supported then
559        local v = values[n]
560        local d
561        if not v then
562            d = inject_transparency(0)
563        else
564            d = inject_transparency(n)
565            register_transparency(n,v[1],v[2])
566        end
567        data[n] = d
568        return d
569    else
570        return ""
571    end
572end
573
574setmetatableindex(transparencies,extender)
575setmetatableindex(transparencies.data,reviver) -- register if used
576
577-- check if there is an identity
578
579function transparencies.value(id)
580    return values[id]
581end
582
583attributes.transparencies.handler = nodes.installattributehandler {
584    name        = "transparency",
585    namespace   = transparencies,
586    initializer = states.initialize,
587    finalizer   = states.finalize,
588    processor   = states.process,
589}
590
591function transparencies.enable(value) -- nil is enable
592    setaction("shipouts","attributes.transparencies.handler",not (value == false or not transparencies.supported))
593end
594
595function transparencies.forcesupport(value) -- can move to attr-div
596    transparencies.supported = value
597    report_transparencies("transparency is %ssupported",value and "" or "not ")
598    transparencies.enable(value)
599end
600
601function transparencies.toattribute(name)
602    return list[a_transparency][name] or unsetvalue
603end
604
605--- colorintents: overprint / knockout
606
607attributes.colorintents = attributes.colorintents or  { }
608local colorintents      = attributes.colorintents
609colorintents.data       = allocate() -- colorintents.data or { }
610colorintents.attribute  = attributes.private('colorintent')
611
612colorintents.registered = allocate {
613    overprint = 1,
614    knockout  = 2,
615}
616
617local data, registered = colorintents.data, colorintents.registered
618
619local function extender(colorintents,key)
620    if key == "none" then
621        local d = data[2]
622        colorintents.none = d
623        return d
624    end
625end
626
627local function reviver(data,n)
628    if n == 1 then
629        local d = nodeinjections.overprint() -- called once
630        data[1] = d
631        return d
632    elseif n == 2 then
633        local d = nodeinjections.knockout() -- called once
634        data[2] = d
635        return d
636    end
637end
638
639setmetatableindex(colorintents, extender)
640setmetatableindex(colorintents.data, reviver)
641
642function colorintents.register(stamp)
643    return registered[stamp] or registered.overprint
644end
645
646colorintents.handler = nodes.installattributehandler {
647    name        = "colorintent",
648    namespace   = colorintents,
649    initializer = states.initialize,
650    finalizer   = states.finalize,
651    processor   = states.process,
652}
653
654function colorintents.enable()
655    enableaction("shipouts","attributes.colorintents.handler")
656end
657
658-- interface
659
660implement { name = "enablecolor",        onlyonce = true, actions = colors.enable }
661implement { name = "enabletransparency", onlyonce = true, actions = transparencies.enable }
662implement { name = "enablecolorintents", onlyonce = true, actions = colorintents.enable }
663
664--------- { name = "registercolor",        actions = { colors        .register, context }, arguments = "string" }
665--------- { name = "registertransparency", actions = { transparencies.register, context }, arguments = "string" }
666implement { name = "registercolorintent",  actions = { colorintents  .register, context }, arguments = "string" }
667