colo-icc.lmt /size: 10 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['colo-icc'] = {
2    version   = 1.000,
3    comment   = "companion to colo-ini.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
9local char, byte, gsub, match, format, strip = string.char, string.byte, string.gsub, string.match, string.format, string.strip
10local readstring, readnumber = io.readstring, io.readnumber
11local next = next
12
13local colors = attributes and attributes.colors or { } -- when used in mtxrun
14
15local report_colors = logs.reporter("colors","icc")
16
17local R, Cs, lpegmatch = lpeg.R, lpeg.Cs, lpeg.match
18
19local invalid = R(char(0)..char(31))
20local cleaned = invalid^0 * Cs((1-invalid)^0)
21
22function colors.iccprofile(filename,verbose)
23    local fullname = resolvers.findfile(filename,"icc") or ""
24    if fullname == "" then
25        local locate = resolvers.finders.byscheme -- not in mtxrun
26        if locate then
27            fullname = locate("loc",filename)
28        end
29    end
30    if fullname == "" then
31        report_colors("profile %a cannot be found",filename)
32        return nil, false
33    end
34    local f = io.open(fullname,"rb")
35    if not f then
36        report_colors("profile %a cannot be loaded",fullname)
37        return nil, false
38    end
39    local header =  {
40        size               = readnumber(f,4),
41        cmmtype            = readnumber(f,4),
42        version            = readnumber(f,4),
43        deviceclass        = strip(readstring(f,4)),
44        colorspace         = strip(readstring(f,4)),
45        connectionspace    = strip(readstring(f,4)),
46        datetime           = {
47            year    = readnumber(f,2),
48            month   = readnumber(f,2),
49            day     = readnumber(f,2),
50            hour    = readnumber(f,2),
51            minutes = readnumber(f,2),
52            seconds = readnumber(f,2),
53        },
54        filesignature      = strip(readstring(f,4)),
55        platformsignature  = strip(readstring(f,4)),
56        options            = readnumber(f,4),
57        devicemanufacturer = strip(readstring(f,4)),
58        devicemodel        = strip(readstring(f,4)),
59        deviceattributes   = readnumber(f,4),
60        renderingintent    = readnumber(f,4),
61        illuminantxyz      = {
62            x = readnumber(f,4),
63            y = readnumber(f,4),
64            z = readnumber(f,4),
65        },
66        profilecreator     = readnumber(f,4),
67        id                 = strip(readstring(f,16)),
68    }
69    local tags = { }
70    for i=1,readnumber(f,128,4) do
71        tags[readstring(f,4)] = {
72            offset = readnumber(f,4),
73            length = readnumber(f,4),
74        }
75    end
76    local o = header.options
77    header.options =
78        o == 0 and "embedded"  or
79        o == 1 and "dependent" or "unknown"
80    local d = header.deviceattributes
81    header.deviceattributes = {
82        [(d & 0x01) ~= 0 and "transparency" or "reflective"] = true,
83        [(d & 0x02) ~= 0 and "mate"         or "glossy"    ] = true,
84        [(d & 0x04) ~= 0 and "negative"     or "positive"  ] = true,
85        [(d & 0x08) ~= 0 and "bw"           or "color"     ] = true,
86    }
87    local r = header.renderingintent
88    header.renderingintent =
89        r == 0 and "perceptual" or
90        r == 1 and "relative"   or
91        r == 2 and "saturation" or
92        r == 3 and "absolute"   or "unknown"
93    for tag, spec in next, tags do
94        if tag then
95            local offset, length = spec.offset, spec.length
96            local variant = readstring(f,offset,4)
97            if variant == "text" or variant == "desc" then
98                local str = readstring(f,length-4)
99                tags[tag] = {
100                    data    = str,
101                    cleaned = lpegmatch(cleaned,str),
102                }
103            else
104                if verbose then
105                    report_colors("ignoring tag %a or type %a in profile %a",tag,variant,fullname)
106                end
107                tags[tag] = nil
108            end
109        end
110    end
111    f:close()
112    local profile = {
113        filename = filename,
114        fullname = fullname,
115        header   = header,
116        tags     = tags,
117    }
118    report_colors("profile %a loaded",fullname)
119    return profile, true
120end
121
122-- This is just some fun stuff I decided to check out when I was making sure that
123-- the 2020 metafun manual could be processed with lmtx 2021. Color conversion has
124-- been part of ConTeXt from the start but it has been extended to the less commonly
125-- used color spaces. We already do some CIE but didn't have lab converters to play
126-- with (although I had some MetaPost done for a friend long ago). So, when we moved
127-- to lmtx it made sense to also move some into the core. When searching for info
128-- I ran into some formulas for lab/xyz: http://www.easyrgb.com/en/math.php and
129-- http://www.brucelindbloom.com/ are useful resources. I didn't touch existing
130-- code (as it works ok).
131--
132-- local illuminants = { -- 2=CIE 1931 10=CIE 1964
133--     A   = { [2] = { 109.850, 100,  35.585 }, [10] = { 111.144, 100,  35.200 } }, -- incandescent/tungsten
134--     B   = { [2] = {  99.093, 100,  85.313 }, [10] = {  99.178, 100,  84.349 } }, -- old direct sunlight at noon
135--     C   = { [2] = {  98.074, 100, 118.232 }, [10] = {  97.285, 100, 116.145 } }, -- old daylight
136--     D50 = { [2] = {  96.422, 100,  82.521 }, [10] = {  96.720, 100,  81.427 } }, -- icc profile pcs
137--     D55 = { [2] = {  95.682, 100,  92.149 }, [10] = {  95.799, 100,  90.926 } }, -- mid-morning daylight
138--     D65 = { [2] = {  95.047, 100, 108.883 }, [10] = {  94.811, 100, 107.304 } }, -- daylight, srgb, adobe-rgb
139--     D75 = { [2] = {  94.972, 100, 122.638 }, [10] = {  94.416, 100, 120.641 } }, -- north sky daylight
140--     E   = { [2] = { 100.000, 100, 100.000 }, [10] = { 100.000, 100, 100.000 } }, -- equal energy
141--     F1  = { [2] = {  92.834, 100, 103.665 }, [10] = {  94.791, 100, 103.191 } }, -- daylight fluorescent
142--     F2  = { [2] = {  99.187, 100,  67.395 }, [10] = { 103.280, 100,  69.026 } }, -- cool fluorescent
143--     F3  = { [2] = { 103.754, 100,  49.861 }, [10] = { 108.968, 100,  51.965 } }, -- white fluorescent
144--     F4  = { [2] = { 109.147, 100,  38.813 }, [10] = { 114.961, 100,  40.963 } }, -- warm white fluorescent
145--     F5  = { [2] = {  90.872, 100,  98.723 }, [10] = {  93.369, 100,  98.636 } }, -- daylight fluorescent
146--     F6  = { [2] = {  97.309, 100,  60.191 }, [10] = { 102.148, 100,  62.074 } }, -- lite white fluorescent
147--     F7  = { [2] = {  95.044, 100, 108.755 }, [10] = {  95.792, 100, 107.687 } }, -- daylight fluorescent, d65 simulator
148--     F8  = { [2] = {  96.413, 100,  82.333 }, [10] = {  97.115, 100,  81.135 } }, -- sylvania f40, d50 simulator
149--     F9  = { [2] = { 100.365, 100,  67.868 }, [10] = { 102.116, 100,  67.826 } }, -- cool white fluorescent
150--     F10 = { [2] = {  96.174, 100,  81.712 }, [10] = {  99.001, 100,  83.134 } }, -- ultralume 50, philips tl85
151--     F11 = { [2] = { 100.966, 100,  64.370 }, [10] = { 103.866, 100,  65.627 } }, -- ultralume 40, philips tl84
152--     F12 = { [2] = { 108.046, 100,  39.228 }, [10] = { 111.428, 100,  40.353 } }, -- ultralume 30, philips tl83
153-- }
154--
155-- local D65  = illuminants.D65
156-- local D652 = {  95.047, 100, 108.883 }
157--
158-- local function labref(illuminate,observer)
159--     local r = illuminants[illuminant or "D65"] or D65
160--     return r[observer or 2] or r[2] or D652
161-- end
162--
163-- This is hardly useful but nice for metafun demos:
164
165
166----- default = { 95.047, 100, 108.883 } -- D652
167local default = { 96.422, 100,  82.521 } -- D502
168----- default = { 96.720, 100,  81.427 } -- D510
169
170local function xyztolab(x,y,z,mapping)
171    if not mapping then
172        mapping = default
173    end
174    x = x / mapping[1]
175    y = y / mapping[2]
176    z = z / mapping[3]
177    x = (x > 0.008856) and x^(1/3) or (7.787 * x) + (16/116)
178    y = (y > 0.008856) and y^(1/3) or (7.787 * y) + (16/116)
179    z = (z > 0.008856) and z^(1/3) or (7.787 * z) + (16/116)
180    return
181        116 * y  - 16,
182        500 * (x - y),
183        200 * (y - z)
184end
185
186local function labtoxyz(l,a,b,mapping)
187    if not mapping then
188        mapping = default
189    end
190    local y = (l + 16) / 116
191    local x = a / 500 + y
192    local z = y - b / 200
193    return
194        mapping[1] * ((x^3 > 0.008856) and x^3 or (x - 16/116) / 7.787),
195        mapping[2] * ((y^3 > 0.008856) and y^3 or (y - 16/116) / 7.787),
196        mapping[3] * ((z^3 > 0.008856) and z^3 or (z - 16/116) / 7.787)
197end
198
199local function xyztorgb(x,y,z)
200    -- D65/2°
201 -- local r = (x *  3.2404542 + y * -1.5371385 + z * -0.4985314) / 100
202 -- local g = (x * -0.9692660 + y *  1.8760108 + z *  0.0415560) / 100
203 -- local b = (x *  0.0556434 + y * -0.2040259 + z *  1.0572252) / 100
204    -- D50/2°
205    local r = (x *  3.1338561 + y * -1.6168667 + z * -0.4906146) / 100
206    local g = (x * -0.9787684 + y *  1.9161415 + z *  0.0334540) / 100
207    local b = (x *  0.0719453 + y * -0.2289914 + z *  1.4052427) / 100
208    --
209    r = (r > 0.0031308) and (1.055 * r^(1/2.4) - 0.055) or (12.92 * r)
210    g = (g > 0.0031308) and (1.055 * g^(1/2.4) - 0.055) or (12.92 * g)
211    b = (b > 0.0031308) and (1.055 * b^(1/2.4) - 0.055) or (12.92 * b)
212    if r < 0 then r = 0 elseif r > 1 then r = 1 end
213    if g < 0 then g = 0 elseif g > 1 then g = 1 end
214    if b < 0 then b = 0 elseif b > 1 then b = 1 end
215    return r, g, b
216end
217
218local function rgbtoxyz(r,g,b)
219    r = 100 * ((r > 0.04045) and ((r + 0.055)/1.055)^2.4 or (r / 12.92))
220    g = 100 * ((g > 0.04045) and ((g + 0.055)/1.055)^2.4 or (g / 12.92))
221    b = 100 * ((b > 0.04045) and ((b + 0.055)/1.055)^2.4 or (b / 12.92))
222    return
223        r * 0.4124 + g * 0.3576 + b * 0.1805,
224        r * 0.2126 + g * 0.7152 + b * 0.0722,
225        r * 0.0193 + g * 0.1192 + b * 0.9505
226end
227
228local function labtorgb(l,a,b,mapping)
229    return xyztorgb(labtoxyz(l,a,b,mapping))
230end
231
232colors.xyztolab = xyztolab
233colors.labtoxyz = labtoxyz
234colors.xyztorgb = xyztorgb
235colors.rgbtoxyz = rgbtoxyz
236colors.labtorgb = labtorgb
237
238-- from https://www.w3.org/TR/css-color-4/#specifying-lab-lch
239--
240-- this is unchecked
241
242local sin, cos, atan2, sqrt = math.sin, math.cos, math.atan2, math.sqrt
243
244local pi <const> = math.pi
245
246local function labtolch(l,a,b)
247    local h = atan2(b,a) * 180/pi -- hue
248    local c = sqrt(a^2 + b^2)     -- chroma
249    return l, c, h >= 0 and h or (h + 360) -- degrees 0..360
250end
251
252local function lchtolab(l,c,h)
253    local v = c * pi/180
254    local a = h * cos(v)
255    local b = h * sin(v)
256    return l, a, b
257end
258
259local function lchtorgb(l,c,h)
260    local l, a, b = lchtolab(l,h,c)
261    local x, y, z = labtoxyz(l,a,b)
262    return xyztorgb(x,y,z)
263end
264
265colors.labtolch = labtolch
266colors.lchtolab = lchtolab
267colors.lchtorgb = lchtorgb
268