feat: pack large artworks in Artes avulsas from a streamed copy
Some checks failed
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Failing after 2m36s
Build and deploy / Secret scan and release gate (push) Successful in 8s
Build and deploy / Publish images (push) Has been skipped

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 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-29 16:02:35 -03:00
parent 64e47ee481
commit f4aacb5776
4 changed files with 51 additions and 4 deletions

View File

@@ -12,6 +12,18 @@ function carregarImagem(file){
fr.readAsDataURL(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. // 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. // Sem isso o cliente não tem noção de escala e acha que está mal encaixado.
function molduraFolha(ctx,W,H,larguraCm,metrosReais){ function molduraFolha(ctx,W,H,larguraCm,metrosReais){
@@ -199,12 +211,14 @@ function desenhaMontagem(area,lista,mts,W,fora,aoTerminar){
return; 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; if(area.montagemVersao!==versao) return;
const cargas=imagens.flatMap(c=>Array.from({length:c.a.q||1},(_,copyIndex)=>({...c,copyIndex}))); const cargas=imagens.flatMap(c=>Array.from({length:c.a.q||1},(_,copyIndex)=>({...c,copyIndex})));
cargas.forEach(c=>{ cargas.forEach(c=>{
c.w = c.a.cm; // o motor trabalha em centímetros 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 c.travado = !!c.a.giro; // giro do cliente manda no encaixe
}); });
const enc=encaixar(cargas,filme); const enc=encaixar(cargas,filme);

View File

@@ -492,11 +492,23 @@ function sugestao(a){
} }
const n1=v=>v.toFixed(1).replace('.',','); 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(){ function pintaArtes(){
invalidaItemAtual(); invalidaItemAtual();
$('lista').innerHTML=artes.map((a,i)=>{ $('lista').innerHTML=artes.map((a,i)=>{
let barra='', dica='', calc='<span>informe a largura para ver a qualidade</span>'; let barra='', dica='', calc='<span>informe a largura para ver a qualidade</span>';
if(a.decodeError){ if(a.lendo!=null){
barra='<div class="prog"><i style="width:'+a.lendo+'%"></i></div>';
calc='<span class="progT">arquivo grande · gerando a prévia… '+a.lendo+'%</span>';
}else if(a.decodeError==='grande'){
calc='<span class="er">Arquivo grande em JPG ou WebP: exporte em PNG para montar a folha</span>';
}else if(a.decodeError){
calc='<span class="er">Não conseguimos ler esta imagem; exporte novamente em PNG ou JPG</span>'; calc='<span class="er">Não conseguimos ler esta imagem; exporte novamente em PNG ou JPG</span>';
}else if(a.cm>larguraFilme()){ }else if(a.cm>larguraFilme()){
calc='<span class="er">Largura maior que o filme de '+n1(larguraFilme())+' cm</span>'; calc='<span class="er">Largura maior que o filme de '+n1(larguraFilme())+' cm</span>';
@@ -518,7 +530,7 @@ function pintaArtes(){
n1(sug.cm)+' cm.<button data-usar="'+i+'" data-cm="'+sug.alvo+'">Usar '+n1(sug.alvo)+' cm</button></div>' n1(sug.cm)+' cm.<button data-usar="'+i+'" data-cm="'+sug.alvo+'">Usar '+n1(sug.alvo)+' cm</button></div>'
: ''; : '';
} }
return '<div class="art'+(a===recemChegada?' nova':'')+'"><div class="l1"><b>'+escapeHTML(a.f.name)+'</b>'+ return '<div class="art'+(a===recemChegada?' nova':'')+'" data-arte="'+i+'"><div class="l1"><b>'+escapeHTML(a.f.name)+'</b>'+
'<span style="color:var(--fraco);font-size:10.5px">'+ '<span style="color:var(--fraco);font-size:10.5px">'+
(a.px>0?a.px+' × '+a.py+' px':'a ler')+'</span>'+ (a.px>0?a.px+' × '+a.py+' px':'a ler')+'</span>'+
'<button data-rm="'+i+'">×</button></div><div class="cps">'+ '<button data-rm="'+i+'">×</button></div><div class="cps">'+

View File

@@ -63,6 +63,9 @@ function avaliar(){
return {pronto:true}; return {pronto:true};
} }
if(!artes.length) return {pronto:false, falta:'Arraste suas artes para começar.'}; 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))) 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.'}; 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())) if(artes.some(a=>!Number.isFinite(a.cm) || a.cm>larguraFilme()))

View File

@@ -136,6 +136,24 @@ function sel(fs){
Promise.all(novos.map(medir)).then(pintaArtes); Promise.all(novos.map(medir)).then(pintaArtes);
} }
function medir(a){ 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=>{ 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; } if(img){ a.prop=img.height/img.width; a.px=img.width; a.py=img.height; a.src=img.src; }
else a.decodeError=true; else a.decodeError=true;