chore: remove unused assets and polyfills, add puppeteer for CMYK export automation

This commit is contained in:
Lucas Rayan committed 2026-08-23 12:19:21 -03:00
1 parent 37971056c2
commit 8ad99a971d
15 files changed
+918 -6

No files matched your search

+1
View File
@@ -19,6 +19,7 @@
# production # production
/build /build
/dist
# misc # misc
.DS_Store .DS_Store
+134
View File
@@ -0,0 +1,134 @@
import { execSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { NextResponse } from "next/server";
import puppeteer from "puppeteer";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function GET(request) {
const { searchParams } = new URL(request.url);
const modulo = searchParams.get("modulo") || "1";
const publicDir = path.resolve(process.cwd(), "public/formacoes", modulo);
const htmlFile = path.join(publicDir, "index.html");
if (!fs.existsSync(htmlFile)) {
return NextResponse.json(
{ error: `Módulo ${modulo} não encontrado.` },
{ status: 404 }
);
}
const tmpDir = os.tmpdir();
const tmpRgbPdf = path.join(tmpDir, `espdocs-mod-${modulo}-${Date.now()}-rgb.pdf`);
const tmpCmykPdf = path.join(tmpDir, `espdocs-mod-${modulo}-${Date.now()}-cmyk.pdf`);
let browser = null;
try {
browser = await puppeteer.launch({
headless: "new",
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-web-security",
"--allow-file-access-from-files"
]
});
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 1600, deviceScaleFactor: 2 });
await page.goto(`file://${htmlFile}`, { waitUntil: "networkidle0", timeout: 60000 });
// Aguarda o Paged.js concluir a montagem
await page.waitForFunction(() => {
const pages = document.querySelectorAll(".pagedjs_page");
return pages.length > 0 && !document.querySelector(".aviso-carregando");
}, { timeout: 30000 });
await new Promise((r) => setTimeout(r, 1000));
await page.pdf({
path: tmpRgbPdf,
format: "A4",
printBackground: true,
preferCSSPageSize: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 }
});
await browser.close();
browser = null;
// Converte para CMYK (DeviceCMYK / ISO Coated)
const gsCmd = `gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sColorConversionStrategy=CMYK -dProcessColorModel=/DeviceCMYK -dCompatibilityLevel=1.4 -sOutputFile="${tmpCmykPdf}" "${tmpRgbPdf}"`;
execSync(gsCmd, { stdio: "pipe" });
const pdfBuffer = fs.readFileSync(tmpCmykPdf);
return new NextResponse(pdfBuffer, {
status: 200,
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="LASER-ESP32-Modulo-${modulo}-CMYK.pdf"`,
"Content-Length": pdfBuffer.length.toString()
}
});
} catch (error) {
console.error("Erro na conversão CMYK:", error);
return NextResponse.json(
{ error: "Falha ao gerar PDF CMYK", details: error.message },
{ status: 500 }
);
} finally {
if (browser) await browser.close();
if (fs.existsSync(tmpRgbPdf)) fs.unlinkSync(tmpRgbPdf);
if (fs.existsSync(tmpCmykPdf)) fs.unlinkSync(tmpCmykPdf);
}
}
export async function POST(request) {
try {
const formData = await request.formData();
const file = formData.get("pdf");
if (!file) {
return NextResponse.json({ error: "Nenhum arquivo PDF enviado." }, { status: 400 });
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const tmpDir = os.tmpdir();
const inputPath = path.join(tmpDir, `input-${Date.now()}.pdf`);
const outputPath = path.join(tmpDir, `cmyk-${Date.now()}.pdf`);
fs.writeFileSync(inputPath, buffer);
// Converte com Ghostscript
const gsCmd = `gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sColorConversionStrategy=CMYK -dProcessColorModel=/DeviceCMYK -dCompatibilityLevel=1.4 -sOutputFile="${outputPath}" "${inputPath}"`;
execSync(gsCmd, { stdio: "pipe" });
const outputBuffer = fs.readFileSync(outputPath);
// Limpa temporários
if (fs.existsSync(inputPath)) fs.unlinkSync(inputPath);
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath);
return new NextResponse(outputBuffer, {
status: 200,
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="documento-CMYK.pdf"`,
"Content-Length": outputBuffer.length.toString()
}
});
} catch (error) {
console.error("Erro ao converter PDF POST:", error);
return NextResponse.json(
{ error: "Erro ao converter PDF para CMYK", details: error.message },
{ status: 500 }
);
}
}
+329
View File
@@ -25,6 +25,7 @@
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.5.6", "eslint-config-next": "15.5.6",
"puppeteer": "^25.8.0",
"tailwindcss": "^4" "tailwindcss": "^4"
} }
}, },
@@ -1542,6 +1543,35 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@puppeteer/browsers": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.1.tgz",
"integrity": "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"modern-tar": "^0.8.0",
"yargs": "^18.0.0"
},
"bin": {
"browsers": "lib/main-cli.js"
},
"engines": {
"node": ">=22.12.0"
},
"peerDependencies": {
"proxy-agent": ">=8.0.1",
"yauzl": "^2.10.0 || ^3.4.0"
},
"peerDependenciesMeta": {
"proxy-agent": {
"optional": true
},
"yauzl": {
"optional": true
}
}
},
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -2904,12 +2934,100 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chromium-bidi": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz",
"integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"engines": {
"node": ">=20.19.0 <22.0.0 || >=22.12.0"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/client-only": { "node_modules/client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/cliui": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cliui/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -3126,6 +3244,13 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/devtools-protocol": {
"version": "0.0.1666840",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1666840.tgz",
"integrity": "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/doctrine": { "node_modules/doctrine": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -3381,6 +3506,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -4128,6 +4263,29 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -5436,6 +5594,19 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -5567,6 +5738,23 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"dev": true,
"license": "MIT"
},
"node_modules/modern-tar": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.4.tgz",
"integrity": "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/motion-dom": { "node_modules/motion-dom": {
"version": "12.40.0", "version": "12.40.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz",
@@ -6127,6 +6315,46 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/puppeteer": {
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.8.0.tgz",
"integrity": "sha512-3gcUJ+Jfodb5zNa/lWLZukBUwYiRIwAc8WRICoqfi+ZYmNWqpsPFyanTU3Gw/lhgII9aotVbAmhT6/NKHWgyUA==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.2.1",
"chromium-bidi": "17.0.2",
"devtools-protocol": "0.0.1666840",
"lilconfig": "^3.1.3",
"puppeteer-core": "25.8.0",
"typed-query-selector": "^2.12.2"
},
"bin": {
"puppeteer": "lib/puppeteer/node/cli.js"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/puppeteer-core": {
"version": "25.8.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.8.0.tgz",
"integrity": "sha512-LDOrawV8vfCVk+yLj2ozvajNP4Sv3OV9y3Tpiyy2g2Z+aQlbcozP6KJfI4iSBq7YQER+86ihEtPa5ioiZyWxMQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.2.1",
"chromium-bidi": "17.0.2",
"devtools-protocol": "0.0.1666840",
"typed-query-selector": "^2.12.2",
"webdriver-bidi-protocol": "0.4.2",
"ws": "^8.21.1"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.1", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
@@ -7162,6 +7390,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/typed-query-selector": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -7256,6 +7491,13 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
"integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7458,6 +7700,38 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/ws": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yaml": { "node_modules/yaml": {
"version": "1.10.2", "version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
@@ -7467,6 +7741,51 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/yargs": {
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz",
"integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^8.2.1",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs-parser": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs/node_modules/string-width": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
"integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -7479,6 +7798,16 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }
+3 -1
View File
@@ -7,7 +7,8 @@
"build": "next build --turbopack", "build": "next build --turbopack",
"start": "next start", "start": "next start",
"lint": "eslint", "lint": "eslint",
"auditar-temas": "node scripts/auditar-temas.mjs" "auditar-temas": "node scripts/auditar-temas.mjs",
"exportar-cmyk": "node scripts/exportar-cmyk.mjs"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
@@ -27,6 +28,7 @@
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.5.6", "eslint-config-next": "15.5.6",
"puppeteer": "^25.8.0",
"tailwindcss": "^4" "tailwindcss": "^4"
} }
} }
File renamed without changes.
@@ -1960,12 +1960,147 @@ 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 convertido para espaço de cor 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>
// Tira o aviso do fluxo antes da paginação começar, senão o Paged.js o let paginacaoConcluida = false;
// trataria como conteúdo e ele apareceria impresso na primeira página. 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: () => document.querySelector(".aviso-carregando")?.remove(), before: () => {
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
window.addEventListener("beforeprint", () => {
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) {
const toast = document.getElementById("toastExportacao") || toastExportacao;
if (!toast) return;
if (!document.body.contains(toast)) document.body.appendChild(toast);
toast.textContent = mensagem;
toast.className = `toast-exportacao ativo ${tipo}`;
clearTimeout(toast._timer);
toast._timer = setTimeout(() => {
toast.classList.remove("ativo");
}, duracao);
}
document.addEventListener("click", async (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();
}
if (btnCmyk) {
const textoOriginal = btnCmyk.innerHTML;
try {
btnCmyk.disabled = true;
btnCmyk.classList.add("carregando");
btnCmyk.innerHTML = `<span class="spinner-exportar"></span><span>Convertendo CMYK...</span>`;
exibirToast("Gerando PDF e convertendo para CMYK (ISO Coated)... Aguarde alguns segundos.", "info", 8000);
const resposta = await fetch("/api/export-cmyk?modulo=1");
if (!resposta.ok) {
const erroJson = await resposta.json().catch(() => null);
throw new Error(erroJson?.error || "Serviço de conversão indisponível no momento.");
}
const blob = await resposta.blob();
const url = window.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();
window.URL.revokeObjectURL(url);
a.remove();
exibirToast("✅ PDF em CMYK gerado e baixado com sucesso!", "sucesso", 5000);
} catch (erro) {
console.warn("Erro ao gerar CMYK via API:", erro);
exibirToast(
"💡 Dica: execute 'npm run exportar-cmyk 1' no terminal para gerar o arquivo CMYK imediatamente!",
"erro",
7000
);
} finally {
btnCmyk.disabled = false;
btnCmyk.classList.remove("carregando");
btnCmyk.innerHTML = textoOriginal;
}
}
});
</script> </script>
<script src="../assets/paged.polyfill.js"></script> <script src="../assets/paged.polyfill.js"></script>
</body> </body>
File renamed without changes.
File renamed without changes.
@@ -665,6 +665,237 @@ figcaption {
} }
} }
@media print { /* -------------------------------------------------------------------------
.aviso-carregando { display: none; } 8. Barra Flutuante de Exportação e Impressão (Apenas em Tela)
------------------------------------------------------------------------- */
.painel-exportacao {
position: fixed !important;
top: 18px !important;
right: 20px !important;
z-index: 999999 !important;
display: flex !important;
align-items: center;
gap: 10px;
background: rgba(6, 38, 57, 0.92);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
padding: 6px 10px;
border-radius: 9999px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow:
0 8px 30px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(0, 0, 0, 0.2);
transition: opacity 0.2s ease, transform 0.2s ease;
} }
.painel-exportacao.oculto {
opacity: 0;
pointer-events: none;
transform: translateY(-8px);
}
.painel-exportacao-titulo {
font-family: "Inter", sans-serif;
font-size: 11px;
font-weight: 600;
color: #94a3b8;
padding: 0 4px 0 6px;
letter-spacing: 0.02em;
display: flex;
align-items: center;
gap: 5px;
}
.btn-exportar {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 13px;
border-radius: 9999px;
font-family: "Inter", sans-serif;
font-size: 11.5px;
font-weight: 600;
cursor: pointer;
border: none;
outline: none;
text-decoration: none;
transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1);
white-space: nowrap;
user-select: none;
}
.btn-exportar svg {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.btn-exportar-rgb {
background: rgba(255, 255, 255, 0.12);
color: #f1f5f9;
border: 1px solid rgba(255, 255, 255, 0.18);
}
.btn-exportar-rgb:hover {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
transform: translateY(-1px);
}
.btn-exportar-rgb:active {
transform: translateY(0);
background: rgba(255, 255, 255, 0.1);
}
.btn-exportar-cmyk {
background: linear-gradient(135deg, #F77F00, #D62828);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 10px rgba(214, 40, 40, 0.35);
}
.btn-exportar-cmyk:hover {
background: linear-gradient(135deg, #ff911a, #e63939);
box-shadow: 0 4px 14px rgba(214, 40, 40, 0.5);
transform: translateY(-1px);
}
.btn-exportar-cmyk:active {
transform: translateY(0);
box-shadow: 0 1px 4px rgba(214, 40, 40, 0.3);
}
.btn-exportar:disabled,
.btn-exportar.carregando {
opacity: 0.75;
cursor: wait;
pointer-events: none;
}
.spinner-exportar {
width: 13px;
height: 13px;
border: 2px solid rgba(255, 255, 255, 0.35);
border-top-color: #ffffff;
border-radius: 50%;
animation: girar-spinner 0.75s linear infinite;
}
@keyframes girar-spinner {
to { transform: rotate(360deg); }
}
/* Toast de notificação de exportação */
.toast-exportacao {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 10001;
background: #062639;
color: #e2e8f0;
border: 1px solid #33566a;
padding: 10px 16px;
border-radius: 8px;
font-family: "Inter", sans-serif;
font-size: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
gap: 10px;
transition: all 0.3s ease;
opacity: 0;
transform: translateY(12px);
pointer-events: none;
}
.toast-exportacao.ativo {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.toast-exportacao.sucesso { border-color: #10b981; }
.toast-exportacao.erro { border-color: #ef4444; }
/* -------------------------------------------------------------------------
9. Regras Especiais para Impressão Física e Geração de PDF (@media print)
------------------------------------------------------------------------- */
@media print {
.aviso-carregando {
display: none !important;
}
/*
BLOCO DE CÓDIGO - TEMA CLARO DE ALTO CONTRASTE
Elimina o fundo escuro pesado no papel e resolve o problema de
sangria de tinta (dot gain) que engolia as letras.
*/
pre {
background: #f8fafc !important;
border: 0.35mm solid #cbd5e1 !important;
color: #0f172a !important;
box-shadow: none !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
pre code {
background: transparent !important;
border: none !important;
padding: 0 !important;
color: #0f172a !important;
font-weight: 500 !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
pre span {
background: transparent !important;
border: none !important;
padding: 0 !important;
}
/* Cores de sintaxe de alta densidade e seguras para CMYK/P&B */
.c-com {
color: #475569 !important;
font-style: italic !important;
}
.c-key {
color: #6b21a8 !important;
font-weight: 600 !important;
}
.c-fun {
color: #0369a1 !important;
font-weight: 600 !important;
}
.c-str {
color: #15803d !important;
}
.c-num {
color: #b45309 !important;
font-weight: 600 !important;
}
.c-pre {
color: #be185d !important;
font-weight: 600 !important;
}
/* Código inline (apenas fora do pre) */
:not(pre) > code {
background: #f1f5f9 !important;
color: #9a3412 !important;
border: 0.2mm solid #cbd5e1 !important;
padding: 0.4mm 1.2mm !important;
border-radius: 1mm !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
}
File renamed without changes.
+80
View File
@@ -0,0 +1,80 @@
import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import puppeteer from "puppeteer";
const modulo = process.argv[2] || "1";
const publicDir = path.resolve("public/formacoes", modulo);
const htmlFile = path.join(publicDir, "index.html");
if (!fs.existsSync(htmlFile)) {
console.error(`❌ Erro: Apostila do módulo ${modulo} não encontrada em ${htmlFile}`);
process.exit(1);
}
const outputDir = path.resolve("dist/apostilas");
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const tmpRgbPdf = path.join(outputDir, `modulo-${modulo}-rgb-temp.pdf`);
const finalCmykPdf = path.join(outputDir, `LASER-ESP32-Modulo-${modulo}-CMYK.pdf`);
console.log(`\n🚀 Iniciando exportação da Apostila (Módulo ${modulo})...`);
console.log(`📄 Arquivo fonte: ${htmlFile}`);
async function exportar() {
console.log("\n[1/3] Renderizando páginas no Puppeteer e aguardando Paged.js...");
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-web-security", "--allow-file-access-from-files"]
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 1600, deviceScaleFactor: 2 });
await page.goto(`file://${htmlFile}`, { waitUntil: "networkidle0", timeout: 60000 });
// Aguarda o Paged.js concluir a montagem de todas as páginas
await page.waitForFunction(() => {
const pages = document.querySelectorAll(".pagedjs_page");
return pages.length > 0 && !document.querySelector(".aviso-carregando");
}, { timeout: 30000 });
// Aguarda 1 segundo adicional para renderização de fontes e SVG
await new Promise((r) => setTimeout(r, 1000));
console.log("[2/3] Gerando PDF em alta resolução...");
await page.pdf({
path: tmpRgbPdf,
format: "A4",
printBackground: true,
preferCSSPageSize: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 }
});
console.log(`[3/3] Convertendo espaço de cor para CMYK (ISO Coated) via Ghostscript...`);
const gsCmd = `gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sColorConversionStrategy=CMYK -dProcessColorModel=/DeviceCMYK -dCompatibilityLevel=1.4 -sOutputFile="${finalCmykPdf}" "${tmpRgbPdf}"`;
execSync(gsCmd, { stdio: "pipe" });
if (fs.existsSync(tmpRgbPdf)) {
fs.unlinkSync(tmpRgbPdf);
}
const stats = fs.statSync(finalCmykPdf);
const tamanhoMB = (stats.size / (1024 * 1024)).toFixed(2);
console.log(`\n✅ PDF em CMYK gerado com sucesso!`);
console.log(`📂 Destino: ${finalCmykPdf}`);
console.log(`📊 Tamanho: ${tamanhoMB} MB\n`);
} catch (err) {
console.error("❌ Erro durante a geração:", err);
process.exit(1);
} finally {
await browser.close();
}
}
exportar();