feat: preview and check large PNG sheets by reading them as a stream
All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 2m33s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 1m0s
All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 2m33s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 1m0s
A large sheet had no preview and no automatic check of pieces, residue, gaps and background, because decoding it whole would crash the browser. A PNG over 150 MB is now inflated as a stream (DecompressionStream) and unfiltered row by row, keeping every n-th pixel: a 600-pixel-wide copy in about 100 MB of memory whatever the file's size (6.6 s for 500 MB, 22.7 s for 2.4 GB of random pixels, identical to Pillow's output for RGB, RGBA, grey, grey+alpha, palette and 16-bit). The copy is the preview and what the sheet check reads; the grade still comes at once from the header. The preview note promising a server-side re-nesting that does not exist is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1050,7 +1050,6 @@ footer a:hover{color:var(--laranja2)}
|
|||||||
<span id="vMt">—</span></div>
|
<span id="vMt">—</span></div>
|
||||||
<div id="vArea"></div>
|
<div id="vArea"></div>
|
||||||
<div id="vUso"></div>
|
<div id="vUso"></div>
|
||||||
<p class="montpe">Prévia. O encaixe final é refeito no servidor e nunca sai pior.</p>
|
|
||||||
<div class="compra" id="compra">
|
<div class="compra" id="compra">
|
||||||
<div class="aoVivo" id="aoVivo"></div>
|
<div class="aoVivo" id="aoVivo"></div>
|
||||||
<div id="compraVals" hidden>
|
<div id="compraVals" hidden>
|
||||||
|
|||||||
111
web/site-pdf.js
111
web/site-pdf.js
@@ -168,6 +168,117 @@ async function dimensoesImagem(file){
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// A small copy of a large PNG, read as a stream: the file is inflated in
|
||||||
|
// pieces and only every n-th pixel of every n-th row is kept, so memory stays
|
||||||
|
// at a few MB whatever the file's size. It is the preview, and what the sheet
|
||||||
|
// check (pieces, gaps, background) reads. 8- and 16-bit greyscale, RGB,
|
||||||
|
// palette and alpha; interlaced files return null.
|
||||||
|
async function previaPngGrande(file, larguraAlvo, progresso){
|
||||||
|
const leitor=file.stream().getReader();
|
||||||
|
let pedacos=[], guardado=0, lidos=0, fim=false;
|
||||||
|
// A queue of the file's pieces: take() never copies more than it returns.
|
||||||
|
const enche=async n=>{
|
||||||
|
while(guardado<n && !fim){
|
||||||
|
const {done,value}=await leitor.read();
|
||||||
|
if(done){ fim=true; break; }
|
||||||
|
pedacos.push(value); guardado+=value.length; lidos+=value.length;
|
||||||
|
progresso && progresso(lidos/file.size);
|
||||||
|
}
|
||||||
|
return guardado>=n;
|
||||||
|
};
|
||||||
|
const tira=n=>{
|
||||||
|
const out=new Uint8Array(n); let k=0;
|
||||||
|
while(k<n){ const p=pedacos[0], m=Math.min(p.length,n-k);
|
||||||
|
out.set(p.subarray(0,m),k); k+=m;
|
||||||
|
if(m===p.length) pedacos.shift(); else pedacos[0]=p.subarray(m); }
|
||||||
|
guardado-=n; return out;
|
||||||
|
};
|
||||||
|
const u32=b=>((b[0]<<24)>>>0)+(b[1]<<16)+(b[2]<<8)+b[3];
|
||||||
|
if(!await enche(8)) return null; tira(8);
|
||||||
|
// 1 · header chunks, up to the first image data
|
||||||
|
let w=0,h=0,prof=0,tipo=-1,paleta=null,trns=null, idat=0;
|
||||||
|
for(;;){
|
||||||
|
if(!await enche(8)) return null;
|
||||||
|
const cab=tira(8), len=u32(cab), nome=String.fromCharCode(...cab.subarray(4,8));
|
||||||
|
if(nome==='IDAT'){ idat=len; break; }
|
||||||
|
if(!await enche(len+4)) return null;
|
||||||
|
const d=tira(len); tira(4);
|
||||||
|
if(nome==='IHDR'){ w=u32(d); h=u32(d.subarray(4)); prof=d[8]; tipo=d[9];
|
||||||
|
if(d[12]!==0 || ![8,16].includes(prof) || !{0:1,2:1,3:1,4:1,6:1}[tipo]) return null; }
|
||||||
|
else if(nome==='PLTE') paleta=d;
|
||||||
|
else if(nome==='tRNS') trns=d;
|
||||||
|
else if(nome==='IEND') return null;
|
||||||
|
}
|
||||||
|
if(!(w>0 && h>0) || (tipo===3 && (!paleta || prof!==8))) return null;
|
||||||
|
// 2 · image data streamed into the inflater, a piece at a time
|
||||||
|
const inflador=new DecompressionStream('deflate'), escritor=inflador.writable.getWriter();
|
||||||
|
const alimenta=(async()=>{
|
||||||
|
let resta=idat;
|
||||||
|
for(;;){
|
||||||
|
while(resta>0){
|
||||||
|
if(!guardado && !await enche(1)) { await escritor.close(); return; }
|
||||||
|
const n=Math.min(resta,guardado); await escritor.write(tira(n)); resta-=n;
|
||||||
|
}
|
||||||
|
if(!await enche(12)) break;
|
||||||
|
tira(4); // CRC of the chunk just sent
|
||||||
|
const cab=tira(8), len=u32(cab), nome=String.fromCharCode(...cab.subarray(4,8));
|
||||||
|
if(nome==='IDAT'){ resta=len; continue; }
|
||||||
|
break; // anything after the image data
|
||||||
|
}
|
||||||
|
await escritor.close();
|
||||||
|
})();
|
||||||
|
alimenta.catch(()=>{});
|
||||||
|
// 3 · rows unfiltered one at a time; every passo-th pixel is kept
|
||||||
|
const canais={0:1,2:3,3:1,4:2,6:4}[tipo], b=prof/8, bpp=canais*b, linha=w*bpp;
|
||||||
|
const passo=Math.max(1,Math.ceil(w/larguraAlvo));
|
||||||
|
const cw=Math.ceil(w/passo), ch=Math.ceil(h/passo);
|
||||||
|
const cv=document.createElement('canvas'); cv.width=cw; cv.height=ch;
|
||||||
|
const ctx=cv.getContext('2d'), img=ctx.createImageData(cw,1), px=img.data;
|
||||||
|
// Each row is gathered whole (filter byte + data), then unfiltered in one
|
||||||
|
// tight loop for its filter type.
|
||||||
|
let ant=new Uint8Array(linha), atual=new Uint8Array(linha), y=0;
|
||||||
|
const bruta=new Uint8Array(linha+1); let cheio=0;
|
||||||
|
const desfiltra=()=>{
|
||||||
|
const f=bruta[0], d=bruta.subarray(1);
|
||||||
|
if(f===0) atual.set(d);
|
||||||
|
else if(f===1){ for(let i=0;i<bpp;i++) atual[i]=d[i]; for(let i=bpp;i<linha;i++) atual[i]=d[i]+atual[i-bpp]; }
|
||||||
|
else if(f===2){ for(let i=0;i<linha;i++) atual[i]=d[i]+ant[i]; }
|
||||||
|
else if(f===3){ for(let i=0;i<bpp;i++) atual[i]=d[i]+(ant[i]>>1);
|
||||||
|
for(let i=bpp;i<linha;i++) atual[i]=d[i]+((atual[i-bpp]+ant[i])>>1); }
|
||||||
|
else { for(let i=0;i<bpp;i++) atual[i]=d[i]+ant[i];
|
||||||
|
for(let i=bpp;i<linha;i++){ const a=atual[i-bpp], up=ant[i], c=ant[i-bpp], p=a+up-c;
|
||||||
|
const pa=p>a?p-a:a-p, pb=p>up?p-up:up-p, pc=p>c?p-c:c-p;
|
||||||
|
atual[i]=d[i]+(pa<=pb&&pa<=pc?a:pb<=pc?up:c); } }
|
||||||
|
};
|
||||||
|
const leitorInflado=inflador.readable.getReader();
|
||||||
|
for(;;){
|
||||||
|
const {done,value}=await leitorInflado.read(); if(done) break;
|
||||||
|
let j=0;
|
||||||
|
while(j<value.length){
|
||||||
|
const n=Math.min(linha+1-cheio, value.length-j);
|
||||||
|
bruta.set(value.subarray(j,j+n),cheio); cheio+=n; j+=n;
|
||||||
|
if(cheio<linha+1) break;
|
||||||
|
cheio=0;
|
||||||
|
// Every row is unfiltered: Up, Average and Paeth read the previous one.
|
||||||
|
desfiltra();
|
||||||
|
if(y%passo===0 && y/passo<ch){
|
||||||
|
for(let xx=0,k=0;xx<w;xx+=passo,k+=4){
|
||||||
|
const i=xx*bpp;
|
||||||
|
if(tipo===3){ const q=atual[i]; px[k]=paleta[q*3]; px[k+1]=paleta[q*3+1]; px[k+2]=paleta[q*3+2];
|
||||||
|
px[k+3]=trns&&q<trns.length?trns[q]:255; }
|
||||||
|
else if(tipo===0){ px[k]=px[k+1]=px[k+2]=atual[i]; px[k+3]=255; }
|
||||||
|
else if(tipo===4){ px[k]=px[k+1]=px[k+2]=atual[i]; px[k+3]=atual[i+b]; }
|
||||||
|
else { px[k]=atual[i]; px[k+1]=atual[i+b]; px[k+2]=atual[i+2*b]; px[k+3]=tipo===6?atual[i+3*b]:255; }
|
||||||
|
}
|
||||||
|
ctx.putImageData(img,0,y/passo);
|
||||||
|
}
|
||||||
|
const t=ant; ant=atual; atual=t; y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await alimenta.catch(()=>{});
|
||||||
|
leitor.cancel().catch(()=>{});
|
||||||
|
return y>=h ? cv : null;
|
||||||
|
}
|
||||||
// Large PDFs are read in ranges, never loaded whole into memory.
|
// Large PDFs are read in ranges, never loaded whole into memory.
|
||||||
async function fontePdf(lib, file){
|
async function fontePdf(lib, file){
|
||||||
if(file.size<=GRANDE_BYTES) return {data:await file.arrayBuffer()};
|
if(file.size<=GRANDE_BYTES) return {data:await file.arrayBuffer()};
|
||||||
|
|||||||
@@ -75,11 +75,25 @@ function sel(fs){
|
|||||||
if(md && md.dpiFolha!=null && md.dpiFolha<DPI_RECUSA) return recusaFolha(x, md);
|
if(md && md.dpiFolha!=null && md.dpiFolha<DPI_RECUSA) return recusaFolha(x, md);
|
||||||
x.med=md; if(md) x.m=md.alt/100; x.pct=55; pintaFolha();
|
x.med=md; if(md) x.m=md.alt/100; x.pct=55; pintaFolha();
|
||||||
if(md && md.grande){
|
if(md && md.grande){
|
||||||
// Graded from its size in pixels; the pieces, gaps and background
|
// Graded at once from its size in pixels. A PNG is then read as a
|
||||||
// are checked by the team, since the image is never opened here.
|
// stream into a small copy: the preview, and the sheet check.
|
||||||
x.an={dpi:md.dpiFolha, grande:true, artes:0, residuos:0, encostadas:0,
|
x.an={dpi:md.dpiFolha, grande:true, artes:0, residuos:0, encostadas:0,
|
||||||
fundoChapado:false, aproveitamento:0, alturaCm:md.alt, fonteDpi:'arquivo'};
|
fundoChapado:false, aproveitamento:0, alturaCm:md.alt, fonteDpi:'arquivo'};
|
||||||
x.pct=null; pintaFolha();
|
if(!/\.png$/i.test(x.f.name)){ x.pct=null; pintaFolha(); return; }
|
||||||
|
x.pct=60; pintaFolha();
|
||||||
|
let mostrado=0;
|
||||||
|
previaPngGrande(x.f, 600, p=>{
|
||||||
|
const pct=60+Math.floor(p*38);
|
||||||
|
if(pct-mostrado>=3){ mostrado=pct; x.pct=pct; pintaFolha(); }
|
||||||
|
}).then(cv=>{
|
||||||
|
if(cv && folhas.includes(x)){
|
||||||
|
x.previewSrc=cv.toDataURL('image/webp',0.85);
|
||||||
|
try{
|
||||||
|
const an=analisarFolha(cv, md.larg);
|
||||||
|
x.an={...an, dpi:md.dpiFolha, fonteDpi:'arquivo', grande:false};
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
}).catch(()=>{}).finally(()=>{ x.pct=null; pintaFolha(); });
|
||||||
}else if(md && RENDERIZA.test(x.f.name)){
|
}else if(md && RENDERIZA.test(x.f.name)){
|
||||||
carregarImagem(x.f).then(img=>{
|
carregarImagem(x.f).then(img=>{
|
||||||
if(img){ x.previewSrc=img.src; try{ x.an=analisarFolha(img, md.larg); if(x.an) x.an.fonteDpi='arquivo'; }
|
if(img){ x.previewSrc=img.src; try{ x.an=analisarFolha(img, md.larg); if(x.an) x.an.fonteDpi='arquivo'; }
|
||||||
|
|||||||
Reference in New Issue
Block a user