fix: harden week-two ordering, artwork and operations
This commit is contained in:
189
web/site-pdf.js
189
web/site-pdf.js
@@ -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>':'');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user