code wiki / (root) / nx_eink_tone.nx

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}