/* Site DTF — PDF measurement and rasterisation, and the DPI of images inside one. Extracted verbatim from the single inline script in web/index.html. Loaded as classic scripts in the order listed there: they share one global scope and run top to bottom, exactly as the original did. */ // ── PDF também é conferido: rasteriza a página e mede o DPI das imagens de dentro. // Arte vetorial não tem resolução — nesses casos a nota é máxima, e isso é correto. // Served from this origin, not a CDN: the Site keeps working when a third party // does not, and the CSP can name only 'self' for scripts and workers. Provenance // and hashes are recorded in web/vendor/README.md. 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; // 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; } } function carregarPdfJs(){ if(typeof window==='undefined') return Promise.resolve(null); if(window.pdfjsLib) return Promise.resolve(window.pdfjsLib); if(pdfLibP) return pdfLibP; pdfLibP=new Promise(res=>{ const sc=document.createElement('script'); sc.src=PDFJS_URL; sc.onload=()=>{ try{ temWorker=testaWorker(PDFJS_WORKER); window.pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER; }catch(e){ temWorker=false; try{ window.pdfjsLib.GlobalWorkerOptions.workerSrc=PDFJS_WORKER; }catch(e2){} } res(window.pdfjsLib||null); }; sc.onerror=()=>res(null); document.head.appendChild(sc); }); return pdfLibP; } // DPI real de cada imagem embutida: acompanha a matriz de transformação e compara // o tamanho em que a imagem foi colocada com quantos pixels ela tem. function dpiDasImagens(page, ops){ const mul=(a,b)=>[a[0]*b[0]+a[2]*b[1], a[1]*b[0]+a[3]*b[1], a[0]*b[2]+a[2]*b[3], a[1]*b[2]+a[3]*b[3], a[0]*b[4]+a[2]*b[5]+a[4], a[1]*b[4]+a[3]*b[5]+a[5]]; const O=window.pdfjsLib.OPS; let ctm=[1,0,0,1,0,0]; const pilha=[]; const dpis=[]; for(let i=0;it+x.area,0) || 1; const abaixoPiso=lista.filter(x=>x.dpix.dpit+x.dpi*x.area,0)/total; return { pior: Math.round(Math.min(...lista.map(x=>x.dpi))), ponderado: Math.round(pond), fracBaixa: abaixoPiso.reduce((t,x)=>t+x.area,0)/total, fracAviso: abaixoAviso.reduce((t,x)=>t+x.area,0)/total, quantasBaixas: abaixoPiso.length, quantasAviso: abaixoAviso.length, total: lista.length }; } // Rasteriza a primeira página a 1 mm por pixel e devolve a tela para a análise 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}; } // Sheets of several GB are the normal order. The browser cannot decode an image // that size (it would freeze or crash the tab), and it does not need to: the // grade comes from the width in pixels, which PNG, JPEG and WebP store in their // first bytes. Above GRANDE_BYTES only those bytes are read. const GRANDE_BYTES=150*1048576; async function dimensoesImagem(file){ const b=new Uint8Array(await file.slice(0,Math.min(file.size,4*1048576)).arrayBuffer()); const u16=(i,le)=>le? b[i]|(b[i+1]<<8) : (b[i]<<8)|b[i+1]; const u32=(i)=>((b[i]<<24)>>>0)+(b[i+1]<<16)+(b[i+2]<<8)+b[i+3]; if(b[0]===0x89 && b[1]===0x50 && b[2]===0x4E && b[3]===0x47) // PNG · IHDR return {w:u32(16), h:u32(20)}; if(b[0]===0xFF && b[1]===0xD8){ // JPEG · SOFn let i=2; while(i+9=0xC0 && m<=0xCF && ![0xC4,0xC8,0xCC].includes(m)) return {w:u16(i+7), h:u16(i+5)}; i+=2+len; } return null; } const tag=String.fromCharCode(...b.slice(8,16)); if(String.fromCharCode(...b.slice(0,4))==='RIFF' && tag.startsWith('WEBP')){ // WebP if(tag==='WEBPVP8X') return {w:1+(b[24]|(b[25]<<8)|(b[26]<<16)), h:1+(b[27]|(b[28]<<8)|(b[29]<<16))}; if(tag==='WEBPVP8L'){ const n=b[21]|(b[22]<<8)|(b[23]<<16)|(b[24]<<24); return {w:(n&0x3FFF)+1, h:((n>>14)&0x3FFF)+1}; } if(tag==='WEBPVP8 ') return {w:u16(26,true)&0x3FFF, h:u16(28,true)&0x3FFF}; } 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()}; const inicio=new Uint8Array(await file.slice(0,262144).arrayBuffer()); const t=new lib.PDFDataRangeTransport(file.size, inicio); t.requestDataRange=(de,ate)=>{ file.slice(de,ate).arrayBuffer().then(buf=>t.onDataRange(de,new Uint8Array(buf))); }; return {range:t, rangeChunkSize:262144, disableAutoFetch:true, disableStream:true}; } function medirFolha(file){ return new Promise(res=>{ const nome=(file.name||'').toLowerCase(); if(RENDERIZA.test(nome) && file.size>GRANDE_BYTES){ dimensoesImagem(file).then(d=>{ if(!d || !(d.w>0) || !(d.h>0)) return res(null); const L=larguraFilme(); res({larg:L, alt:+(d.h/d.w*L).toFixed(1), fonte:'dimensões do arquivo', dpiFolha:Math.round(d.w/(L/2.54)), px:d.w, grande:true}); }).catch(()=>res(null)); return; } if(RENDERIZA.test(nome)){ carregarImagem(file).then(img=>{ if(!img || !img.width) return res(null); const L=larguraFilme(); // Uma imagem só é folha montada se tiver pixel para ocupar a largura do // filme. Sem esta conferência, qualquer arte pequena vira "folha" e leva // o preço por metro sem nunca ter sido montada. res({larg:L, alt:+(img.height/img.width*L).toFixed(1), fonte:'proporção da imagem', dpiFolha:Math.round(img.width/(L/2.54)), px:img.width}); }); return; } if(/\.pdf$/.test(nome)){ // 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({...await fontePdf(lib,file), 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); }); } function textoProgresso(x){ if(x.med && x.med.grande) return 'gerando a prévia e conferindo a arte… '+x.pct+'%'; return x.pct<50?'lendo o arquivo…':x.pct<70?'medindo a folha…':'conferindo a arte…'; } // The file card's thumbnail (52 x 96 px, drawn at twice that for sharp screens). function miniatura(fonte){ const w=fonte.naturalWidth||fonte.width, h=fonte.naturalHeight||fonte.height; if(!(w>0 && h>0)) return null; const s=Math.min(1,104/w,192/h), cv=document.createElement('canvas'); cv.width=Math.max(1,Math.round(w*s)); cv.height=Math.max(1,Math.round(h*s)); cv.getContext('2d').drawImage(fonte,0,0,cv.width,cv.height); return cv.toDataURL('image/webp',0.8); } // Moves one sheet's progress bar in place: repainting the whole list and the // preview on every step made "Sua folha" flicker while a large file was read. function pintaProgressoFolha(x){ const el=$('lista').querySelector('.rep[data-folha="'+folhas.indexOf(x)+'"]'); const barra=el && el.querySelector('.prog i'), texto=el && el.querySelector('.progT'); if(!barra){ pintaFolha(); return; } barra.style.width=x.pct+'%'; if(texto) texto.textContent=textoProgresso(x); } // What the check found, as chips: ok (verde), av (atenção), er (resolva você), // fix (arrumamos de graça), nt (a equipe confere). Details stay one click away. function chipsDaFolha(a){ const chip=(k,t)=>'
  • '+t+'
  • '; const dpi = a.vetor ? chip('ok','Arte vetorial') : chip(a.dpi>=DPI_AVISA?'ok':a.dpi>=DPI_RECUSA?'av':'er', a.dpi+' DPI'); if(a.grande) return dpi+chip('nt','Peças, resíduos e fundo: a equipe confere'); return dpi+ (a.fundoChapado? chip('er','Fundo chapado') : chip('ok','Fundo transparente'))+ (a.residuos? chip('er',a.residuos+' resíduo'+(a.residuos>1?'s':'')) : chip('ok','Sem resíduos'))+ (a.encostadas? chip('fix',a.encostadas+' peças coladas · separamos grátis') : chip('ok','Peças separadas')); } function avisosDaFolha(a){ if(a.grande) return ''; return (a.fundoChapado? '

    O fundo não é transparente. '+ 'Branco parecendo transparente sai branco no filme. Exporte com fundo transparente.

    ' : '')+ (a.residuos? '

    '+a.residuos+' pontinho'+(a.residuos>1?'s soltos serão impressos':' solto será impresso')+ '. A impressora imprime qualquer resíduo de recorte. Apague no arquivo e envie de novo.

    ' : ''); } function detalhesDaFolha(a,i,aberto){ const linhas = a.grande ? ['Resolução '+a.dpi+' DPI, lida do arquivo', 'Arquivo grande: peças, resíduos e fundo são conferidos pela equipe'] : [a.artes+' peça'+(a.artes===1?'':'s')+' · '+a.aproveitamento+'% da folha com arte', 'Resolução '+a.dpi+' DPI · '+(a.fonteDpi==='imagens'? 'medida imagem por imagem dentro do PDF' : a.fonteDpi==='vetor'? 'arte vetorial, sem imagem embutida' : 'lida do arquivo'), 'Conferido numa grade de '+n1(a.grade)+' mm']; return '
    '+ 'Ver detalhes da conferência'+ linhas.map(l=>''+l+'').join('')+'
    '; } function pintaFolha(){ invalidaItemAtual(); const L=larguraFilme(); pintaTipoEnvio(); // trava o seletor enquanto houver folha $('zona').classList.toggle('compacta', folhas.length>0); if(!folhas.length){ $('lista').innerHTML=''; agendaAvaliacao(); previaAoVivo(); return; } const tot=folhaTotalM(); $('lista').innerHTML = '

    Seus arquivos

    '+ folhas.length+' folha'+(folhas.length>1?'s':'')+(tot>0? ' · '+fmtM(tot)+' m' : '')+'
    '+ folhas.map((x,i)=>{ const lido=!!x.med, larga=lido && x.med.larg>L, suspeito=lido && x.med.foraDoPadrao; const sub=(x.m||0)*(x.rep||1), a=lido && x.an; let medida='', manual=''; if(lido){ medida='
    '+n1(x.med.larg)+' × '+n1(x.med.alt)+' cm'+ ''+escapeHTML(extDe(x.f.name))+' · '+fmt(x.f.size)+'
    '+ (a? '
      '+chipsDaFolha(a)+'
    '+avisosDaFolha(a)+detalhesDaFolha(a,i,x.detAberto) : '')+ (suspeito? '

    Este PDF passa do tamanho que o formato comporta. '+ 'A especificação para em 508 cm e o seu tem '+n1(x.med.alt)+' cm. '+ 'Ele abre certo só em quem o gerou — aqui vai precisar de tratamento. '+ 'Manda em PNG ou TIFF, ou divide em partes de até 5 m, que sai na hora.

    ':'')+ (larga? '

    Esta folha não roda neste filme. Ela tem '+n1(x.med.larg)+ ' cm de largura e o filme aqui tem '+n1(L)+' cm. Não dá para reduzir: '+ 'encolher a folha encolheria cada arte junto.'+ (x.med.larg<=57.5 ? ' Ela cabe no DTF têxtil de 57 cm — troque de produto ali em cima.' : ' Remonte a folha em '+n1(L)+' cm e suba de novo.')+'

    ':''); }else if(x.measurementError){ medida=''+escapeHTML(extDe(x.f.name))+' · '+fmt(x.f.size)+''+ '

    Este PDF não pode ser orçado. '+escapeHTML(x.measurementError)+'

    '; }else{ const teto=TABELA[modo], piso=pisoEscada(); const dif=(x.m>0)? (teto-piso)*cobrar(x.m*(x.rep||1)) : 0; medida='Conferência manual · preço cheio'+ '

    O navegador não abre '+escapeHTML(extDe(x.f.name))+', então alguém da equipe '+ 'confere antes de imprimir e o metro sai pela tabela, '+rs(teto)+'. '+ 'Exportando em PNG ou PDF, o metro pode cair até '+rs(piso)+''+ (dif>0.5? ' — '+rs(dif)+' a menos só nesta folha' : '')+'.

    '; manual='
    '+ 'm
    '; } const barra = x.pct!=null ? '
    '+ ''+textoProgresso(x)+'' : x.semAnalise ? '
    Não conseguimos conferir esta folha. '+escapeHTML(x.semAnalise)+ '. Seguimos com o preço de tabela — se quiser a nota e o desconto, exporte a mesma '+ 'folha em PNG, que a conferência é bem mais leve.
    ' : ''; // A small thumbnail made once: the list is rebuilt on every change, and the // full preview of a file up to 150 MB would be re-parsed each time. const mini = x.miniSrc ? '' : ''+escapeHTML(extDe(x.f.name))+''; const copias = x.m>0 || manual ? '
    '+manual+ (x.m>0 ? '
    '+ ''+ ''+ '
    '+ '
    '+(x.rep>1? x.rep+' × '+fmtM(x.m)+' m = ' : '')+''+fmtM(sub)+' m
    ' : '')+ '
    ' : ''; return '
    '+ '
    '+mini+'
    '+ '
    '+(folhas.length>1? (i+1)+'. ':'')+escapeHTML(x.f.name)+''+ '
    '+ (lido || x.measurementError ? '' : ''+escapeHTML(extDe(x.f.name))+' · '+fmt(x.f.size)+'')+ barra+medida+ '
    '+copias+ '
    '; }).join(''); $('lista').querySelectorAll('[data-rmf]').forEach(b=>b.addEventListener('click',()=>{ folhas.splice(+b.dataset.rmf,1); pintaFolha(); })); $('lista').querySelectorAll('[data-det]').forEach(d=>d.addEventListener('toggle',()=>{ folhas[+d.dataset.det].detAberto=d.open; })); $('lista').querySelectorAll('[data-passo]').forEach(b=>b.addEventListener('click',()=>{ const el=$('repf'+b.dataset.i); el.value=Math.max(1,Math.min(200,(+el.value||1)+(+b.dataset.passo))); el.dispatchEvent(new Event('change')); })); $('lista').querySelectorAll('[data-repf]').forEach(el=>el.addEventListener('change',e=>{ folhas[+el.dataset.repf].rep=Math.max(1,Math.min(200,+e.target.value||1)); 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(); } // sugestão de largura const cabemNaLinha=w=>Math.max(1,Math.floor((larguraFilme()+GAP_CM)/(w+GAP_CM))); const larguraPara=n=>(larguraFilme()-(n-1)*GAP_CM)/n; // Só sugere se a folha realmente encolher. Caber mais um por fileira não adianta // nada se a quantidade pedida continuar ocupando o mesmo número de fileiras. function sugestao(a){ const w=a.cm, q=Math.max(1,a.q||1), prop=propDe(a); const n=cabemNaLinha(w), alvo=Math.floor(larguraPara(n+1)*10)/10; if(!(alvo>0) || alvo>=w) return null; if((w-alvo)/w>0.12) return null; // não corta mais que 12% da peça const fa=Math.ceil(q/n), fd=Math.ceil(q/(n+1)); if(fd>=fa) return null; // mesmo número de fileiras: não muda nada const economiaCm=fa*w*prop - fd*alvo*prop; if(economiaCm<0.5) return null; // menos de meio centímetro não é notícia return {alvo, de:n, para:n+1, fileiras:fa-fd, cm:economiaCm}; } 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('[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(); $('zona').classList.toggle('compacta', artes.length>0); // The same card as a finished sheet: thumbnail, size, a DPI chip, then the // width (with the usual sizes), copies, rotation and mirroring. const copiasTot=artes.reduce((t,a)=>t+(a.q||1),0); $('lista').innerHTML=(artes.length? '

    Suas artes

    '+ artes.length+' arte'+(artes.length>1?'s':'')+' · '+copiasTot+' cópia'+(copiasTot>1?'s':'')+'
    ' : '')+ artes.map((a,i)=>{ let barra='', dica='', chips='', aviso='', medida='Informe a largura'; if(a.lendo!=null){ barra='
    '+ 'arquivo grande · gerando a prévia… '+a.lendo+'%'; }else if(a.decodeError){ aviso='

    Não conseguimos ler esta imagem. Exporte novamente em PNG ou JPG.

    '; }else if(a.cm>larguraFilme()){ aviso='

    Largura maior que o filme. O filme aqui tem '+n1(larguraFilme())+' cm.

    '; }else if(a.cm>0 && (a.src||a.semPrevia)){ const dpi=Math.round(dpiDe(a)); medida=''+n1(a.cm)+' × '+n1(a.cm*propDe(a))+' cm'; chips='
    • '+dpi+' DPI
    • '+ '
    • '+cabemNaLinha(a.cm)+' por fileira
    '; const sug=sugestao(a); // One hint per artwork: resolution first, since it changes the price; // otherwise a width that saves sheet. const maxCm=Math.floor(a.px/DPI_AVISA*2.54*10)/10; dica = dpi0 && maxCmPequena para '+n1(a.cm)+' cm. Em '+DPI_AVISA+' DPI ela sai com até '+ n1(maxCm)+' cm, ou envie um arquivo maior.' : sug ? '
    Com '+n1(sug.alvo)+' cm cabem '+sug.para+' por fileira e a folha encolhe '+ n1(sug.cm)+' cm.
    ' : ''; } if(a.semPrevia && !a.decodeError) aviso+='

    Sem prévia para '+escapeHTML(extDe(a.f.name))+' deste tamanho. '+ 'A arte entra na folha pela medida, e a equipe confere antes de imprimir. Em PNG, a prévia aparece.

    '; const mini = a.miniSrc ? '' : ''+escapeHTML(extDe(a.f.name))+''; const sub = a.cm>0 && (a.src||a.semPrevia) ? (a.q>1? a.q+' × ' : '')+''+n1(a.cm)+' × '+n1(a.cm*propDe(a))+' cm' : ''; return '
    '+ '
    '+mini+'
    '+ '
    '+escapeHTML(a.f.name)+''+(a===recemChegada?'nova':'')+ '
    '+ '
    '+medida+''+(a.px>0? a.px+' × '+a.py+' px · ' : '')+ escapeHTML(extDe(a.f.name))+' · '+fmt(a.f.size)+'
    '+ barra+chips+aviso+dica+ '
    '+ '
    '+ '
    '+ 'cm
    '+ '
    '+ ''+ ''+ '
    '+ ''+ ''+ ''+ (sub? '
    '+sub+'
    ' : '')+ '
    '+(TAMANHOS[larguraFilme()]||[]).map(t=>'').join('')+'
    '+ '
    '+ '
    '; }).join(''); $('lista').querySelectorAll('[data-passoa]').forEach(b=>b.addEventListener('click',()=>{ const el=$('q'+b.dataset.i); el.value=Math.max(1,Math.min(200,(+el.value||1)+(+b.dataset.passoa))); el.dispatchEvent(new Event('change')); })); $('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.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.min(200,Math.floor(+i.value||1))); invalidaItemAtual(); previaAoVivo(); })); $('lista').querySelectorAll('[data-usar]').forEach(b=>b.addEventListener('click',()=>{ artes[+b.dataset.usar].cm=+b.dataset.cm; pintaArtes(); })); $('lista').querySelectorAll('[data-tam]').forEach(b=>b.addEventListener('click',()=>{ artes[+b.dataset.tam].cm=+b.dataset.t; pintaArtes(); })); $('lista').querySelectorAll('[data-giro]').forEach(b=>b.addEventListener('click',()=>{ const a=artes[+b.dataset.giro]; a.giro=a.giro?0:90; pintaArtes(); })); $('lista').querySelectorAll('[data-esp]').forEach(b=>b.addEventListener('click',()=>{ const a=artes[+b.dataset.esp]; a.esp=!a.esp; pintaArtes(); })); resumoArtes(); previaAoVivo(); } function resumoArtes(){ const r=$('rA'), prontas=artes.filter(a=>a.cm>0 && a.cm<=larguraFilme() && (a.src||a.semPrevia)); 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(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'; r.innerHTML='
    artes com tamanho'+prontas.length+' de '+artes.length+'
    '+ '
    qualidade média'+media+'%
    '+ (piora pior arte'+pior+'%':'')+ (m>0?'
    folha montada'+fmtM(m)+' m
    '+ (cobrar(m)>m+0.001?'
    cobrança'+fmtM(cobrar(m))+ ' m'+(cobrar(m)===MINIMO_M?' · mínimo':'')+'
    ':''):'')+ (falta?'
    faltam informar'+falta+'
    ':''); }