clone

a photo toolkit for your phone that never uploads: fuji film looks for raw files, social crops, error level analysis

main
gudpics / rawworker.mjs
rawworker.mjs 4.8K
1// SPDX-License-Identifier: AGPL-3.0-or-later2// Develops Fujifilm RAF files with LibRaw and applies film simulation LUTs off the main thread.3import createRaw from './assets/raw.mjs';4import {parseLens,lensTables,correctLens} from './lens.mjs';5const corePromise=createRaw();6const N=33; // neutral LUT grid7let developed=null,corrected=null,full=null,preview=null,lens=null; // {w,h,rgb:Uint8Array}8let luts={neutral:{},sim:{}}; // decoded LUT tables: neutral[dr] = Uint8Array(N^3*3), sim[name] = Uint8Array(16^3*3)9let composed=new Map(); // key dr:sim -> Uint8Array(N^3*3)1011function downscale(src,w,h,factor){12 const ow=Math.floor(w/factor),oh=Math.floor(h/factor),out=new Uint8Array(ow*oh*3),n=factor*factor;13 for(let y=0;y<oh;y++)for(let x=0;x<ow;x++){14 let r=0,g=0,b=0;15 for(let dy=0;dy<factor;dy++){let i=((y*factor+dy)*w+x*factor)*3;for(let dx=0;dx<factor;dx++){r+=src[i];g+=src[i+1];b+=src[i+2];i+=3;}}16 const o=(y*ow+x)*3;out[o]=r/n;out[o+1]=g/n;out[o+2]=b/n;17 }18 return {w:ow,h:oh,rgb:out};19}20// Trilinear sample of a cubic LUT laid out [b][g][r] with `size` points per axis; input coords in [0,size-1].21function sample(lut,size,r,g,b,out,o){22 let ri=Math.floor(r),gi=Math.floor(g),bi=Math.floor(b);23 if(ri>=size-1)ri=size-2;if(gi>=size-1)gi=size-2;if(bi>=size-1)bi=size-2;24 const fr=r-ri,fg=g-gi,fb=b-bi,s2=size*size;25 const i000=((bi*size+gi)*size+ri)*3,i100=i000+3,i010=i000+size*3,i110=i010+3,i001=i000+s2*3,i101=i001+3,i011=i001+size*3,i111=i011+3;26 for(let c=0;c<3;c++){27 const c00=lut[i000+c]*(1-fr)+lut[i100+c]*fr,c10=lut[i010+c]*(1-fr)+lut[i110+c]*fr;28 const c01=lut[i001+c]*(1-fr)+lut[i101+c]*fr,c11=lut[i011+c]*(1-fr)+lut[i111+c]*fr;29 out[o+c]=(c00*(1-fg)+c10*fg)*(1-fb)+(c01*(1-fg)+c11*fg)*fb;30 }31}32function compose(dr,sim){33 const key=dr+':'+sim;if(composed.has(key))return composed.get(key);34 const neutral=luts.neutral[dr],simLut=sim==='neutral'?null:luts.sim[sim];35 if(!neutral||(sim!=='neutral'&&!simLut))throw Error('Look not loaded.');36 const table=new Uint8Array(N*N*N*3),tmp=new Float32Array(3);37 for(let i=0;i<N*N*N;i++){38 const r=neutral[i*3],g=neutral[i*3+1],b=neutral[i*3+2];39 if(simLut){sample(simLut,16,r/17,g/17,b/17,tmp,0);table[i*3]=tmp[0]+.5;table[i*3+1]=tmp[1]+.5;table[i*3+2]=tmp[2]+.5;}40 else{table[i*3]=r;table[i*3+1]=g;table[i*3+2]=b;}41 }42 composed.set(key,table);return table;43}44function render(img,table){45 const {w,h,rgb}=img,out=new Uint8ClampedArray(w*h*4),tmp=new Float32Array(3),k=(N-1)/255;46 for(let i=0,o=0;i<rgb.length;i+=3,o+=4){47 sample(table,N,rgb[i]*k,rgb[i+1]*k,rgb[i+2]*k,tmp,0);48 out[o]=tmp[0]+.5;out[o+1]=tmp[1]+.5;out[o+2]=tmp[2]+.5;out[o+3]=255;49 }50 return {w,h,data:out};51}52function select(useLens){53 full=useLens&&corrected?corrected:developed;54 const factor=Math.max(1,Math.ceil(Math.max(full.w,full.h)/1600));55 preview=factor>1?downscale(full.rgb,full.w,full.h,factor):full;56}57async function open(buffer,cropf){58 const core=await corePromise;59 const params=parseLens(buffer);60 const p=core._malloc(buffer.byteLength);if(!p)throw Error('Not enough memory for this file.');61 core.HEAPU8.set(new Uint8Array(buffer),p);62 try{63 let r=core._raw_open(p,buffer.byteLength);if(r)throw Error('LibRaw could not read this file.');64 const info={make:core.UTF8ToString(core._raw_make()),model:core.UTF8ToString(core._raw_model()),width:core._raw_width(),height:core._raw_height(),film:core._raw_film_mode(),dr:[core._raw_dev_dr(),core._raw_auto_dr()].find(v=>v===100||v===200||v===400)||100,xtrans:Boolean(core._raw_is_xtrans())};65 if(!/fuji/i.test(info.make))throw Error('Only Fujifilm RAF files are supported.');66 r=core._raw_develop(1,1,1,8);if(r)throw Error('Developing failed.');67 const w=core._raw_out_width(),h=core._raw_out_height(),d=core._raw_data();68 developed={w,h,rgb:core.HEAPU8.slice(d,d+w*h*3)};69 lens=params?lensTables(params,cropf,w/h):null;70 corrected=lens&&(lens.hasDist||lens.hasVig)?correctLens(developed,lens):null;71 info.lens=Boolean(corrected);72 select(true);73 return info;74 }finally{core._raw_free();core._free(p);}75}76self.onmessage=async({data})=>{77 try{78 let result,transfer=[];79 if(data.type==='open'){result=await open(data.buffer,data.cropf||1);}80 else if(data.type==='lens'){select(data.on);result={ok:true};}81 else if(data.type==='lut'){luts[data.kind][data.name]=data.table;composed.clear();result={ok:true};}82 else if(data.type==='render'){const img=data.full?full:preview;if(!img)throw Error('Open a RAF first.');result=render(img,compose(data.dr,data.sim));transfer=[result.data.buffer];}83 else if(data.type==='close'){developed=corrected=full=preview=null;result={ok:true};}84 else throw Error('Unknown operation.');85 self.postMessage({id:data.id,result},transfer);86 }catch(e){self.postMessage({id:data.id,error:e.message||'Could not process this file.'});}87};