fix: harden week-two ordering, artwork and operations

This commit is contained in:
Cauê Faleiros
2026-09-23 10:40:18 -03:00
parent ccc25a2d5d
commit 24013458c9
43 changed files with 1064 additions and 228 deletions

View File

@@ -10,9 +10,8 @@
const PDFJS_URL='/vendor/pdf.min.js';
const PDFJS_WORKER=new URL('/vendor/pdf.worker.min.js', location.origin).href;
let pdfLibP=null, temWorker=null;
// Alguns navegadores e iframes bloqueiam Worker de blob. Descobrimos antes de
// tentar, porque sem worker o PDF é lido na thread principal e precisa de outra
// estratégia: grade mais grossa e mais tempo.
// Check whether this browser can create a same-origin Worker. The worker file
// is served alongside this script and does not need a blob URL.
function testaWorker(url){
try{ const w=new Worker(url); w.terminate(); return true; }catch(e){ return false; }
}
@@ -25,10 +24,8 @@ function carregarPdfJs(){
sc.src=PDFJS_URL;
sc.onload=()=>{
try{
const codigo='importScripts("'+PDFJS_WORKER+'");';
const url=URL.createObjectURL(new Blob([codigo],{type:'application/javascript'}));
temWorker=testaWorker(url);
window.pdfjsLib.GlobalWorkerOptions.workerSrc = temWorker? url : PDFJS_WORKER;
temWorker=testaWorker(PDFJS_WORKER);
window.pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER;
}catch(e){
temWorker=false;
try{ window.pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER; }catch(e2){}
@@ -61,7 +58,7 @@ function dpiDasImagens(page, ops){
const larguraPt=Math.hypot(ctm[0],ctm[1]);
if(larguraPt<1) continue;
const areaPt=Math.abs(ctm[0]*ctm[3]-ctm[1]*ctm[2]);
dpis.push({dpi: im.width/(larguraPt/72), area: areaPt});
dpis.push({dpi: im.width/(larguraPt*page.userUnit/72), area: areaPt});
}
}
return dpis;
@@ -88,43 +85,58 @@ function resumoDpi(lista){
}
// Rasteriza a primeira página a 1 mm por pixel e devolve a tela para a análise
function rasterizarPdf(file, larguraCm, alturaCm){
let motivo=null;
const trabalho=carregarPdfJs().then(lib=>{
if(!lib){ motivo='não foi possível carregar o leitor de PDF'; return null; }
// sem worker tudo roda na thread principal · grade mais grossa para caber
const folga = temWorker===false ? 2.2 : 1;
return file.arrayBuffer().then(buf=>lib.getDocument({
data:buf, disableFontFace:true, isEvalSupported:false, useSystemFonts:false,
verbosity:0 // sem worker é fallback, não erro
}).promise).then(doc=>
doc.getPage(1).then(page=>{
const vp1=page.getViewport({scale:1});
const passo=passoDe(larguraCm, alturaCm||larguraCm)*folga;
const alvo=Math.max(8, Math.round(larguraCm/passo));
const esc=alvo/vp1.width;
const vp=page.getViewport({scale:esc});
const cv=document.createElement('canvas');
cv.width=Math.max(8,Math.round(vp.width)); cv.height=Math.max(8,Math.round(vp.height));
const ctx=cv.getContext('2d',{willReadFrequently:true});
return page.render({canvasContext:ctx, viewport:vp, background:'rgba(0,0,0,0)'})
.promise.then(()=>page.getOperatorList()).then(ops=>{
let dpis=[];
try{ dpis=dpiDasImagens(page,ops); }catch(e){}
const r=resumoDpi(dpis);
return {tela:cv, dpi: r? r.ponderado : null, res:r};
});
})
);
}).catch(e=>{ motivo=(e&&e.message)? e.message : 'erro ao abrir o PDF'; return null; });
// sem worker o processo é lento: damos mais tempo antes de desistir
const espera = temWorker===false ? 90000 : 30000;
const tarde=new Promise(r=>setTimeout(()=>{ motivo='demorou demais neste navegador'; r('tempo'); }, espera));
return Promise.race([trabalho, tarde]).then(r=>{
const saida = r==='tempo'? null : r;
if(!saida) return {erro: motivo||'não deu para conferir', semWorker: temWorker===false};
return saida;
async function rasterizarPdf(file, larguraCm, alturaCm){
const lib=await carregarPdfJs();
if(!lib) return {erro:'não foi possível carregar o leitor de PDF'};
// Decide after the worker check; the old timeout always used 30 seconds.
const espera=temWorker===false ? 90000 : 30000;
const folga=temWorker===false ? 2.2 : 1;
let loading=null, doc=null, rendering=null, expired=false, timer=null;
const trabalho=(async()=>{
try{
const buf=await file.arrayBuffer();
if(expired) return null;
loading=lib.getDocument({data:buf, disableFontFace:true,
isEvalSupported:false, useSystemFonts:false, verbosity:0});
doc=await loading.promise;
if(expired || doc.numPages!==1) return null;
const page=await doc.getPage(1);
if(expired) return null;
const vp1=page.getViewport({scale:1});
const passo=passoDe(larguraCm, alturaCm||larguraCm)*folga;
const alvo=Math.max(8,Math.round(larguraCm/passo));
const vp=page.getViewport({scale:alvo/vp1.width});
const cv=document.createElement('canvas');
cv.width=Math.max(8,Math.round(vp.width)); cv.height=Math.max(8,Math.round(vp.height));
const ctx=cv.getContext('2d',{willReadFrequently:true});
rendering=page.render({canvasContext:ctx, viewport:vp, background:'rgba(0,0,0,0)'});
await rendering.promise;
if(expired) return null;
const ops=await page.getOperatorList();
if(expired) return null;
let dpis=[];
try{ dpis=dpiDasImagens(page,ops); }catch(e){}
const r=resumoDpi(dpis);
return {tela:cv, dpi:r? r.ponderado:null, res:r};
}catch(e){
if(expired) return null;
return {erro:(e&&e.message)||'erro ao abrir o PDF', semWorker:temWorker===false};
}finally{
if(doc) doc.destroy().catch(()=>{});
else if(loading) loading.destroy().catch(()=>{});
}
})();
const tardio=new Promise(resolve=>{
timer=setTimeout(()=>{
expired=true;
try{ if(rendering) rendering.cancel(); }catch(e){}
if(loading) loading.destroy().catch(()=>{});
resolve({erro:'demorou demais neste navegador',semWorker:temWorker===false});
},espera);
});
const result=await Promise.race([trabalho,tardio]);
clearTimeout(timer);
return result||{erro:'não deu para conferir',semWorker:temWorker===false};
}
function medirFolha(file){
@@ -143,29 +155,40 @@ function medirFolha(file){
return;
}
if(/\.pdf$/.test(nome)){
const pedaco=(inicio,fim)=>new Promise(r=>{
const fr=new FileReader();
fr.onload=()=>r(new TextDecoder('latin1').decode(new Uint8Array(fr.result)));
fr.onerror=()=>r('');
fr.readAsArrayBuffer(file.slice(inicio,fim));
});
const M=4*1048576;
Promise.all([pedaco(0,M), pedaco(Math.max(0,file.size-M), file.size)]).then(([a,b])=>{
const txt=a+b;
const re=/\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)/;
const m=txt.match(re);
if(!m) return res(null);
// UserUnit escala a página inteira · sem ele, 1 unidade = 1/72 pol
const uu=txt.match(/\/UserUnit\s+([\d.]+)/);
const esc=uu? Math.max(1,Math.min(75000,+uu[1])) : 1;
const ptW=Math.abs(+m[3]-+m[1]), ptH=Math.abs(+m[4]-+m[2]);
if(!(ptW>0&&ptH>0)) return res(null);
const w=ptW*PT_CM*esc, h=ptH*PT_CM*esc;
// acima de 14.400 unidades sem UserUnit o arquivo está fora da especificação
const foraDoPadrao = !uu && (ptW>PDF_MAX_PT || ptH>PDF_MAX_PT);
res({larg:+w.toFixed(1), alt:+h.toFixed(1),
fonte:'página do PDF'+(uu? ' · UserUnit '+esc : ''), foraDoPadrao});
});
// PDF boxes can be inherited, compressed, rotated, and scaled by UserUnit.
// Searching raw bytes for /MediaBox can read the wrong object or miss it.
let doc=null;
carregarPdfJs().then(async lib=>{
if(!lib) throw new Error('Não foi possível carregar o leitor de PDF.');
doc=await lib.getDocument({data:await file.arrayBuffer(),
disableFontFace:true, isEvalSupported:false, useSystemFonts:false,
verbosity:0}).promise;
if(doc.numPages!==1)
return {rejected:true, reason:'O PDF tem '+doc.numPages+
' páginas. Envie cada folha como um PDF de uma página para calcular o preço correto.'};
const page=await doc.getPage(1);
const view=page.view, unit=page.userUnit, vp=page.getViewport({scale:1});
if(!Array.isArray(view) || view.length!==4 ||
!Number.isFinite(unit) || unit<=0 ||
!Number.isFinite(vp.width) || !Number.isFinite(vp.height) ||
vp.width<=0 || vp.height<=0)
return {rejected:true, reason:'Não conseguimos determinar o tamanho desta página PDF. Exporte-a novamente.'};
const ptW=Math.abs(view[2]-view[0]), ptH=Math.abs(view[3]-view[1]);
if(!(ptW>0 && ptH>0) ||
(unit===1 && (ptW>PDF_MAX_PT || ptH>PDF_MAX_PT)))
return {rejected:true, reason:'A página passa do limite de 508 cm sem UserUnit. Divida a folha ou exporte em PNG.'};
// This pinned PDF.js build exposes the page's rotated view in points;
// userUnit is separate and must scale both physical dimensions.
const w=vp.width*unit*PT_CM, h=vp.height*unit*PT_CM;
if(!Number.isFinite(w) || !Number.isFinite(h) || w<=0 || h<=0 || h>6000)
return {rejected:true, reason:'O tamanho desta página PDF não é suportado. Divida a folha em partes menores.'};
// Keep precise geometry for the fit and quote; the UI rounds only
// when displaying dimensions.
return {larg:+w.toFixed(3), alt:+h.toFixed(3),
fonte:'página do PDF'+(unit!==1 ? ' · UserUnit '+unit : '')};
}).then(res).catch(()=>res({rejected:true,
reason:'Não conseguimos ler este PDF. Exporte-o novamente como PDF de uma página ou PNG.'}))
.finally(()=>{ if(doc) doc.destroy().catch(()=>{}); });
return;
}
res(null);
@@ -173,12 +196,13 @@ function medirFolha(file){
}
function pintaFolha(){
invalidaItemAtual();
const L=larguraFilme();
pintaTipoEnvio(); // trava o seletor enquanto houver folha
if(!folhas.length){ $('lista').innerHTML=''; agendaAvaliacao(); previaAoVivo(); return; }
$('lista').innerHTML = folhas.map((x,i)=>{
const lido=!!x.med, larga=lido && x.med.larg>L+0.5, suspeito=lido && x.med.foraDoPadrao;
const lido=!!x.med, larga=lido && x.med.larg>L, suspeito=lido && x.med.foraDoPadrao;
const sub=(x.m||0)*(x.rep||1);
let medida='';
if(lido){
@@ -210,6 +234,9 @@ function pintaFolha(){
? ' Ela cabe no <b>DTF têxtil de 57 cm</b> — troque de produto ali em cima.'
: ' Remonte a folha em '+n1(L)+' cm e suba de novo.')+'</em>':'')+
'</div>';
}else if(x.measurementError){
medida='<div class="med alerta"><b>Este PDF não pode ser orçado</b><span>'+
escapeHTML(x.measurementError)+'</span></div>';
}else{
const teto=TABELA[modo], piso=pisoEscada();
const dif=(x.m>0)? (teto-piso)*cobrar(x.m*(x.rep||1)) : 0;
@@ -231,7 +258,7 @@ function pintaFolha(){
'<span class="progT">'+(x.pct<50?'lendo o arquivo…':
x.pct<70?'medindo a folha…':'conferindo a arte…')+'</span>'
: x.semAnalise
? '<div class="progF"><b>Não conseguimos conferir esta folha.</b> '+x.semAnalise+
? '<div class="progF"><b>Não conseguimos conferir esta folha.</b> '+escapeHTML(x.semAnalise)+
'. Seguimos com o preço de tabela — se quiser a nota e o desconto, exporte a mesma '+
'folha em <b>PNG</b>, que a conferência é bem mais leve.</div>'
: '';
@@ -271,6 +298,8 @@ function pintaFolha(){
$('lista').querySelectorAll('[data-comp]').forEach(el=>el.addEventListener('change',e=>{
folhas[+el.dataset.comp].m=Math.max(0,Math.min(60,+e.target.value||0)); pintaFolha();
}));
$('lista').querySelectorAll('[data-repf], [data-comp]').forEach(el=>
el.addEventListener('input',invalidaItemAtual));
agendaAvaliacao(); previaAoVivo();
}
@@ -323,10 +352,15 @@ function vizPeca(a){
const n1=v=>v.toFixed(1).replace('.',',');
function pintaArtes(){
invalidaItemAtual();
$('lista').innerHTML=artes.map((a,i)=>{
let barra='', dica='', calc='<span>informe a largura para ver a qualidade</span>';
if(a.cm>0){
const dpi=Math.round(a.px/(a.cm/2.54)), n=Math.max(6,Math.min(100,Math.round(dpi/300*100)));
if(a.decodeError){
calc='<span class="er">Não conseguimos ler esta imagem; exporte novamente em PNG ou JPG</span>';
}else if(a.cm>larguraFilme()){
calc='<span class="er">Largura maior que o filme de '+n1(larguraFilme())+' cm</span>';
}else if(a.cm>0 && a.src){
const dpi=Math.round(dpiDe(a)), n=Math.max(6,Math.min(100,Math.round(dpi/300*100)));
barra='<div class="qb2"><i class="'+cls(n)+'" style="width:'+n+'%"></i></div>';
calc='<span class="'+cls(n)+'"><b>'+n+'%</b> · '+dpi+' DPI</span><span>'+
a.q+' × '+n1(a.cm)+' × '+n1(a.cm*propDe(a))+' cm</span>';
@@ -341,7 +375,8 @@ function pintaArtes(){
dica = vizPeca(a) + dica;
}
return '<div class="art'+(a===recemChegada?' nova':'')+'"><div class="l1"><b>'+escapeHTML(a.f.name)+'</b>'+
'<span style="color:var(--fraco);font-size:10.5px">'+a.px+' px</span>'+
'<span style="color:var(--fraco);font-size:10.5px">'+
(a.px>0?a.px+' × '+a.py+' px':'a ler')+'</span>'+
'<button data-rm="'+i+'">×</button></div><div class="cps">'+
'<div><label>largura na peça</label><input type="number" min="1" max="'+larguraFilme()+
'" placeholder="cm" data-cm="'+i+'" value="'+(a.cm||'')+'"></div>'+
@@ -358,11 +393,12 @@ function pintaArtes(){
}).join('');
$('lista').querySelectorAll('[data-rm]').forEach(b=>b.addEventListener('click',()=>{artes.splice(+b.dataset.rm,1);pintaArtes();}));
$('lista').querySelectorAll('[data-cm]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.cm].cm=+i.value||0;pintaArtes();}));
$('lista').querySelectorAll('[data-q]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.q].q=Math.max(1,Math.floor(+i.value||1));pintaArtes();}));
$('lista').querySelectorAll('[data-q]').forEach(i=>i.addEventListener('change',()=>{artes[+i.dataset.q].q=Math.max(1,Math.min(200,Math.floor(+i.value||1)));pintaArtes();}));
$('lista').querySelectorAll('[data-cm], [data-q]').forEach(i=>i.addEventListener('input',()=>{
const largura=i.hasAttribute('data-cm');
const a=artes[+(largura?i.dataset.cm:i.dataset.q)];
a[largura?'cm':'q']=largura?Math.max(0,+i.value||0):Math.max(1,Math.floor(+i.value||1));
a[largura?'cm':'q']=largura?Math.max(0,+i.value||0):Math.max(1,Math.min(200,Math.floor(+i.value||1)));
invalidaItemAtual();
previaAoVivo();
}));
$('lista').querySelectorAll('[data-usar]').forEach(b=>b.addEventListener('click',()=>{
@@ -381,10 +417,10 @@ function pintaArtes(){
previaAoVivo();
}
function resumoArtes(){
const r=$('rA'), prontas=artes.filter(a=>a.cm>0);
const r=$('rA'), prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && a.src);
if(!prontas.length){ r.style.display='none'; return; }
const m=montagemCm/100; // a mesma folha que aparece ao lado
const notas=prontas.map(a=>Math.max(6,Math.min(100,Math.round((a.px/(a.cm/2.54))/300*100))));
const notas=prontas.map(a=>Math.max(6,Math.min(100,Math.round(dpiDe(a)/300*100))));
const media=Math.round(notas.reduce((s,n)=>s+n,0)/notas.length), pior=Math.min(...notas);
const falta=artes.length-prontas.length;
r.style.display='block'; r.className='resumoA on';
@@ -397,4 +433,3 @@ function resumoArtes(){
' m'+(cobrar(m)===MINIMO_M?' · mínimo':'')+'</b></div>':''):'')+
(falta?'<div class="r" style="color:var(--laranja)"><span>faltam informar</span><b>'+falta+'</b></div>':'');
}