clone

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

main
gudpics / elaworker.mjs
elaworker.mjs 1.8K
1// SPDX-License-Identifier: AGPL-3.0-or-later2// Error level analysis: re-encode the picture as JPEG at a chosen quality and amplify the difference.3let w=0,h=0,original=null,diff=null,diffQuality=-1;4async function load(bitmap){5 w=bitmap.width;h=bitmap.height;6 const c=new OffscreenCanvas(w,h),x=c.getContext('2d',{willReadFrequently:true});x.drawImage(bitmap,0,0);bitmap.close();7 original=x.getImageData(0,0,w,h).data;diff=null;diffQuality=-1;8 return {w,h};9}10async function computeDiff(quality){11 if(diffQuality===quality&&diff)return;12 const c=new OffscreenCanvas(w,h),x=c.getContext('2d',{willReadFrequently:true});13 x.putImageData(new ImageData(original,w,h),0,0);14 const blob=await c.convertToBlob({type:'image/jpeg',quality:quality/100});15 const bmp=await createImageBitmap(blob);x.drawImage(bmp,0,0);bmp.close();16 const re=x.getImageData(0,0,w,h).data,d=new Uint8Array(w*h*3);17 for(let i=0,o=0;i<re.length;i+=4,o+=3){d[o]=Math.abs(original[i]-re[i]);d[o+1]=Math.abs(original[i+1]-re[i+1]);d[o+2]=Math.abs(original[i+2]-re[i+2]);}18 diff=d;diffQuality=quality;19}20function amplify(scale){21 const out=new Uint8ClampedArray(w*h*4);22 for(let i=0,o=0;i<diff.length;i+=3,o+=4){out[o]=diff[i]*scale;out[o+1]=diff[i+1]*scale;out[o+2]=diff[i+2]*scale;out[o+3]=255;}23 return out;24}25self.onmessage=async({data})=>{26 try{27 let result,transfer=[];28 if(data.type==='load')result=await load(data.bitmap);29 else if(data.type==='render'){if(!original)throw Error('Open a photo first.');await computeDiff(data.quality);const px=amplify(data.scale);result={w,h,data:px};transfer=[px.buffer];}30 else if(data.type==='close'){original=diff=null;result={ok:true};}31 else throw Error('Unknown operation.');32 self.postMessage({id:data.id,result},transfer);33 }catch(e){self.postMessage({id:data.id,error:e.message||'Analysis failed.'});}34};