From bf4d4b78cb09d8e201356de69302c67841e9bbbb Mon Sep 17 00:00:00 2001 From: Surma Date: Tue, 12 Mar 2019 14:09:35 +0000 Subject: [PATCH] Implement sRGB color conversion (#510) * Add sRGB -> RGB conversion before resize * Add clamping for color space conversions * Clip for demultiplication as well * Fixing linear <-> srgb conversion * Update benchmark * Decouple srgb calculations * Generate lookup tables * Update src/codecs/resize/options.tsx * Defaulting on, renaming, removing redundant state --- codecs/resize/.gitignore | 1 + codecs/resize/Cargo.toml | 2 +- codecs/resize/benchmark.js | 48 +++++++++++++--------- codecs/resize/build.rs | 23 +++++++++++ codecs/resize/pkg/resize.d.ts | 3 +- codecs/resize/pkg/resize.js | 5 ++- codecs/resize/pkg/resize_bg.d.ts | 2 +- codecs/resize/pkg/resize_bg.wasm | Bin 17607 -> 19781 bytes codecs/resize/src/lib.rs | 60 +++++++++++++++++++++++----- codecs/resize/src/srgb.rs | 29 ++++++++++++++ src/codecs/resize/options.tsx | 16 ++++++-- src/codecs/resize/processor-meta.ts | 2 + src/codecs/resize/processor.ts | 2 +- 13 files changed, 157 insertions(+), 36 deletions(-) create mode 100644 codecs/resize/build.rs create mode 100644 codecs/resize/src/srgb.rs diff --git a/codecs/resize/.gitignore b/codecs/resize/.gitignore index 53f30e50..45db9b3b 100644 --- a/codecs/resize/.gitignore +++ b/codecs/resize/.gitignore @@ -3,3 +3,4 @@ target Cargo.lock bin/ pkg/README.md +lut.inc diff --git a/codecs/resize/Cargo.toml b/codecs/resize/Cargo.toml index b1f14fe6..cd47881c 100644 --- a/codecs/resize/Cargo.toml +++ b/codecs/resize/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "resize" +name = "squooshresize" version = "0.1.0" authors = ["Surma "] diff --git a/codecs/resize/benchmark.js b/codecs/resize/benchmark.js index 5053cbde..4182f611 100644 --- a/codecs/resize/benchmark.js +++ b/codecs/resize/benchmark.js @@ -4,7 +4,7 @@ // [1]: https://github.com/GoogleChromeLabs/jsvu self = global = this; -load('./pkg/resize.js'); +load("./pkg/resize.js"); async function init() { // Adjustable constants. @@ -19,23 +19,35 @@ async function init() { const module = await WebAssembly.compile(readbuffer("./pkg/resize_bg.wasm")); await wasm_bindgen(module); - [false, true].forEach(premulti => { - print(`\npremultiplication: ${premulti}`); - print(`==============================`); - for (let i = 0; i < 100; i++) { - const start = Date.now(); - wasm_bindgen.resize(imageBuffer, inputDimensions, inputDimensions, outputDimensions, outputDimensions, algorithm, premulti); - iterations[i] = Date.now() - start; + [[false, false], [true, false], [false, true], [true, true]].forEach( + opts => { + print(`\npremultiplication: ${opts[0]}`); + print(`color space conversion: ${opts[1]}`); + print(`==============================`); + for (let i = 0; i < 100; i++) { + const start = Date.now(); + wasm_bindgen.resize( + imageBuffer, + inputDimensions, + inputDimensions, + outputDimensions, + outputDimensions, + algorithm, + ...opts + ); + iterations[i] = Date.now() - start; + } + const average = + iterations.reduce((sum, c) => sum + c) / iterations.length; + const stddev = Math.sqrt( + iterations + .map(i => Math.pow(i - average, 2)) + .reduce((sum, c) => sum + c) / iterations.length + ); + print(`n = ${iterations.length}`); + print(`Average: ${average}`); + print(`StdDev: ${stddev}`); } - const average = iterations.reduce((sum, c) => sum + c) / iterations.length; - const stddev = Math.sqrt( - iterations - .map(i => Math.pow(i - average, 2)) - .reduce((sum, c) => sum + c) / iterations.length - ); - print(`n = ${iterations.length}`); - print(`Average: ${average}`); - print(`StdDev: ${stddev}`); - }); + ); } init().catch(e => console.error(e, e.stack)); diff --git a/codecs/resize/build.rs b/codecs/resize/build.rs new file mode 100644 index 00000000..8feb48a8 --- /dev/null +++ b/codecs/resize/build.rs @@ -0,0 +1,23 @@ +include!("./src/srgb.rs"); + +use std::io::Write; + +fn main() -> std::io::Result<()> { + let mut srgb_to_linear_lut = String::from("static SRGB_TO_LINEAR_LUT: [f32; 256] = ["); + let mut linear_to_srgb_lut = String::from("static LINEAR_TO_SRGB_LUT: [f32; 256] = ["); + for i in 0..256 { + srgb_to_linear_lut.push_str(&format!("{0:.7}", srgb_to_linear((i as f32) / 255.0))); + srgb_to_linear_lut.push_str(","); + linear_to_srgb_lut.push_str(&format!("{0:.7}", linear_to_srgb((i as f32) / 255.0))); + linear_to_srgb_lut.push_str(","); + } + srgb_to_linear_lut.pop().unwrap(); + linear_to_srgb_lut.pop().unwrap(); + srgb_to_linear_lut.push_str("];"); + linear_to_srgb_lut.push_str("];"); + + let mut file = std::fs::File::create("src/lut.inc")?; + file.write_all(srgb_to_linear_lut.as_bytes())?; + file.write_all(linear_to_srgb_lut.as_bytes())?; + Ok(()) +} diff --git a/codecs/resize/pkg/resize.d.ts b/codecs/resize/pkg/resize.d.ts index 9e5b957e..b0cc0a7b 100644 --- a/codecs/resize/pkg/resize.d.ts +++ b/codecs/resize/pkg/resize.d.ts @@ -7,6 +7,7 @@ * @param {number} arg4 * @param {number} arg5 * @param {boolean} arg6 +* @param {boolean} arg7 * @returns {Uint8Array} */ -export function resize(arg0: Uint8Array, arg1: number, arg2: number, arg3: number, arg4: number, arg5: number, arg6: boolean): Uint8Array; +export function resize(arg0: Uint8Array, arg1: number, arg2: number, arg3: number, arg4: number, arg5: number, arg6: boolean, arg7: boolean): Uint8Array; diff --git a/codecs/resize/pkg/resize.js b/codecs/resize/pkg/resize.js index 9dffc88b..656cee33 100644 --- a/codecs/resize/pkg/resize.js +++ b/codecs/resize/pkg/resize.js @@ -47,13 +47,14 @@ * @param {number} arg4 * @param {number} arg5 * @param {boolean} arg6 + * @param {boolean} arg7 * @returns {Uint8Array} */ - __exports.resize = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { + __exports.resize = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { const ptr0 = passArray8ToWasm(arg0); const len0 = WASM_VECTOR_LEN; const retptr = globalArgumentPtr(); - wasm.resize(retptr, ptr0, len0, arg1, arg2, arg3, arg4, arg5, arg6); + wasm.resize(retptr, ptr0, len0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); const mem = getUint32Memory(); const rustptr = mem[retptr / 4]; const rustlen = mem[retptr / 4 + 1]; diff --git a/codecs/resize/pkg/resize_bg.d.ts b/codecs/resize/pkg/resize_bg.d.ts index 70e7200b..d8049a99 100644 --- a/codecs/resize/pkg/resize_bg.d.ts +++ b/codecs/resize/pkg/resize_bg.d.ts @@ -3,4 +3,4 @@ export const memory: WebAssembly.Memory; export function __wbindgen_global_argument_ptr(): number; export function __wbindgen_malloc(a: number): number; export function __wbindgen_free(a: number, b: number): void; -export function resize(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number): void; +export function resize(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number): void; diff --git a/codecs/resize/pkg/resize_bg.wasm b/codecs/resize/pkg/resize_bg.wasm index 54a6472fa57f87732f332d99d687206de406935b..dadb009eb0aa4fb0d2d0f1168e2ecfe532f83f4b 100644 GIT binary patch delta 6469 zcmcIoeQX@Zb)T8tyW`#7l1oYaro^|)FNvfieuzivi@fEDq$rA#Y*DU&MwV!bu5+Se zlcv?wki4{E#7QJO(d{Wv0vC;22X$mXNJ|tI5S3QZv^X0xMDR`$Ut5^qb>$A zIHLmY%ZoC&vSVkE2p&9wfsBQ(abmJ`!={a@w6shru~8;fLM4SLO(c@>NF-_}5~dNc zW5P6IC2{aE5_VE1qb1UcNXxPeyv*H@hGUhNM^+y*BA=BlEfGVC@@O1FUHM`66Qkv) zPyhB4pZ(m~-8Gb=XP=oCRJIQG%m^88m%^7;b5~<)T~Eo@ zUDeyh+uA!0r~RQLrNbi=PUgVTV~M6mHf(%!?D(O?y+t{>^c(WuWum#3OkXYdmtPH! z8GGa#OP7q76`InhjLEl`9J|p7u2mYNf$^eou~GPo@|>4w5|kj7QfCUyXVA!$I!Ot? zI*ZR8k#)*cdi6&!-#lt#$yQbGTHI8-Ns3d+yk`&Fl1wsYz0zS@`N}t_q|Pn#O+P~M zI@j@qTkh9OcZ09ojlSV-@=dn_z)HMTNyBn>hs#c?CgFEx{Ss!~#O+okQi?@Pgm!sP zknk557w;P>br1;mzH}Rt2a*v8;@=j#1Qo zN^0nx53c`m>4WPZ4D>M*CcbDEh<|vgK-{#TL2*SSAjLQ@1E+}2T`fxy-B`!K1OukX z&)}yrP%J(4U@BkzvSw=e(q%CFC2diZBC~ECrkx;520ZN5Qgqg<_oY+LF4*L5=EiZi z0Ow}{I21t-7I93pa3TB>U4v{@XWRx}@fnx6IqACGeBSlAS#TS+mZu?ni-)P;`Mqh91Fr#a6k*GtS|Nmto2pa zm1tzvt9BJhY96zN1&rzp#NeE(%+K`l5 z3o}}n;!NwPZICf+ClGU6wdve#pwtG6?FhdP*yKZzb+-=*_fbFT?(iLVr+>liq@>#g zyl&uCA-sg;?1y*>@b}*n*_7(U!utrgXdw=MiY_>ENC2`xi6Ns;VJG=-Yky)hp$~VY z)aP}#6TN43@9!Cy)?GL*rJe#J{N>-qLT9t)xt`_O|4*Lp6?ukN>M8@ODm8^vhG9{X z%Cl}YRnms6TZ2(0ZCs#D^ImOM?_GASMdfg=CnszZVZpuaaI;e$749S^YbWdooIFXD z3@$5=6`}wHJw=&vMk>y}fETS+xkIu_V5*k2_#BKn7I8~q+Vwk(Pa+Z+|cyhoA= zi;mHF)~!P99HYYvG%@d0XWd#xhFq<5s3@0#el?4CR0Bv0Sj*z{mzN0IT^=vWG!Efi zH4Skmv|xwmNY*`sWoV94m3xe;sBX-TBq7D^*KP|ST27TH4r=4$5 zTNR$F>|`OlwQm$DARs6}B@0I^<#YWQD_+bgp&gE(j#1==fNI@d?D*6u%pSK`3qfNnPIQ(I zWT6Z&(*<#cMzG`Ssd^0JjM4tAJ4T~CS4*P>aWb?Q;%Eb~L<&(-NRb@1Bv(aJS@en$ z1t0)s11t+f8HFeZS(KRNU!ZEI0#F#hsnl&fx2tewz~1Ii6^PwomS~uvoF~D0AGIE} zrN1Iwsn!GmYj2WHJZ=l1N(v_Fu~W7H!lYw)lNlivkDelKg*Y=;Kmr$<{I-j43+>C2 zB;=H!&yMA?O_M@&=zXbQ6>wL23pj5YU8*<+Ah`w z=)e)2_S$$^n|2-Sv{T!uhoVJBKjX-1qk7GV4Z@bl&_LGB=+oi=#Pkj@x}dsYY*ADa zCO990*~XE$4+q#S6G+!bsKq^siDSHb*sRK}(x%0!&I+Io=;G+__$=Krrx$Fo+Z3I9R4}D)>*q(m}>Op$+an zh67@VwzwHAJHR3&l?B&q<}+^#ZRP^Z#Gwd~#~{x)7vRGt)G{1w04v_o9XzM}3v5E_ zC@u=$tgS3-z+0SmTVi`IX#RV;{Kgw~6LZk%0J_>s8f=`!fPF%p2WyyY4G3ZB@U zXF`$(cxn|I(N$Y)9LT2s5(>B&*N{pV46q*LkvWK<+$iQj=tt%_BF)sR;6vU8JX47i zyz2ZpzhTaco%c#8wm^Y7itVGQ#FumD+$aYHaAExQzg0>jnm>xM`!TGdf>g$c9YdF; z0xKL-+RNY+9I7j`xJ6Y%un@XfdJp)r&|L)YKtXtsFSE3jNL&S;^2I@L5?9a{m)tm? zSVeA&=Db!a1|kK!qQ+x%1C?okvAn#>yFfC273H-7oH|g_xG-28q@%tZu>(m_WIAvQ zJ$%4krMX^YF0Wk3&U0YIFM|SOvp#W(uVOO&ciqG%jD_{uM|Vx$j}iag%F2Hl$WdAb z*e8)WGxYm*zVIyzK#}Q;=WkJ@z}6h|%sMa1el+I1qLJs3(xZOW^ZviS=)V+@S(kOA z@RW%qAAj-Zg0pqR|A&A1mEvqDU6Z3C;=jsHnZO+MKa9kX$o%!kV{`e!S6e!Oo)`GM zfPNrpL3Wg)5&xA}QEqV>g*%qLBxwEO3q^<>dZh)^+LX*)^~&;I{Fc9XW^S1+2xoc@ zrqk&hY$bA3Mx}r%U!M0DKU`Uf%&}<~)jT*=EMx;7sqC^>o)3YBQi<#^%!hd|nP*$) znP#5J7Jiwb|2RI7&z$I<9mwS~kw3V^%{$kxbMvQ9|A?D!e7$Bcm(O{2ZdIzhR|?7? z6DOMl3wJw54%pTGG~Ktet!WHm3sdF{Mso+HNR?QX&(78AGt zmr7p!JQ;J#p=af8;d-d`4bt2o7ONJtlrJKRky+-u;Iue$+2!STd)aAhf-qCf@SK-~ zH38GVJQqEMU*_<4yL$4O(vN=pCkVq6OJD9sGi9Z|i{_o{ZHUd#V~?==Z|AOZNKz0F z0b~aG92oFX0phn%F6sS$2u4C0mj_{d4qOrBU3~@w8UveJVgBV83nOT-QdE2u7ljH= zFtv;f%ZR}dPIk;DSs*n26Txd=3J!sT>HiQHN(D4fdKvkfO}L888ATznpvpnRR9TR! z1vc<~BfFf>Fa1v4_ad@49B{qJ9$fkMhBNN_vOMhb?v(EIN>Me%aL?iY0>aIWF8~w; z2sKAUgTB=`B~xLn>6fxT{I901M!ju?D@_gIXmbT1r}o$JBjNA1Pva{)DiT~;vi+QjCF*D}O5c7RNzB8Ame;=(PV8(L zTD*jt;wBgQnG1^~$}z2?FSe?;UQee~2|EYaC0U&U{cA;h76I4ylzJFS97fWbB4n_V}$_IZZQ^&2WKgT)7F{C5C7k&7UHO7UL6zdksIodLLJFkI{&kw!Ss zJ_wfh)sh)Nj4PWcY;QM~hguE9v>}rjD@inm8BElc{`0@@zqIWMdB)qk2NZt%r zc5Mr`2?PKi_z~wvE+!9Wb zqQal9h16tuQ5y-#1i8JKIPMZR1ceC_K0wzy8y z9VB@PX%(V|&-5q5)BW{$+k$Hx#Wlj(pcP`VSTMe9SV`T-4a{&nD^VOc-T0H5L@x|4 zo!A=Kw&DP5vDWqQF9_rH1JC~^?+$Q^0p=iv^<1}fl05kx76V?02q(4rjWS}(Lm-|G zTXuIhz6X5E5W=#>(BJ&gn;$QJ{gq#T|KU;i((bD8E4!n6t?yMDd}w0D-T2P==bu}c z-uYyA_u1*G&Z({^W}e*DwX18UueYZ=H9a*oJ>4^Xws-fFJ!gBLnCV~oyWPK5t^Y^l z@aOwlsyTddl;IWPAOH6KUT($hiovCt{f|rH`>}P`I3^tN2;k?X5EW=GM#3Y*hQZcw zdEn&mMvTnjy9q60RpPC}%MTU`@Deq6Yw_0M<>w)I_?v@u`|ANM!OPS(7vC*t8;UT7 uQ}K1tdU*L^N{!*tV7uL<#|21p_-}*Lax6TWu8C~(hR@%?W9Gv36zzT0tjZ6~pJY_BhI+&E2(f@qtI^+M8+c3KhfR8<8^fvU(46!8+Eg%$}ZNK}L;(g#G5Kop5e6{-r~Kf8{jLXnV| z)y%p5m+$slx#bPXc3a4agLG7^%JNJPWSVl}BHBBmMIdi0PyE8E+j z{?p!1PgET}`o$Bc??3t3)%%a$cYJN_b1R~)@|L+%>#M7xE4;pX?({>eLdH6!aHY}K z9hvS-?wHDsj^)k0`(`S~_sq`4Xuhg?A-CU+|McwQ=Hv3OGHy4M?y3#(ZDJVuNqmGrDyU@stIzlnG(ZlDu z@T>}z+xk(&)eo9rNvT#l&P}6jP&98!(n(u$5_wa(!ZuxLSGdZqbT!+W7NRh1A9QuQ z%1sEn8tXL)&9DajkE~2n%pLUHD62|xn^Gd97*fn%s@heExy54fLoK7`L2%>At5}># zgbWO>iZWZkzRi}_2Au<0Ek_5wZL37saHFnn^*SnVhDl9*_wAS8-+cS!wLFmWCECgEPY0Gbwe< zZe@^5$W*zMdrXm@b4VG}oJZJQK#W*iA?R)vJhQ2|}uX~@IliW&%`sFgOTkuf=L z;eaVQFvwv72k?5G&Qbsp1D5P$LK+AT z2(mlTCJidYD4tQvuuLuKYYxnF2Y|%qOdzFXX(a$tUP;cG8fkemj@a8lY&v@PL!?w%6%lAEx5T+8ltAGZ6*u=_zb0J<84moTgmz>7h@drf#5H3-HJ5pYn$ zaq#EuE@n0;Hl<}1GRM9ARSqH~5BhLNMm-pG2hlql^nSyWxu6TjWz@YOgumREz-ToG zI&W2Za$C{=U*~IOo&EzowFQ8xsnCT~nr4terf1huHC1?a9Y)nuxk1*tQ||@Y%g&9H z3HLg3$<)!WUot~*49H6XQOj$pPl!#b=I^s~@HuB02SM@R-NdaB#~v^ROx^&W)ADx; zc7wqo#?gXYK!jL9gqT$2*(TN5)l^4`1v3H`m8e0E6J7{NZP*p7H;8@c)K=KER%* z1<%gYes1!_ zAOOn77z?0G0m?K(i5Tv~)L_+O76!2Dg0_jYi1HjG%JD3pyHK5_gy4fZ9Y|99*~t%5?=ga0H*IUCh=MxDIye z4eT^X;j*IZ99~`26ewbYuq7sF%(Ewg+h-Cmok=Da)R;#yqa4wpIS;eV;v$$UAZ6xD zRmfr70t?h>@5lNgXA)ai*)@TMQ7rI2P+s(N5cSgl2hjj&2!`Oq5g^@7VZK%}5Re7= zD6Wt(7@n_{aa=36LBSqeEqhD)4)Ix+687!5W%f~vJpr~!R+vzR%26|4oGsK0iKsc! z{#(sdmkw@Q_hB{;rMFf-YEEX-%+kust**X-WN3Gzh;?LHjufb!PhzdVw~e{lsSSLr zhy}%Y%Ah!|ZCvTWb$Seqli{0;I!iTRP?!ZZAmOA*YteF9BfX@kWOJ1)cQZsck2etfp+tSx7QceXTCt^?)AoTiL_d}QpV+sZj1ydKgkIO#)FWv8J zO=iNc=rqOhW&=&Gg9}zGQ2{%jnqOV$EXRoQ6?84V z#q<_KlE#mu^k2;6?kiysj6o}7h(ZOV0!;#QI#+}Mp~3Yf5UdFpw;*UYA>2ZK3x+5V zl*ljohqCiVfbL3w&VMocHJS29MoFgqJ4buNukv(*|M=)DBfA{Utfra@jQ&zj*GLfl!71%$%((nIZ;#0_u=%gce z`aP2^gN?%I3*>SfS%#yt=L1tB+7MieEmmtY8yKqz#I66$Ns=-D=aVzlh9j96XDkWJ zZZ=5zujbnP=W?@!BB~3kK^WD7!7>%oao!t=pwD4#br|vT!0Is!baw;3w8God2_qU% zSOb`IpI~o5Y6NpuyVzPYcJ#my#t1W^Qyv5>LEpdV4)1&o42C9zVT!4@ z{`lrUu08e6x1KM2Y^3~m+~$48_v$tLv04Pfwb;P==g)1d42+JR%I@BMYV`Q%$jMVf zC&yMZCx)^sr-lYs#xloOM^B9H8W|lrF}yn9pPy;mJUsP#mHwYt66e-e2G&lW=wCmF z%Hr$!d*lq?cgDNgvarMB6;78k7ew)xX4XwfIBx jJ!We#F3q(0r{`A?b-$W#iUh{L9^L%g{2lr;3;MqRU1|-R diff --git a/codecs/resize/src/lib.rs b/codecs/resize/src/lib.rs index d3229f8a..581de2cb 100644 --- a/codecs/resize/src/lib.rs +++ b/codecs/resize/src/lib.rs @@ -9,6 +9,9 @@ use resize::Pixel::RGBA; use resize::Type; use wasm_bindgen::prelude::*; +mod srgb; +use srgb::Clamp; + cfg_if! { // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. @@ -19,6 +22,39 @@ cfg_if! { } } +include!("./lut.inc"); + +// If `with_space_conversion` is true, this function returns 2 functions that +// convert from sRGB to linear RGB and vice versa. If `with_space_conversion` is +// false, the 2 functions returned do nothing. +fn converter_funcs(with_space_conversion: bool) -> ((fn(u8) -> f32), (fn(f32) -> u8)) { + if with_space_conversion { + ( + |v| SRGB_TO_LINEAR_LUT[v as usize] * 255.0, + |v| (LINEAR_TO_SRGB_LUT[v as usize] * 255.0) as u8, + ) + } else { + (|v| v as f32, |v| v as u8) + } +} + +// If `with_alpha_premultiplication` is true, this function returns a function +// that premultiply the alpha channel with the given channel value and another +// function that reverses that process. If `with_alpha_premultiplication` is +// false, the functions just return the channel value. +fn alpha_multiplier_funcs( + with_alpha_premultiplication: bool, +) -> ((fn(f32, u8) -> u8), (fn(u8, u8) -> f32)) { + if with_alpha_premultiplication { + ( + |v, a| (v * (a as f32) / 255.0) as u8, + |v, a| (v as f32) * 255.0 / (a as f32).clamp(0.0, 255.0), + ) + } else { + (|v, _a| v as u8, |v, _a| v as f32) + } +} + #[wasm_bindgen] #[no_mangle] pub fn resize( @@ -29,6 +65,7 @@ pub fn resize( output_height: usize, typ_idx: usize, premultiply: bool, + color_space_conversion: bool, ) -> Vec { let typ = match typ_idx { 0 => Type::Triangle, @@ -40,12 +77,16 @@ pub fn resize( let num_input_pixels = input_width * input_height; let num_output_pixels = output_width * output_height; - if premultiply { + let (to_linear, to_color_space) = converter_funcs(color_space_conversion); + let (premultiplier, demultiplier) = alpha_multiplier_funcs(premultiply); + + // If both options are false, there is no preprocessing on the pixel valus + // and we can skip the loop. + if premultiply || color_space_conversion { for i in 0..num_input_pixels { for j in 0..3 { - input_image[4 * i + j] = ((input_image[4 * i + j] as f32) - * (input_image[4 * i + 3] as f32) - / 255.0) as u8; + input_image[4 * i + j] = + premultiplier(to_linear(input_image[4 * i + j]), input_image[4 * i + 3]); } } } @@ -62,15 +103,16 @@ pub fn resize( output_image.resize(num_output_pixels * 4, 0); resizer.resize(input_image.as_slice(), output_image.as_mut_slice()); - if premultiply { + if premultiply || color_space_conversion { for i in 0..num_output_pixels { for j in 0..3 { // We don’t need to worry about division by zero, as division by zero - // is well-defined on floats to return `±Inf`. ±Inf is converted to 0 + // is well-defined on floats to return ±Inf. ±Inf is converted to 0 // when casting to integers. - output_image[4 * i + j] = ((output_image[4 * i + j] as f32) * 255.0 - / (output_image[4 * i + 3] as f32)) - as u8; + output_image[4 * i + j] = to_color_space(demultiplier( + output_image[4 * i + j], + output_image[4 * i + 3], + )); } } } diff --git a/codecs/resize/src/srgb.rs b/codecs/resize/src/srgb.rs new file mode 100644 index 00000000..5781c5fd --- /dev/null +++ b/codecs/resize/src/srgb.rs @@ -0,0 +1,29 @@ +pub trait Clamp: std::cmp::PartialOrd + Sized { + fn clamp(self, min: Self, max: Self) -> Self { + if self.lt(&min) { + min + } else if self.gt(&max) { + max + } else { + self + } + } +} + +impl Clamp for f32 {} + +pub fn srgb_to_linear(v: f32) -> f32 { + if v < 0.04045 { + v / 12.92 + } else { + ((v + 0.055) / 1.055).powf(2.4).clamp(0.0, 1.0) + } +} + +pub fn linear_to_srgb(v: f32) -> f32 { + if v < 0.0031308 { + v * 12.92 + } else { + (1.055 * v.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0) + } +} diff --git a/src/codecs/resize/options.tsx b/src/codecs/resize/options.tsx index 3c6fc6e7..1b856d75 100644 --- a/src/codecs/resize/options.tsx +++ b/src/codecs/resize/options.tsx @@ -19,13 +19,11 @@ interface Props { interface State { maintainAspect: boolean; - premultiply: boolean; } export default class ResizerOptions extends Component { state: State = { maintainAspect: true, - premultiply: true, }; form?: HTMLFormElement; @@ -43,6 +41,7 @@ export default class ResizerOptions extends Component { height: inputFieldValueAsNumber(height), method: form.resizeMethod.value, premultiply: inputFieldChecked(form.premultiply, true), + linearRGB: inputFieldChecked(form.linearRGB, true), // Casting, as the formfield only returns the correct values. fitMethod: inputFieldValue(form.fitMethod, options.fitMethod) as ResizeOptions['fitMethod'], }; @@ -95,7 +94,7 @@ export default class ResizerOptions extends Component { - + @@ -138,6 +137,17 @@ export default class ResizerOptions extends Component { : null } + {isWorkerOptions(options) ? + + : null + }