clone

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

main
gudpics / worker.mjs
worker.mjs 3.0K
1// SPDX-License-Identifier: AGPL-3.0-or-later2import createCore from './assets/core.mjs';3import {cropRect,inverse} from './geometry.mjs';4const corePromise=createCore();5let photo=null, preview=null, detected=false;6function alloc(core,data) {7 const p=core._malloc(data.byteLength);8 if(!p) throw Error('Not enough memory. Try a smaller photo.');9 core.HEAPU8.set(new Uint8Array(data.buffer,data.byteOffset,data.byteLength),p);10 return p;11}12async function handle(msg) {13 const core=await corePromise;14 if(msg.type==='init') return {ready:true};15 if(msg.type==='load') {photo=msg.photo;preview=msg.preview;detected=false;return {loaded:true};}16 if(msg.type==='detect') {17 detected=false;18 const p=alloc(core,msg.pixels);19 let n;20 try {n=core._detect(p,msg.w,msg.h);} finally {core._free(p);}21 if(n<0) throw Error('Line detection could not finish. Try a smaller photo.');22 detected=true;23 const ptr=core._lines();24 const lines=Array.from(core.HEAPF32.subarray(ptr/4,ptr/4+n*6));25 return {lines,w:msg.w,h:msg.h};26 }27 if(msg.type==='fit') {28 if(!detected) throw Error('Find the structure first.');29 const code=core._fit(msg.mode);30 if(code) throw Error(code===1?'Not enough matching lines. Try another direction or a more architectural photo.':code===2?'The fit did not settle. Try vertical or horizontal correction on its own.':'This fit would stretch the photo too far. Try another direction.');31 const p=core._parameters()/4;32 return {params:Array.from(core.HEAPF32.subarray(p,p+4))};33 }34 if(msg.type==='render') {35 const src=msg.export?photo:preview;36 if(!src) throw Error('Open a photo first.');37 const p=core._matrix(src.w,src.h,...msg.params)/4;38 const matrix=Array.from(core.HEAPF32.subarray(p+4,p+13));39 const crop=cropRect(matrix,src.w,src.h,msg.crop);40 const max=msg.export?2560:1200;41 const scale=Math.min(1,max/Math.max(crop.w,crop.h),Math.sqrt(4e6/(crop.w*crop.h)));42 const w=Math.max(2,Math.round(crop.w*scale)),h=Math.max(2,Math.round(crop.h*scale));43 const pointers=[];44 try {45 const input=alloc(core,src.pixels);pointers.push(input);46 const transform=alloc(core,new Float32Array(inverse(matrix)));pointers.push(transform);47 const output=core._malloc(w*h*4); if(!output) throw Error('Not enough memory for export.');pointers.push(output);48 core._warp(input,src.w,src.h,output,w,h,transform,crop.x,crop.y,crop.w,crop.h);49 const pixels=core.HEAPU8.slice(output,output+w*h*4);50 return {pixels,w,h,crop,matrix,sourceWidth:src.w,sourceHeight:src.h};51 } finally {pointers.forEach(p=>core._free(p));}52 }53 throw Error('Unknown operation.');54}55// Serialize requests: fitting and rendering share the same WASM module state.56let queue=Promise.resolve();57self.onmessage=({data})=>{58 queue=queue.then(async()=>{59 try {60 const result=await handle(data);61 self.postMessage({id:data.id,result},result.pixels?[result.pixels.buffer]:[]);62 } catch(error) {self.postMessage({id:data.id,error:error.message||'Could not process this photo.'});}63 });64};