diff --git a/web/index.html b/web/index.html
index a4f9c6d..2f2f0ac 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1050,7 +1050,6 @@ footer a:hover{color:var(--laranja2)}
—
Prévia. O encaixe final é refeito no servidor e nunca sai pior.
diff --git a/web/site-pdf.js b/web/site-pdf.js
index 97d3576..a10b2dd 100644
--- a/web/site-pdf.js
+++ b/web/site-pdf.js
@@ -168,6 +168,117 @@ async function dimensoesImagem(file){
}
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;
+ };
+ const tira=n=>{
+ const out=new Uint8Array(n); let k=0;
+ while(k((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>1);
+ for(let i=bpp;i>1); }
+ else { for(let i=0;ia?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{});
+ leitor.cancel().catch(()=>{});
+ return y>=h ? cv : null;
+}
// Large PDFs are read in ranges, never loaded whole into memory.
async function fontePdf(lib, file){
if(file.size<=GRANDE_BYTES) return {data:await file.arrayBuffer()};
diff --git a/web/site-upload.js b/web/site-upload.js
index b783afd..1cec483 100644
--- a/web/site-upload.js
+++ b/web/site-upload.js
@@ -75,11 +75,25 @@ function sel(fs){
if(md && md.dpiFolha!=null && md.dpiFolha{
+ 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)){
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'; }