clone

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

main
gudpics / geometry.mjs
geometry.mjs 2.3K
1// SPDX-License-Identifier: AGPL-3.0-or-later2export function project(m, x, y) {3 const z=m[6]*x+m[7]*y+m[8];4 if(!Number.isFinite(z)||z<=1e-6) throw Error('This correction folds the image. Reduce the strength.');5 return [(m[0]*x+m[1]*y+m[2])/z,(m[3]*x+m[4]*y+m[5])/z];6}7export function inverse(m) {8 const [a,b,c,d,e,f,g,h,i]=m;9 const det=a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g);10 if(!Number.isFinite(det)||Math.abs(det)<1e-12) throw Error('The correction is too extreme.');11 return [e*i-f*h,c*h-b*i,b*f-c*e,f*g-d*i,a*i-c*g,c*d-a*f,d*h-e*g,b*g-a*h,a*e-b*d].map(v=>v/det);12}13// Conservative scan of a convex projected image. Rows use the intersection14// at both ends, so every point of the returned rectangle is inside the polygon.15// This is a new browser crop implementation, not darktable's crop optimizer.16export function cropRect(m,w,h,mode='largest') {17 const q=[[1,1],[w-2,1],[w-2,h-2],[1,h-2]].map(p=>project(m,...p));18 const xs=q.map(p=>p[0]),ys=q.map(p=>p[1]);19 const left=Math.min(...xs),right=Math.max(...xs),top=Math.min(...ys),bottom=Math.max(...ys);20 if(![left,right,top,bottom].every(Number.isFinite)||(right-left)*(bottom-top)>w*h*5) throw Error('This correction stretches the image too far.');21 if(mode==='none') return {x:left,y:top,w:right-left,h:bottom-top};22 const interval=y=>{23 const values=[];24 for(let i=0;i<4;i++) {25 const a=q[i],b=q[(i+1)%4];26 if(Math.abs(a[1]-b[1])<1e-8) { if(Math.abs(y-a[1])<1e-6) values.push(a[0],b[0]); }27 else if(y>=Math.min(a[1],b[1])-1e-6&&y<=Math.max(a[1],b[1])+1e-6)28 values.push(a[0]+(y-a[1])*(b[0]-a[0])/(b[1]-a[1]));29 }30 return values.length?[Math.min(...values),Math.max(...values)]:[Infinity,-Infinity];31 };32 const n=256,dy=(bottom-top)/n,rows=[];33 for(let i=0;i<n;i++) {34 const a=interval(top+i*dy),b=interval(top+(i+1)*dy);35 rows.push([Math.max(a[0],b[0]),Math.min(a[1],b[1])]);36 }37 let best=null,area=0;38 for(let i=0;i<n;i++) {39 let l=-Infinity,r=Infinity;40 for(let j=i;j<n;j++) {41 l=Math.max(l,rows[j][0]); r=Math.min(r,rows[j][1]);42 if(r<=l) break;43 let cw=r-l,ch=(j-i+1)*dy;44 if(mode==='original') {cw=Math.min(cw,ch*w/h);ch=cw*h/w;}45 if(cw*ch>area) {area=cw*ch;best={x:(l+r-cw)/2,y:top+i*dy+((j-i+1)*dy-ch)/2,w:cw,h:ch};}46 }47 }48 if(!best||area<w*h*.08) throw Error('Too little image remains. Try a gentler correction.');49 return best;50}