refactor: implement server-side CMYK PDF caching and improve frontend export UI with dynamic DOM injection
This commit is contained in:
1 parent
afa1a6e027
commit
d530356101
3 files changed
+120
-118
No files matched your search
@@ -23,10 +23,28 @@ export async function GET(request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tmpDir = os.tmpdir();
|
const tmpDir = os.tmpdir();
|
||||||
const tmpRgbPdf = path.join(tmpDir, `espdocs-mod-${modulo}-${Date.now()}-rgb.pdf`);
|
const cachedCmykPdf = path.join(tmpDir, `espdocs-cached-cmyk-mod-${modulo}.pdf`);
|
||||||
const tmpCmykPdf = path.join(tmpDir, `espdocs-mod-${modulo}-${Date.now()}-cmyk.pdf`);
|
const htmlMtime = fs.statSync(htmlFile).mtimeMs;
|
||||||
|
|
||||||
|
// Se já foi gerado no /tmp e o HTML não mudou, entrega instantaneamente
|
||||||
|
if (fs.existsSync(cachedCmykPdf)) {
|
||||||
|
const cachedMtime = fs.statSync(cachedCmykPdf).mtimeMs;
|
||||||
|
if (cachedMtime >= htmlMtime) {
|
||||||
|
const cachedBuffer = fs.readFileSync(cachedCmykPdf);
|
||||||
|
return new NextResponse(cachedBuffer, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/pdf",
|
||||||
|
"Content-Disposition": `attachment; filename="LASER-ESP32-Modulo-${modulo}-CMYK.pdf"`,
|
||||||
|
"Content-Length": cachedBuffer.length.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmpRgbPdf = path.join(tmpDir, `espdocs-tmp-${modulo}-${Date.now()}-rgb.pdf`);
|
||||||
let browser = null;
|
let browser = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
browser = await puppeteer.launch({
|
browser = await puppeteer.launch({
|
||||||
headless: "new",
|
headless: "new",
|
||||||
@@ -40,15 +58,15 @@ export async function GET(request) {
|
|||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setViewport({ width: 1200, height: 1600, deviceScaleFactor: 2 });
|
await page.setViewport({ width: 1200, height: 1600, deviceScaleFactor: 2 });
|
||||||
await page.goto(`file://${htmlFile}`, { waitUntil: "networkidle0", timeout: 60000 });
|
await page.goto(`file://${htmlFile}`, { waitUntil: "load", timeout: 30000 });
|
||||||
|
|
||||||
// Aguarda o Paged.js concluir a montagem
|
// Aguarda o Paged.js concluir a montagem
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const pages = document.querySelectorAll(".pagedjs_page");
|
const pages = document.querySelectorAll(".pagedjs_page");
|
||||||
return pages.length > 0 && !document.querySelector(".aviso-carregando");
|
return pages.length > 0 && !document.querySelector(".aviso-carregando");
|
||||||
}, { timeout: 30000 });
|
}, { timeout: 25000 });
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 1000));
|
await new Promise((r) => setTimeout(r, 600));
|
||||||
|
|
||||||
await page.pdf({
|
await page.pdf({
|
||||||
path: tmpRgbPdf,
|
path: tmpRgbPdf,
|
||||||
@@ -62,10 +80,10 @@ export async function GET(request) {
|
|||||||
browser = null;
|
browser = null;
|
||||||
|
|
||||||
// Converte para CMYK (DeviceCMYK / ISO Coated)
|
// Converte para CMYK (DeviceCMYK / ISO Coated)
|
||||||
const gsCmd = `gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sColorConversionStrategy=CMYK -dProcessColorModel=/DeviceCMYK -dCompatibilityLevel=1.4 -sOutputFile="${tmpCmykPdf}" "${tmpRgbPdf}"`;
|
const gsCmd = `gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sColorConversionStrategy=CMYK -dProcessColorModel=/DeviceCMYK -dCompatibilityLevel=1.4 -sOutputFile="${cachedCmykPdf}" "${tmpRgbPdf}"`;
|
||||||
execSync(gsCmd, { stdio: "pipe" });
|
execSync(gsCmd, { stdio: "pipe" });
|
||||||
|
|
||||||
const pdfBuffer = fs.readFileSync(tmpCmykPdf);
|
const pdfBuffer = fs.readFileSync(cachedCmykPdf);
|
||||||
|
|
||||||
return new NextResponse(pdfBuffer, {
|
return new NextResponse(pdfBuffer, {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -84,7 +102,6 @@ export async function GET(request) {
|
|||||||
} finally {
|
} finally {
|
||||||
if (browser) await browser.close();
|
if (browser) await browser.close();
|
||||||
if (fs.existsSync(tmpRgbPdf)) fs.unlinkSync(tmpRgbPdf);
|
if (fs.existsSync(tmpRgbPdf)) fs.unlinkSync(tmpRgbPdf);
|
||||||
if (fs.existsSync(tmpCmykPdf)) fs.unlinkSync(tmpCmykPdf);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
[{"name":"generate-buildid","duration":224,"timestamp":13103889101,"id":4,"parentId":1,"tags":{},"startTime":1787229898989,"traceId":"aad4d6d36b5e5e6b"},{"name":"load-custom-routes","duration":309,"timestamp":13103889458,"id":5,"parentId":1,"tags":{},"startTime":1787229898989,"traceId":"aad4d6d36b5e5e6b"},{"name":"next-build","duration":129948,"timestamp":13103762107,"id":1,"tags":{"buildMode":"default","isTurboBuild":"false","version":"15.5.9"},"startTime":1787229898862,"traceId":"aad4d6d36b5e5e6b"}]
|
|
||||||
+95
-109
@@ -1961,126 +1961,112 @@ Temperatura: 26.1 C | Umidade: 62.0 %</code></pre>
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ══════════════════════ PAINEL DE EXPORTAÇÃO ══════════════════════ -->
|
|
||||||
<div class="painel-exportacao" id="painelExportacao" style="position: fixed !important; top: 18px !important; right: 20px !important; z-index: 999999 !important; display: flex !important;">
|
|
||||||
<span class="painel-exportacao-titulo">
|
|
||||||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<polyline points="6 9 6 2 18 2 18 9"></polyline>
|
|
||||||
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
|
|
||||||
<rect x="6" y="14" width="12" height="8"></rect>
|
|
||||||
</svg>
|
|
||||||
Apostila:
|
|
||||||
</span>
|
|
||||||
<button type="button" class="btn-exportar btn-exportar-rgb" id="btnExportarRgb" title="Imprimir ou Salvar em PDF (padrão RGB do navegador)">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
||||||
<polyline points="14 2 14 8 20 8"></polyline>
|
|
||||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
|
||||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
|
||||||
<polyline points="10 9 9 9 8 9"></polyline>
|
|
||||||
</svg>
|
|
||||||
<span>Exportar PDF</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn-exportar btn-exportar-cmyk" id="btnExportarCmyk" title="Exportar PDF em CMYK (ISO Coated) para gráfica">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path>
|
|
||||||
<path d="M2 12h20"></path>
|
|
||||||
</svg>
|
|
||||||
<span>Exportar em CMYK</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toast-exportacao" id="toastExportacao" style="position: fixed !important; bottom: 24px !important; right: 24px !important; z-index: 1000000 !important;"></div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let paginacaoConcluida = false;
|
|
||||||
const painelExportacao = document.getElementById("painelExportacao");
|
|
||||||
const toastExportacao = document.getElementById("toastExportacao");
|
|
||||||
|
|
||||||
function fixarPainelNaTela() {
|
|
||||||
if (painelExportacao) {
|
|
||||||
painelExportacao.style.cssText = "position: fixed !important; top: 18px !important; right: 20px !important; z-index: 999999 !important; display: flex !important;";
|
|
||||||
if (!document.body.contains(painelExportacao)) {
|
|
||||||
document.body.appendChild(painelExportacao);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (toastExportacao) {
|
|
||||||
toastExportacao.style.cssText = "position: fixed !important; bottom: 24px !important; right: 24px !important; z-index: 1000000 !important;";
|
|
||||||
if (!document.body.contains(toastExportacao)) {
|
|
||||||
document.body.appendChild(toastExportacao);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.PagedConfig = {
|
window.PagedConfig = {
|
||||||
before: () => {
|
before: () => document.querySelector(".aviso-carregando")?.remove(),
|
||||||
document.querySelector(".aviso-carregando")?.remove();
|
|
||||||
// Remove do fluxo do DOM para o Paged.js não paginar como conteúdo
|
|
||||||
painelExportacao?.remove();
|
|
||||||
toastExportacao?.remove();
|
|
||||||
},
|
|
||||||
after: () => {
|
|
||||||
paginacaoConcluida = true;
|
|
||||||
document.body.classList.add("pagedjs-pronto");
|
|
||||||
fixarPainelNaTela();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Oculta painel na impressão nativa
|
function montarPainelFlutuante() {
|
||||||
window.addEventListener("beforeprint", () => {
|
if (document.getElementById("painelExportacao")) return;
|
||||||
if (painelExportacao) painelExportacao.style.setProperty("display", "none", "important");
|
|
||||||
if (toastExportacao) toastExportacao.style.setProperty("display", "none", "important");
|
|
||||||
});
|
|
||||||
window.addEventListener("afterprint", () => {
|
|
||||||
if (painelExportacao) painelExportacao.style.setProperty("display", "flex", "important");
|
|
||||||
});
|
|
||||||
|
|
||||||
function exibirToast(mensagem, tipo = "info", duracao = 4000) {
|
// Injeta estilo para position fixed sem ser sanitizado pelo parser do Paged.js
|
||||||
const toast = document.getElementById("toastExportacao") || toastExportacao;
|
const style = document.createElement("style");
|
||||||
if (!toast) return;
|
style.textContent = `
|
||||||
if (!document.body.contains(toast)) document.body.appendChild(toast);
|
#painelExportacao {
|
||||||
toast.textContent = mensagem;
|
position: fixed !important;
|
||||||
toast.className = `toast-exportacao ativo ${tipo}`;
|
top: 18px !important;
|
||||||
clearTimeout(toast._timer);
|
right: 20px !important;
|
||||||
toast._timer = setTimeout(() => {
|
z-index: 999999 !important;
|
||||||
toast.classList.remove("ativo");
|
display: flex !important;
|
||||||
}, duracao);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
|
||||||
const btnRgb = e.target.closest("#btnExportarRgb");
|
|
||||||
const btnCmyk = e.target.closest("#btnExportarCmyk");
|
|
||||||
|
|
||||||
if (btnRgb) {
|
|
||||||
if (!paginacaoConcluida) {
|
|
||||||
exibirToast("Aguarde a paginação terminar antes de exportar...", "info", 2500);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
window.print();
|
#toastExportacao {
|
||||||
}
|
position: fixed !important;
|
||||||
|
bottom: 24px !important;
|
||||||
|
right: 24px !important;
|
||||||
|
z-index: 1000000 !important;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
#painelExportacao, #toastExportacao {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
if (btnCmyk) {
|
const painel = document.createElement("div");
|
||||||
const textoOriginal = btnCmyk.innerHTML;
|
painel.id = "painelExportacao";
|
||||||
btnCmyk.disabled = true;
|
painel.className = "painel-exportacao";
|
||||||
btnCmyk.classList.add("carregando");
|
painel.innerHTML = `
|
||||||
btnCmyk.innerHTML = `<span class="spinner-exportar"></span><span>Processando CMYK...</span>`;
|
<span class="painel-exportacao-titulo">
|
||||||
|
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
|
||||||
|
Apostila:
|
||||||
|
</span>
|
||||||
|
<button type="button" class="btn-exportar btn-exportar-rgb" id="btnExportarRgb" title="Imprimir ou Salvar em PDF (padrão RGB)">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
||||||
|
<span>Exportar PDF</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn-exportar btn-exportar-cmyk" id="btnExportarCmyk" title="Exportar PDF em CMYK (ISO Coated) para gráfica">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path><path d="M2 12h20"></path></svg>
|
||||||
|
<span>Exportar em CMYK</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const toast = document.createElement("div");
|
||||||
|
toast.id = "toastExportacao";
|
||||||
|
toast.className = "toast-exportacao";
|
||||||
|
|
||||||
|
document.body.appendChild(painel);
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
document.getElementById("btnExportarRgb").onclick = () => window.print();
|
||||||
|
|
||||||
|
document.getElementById("btnExportarCmyk").onclick = async () => {
|
||||||
|
const btn = document.getElementById("btnExportarCmyk");
|
||||||
|
const txt = btn.innerHTML;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = `<span class="spinner-exportar"></span><span>Baixando CMYK...</span>`;
|
||||||
exibirToast("Gerando PDF em CMYK (ISO Coated)... O download iniciará em instantes.", "info", 15000);
|
exibirToast("Gerando PDF em CMYK (ISO Coated)... O download iniciará em instantes.", "info", 15000);
|
||||||
|
|
||||||
const iframe = document.createElement("iframe");
|
try {
|
||||||
iframe.style.display = "none";
|
const res = await fetch("/api/export-cmyk?modulo=1");
|
||||||
iframe.src = "/api/export-cmyk?modulo=1";
|
if (!res.ok) throw new Error("Erro no servidor (" + res.status + ")");
|
||||||
document.body.appendChild(iframe);
|
const blob = await res.blob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.style.display = "none";
|
||||||
|
a.href = url;
|
||||||
|
a.download = "LASER-ESP32-Modulo-1-CMYK.pdf";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, 1000);
|
||||||
|
exibirToast("✅ PDF em CMYK baixado com sucesso!", "sucesso", 4000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro CMYK:", err);
|
||||||
|
exibirToast("❌ Erro ao baixar PDF: " + err.message, "erro", 6000);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = txt;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
function exibirToast(msg, tipo = "info", dur = 4000) {
|
||||||
btnCmyk.disabled = false;
|
const t = document.getElementById("toastExportacao");
|
||||||
btnCmyk.classList.remove("carregando");
|
if (!t) return;
|
||||||
btnCmyk.innerHTML = textoOriginal;
|
t.textContent = msg;
|
||||||
setTimeout(() => iframe.remove(), 60000);
|
t.className = "toast-exportacao ativo " + tipo;
|
||||||
}, 8000);
|
clearTimeout(t._timer);
|
||||||
}
|
t._timer = setTimeout(() => t.classList.remove("ativo"), dur);
|
||||||
});
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="../assets/paged.polyfill.js"></script>
|
<script src="../assets/paged.polyfill.js"></script>
|
||||||
|
<script>
|
||||||
|
class ExportHandler extends window.Paged.Handler {
|
||||||
|
afterRendered() {
|
||||||
|
montarPainelFlutuante();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.Paged.registerHandlers(ExportHandler);
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user