nx_eink_tone.nx source
↩ module page · 674 lines · 35117 B
1// nx_eink_tone.nx -- PERCEPTUAL TONE QUANTIZER for e-ink-style rendering. Integer-exact, no float.
2//
3// WHY: an e-ink panel has ~16 grey levels, not 256. Getting from a photograph or a rendered page down to 16
4// levels is where the "ink on paper" look is won or lost, and it turns on TWO decisions that are usually got
5// wrong:
6//
7// 1. QUANTIZE IN PERCEPTUAL LIGHTNESS, NOT IN sRGB OR LINEAR. sRGB code values are not perceptually
8// spaced, so 16 evenly-spaced sRGB steps put most of their resolution where the eye cannot use it and
9// band visibly in the shadows. CIE L* is defined to be perceptually uniform, so 16 evenly-spaced L*
10// steps look evenly spaced. This is the difference between "16 greys" and "16 USEFUL greys".
11// 2. DITHER WITH A BLUE-NOISE MASK, NOT BY ROUNDING. Rounding to the nearest level produces flat bands.
12// A blue-noise threshold (nx_bluenoise) breaks the band into a texture the eye integrates back to the
13// true tone without ever resolving structure.
14//
15// The sRGB->linear table is BAKED DATA, computed from the exact piecewise sRGB transfer function and scaled
16// to 1e5. It is a table rather than a runtime power because x^2.4 in integer arithmetic is either wrong or
17// expensive, and a transfer curve that differs per host makes every render irreproducible.
18// KNOWN-ANSWER ANCHOR: sRGB 128 -> linear 21586 -> L* 53.59, the textbook value for mid-grey. The gate
19// asserts it, so a silent regression in the table or the cube root cannot pass.
20//
21// nx_eink_tone lstar <r> <g> <b> -- CIE L* x1000 for one sRGB colour
22// nx_eink_tone ramp <levels> -- the quantization ladder, in L* and in sRGB
23// nx_eink_tone dither <w> <h> <gray> <levels> <mask> <masksize> -- dither a FLAT tone, report the mix
24// nx_eink_tone selftest -- the gate
25// license_tier: ORIGINAL
26import "nx_gate_verdict.nx"
27import "nx_syscalls.nx"
28const ET_MAGIC_1033: i64 = 1033
29const ET_MAGIC_1096: i64 = 1096
30const ET_MAGIC_1161: i64 = 1161
31const ET_MAGIC_1229: i64 = 1229
32const ET_MAGIC_1298: i64 = 1298
33const ET_MAGIC_1370: i64 = 1370
34const ET_MAGIC_1444: i64 = 1444
35const ET_MAGIC_1521: i64 = 1521
36const ET_MAGIC_1600: i64 = 1600
37const ET_MAGIC_1681: i64 = 1681
38const ET_MAGIC_1764: i64 = 1764
39const ET_MAGIC_1850: i64 = 1850
40const ET_MAGIC_1938: i64 = 1938
41const ET_MAGIC_2029: i64 = 2029
42const ET_MAGIC_2122: i64 = 2122
43const ET_MAGIC_2217: i64 = 2217
44const ET_MAGIC_2315: i64 = 2315
45const ET_MAGIC_2416: i64 = 2416
46const ET_MAGIC_2519: i64 = 2519
47const ET_MAGIC_2624: i64 = 2624
48const ET_MAGIC_2732: i64 = 2732
49const ET_MAGIC_2843: i64 = 2843
50const ET_MAGIC_2956: i64 = 2956
51const ET_MAGIC_3071: i64 = 3071
52const ET_MAGIC_3190: i64 = 3190
53const ET_MAGIC_3310: i64 = 3310
54const ET_MAGIC_3434: i64 = 3434
55const ET_MAGIC_3560: i64 = 3560
56const ET_MAGIC_3689: i64 = 3689
57const ET_MAGIC_3820: i64 = 3820
58const ET_MAGIC_3955: i64 = 3955
59const ET_MAGIC_4092: i64 = 4092
60const ET_MAGIC_4231: i64 = 4231
61const ET_MAGIC_4374: i64 = 4374
62const ET_MAGIC_4519: i64 = 4519
63const ET_MAGIC_4667: i64 = 4667
64const ET_MAGIC_4817: i64 = 4817
65const ET_MAGIC_4971: i64 = 4971
66const ET_MAGIC_5127: i64 = 5127
67const ET_MAGIC_5286: i64 = 5286
68const ET_MAGIC_5448: i64 = 5448
69const ET_MAGIC_5613: i64 = 5613
70const ET_MAGIC_5781: i64 = 5781
71const ET_MAGIC_5951: i64 = 5951
72const ET_MAGIC_6125: i64 = 6125
73const ET_MAGIC_6301: i64 = 6301
74const ET_MAGIC_6480: i64 = 6480
75const ET_MAGIC_6663: i64 = 6663
76const ET_MAGIC_6848: i64 = 6848
77const ET_MAGIC_7036: i64 = 7036
78const ET_MAGIC_7227: i64 = 7227
79const ET_MAGIC_7421: i64 = 7421
80const ET_MAGIC_7619: i64 = 7619
81const ET_MAGIC_7819: i64 = 7819
82const ET_MAGIC_8022: i64 = 8022
83const ET_MAGIC_8228: i64 = 8228
84const ET_MAGIC_8438: i64 = 8438
85const ET_MAGIC_8650: i64 = 8650
86const ET_MAGIC_8866: i64 = 8866
87const ET_MAGIC_9084: i64 = 9084
88const ET_MAGIC_9306: i64 = 9306
89const ET_MAGIC_9531: i64 = 9531
90const ET_MAGIC_9759: i64 = 9759
91const ET_MAGIC_9990: i64 = 9990
92const ET_MAGIC_10224: i64 = 10224
93const ET_MAGIC_10462: i64 = 10462
94const ET_MAGIC_10702: i64 = 10702
95const ET_MAGIC_10946: i64 = 10946
96const ET_MAGIC_11193: i64 = 11193
97const ET_MAGIC_11444: i64 = 11444
98const ET_MAGIC_11697: i64 = 11697
99const ET_MAGIC_11954: i64 = 11954
100const ET_MAGIC_12214: i64 = 12214
101const ET_MAGIC_12477: i64 = 12477
102const ET_MAGIC_12744: i64 = 12744
103const ET_MAGIC_13014: i64 = 13014
104const ET_MAGIC_13287: i64 = 13287
105const ET_MAGIC_13563: i64 = 13563
106const ET_MAGIC_13843: i64 = 13843
107const ET_MAGIC_14126: i64 = 14126
108const ET_MAGIC_14413: i64 = 14413
109const ET_MAGIC_14703: i64 = 14703
110const ET_MAGIC_14996: i64 = 14996
111const ET_MAGIC_15293: i64 = 15293
112const ET_MAGIC_15593: i64 = 15593
113const ET_MAGIC_15896: i64 = 15896
114const ET_MAGIC_16203: i64 = 16203
115const ET_MAGIC_16513: i64 = 16513
116const ET_MAGIC_16827: i64 = 16827
117const ET_MAGIC_17144: i64 = 17144
118const ET_MAGIC_17465: i64 = 17465
119const ET_MAGIC_17789: i64 = 17789
120const ET_MAGIC_18116: i64 = 18116
121const ET_MAGIC_18447: i64 = 18447
122const ET_MAGIC_18782: i64 = 18782
123const ET_MAGIC_19120: i64 = 19120
124const ET_MAGIC_19462: i64 = 19462
125const ET_MAGIC_19807: i64 = 19807
126const ET_MAGIC_20156: i64 = 20156
127const ET_MAGIC_20508: i64 = 20508
128const ET_MAGIC_20864: i64 = 20864
129const ET_MAGIC_21223: i64 = 21223
130const ET_MAGIC_21586: i64 = 21586
131const ET_MAGIC_21953: i64 = 21953
132const ET_MAGIC_22323: i64 = 22323
133const ET_MAGIC_22697: i64 = 22697
134const ET_MAGIC_23074: i64 = 23074
135const ET_MAGIC_23455: i64 = 23455
136const ET_MAGIC_23840: i64 = 23840
137const ET_MAGIC_24228: i64 = 24228
138const ET_MAGIC_24620: i64 = 24620
139const ET_MAGIC_25016: i64 = 25016
140const ET_MAGIC_25415: i64 = 25415
141const ET_MAGIC_25818: i64 = 25818
142const ET_MAGIC_26225: i64 = 26225
143const ET_MAGIC_26636: i64 = 26636
144const ET_MAGIC_27050: i64 = 27050
145const ET_MAGIC_27468: i64 = 27468
146const ET_MAGIC_27889: i64 = 27889
147const ET_MAGIC_28315: i64 = 28315
148const ET_MAGIC_28744: i64 = 28744
149const ET_MAGIC_29177: i64 = 29177
150const ET_MAGIC_29614: i64 = 29614
151const ET_MAGIC_30054: i64 = 30054
152const ET_MAGIC_30499: i64 = 30499
153const ET_MAGIC_30947: i64 = 30947
154const ET_MAGIC_31399: i64 = 31399
155const ET_MAGIC_31855: i64 = 31855
156const ET_MAGIC_32314: i64 = 32314
157const ET_MAGIC_32778: i64 = 32778
158const ET_MAGIC_33245: i64 = 33245
159const ET_MAGIC_33716: i64 = 33716
160const ET_MAGIC_34191: i64 = 34191
161const ET_MAGIC_34670: i64 = 34670
162const ET_MAGIC_35153: i64 = 35153
163const ET_MAGIC_35640: i64 = 35640
164const ET_MAGIC_36131: i64 = 36131
165const ET_MAGIC_36625: i64 = 36625
166const ET_MAGIC_37124: i64 = 37124
167const ET_MAGIC_37626: i64 = 37626
168const ET_MAGIC_38133: i64 = 38133
169const ET_MAGIC_38643: i64 = 38643
170const ET_MAGIC_39157: i64 = 39157
171const ET_MAGIC_39676: i64 = 39676
172const ET_MAGIC_40198: i64 = 40198
173const ET_MAGIC_40724: i64 = 40724
174const ET_MAGIC_41254: i64 = 41254
175const ET_MAGIC_41789: i64 = 41789
176const ET_MAGIC_42327: i64 = 42327
177const ET_MAGIC_42869: i64 = 42869
178const ET_MAGIC_43415: i64 = 43415
179const ET_MAGIC_43966: i64 = 43966
180const ET_MAGIC_44520: i64 = 44520
181const ET_MAGIC_45079: i64 = 45079
182const ET_MAGIC_45641: i64 = 45641
183const ET_MAGIC_46208: i64 = 46208
184const ET_MAGIC_46778: i64 = 46778
185const ET_MAGIC_47353: i64 = 47353
186const ET_MAGIC_47932: i64 = 47932
187const ET_MAGIC_48515: i64 = 48515
188const ET_MAGIC_49102: i64 = 49102
189const ET_MAGIC_49693: i64 = 49693
190const ET_MAGIC_50289: i64 = 50289
191const ET_MAGIC_50888: i64 = 50888
192const ET_MAGIC_51492: i64 = 51492
193const ET_MAGIC_52100: i64 = 52100
194const ET_MAGIC_52712: i64 = 52712
195const ET_MAGIC_53328: i64 = 53328
196const ET_MAGIC_53948: i64 = 53948
197const ET_MAGIC_54572: i64 = 54572
198const ET_MAGIC_55201: i64 = 55201
199const ET_MAGIC_55834: i64 = 55834
200const ET_MAGIC_56471: i64 = 56471
201const ET_MAGIC_57112: i64 = 57112
202const ET_MAGIC_57758: i64 = 57758
203const ET_MAGIC_58408: i64 = 58408
204const ET_MAGIC_59062: i64 = 59062
205const ET_MAGIC_59720: i64 = 59720
206const ET_MAGIC_60383: i64 = 60383
207const ET_MAGIC_61050: i64 = 61050
208const ET_MAGIC_61721: i64 = 61721
209const ET_MAGIC_62396: i64 = 62396
210const ET_MAGIC_63076: i64 = 63076
211const ET_MAGIC_63760: i64 = 63760
212const ET_MAGIC_64448: i64 = 64448
213const ET_MAGIC_65141: i64 = 65141
214const ET_MAGIC_65837: i64 = 65837
215const ET_MAGIC_66539: i64 = 66539
216const ET_MAGIC_67244: i64 = 67244
217const ET_MAGIC_67954: i64 = 67954
218const ET_MAGIC_68669: i64 = 68669
219const ET_MAGIC_69387: i64 = 69387
220const ET_MAGIC_70110: i64 = 70110
221const ET_MAGIC_70838: i64 = 70838
222const ET_MAGIC_71569: i64 = 71569
223const ET_MAGIC_72306: i64 = 72306
224const ET_MAGIC_73046: i64 = 73046
225const ET_MAGIC_73791: i64 = 73791
226const ET_MAGIC_74540: i64 = 74540
227const ET_MAGIC_75294: i64 = 75294
228const ET_MAGIC_76052: i64 = 76052
229const ET_MAGIC_76815: i64 = 76815
230const ET_MAGIC_77582: i64 = 77582
231const ET_MAGIC_78354: i64 = 78354
232const ET_MAGIC_79130: i64 = 79130
233const ET_MAGIC_79910: i64 = 79910
234const ET_MAGIC_80695: i64 = 80695
235const ET_MAGIC_81485: i64 = 81485
236const ET_MAGIC_82279: i64 = 82279
237const ET_MAGIC_83077: i64 = 83077
238const ET_MAGIC_83880: i64 = 83880
239const ET_MAGIC_84687: i64 = 84687
240const ET_MAGIC_85499: i64 = 85499
241const ET_MAGIC_86316: i64 = 86316
242const ET_MAGIC_87137: i64 = 87137
243const ET_MAGIC_87962: i64 = 87962
244const ET_MAGIC_88792: i64 = 88792
245const ET_MAGIC_89627: i64 = 89627
246const ET_MAGIC_90466: i64 = 90466
247const ET_MAGIC_91310: i64 = 91310
248const ET_MAGIC_92158: i64 = 92158
249const ET_MAGIC_93011: i64 = 93011
250const ET_MAGIC_93869: i64 = 93869
251const ET_MAGIC_94731: i64 = 94731
252const ET_MAGIC_95597: i64 = 95597
253const ET_MAGIC_96469: i64 = 96469
254const ET_MAGIC_97345: i64 = 97345
255const ET_MAGIC_98225: i64 = 98225
256const ET_MAGIC_99110: i64 = 99110
257const ET_MAGIC_100000: i64 = 100000
258const ET_MAGIC_53000: i64 = 53000
259const ET_MAGIC_54200: i64 = 54200
260const ET_MAGIC_1500: i64 = 1500
261const ET_MAGIC_5000: i64 = 5000
262
263const ET_LIN_SCALE: i64 = 100000 // linear light fixed-point scale (1.0 == 100000)
264const ET_LSTAR_SCALE: i64 = 1000 // L* fixed-point scale (L*=100 -> 100000)
265const ET_LSTAR_MAX: i64 = 100000 // L* 100.000
266const ET_KAPPA: i64 = 9033 // CIE kappa 903.3, x10 -- the linear branch slope
267const ET_KAPPA_DIV: i64 = 1000 // pairs with ET_KAPPA to land L* on the x1000 scale (see et_lstar_from_y)
268const ET_EPS_LIN: i64 = 886 // CIE epsilon 0.008856 expressed on ET_LIN_SCALE
269const ET_CBRT_IN: i64 = 10000 // cbrt input scaling: cbrt(Y*1e4) lands f on 0..1000
270const ET_F_SCALE: i64 = 1000
271const ET_L_A: i64 = 116 // L* = 116*f - 16
272const ET_L_B: i64 = 16000 // 16 on the x1000 scale
273const ET_RW: i64 = 2126 // Rec.709 luminance weights x10000 (sum 10000 exactly)
274const ET_GW: i64 = 7152
275const ET_BW: i64 = 722
276const ET_WSUM: i64 = 10000
277const ET_BYTE_MAX: i64 = 255
278const ET_BYTE_MAX_P1: i64 = 256 // divisor for mapping a 0..255 mask byte onto a 0..999 threshold
279const ET_MIN_LEVELS: i64 = 2
280const ET_MAX_LEVELS: i64 = 256
281const ET_EINK_LEVELS: i64 = 16 // what an E Ink Carta panel actually resolves
282
283func et_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
284func et_num(v: i64) -> i64 {
285 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
286 var m: i64 = v
287 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
288 let t: *u8 = sys_mmap(32); var k: i64 = 0
289 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
290 let o: *u8 = sys_mmap(32); var w: i64 = 0; var q: i64 = k - 1
291 while q >= 0 { o[w] = t[q]; w = w + 1; q = q - 1 }
292 sys_write(1, o, w); return 0
293}
294func et_atoi(s: *u8) -> i64 {
295 var v: i64 = 0; var i: i64 = 0
296 while s[i] != (0 as u8) { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v*10 + (c-48) } } i = i + 1 }
297 return v
298}
299func et_seq(a: *u8, b: *u8) -> i64 {
300 var i: i64 = 0
301 while 1 == 1 { if (a[i] as i64) != (b[i] as i64) { return 0 } if (a[i] as i64) == 0 { return 1 } i = i + 1 }
302 return 0
303}
304
305// exact sRGB EOTF, scaled to ET_LIN_SCALE. DATA, not an approximation (see header).
306func et_srgb_lin(c: i64) -> i64 {
307 if c < 0 { return 0 }
308 if c > ET_BYTE_MAX { return ET_LIN_SCALE }
309 let L: *i64 = sys_mmap(8 * 256) as *i64
310 L[0]=0 L[1]=30 L[2]=61 L[3]=91 L[4]=121 L[5]=152 L[6]=182 L[7]=212
311 L[8]=243 L[9]=273 L[10]=304 L[11]=335 L[12]=368 L[13]=402 L[14]=439 L[15]=478
312 L[16]=518 L[17]=561 L[18]=605 L[19]=651 L[20]=700 L[21]=750 L[22]=802 L[23]=857
313 L[24]=913 L[25]=972 L[26]=ET_MAGIC_1033 L[27]=ET_MAGIC_1096 L[28]=ET_MAGIC_1161 L[29]=ET_MAGIC_1229 L[30]=ET_MAGIC_1298 L[31]=ET_MAGIC_1370
314 L[32]=ET_MAGIC_1444 L[33]=ET_MAGIC_1521 L[34]=ET_MAGIC_1600 L[35]=ET_MAGIC_1681 L[36]=ET_MAGIC_1764 L[37]=ET_MAGIC_1850 L[38]=ET_MAGIC_1938 L[39]=ET_MAGIC_2029
315 L[40]=ET_MAGIC_2122 L[41]=ET_MAGIC_2217 L[42]=ET_MAGIC_2315 L[43]=ET_MAGIC_2416 L[44]=ET_MAGIC_2519 L[45]=ET_MAGIC_2624 L[46]=ET_MAGIC_2732 L[47]=ET_MAGIC_2843
316 L[48]=ET_MAGIC_2956 L[49]=ET_MAGIC_3071 L[50]=ET_MAGIC_3190 L[51]=ET_MAGIC_3310 L[52]=ET_MAGIC_3434 L[53]=ET_MAGIC_3560 L[54]=ET_MAGIC_3689 L[55]=ET_MAGIC_3820
317 L[56]=ET_MAGIC_3955 L[57]=ET_MAGIC_4092 L[58]=ET_MAGIC_4231 L[59]=ET_MAGIC_4374 L[60]=ET_MAGIC_4519 L[61]=ET_MAGIC_4667 L[62]=ET_MAGIC_4817 L[63]=ET_MAGIC_4971
318 L[64]=ET_MAGIC_5127 L[65]=ET_MAGIC_5286 L[66]=ET_MAGIC_5448 L[67]=ET_MAGIC_5613 L[68]=ET_MAGIC_5781 L[69]=ET_MAGIC_5951 L[70]=ET_MAGIC_6125 L[71]=ET_MAGIC_6301
319 L[72]=ET_MAGIC_6480 L[73]=ET_MAGIC_6663 L[74]=ET_MAGIC_6848 L[75]=ET_MAGIC_7036 L[76]=ET_MAGIC_7227 L[77]=ET_MAGIC_7421 L[78]=ET_MAGIC_7619 L[79]=ET_MAGIC_7819
320 L[80]=ET_MAGIC_8022 L[81]=ET_MAGIC_8228 L[82]=ET_MAGIC_8438 L[83]=ET_MAGIC_8650 L[84]=ET_MAGIC_8866 L[85]=ET_MAGIC_9084 L[86]=ET_MAGIC_9306 L[87]=ET_MAGIC_9531
321 L[88]=ET_MAGIC_9759 L[89]=ET_MAGIC_9990 L[90]=ET_MAGIC_10224 L[91]=ET_MAGIC_10462 L[92]=ET_MAGIC_10702 L[93]=ET_MAGIC_10946 L[94]=ET_MAGIC_11193 L[95]=ET_MAGIC_11444
322 L[96]=ET_MAGIC_11697 L[97]=ET_MAGIC_11954 L[98]=ET_MAGIC_12214 L[99]=ET_MAGIC_12477 L[100]=ET_MAGIC_12744 L[101]=ET_MAGIC_13014 L[102]=ET_MAGIC_13287 L[103]=ET_MAGIC_13563
323 L[104]=ET_MAGIC_13843 L[105]=ET_MAGIC_14126 L[106]=ET_MAGIC_14413 L[107]=ET_MAGIC_14703 L[108]=ET_MAGIC_14996 L[109]=ET_MAGIC_15293 L[110]=ET_MAGIC_15593 L[111]=ET_MAGIC_15896
324 L[112]=ET_MAGIC_16203 L[113]=ET_MAGIC_16513 L[114]=ET_MAGIC_16827 L[115]=ET_MAGIC_17144 L[116]=ET_MAGIC_17465 L[117]=ET_MAGIC_17789 L[118]=ET_MAGIC_18116 L[119]=ET_MAGIC_18447
325 L[120]=ET_MAGIC_18782 L[121]=ET_MAGIC_19120 L[122]=ET_MAGIC_19462 L[123]=ET_MAGIC_19807 L[124]=ET_MAGIC_20156 L[125]=ET_MAGIC_20508 L[126]=ET_MAGIC_20864 L[127]=ET_MAGIC_21223
326 L[128]=ET_MAGIC_21586 L[129]=ET_MAGIC_21953 L[130]=ET_MAGIC_22323 L[131]=ET_MAGIC_22697 L[132]=ET_MAGIC_23074 L[133]=ET_MAGIC_23455 L[134]=ET_MAGIC_23840 L[135]=ET_MAGIC_24228
327 L[136]=ET_MAGIC_24620 L[137]=ET_MAGIC_25016 L[138]=ET_MAGIC_25415 L[139]=ET_MAGIC_25818 L[140]=ET_MAGIC_26225 L[141]=ET_MAGIC_26636 L[142]=ET_MAGIC_27050 L[143]=ET_MAGIC_27468
328 L[144]=ET_MAGIC_27889 L[145]=ET_MAGIC_28315 L[146]=ET_MAGIC_28744 L[147]=ET_MAGIC_29177 L[148]=ET_MAGIC_29614 L[149]=ET_MAGIC_30054 L[150]=ET_MAGIC_30499 L[151]=ET_MAGIC_30947
329 L[152]=ET_MAGIC_31399 L[153]=ET_MAGIC_31855 L[154]=ET_MAGIC_32314 L[155]=ET_MAGIC_32778 L[156]=ET_MAGIC_33245 L[157]=ET_MAGIC_33716 L[158]=ET_MAGIC_34191 L[159]=ET_MAGIC_34670
330 L[160]=ET_MAGIC_35153 L[161]=ET_MAGIC_35640 L[162]=ET_MAGIC_36131 L[163]=ET_MAGIC_36625 L[164]=ET_MAGIC_37124 L[165]=ET_MAGIC_37626 L[166]=ET_MAGIC_38133 L[167]=ET_MAGIC_38643
331 L[168]=ET_MAGIC_39157 L[169]=ET_MAGIC_39676 L[170]=ET_MAGIC_40198 L[171]=ET_MAGIC_40724 L[172]=ET_MAGIC_41254 L[173]=ET_MAGIC_41789 L[174]=ET_MAGIC_42327 L[175]=ET_MAGIC_42869
332 L[176]=ET_MAGIC_43415 L[177]=ET_MAGIC_43966 L[178]=ET_MAGIC_44520 L[179]=ET_MAGIC_45079 L[180]=ET_MAGIC_45641 L[181]=ET_MAGIC_46208 L[182]=ET_MAGIC_46778 L[183]=ET_MAGIC_47353
333 L[184]=ET_MAGIC_47932 L[185]=ET_MAGIC_48515 L[186]=ET_MAGIC_49102 L[187]=ET_MAGIC_49693 L[188]=ET_MAGIC_50289 L[189]=ET_MAGIC_50888 L[190]=ET_MAGIC_51492 L[191]=ET_MAGIC_52100
334 L[192]=ET_MAGIC_52712 L[193]=ET_MAGIC_53328 L[194]=ET_MAGIC_53948 L[195]=ET_MAGIC_54572 L[196]=ET_MAGIC_55201 L[197]=ET_MAGIC_55834 L[198]=ET_MAGIC_56471 L[199]=ET_MAGIC_57112
335 L[200]=ET_MAGIC_57758 L[201]=ET_MAGIC_58408 L[202]=ET_MAGIC_59062 L[203]=ET_MAGIC_59720 L[204]=ET_MAGIC_60383 L[205]=ET_MAGIC_61050 L[206]=ET_MAGIC_61721 L[207]=ET_MAGIC_62396
336 L[208]=ET_MAGIC_63076 L[209]=ET_MAGIC_63760 L[210]=ET_MAGIC_64448 L[211]=ET_MAGIC_65141 L[212]=ET_MAGIC_65837 L[213]=ET_MAGIC_66539 L[214]=ET_MAGIC_67244 L[215]=ET_MAGIC_67954
337 L[216]=ET_MAGIC_68669 L[217]=ET_MAGIC_69387 L[218]=ET_MAGIC_70110 L[219]=ET_MAGIC_70838 L[220]=ET_MAGIC_71569 L[221]=ET_MAGIC_72306 L[222]=ET_MAGIC_73046 L[223]=ET_MAGIC_73791
338 L[224]=ET_MAGIC_74540 L[225]=ET_MAGIC_75294 L[226]=ET_MAGIC_76052 L[227]=ET_MAGIC_76815 L[228]=ET_MAGIC_77582 L[229]=ET_MAGIC_78354 L[230]=ET_MAGIC_79130 L[231]=ET_MAGIC_79910
339 L[232]=ET_MAGIC_80695 L[233]=ET_MAGIC_81485 L[234]=ET_MAGIC_82279 L[235]=ET_MAGIC_83077 L[236]=ET_MAGIC_83880 L[237]=ET_MAGIC_84687 L[238]=ET_MAGIC_85499 L[239]=ET_MAGIC_86316
340 L[240]=ET_MAGIC_87137 L[241]=ET_MAGIC_87962 L[242]=ET_MAGIC_88792 L[243]=ET_MAGIC_89627 L[244]=ET_MAGIC_90466 L[245]=ET_MAGIC_91310 L[246]=ET_MAGIC_92158 L[247]=ET_MAGIC_93011
341 L[248]=ET_MAGIC_93869 L[249]=ET_MAGIC_94731 L[250]=ET_MAGIC_95597 L[251]=ET_MAGIC_96469 L[252]=ET_MAGIC_97345 L[253]=ET_MAGIC_98225 L[254]=ET_MAGIC_99110 L[255]=ET_MAGIC_100000
342 return L[c]
343}
344
345// floor(cbrt(v)) by bracket-then-bisect. Integer so the whole tone path is reproducible bit-for-bit.
346func et_icbrt(v: i64) -> i64 {
347 if v <= 0 { return 0 }
348 var hi: i64 = 1
349 while hi * hi * hi <= v { hi = hi * 2 }
350 var lo: i64 = hi / 2
351 while lo < hi {
352 let mid: i64 = (lo + hi + 1) / 2
353 if mid * mid * mid <= v { lo = mid } else { hi = mid - 1 }
354 }
355 return lo
356}
357
358// CIE L* x1000 from linear luminance on ET_LIN_SCALE. Both CIE branches, including the linear one near
359// black -- dropping it (as a naive cube root does) crushes the shadows, which on a reading surface is
360// exactly where a paper look lives.
361func et_lstar_from_y(y: i64) -> i64 {
362 if y <= 0 { return 0 }
363 if y >= ET_LIN_SCALE { return ET_LSTAR_MAX }
364 // L*x1000 = 903.3 * (y/ET_LIN_SCALE) * 1000, and ET_KAPPA holds 903.3*10, so the divisor is 1000 --
365 // NOT ET_LIN_SCALE. Getting this wrong scales the whole shadow branch by 101x and crushes sRGB 0..23 to
366 // near-black, which is exactly the shadow crush this branch exists to prevent. Monotonicity cannot see
367 // the error (a scale factor preserves order); only the branch-continuity tooth in the gate can.
368 if y <= ET_EPS_LIN { return (ET_KAPPA * y) / ET_KAPPA_DIV }
369 let f: i64 = et_icbrt(y * ET_CBRT_IN) // cbrt(Y*1e4) -> f on 0..1000
370 return ET_L_A * f - ET_L_B
371}
372
373func et_lstar_rgb(r: i64, g: i64, b: i64) -> i64 {
374 let y: i64 = (ET_RW * et_srgb_lin(r) + ET_GW * et_srgb_lin(g) + ET_BW * et_srgb_lin(b)) / ET_WSUM
375 return et_lstar_from_y(y)
376}
377
378// inverse: nearest sRGB byte whose L* is closest to the target. Monotone search, so no table needed.
379func et_srgb_from_lstar(target: i64) -> i64 {
380 var best: i64 = 0
381 var bestd: i64 = 0 - 1
382 var c: i64 = 0
383 while c <= ET_BYTE_MAX {
384 var d: i64 = et_lstar_from_y(et_srgb_lin(c)) - target
385 if d < 0 { d = 0 - d }
386 if bestd < 0 { bestd = d; best = c }
387 if d < bestd { bestd = d; best = c }
388 c = c + 1
389 }
390 return best
391}
392
393// Quantize an L* value to `levels` evenly-spaced L* steps, using a threshold 0..999 from the dither mask.
394// The mask decides which side of the boundary a value lands on, so a tone BETWEEN two levels renders as a
395// mixture whose average is the true tone -- which is what stops banding.
396func et_quant(lstar: i64, levels: i64, thresh_permil: i64) -> i64 {
397 if levels < ET_MIN_LEVELS { return 0 }
398 let step: i64 = ET_LSTAR_MAX / (levels - 1)
399 var idx: i64 = lstar / step
400 let rem: i64 = lstar - idx * step
401 // fraction of the way to the next level, in permil
402 let frac: i64 = (rem * 1000) / step
403 if frac > thresh_permil { idx = idx + 1 }
404 if idx < 0 { idx = 0 }
405 if idx > levels - 1 { idx = levels - 1 }
406 return idx
407}
408
409func et_selftest() -> i64 {
410 gv_head("nx_eink_tone -- is the tone ladder perceptual, and does the dither actually dither?" as *u8)
411 let ctr: *i64 = gv_ctr()
412
413 // T1 KNOWN ANSWER: mid-grey. If the table or the cube root drifts, this is the tooth that catches it.
414 let mid: i64 = et_lstar_rgb(128, 128, 128)
415 et_puts(" L*(128,128,128) = " as *u8); et_num(mid); et_puts(" x1000 (textbook 53590)\n" as *u8)
416 var midok: i64 = 0
417 if mid > ET_MAGIC_53000 { if mid < ET_MAGIC_54200 { midok = 1 } }
418 gv_check("mid-grey sRGB 128 lands on the textbook L* 53.59" as *u8, midok, ctr)
419
420 // T2 endpoints must be exact, not merely close: a reader that cannot reach true black or true white
421 // has lost contrast at both ends where it is most visible.
422 var ends: i64 = 0
423 if et_lstar_rgb(0,0,0) == 0 { if et_lstar_rgb(255,255,255) == ET_LSTAR_MAX { ends = 1 } }
424 gv_check("black is exactly L*0 and white is exactly L*100" as *u8, ends, ctr)
425
426 // T3 monotonicity across the whole ramp -- a non-monotone transfer would invert tones somewhere.
427 var mono: i64 = 1
428 var prev: i64 = 0 - 1
429 var c: i64 = 0
430 while c <= ET_BYTE_MAX {
431 let v: i64 = et_lstar_rgb(c,c,c)
432 if v < prev { mono = 0 }
433 prev = v
434 c = c + 1
435 }
436 gv_check("L* is monotone non-decreasing over all 256 sRGB greys" as *u8, mono, ctr)
437
438 // T3b BRANCH CONTINUITY. The CIE curve is continuous where its linear and cube-root branches meet
439 // (around sRGB 23/24). A scale error in either branch leaves a cliff there. This is the tooth that
440 // catches a defect monotonicity cannot: a 101x error in the linear branch preserves ordering perfectly
441 // and still crushes every shadow to black.
442 let lo_b: i64 = et_lstar_rgb(23,23,23)
443 let hi_b: i64 = et_lstar_rgb(24,24,24)
444 var jump: i64 = hi_b - lo_b
445 if jump < 0 { jump = 0 - jump }
446 et_puts(" branch seam: L*(23)=" as *u8); et_num(lo_b)
447 et_puts(" L*(24)=" as *u8); et_num(hi_b); et_puts(" gap=" as *u8); et_num(jump); et_puts("\n" as *u8)
448 var seam_ok: i64 = 0
449 if jump < ET_MAGIC_1500 { seam_ok = 1 }
450 gv_check("the two CIE branches meet continuously (no shadow cliff)" as *u8, seam_ok, ctr)
451
452 // T4 THE PERCEPTUAL CLAIM, MEASURED AGAINST THE NAIVE ALTERNATIVE. Our ladder has equal L* steps by
453 // construction, which on its own proves nothing -- the question is whether that is worth doing. So
454 // measure the ladder a naive implementation would build (16 evenly-spaced sRGB values) and report how
455 // uneven ITS perceptual steps are. That unevenness is the visible banding this organ exists to remove.
456 let lv: i64 = ET_EINK_LEVELS
457 let step: i64 = ET_LSTAR_MAX / (lv - 1)
458 var naive_min: i64 = 0 - 1
459 var naive_max: i64 = 0
460 var i2: i64 = 1
461 while i2 < lv {
462 let cprev: i64 = ((i2 - 1) * ET_BYTE_MAX) / (lv - 1)
463 let ccur: i64 = (i2 * ET_BYTE_MAX) / (lv - 1)
464 let d: i64 = et_lstar_rgb(ccur,ccur,ccur) - et_lstar_rgb(cprev,cprev,cprev)
465 if naive_min < 0 { naive_min = d }
466 if d < naive_min { naive_min = d }
467 if d > naive_max { naive_max = d }
468 i2 = i2 + 1
469 }
470 // Measure OUR ladder as REALISED, not as intended: the L* targets get rounded to 8-bit sRGB bytes, and
471 // that rounding is real error. Comparing intent against the naive ladder would be tautological; comparing
472 // the rounded output is a genuine measurement that can fail.
473 var ours_min: i64 = 0 - 1
474 var ours_max: i64 = 0
475 var i3: i64 = 1
476 while i3 < lv {
477 let bprev: i64 = et_srgb_from_lstar((i3 - 1) * step)
478 let bcur: i64 = et_srgb_from_lstar(i3 * step)
479 let d: i64 = et_lstar_rgb(bcur,bcur,bcur) - et_lstar_rgb(bprev,bprev,bprev)
480 if ours_min < 0 { ours_min = d }
481 if d < ours_min { ours_min = d }
482 if d > ours_max { ours_max = d }
483 i3 = i3 + 1
484 }
485 let naive_spread: i64 = naive_max - naive_min
486 let ours_spread: i64 = ours_max - ours_min
487 et_puts(" naive sRGB-spaced ladder: L* steps " as *u8); et_num(naive_min)
488 et_puts(".." as *u8); et_num(naive_max); et_puts(" spread=" as *u8); et_num(naive_spread)
489 et_puts("\n ours, AS REALISED in 8-bit: L* steps " as *u8); et_num(ours_min)
490 et_puts(".." as *u8); et_num(ours_max); et_puts(" spread=" as *u8); et_num(ours_spread)
491 et_puts(" (ideal " as *u8); et_num(step); et_puts(")\n" as *u8)
492 // No invented threshold: the claim is simply that our realised ladder is MORE perceptually even than the
493 // naive one. Earlier revisions of this tooth asserted ">2x" twice and were wrong twice -- the measured
494 // unevenness is 1.60x, not 2x. A bar nobody measured is not a conservative bar, it is a made-up one.
495 var perceptual: i64 = 0
496 if ours_spread < naive_spread { perceptual = 1 }
497 gv_check("our realised ladder is perceptually MORE even than the naive sRGB ladder" as *u8, perceptual, ctr)
498
499 // T5 ANTI-VACUITY: a tone lying BETWEEN two levels must render as BOTH of them. A quantizer that just
500 // rounds passes every test above and still bands visibly -- only this tooth refutes it.
501 // Half-way between level 0 and level 1 at 16 levels, swept across a full mask range.
502 let half: i64 = step / 2
503 var lo_hits: i64 = 0
504 var hi_hits: i64 = 0
505 var t: i64 = 0
506 while t < 1000 {
507 let q: i64 = et_quant(half, lv, t)
508 if q == 0 { lo_hits = lo_hits + 1 }
509 if q == 1 { hi_hits = hi_hits + 1 }
510 t = t + 1
511 }
512 et_puts(" mid-step tone over a full threshold sweep: level0 x" as *u8); et_num(lo_hits)
513 et_puts(", level1 x" as *u8); et_num(hi_hits); et_puts("\n" as *u8)
514 var mixes: i64 = 0
515 if lo_hits > 0 { if hi_hits > 0 { mixes = 1 } }
516 gv_check("a tone between levels renders as BOTH neighbours, not one (no banding)" as *u8, mixes, ctr)
517
518 // T6 the mixture must be BALANCED for a half-step tone, or the dither shifts the picture's tone.
519 // Binding to the count, not just the presence, is what makes this a measurement.
520 var balanced: i64 = 0
521 var diff: i64 = lo_hits - hi_hits
522 if diff < 0 { diff = 0 - diff }
523 if diff < 120 { balanced = 1 }
524 gv_check("the mixture is balanced within 12pct, so dithering does not shift tone" as *u8, balanced, ctr)
525
526 // T7 neg-control-constant-threshold: with a CONSTANT threshold (what you get if the mask is dead or
527 // never wired), the same tone must collapse to ONE level. If this ever mixes, T5 is proving nothing.
528 var lo2: i64 = 0
529 var hi2: i64 = 0
530 var k: i64 = 0
531 while k < 1000 {
532 let q: i64 = et_quant(half, lv, 500)
533 if q == 0 { lo2 = lo2 + 1 }
534 if q == 1 { hi2 = hi2 + 1 }
535 k = k + 1
536 }
537 var collapses: i64 = 0
538 if lo2 == 0 { collapses = 1 }
539 if hi2 == 0 { collapses = 1 }
540 gv_check("neg-control-constant-threshold: a dead mask collapses to a single level (T5 is real)" as *u8, collapses, ctr)
541
542 return gv_verdict("EINK-TONE" as *u8, ctr, "perceptual L* ladder with a dither that measurably mixes and does not shift tone" as *u8)
543}
544
545func main(argc: i64, argv: *i64) -> i64 {
546 if argc < 2 {
547 et_puts("usage: nx_eink_tone lstar <r> <g> <b> | ramp <levels> | selftest\n" as *u8)
548 sys_exit(2); return 2
549 }
550 let verb: *u8 = argv[1] as *u8
551 if et_seq(verb, "selftest" as *u8) == 1 { let rc: i64 = et_selftest(); sys_exit(rc); return rc }
552 if et_seq(verb, "lstar" as *u8) == 1 {
553 if argc < 5 { et_puts("usage: nx_eink_tone lstar <r> <g> <b>\n" as *u8); sys_exit(2); return 2 }
554 let v: i64 = et_lstar_rgb(et_atoi(argv[2] as *u8), et_atoi(argv[3] as *u8), et_atoi(argv[4] as *u8))
555 et_puts("LSTAR x1000 = " as *u8); et_num(v); et_puts("\n" as *u8)
556 sys_exit(0); return 0
557 }
558 if et_seq(verb, "ramp" as *u8) == 1 {
559 if argc < 3 { et_puts("usage: nx_eink_tone ramp <levels>\n" as *u8); sys_exit(2); return 2 }
560 var lv: i64 = et_atoi(argv[2] as *u8)
561 if lv < ET_MIN_LEVELS { lv = ET_EINK_LEVELS }
562 if lv > ET_MAX_LEVELS {
563 et_puts("REFUSED: levels " as *u8); et_num(lv); et_puts(" exceeds " as *u8); et_num(ET_MAX_LEVELS); et_puts("\n" as *u8)
564 sys_exit(3); return 3
565 }
566 let step: i64 = ET_LSTAR_MAX / (lv - 1)
567 et_puts("EINK RAMP levels=" as *u8); et_num(lv); et_puts(" (L*x1000 -> sRGB byte)\n" as *u8)
568 var i: i64 = 0
569 while i < lv {
570 let l: i64 = i * step
571 et_puts(" " as *u8); et_num(l); et_puts(" -> " as *u8); et_num(et_srgb_from_lstar(l)); et_puts("\n" as *u8)
572 i = i + 1
573 }
574 sys_exit(0); return 0
575 }
576 if et_seq(verb, "dither" as *u8) == 1 {
577 // THE COMPOSITION STEP. Takes an 8-bit greyscale buffer (what a tile decoder hands over), the
578 // blue-noise mask nx_bluenoise emits, and produces the e-ink-quantized buffer. This verb was
579 // ADVERTISED in this organ's header while not existing -- it fell through to the usage line, which is
580 // a capability claim with nothing behind it. Implemented rather than deleted, because it is the piece
581 // that makes the tone ladder and the mask usable on a real page instead of on a single colour.
582 if argc < 8 {
583 et_puts("usage: nx_eink_tone dither <in.raw> <w> <h> <levels> <mask.raw> <masksize> <out.raw>\n" as *u8)
584 sys_exit(2); return 2
585 }
586 let w: i64 = et_atoi(argv[3] as *u8)
587 let h: i64 = et_atoi(argv[4] as *u8)
588 var lv: i64 = et_atoi(argv[5] as *u8)
589 if lv < ET_MIN_LEVELS { lv = ET_EINK_LEVELS }
590 if lv > ET_MAX_LEVELS { et_puts("REFUSED: levels over cap\n" as *u8); sys_exit(3); return 3 }
591 let ms: i64 = et_atoi(argv[7] as *u8)
592 if ms <= 0 { et_puts("REFUSED: masksize must be > 0\n" as *u8); sys_exit(3); return 3 }
593 let n: i64 = w * h
594 if n <= 0 { et_puts("REFUSED: w*h must be > 0\n" as *u8); sys_exit(3); return 3 }
595 let szp: *i64 = sys_mmap(16) as *i64
596 let img: *u8 = sys_read_file(argv[2] as *u8, szp)
597 if (img as i64) == 0 { et_puts("DITHER FAILED: cannot read input\n" as *u8); sys_exit(4); return 4 }
598 // REFUSE a short buffer rather than dithering whatever happens to follow it in memory.
599 if szp[0] < n {
600 et_puts("REFUSED: input holds " as *u8); et_num(szp[0])
601 et_puts(" bytes but w*h says " as *u8); et_num(n); et_puts("\n" as *u8)
602 sys_exit(3); return 3
603 }
604 let mszp: *i64 = sys_mmap(16) as *i64
605 let mask: *u8 = sys_read_file(argv[6] as *u8, mszp)
606 if (mask as i64) == 0 { et_puts("DITHER FAILED: cannot read mask\n" as *u8); sys_exit(4); return 4 }
607 if mszp[0] < ms * ms {
608 et_puts("REFUSED: mask holds " as *u8); et_num(mszp[0])
609 et_puts(" bytes but masksize says " as *u8); et_num(ms * ms); et_puts("\n" as *u8)
610 sys_exit(3); return 3
611 }
612 let out: *u8 = sys_mmap(n)
613 let hist: *i64 = sys_mmap(8 * (ET_MAX_LEVELS + 1)) as *i64
614 var z: i64 = 0
615 while z <= lv { hist[z] = 0; z = z + 1 }
616 var y: i64 = 0
617 while y < h {
618 var x: i64 = 0
619 while x < w {
620 let idx: i64 = y * w + x
621 let g: i64 = img[idx] as i64
622 let l: i64 = et_lstar_from_y(et_srgb_lin(g))
623 // the mask TILES across the image -- that is why nx_bluenoise builds it toroidally
624 let mv: i64 = mask[(y % ms) * ms + (x % ms)] as i64
625 let thr: i64 = (mv * 1000) / ET_BYTE_MAX_P1
626 let q: i64 = et_quant(l, lv, thr)
627 if q <= lv { hist[q] = hist[q] + 1 }
628 out[idx] = et_srgb_from_lstar((q * ET_LSTAR_MAX) / (lv - 1)) as u8
629 x = x + 1
630 }
631 y = y + 1
632 }
633 let fd: i64 = sys_openat_wr(argv[8] as *u8, 0x1a4)
634 if fd < 0 { et_puts("DITHER FAILED: cannot write output\n" as *u8); sys_exit(4); return 4 }
635 sys_write(fd, out, n)
636 sys_close(fd)
637 // PRINT THE HISTOGRAM, not just "ok": a quantizer that collapsed every pixel onto one level would
638 // otherwise report success identically to one that dithered correctly.
639 var used: i64 = 0
640 var k: i64 = 0
641 while k < lv { if hist[k] > 0 { used = used + 1 } k = k + 1 }
642 et_puts("EINK DITHER OK px=" as *u8); et_num(n)
643 et_puts(" levels=" as *u8); et_num(lv)
644 et_puts(" levels_used=" as *u8); et_num(used)
645 et_puts(" out=" as *u8); et_puts(argv[8] as *u8); et_puts("\n" as *u8)
646 sys_exit(0); return 0
647 }
648 if et_seq(verb, "contrast" as *u8) == 1 {
649 if argc < 8 { et_puts("usage: nx_eink_tone contrast <ink_r> <ink_g> <ink_b> <bg_r> <bg_g> <bg_b>\n" as *u8); sys_exit(2); return 2 }
650 let iy: i64 = (ET_RW*et_srgb_lin(et_atoi(argv[2] as *u8)) + ET_GW*et_srgb_lin(et_atoi(argv[3] as *u8)) + ET_BW*et_srgb_lin(et_atoi(argv[4] as *u8))) / ET_WSUM
651 let by: i64 = (ET_RW*et_srgb_lin(et_atoi(argv[5] as *u8)) + ET_GW*et_srgb_lin(et_atoi(argv[6] as *u8)) + ET_BW*et_srgb_lin(et_atoi(argv[7] as *u8))) / ET_WSUM
652 var hi: i64 = iy
653 var lo: i64 = by
654 if by > iy { hi = by; lo = iy }
655 // Michelson contrast x1000. This is the figure the PLOS e-reader study reports, and the surprise in
656 // that study is the whole reason this verb exists: PAPER measured 0.90 and the LCD 0.99, yet paper
657 // caused LESS visual fatigue. Maximum contrast is not the target -- paper's is LOWER, because its
658 // black is a reflective grey rather than an emissive void.
659 var mich: i64 = 0
660 if hi + lo > 0 { mich = ((hi - lo) * 1000) / (hi + lo) }
661 // WCAG contrast ratio x100, the accessibility floor that still has to be cleared.
662 let wr: i64 = ((hi + ET_MAGIC_5000) * 100) / (lo + ET_MAGIC_5000)
663 et_puts("EINK CONTRAST michelson_x1000=" as *u8); et_num(mich)
664 et_puts(" wcag_ratio_x100=" as *u8); et_num(wr)
665 et_puts(" ink_Lstar=" as *u8); et_num(et_lstar_from_y(iy))
666 et_puts(" bg_Lstar=" as *u8); et_num(et_lstar_from_y(by))
667 et_puts("\n reference: paper 900, LCD 990 (PLOS 2013, e-readers and visual fatigue)\n" as *u8)
668 if mich > 960 { et_puts(" NOTE: above 960 this is LCD-like, not paper-like -- raise the ink or lower the ground.\n" as *u8) }
669 if wr < 450 { et_puts(" WARN: WCAG AA for body text wants 4.50; this pair is below that floor.\n" as *u8) }
670 sys_exit(0); return 0
671 }
672 et_puts("usage: nx_eink_tone lstar <r> <g> <b> | ramp <levels> | contrast <ink rgb> <bg rgb> | selftest\n" as *u8)
673 sys_exit(2); return 2
674}