From f4aacb5776f2210c7a2b7e206e02d43fa42c1d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Tue, 29 Sep 2026 16:02:35 -0300 Subject: [PATCH] feat: pack large artworks in Artes avulsas from a streamed copy Artes avulsas decoded every file whole, and the packing decoded each one again on every repaint, so a large artwork failed to load. A PNG over 150 MB is now measured from its header (pixel size, so DPI and grade stay exact) and packed from a 1200-pixel copy read as a stream, with progress on its card; every artwork's image is loaded once and reused by the packing. A large JPG or WebP asks for a PNG, and the grade card no longer calls a file unreadable while it is still being read. Co-Authored-By: Claude Opus 5.5 --- web/site-packing.js | 18 ++++++++++++++++-- web/site-pdf.js | 16 ++++++++++++++-- web/site-quality.js | 3 +++ web/site-upload.js | 18 ++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/web/site-packing.js b/web/site-packing.js index 2c0f7ed..05c6881 100644 --- a/web/site-packing.js +++ b/web/site-packing.js @@ -12,6 +12,18 @@ function carregarImagem(file){ fr.readAsDataURL(file); }); } +// The image each artwork was measured with, loaded once and reused on every +// repaint: the packing used to read and decode the whole file again each +// time. Kept outside the artwork, which is saved with the cart. +const imagensDasArtes=new WeakMap(); +function imagemDaArte(a){ + if(!a.src) return Promise.resolve(null); + const guardada=imagensDasArtes.get(a); + if(guardada && guardada.src===a.src) return guardada.img; + const img=new Promise(res=>{ const im=new Image(); im.onload=()=>res(im); im.onerror=()=>res(null); im.src=a.src; }); + imagensDasArtes.set(a,{src:a.src,img}); + return img; +} // A folha precisa parecer uma folha: borda, largura marcada e régua de metros. // Sem isso o cliente não tem noção de escala e acha que está mal encaixado. function molduraFolha(ctx,W,H,larguraCm,metrosReais){ @@ -199,12 +211,14 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){ return; } - Promise.all(itens.map(a=>carregarImagem(a.f).then(img=>({a,img})))).then(imagens=>{ + Promise.all(itens.map(a=>imagemDaArte(a).then(img=>({a,img})))).then(imagens=>{ if(area.montagemVersao!==versao) return; const cargas=imagens.flatMap(c=>Array.from({length:c.a.q||1},(_,copyIndex)=>({...c,copyIndex}))); cargas.forEach(c=>{ c.w = c.a.cm; // o motor trabalha em centímetros - c.h = c.w * (c.img ? (c.a.giro? c.img.width/c.img.height : c.img.height/c.img.width) : 1); + // The file's own proportion: a large artwork is drawn from a smaller copy. + const prop = c.a.prop>0 ? c.a.prop : c.img ? c.img.height/c.img.width : 1; + c.h = c.w * (c.a.giro? 1/prop : prop); c.travado = !!c.a.giro; // giro do cliente manda no encaixe }); const enc=encaixar(cargas,filme); diff --git a/web/site-pdf.js b/web/site-pdf.js index 1cb060b..929ad87 100644 --- a/web/site-pdf.js +++ b/web/site-pdf.js @@ -492,11 +492,23 @@ function sugestao(a){ } const n1=v=>v.toFixed(1).replace('.',','); +// Moves one artwork's progress in place, without repainting the list. +function pintaProgressoArte(a){ + const el=$('lista').querySelector('.art[data-arte="'+artes.indexOf(a)+'"]'); + const barra=el && el.querySelector('.prog i'), texto=el && el.querySelector('.progT'); + if(!barra){ pintaArtes(); return; } + barra.style.width=a.lendo+'%'; if(texto) texto.textContent='arquivo grande · gerando a prévia… '+a.lendo+'%'; +} function pintaArtes(){ invalidaItemAtual(); $('lista').innerHTML=artes.map((a,i)=>{ let barra='', dica='', calc='informe a largura para ver a qualidade'; - if(a.decodeError){ + if(a.lendo!=null){ + barra='
'; + calc='arquivo grande · gerando a prévia… '+a.lendo+'%'; + }else if(a.decodeError==='grande'){ + calc='Arquivo grande em JPG ou WebP: exporte em PNG para montar a folha'; + }else if(a.decodeError){ calc='Não conseguimos ler esta imagem; exporte novamente em PNG ou JPG'; }else if(a.cm>larguraFilme()){ calc='Largura maior que o filme de '+n1(larguraFilme())+' cm'; @@ -518,7 +530,7 @@ function pintaArtes(){ n1(sug.cm)+' cm.' : ''; } - return '
'+escapeHTML(a.f.name)+''+ + return '
'+escapeHTML(a.f.name)+''+ ''+ (a.px>0?a.px+' × '+a.py+' px':'a ler')+''+ '
'+ diff --git a/web/site-quality.js b/web/site-quality.js index 6bb31bb..1c6b515 100644 --- a/web/site-quality.js +++ b/web/site-quality.js @@ -63,6 +63,9 @@ function avaliar(){ return {pronto:true}; } if(!artes.length) return {pronto:false, falta:'Arraste suas artes para começar.'}; + if(artes.some(a=>a.lendo!=null)) return {pronto:false, falta:'Gerando a prévia das artes grandes…'}; + if(artes.some(a=>a.decodeError==='grande')) + return {pronto:false, falta:'Uma arte grande está em JPG ou WebP. Exporte-a em PNG para montar a folha.'}; if(artes.some(a=>a.decodeError || !a.src || !(a.px>0) || !(a.py>0))) return {pronto:false, falta:'Não conseguimos ler uma das imagens. Exporte-a novamente em PNG ou JPG antes de continuar.'}; if(artes.some(a=>!Number.isFinite(a.cm) || a.cm>larguraFilme())) diff --git a/web/site-upload.js b/web/site-upload.js index 4ab2114..ec52099 100644 --- a/web/site-upload.js +++ b/web/site-upload.js @@ -136,6 +136,24 @@ function sel(fs){ Promise.all(novos.map(medir)).then(pintaArtes); } function medir(a){ + // A large artwork is measured from its header and packed from a small copy + // read as a stream (previaPngGrande); the file itself is never decoded. + if(a.f.size>GRANDE_BYTES){ + return dimensoesImagem(a.f).then(async d=>{ + if(!d || !(d.w>0) || !(d.h>0)){ a.decodeError=true; return a; } + a.px=d.w; a.py=d.h; a.prop=d.h/d.w; a.grande=true; + if(!/\.png$/i.test(a.f.name)){ a.decodeError='grande'; return a; } + a.lendo=0; pintaArtes(); + let mostrado=0; + const cv=await previaPngGrande(a.f, 1200, p=>{ + const pct=Math.floor(p*100); + if(pct-mostrado>=1){ mostrado=pct; a.lendo=pct; pintaProgressoArte(a); } + }).catch(()=>null); + a.lendo=null; + if(cv && artes.includes(a)) a.src=cv.toDataURL('image/webp',0.9); else if(!cv) a.decodeError=true; + return a; + }).catch(()=>{ a.decodeError=true; return a; }); + } return carregarImagem(a.f).then(img=>{ if(img){ a.prop=img.height/img.width; a.px=img.width; a.py=img.height; a.src=img.src; } else a.decodeError=true;