diff --git a/.gitignore b/.gitignore index 5ef6a52..c5e8b4f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ # production /build +/dist # misc .DS_Store diff --git a/app/api/export-cmyk/route.js b/app/api/export-cmyk/route.js new file mode 100644 index 0000000..6666926 --- /dev/null +++ b/app/api/export-cmyk/route.js @@ -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 } + ); + } +} diff --git a/package-lock.json b/package-lock.json index fb7b9fe..5800c8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@tailwindcss/postcss": "^4", "eslint": "^9", "eslint-config-next": "15.5.6", + "puppeteer": "^25.8.0", "tailwindcss": "^4" } }, @@ -1542,6 +1543,35 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2904,12 +2934,100 @@ "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": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3126,6 +3244,13 @@ "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3381,6 +3506,16 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4128,6 +4263,29 @@ "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": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5436,6 +5594,19 @@ "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": { "version": "1.2.4", "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_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": { "version": "12.40.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", @@ -6127,6 +6315,46 @@ "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": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -7162,6 +7390,13 @@ "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": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -7256,6 +7491,13 @@ "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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7458,6 +7700,38 @@ "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": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", @@ -7467,6 +7741,51 @@ "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": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -7479,6 +7798,16 @@ "funding": { "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" + } } } } diff --git a/package.json b/package.json index 730f112..656034d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "build": "next build --turbopack", "start": "next start", "lint": "eslint", - "auditar-temas": "node scripts/auditar-temas.mjs" + "auditar-temas": "node scripts/auditar-temas.mjs", + "exportar-cmyk": "node scripts/exportar-cmyk.mjs" }, "dependencies": { "@emotion/react": "^11.14.0", @@ -27,6 +28,7 @@ "@tailwindcss/postcss": "^4", "eslint": "^9", "eslint-config-next": "15.5.6", + "puppeteer": "^25.8.0", "tailwindcss": "^4" } } diff --git a/public/apostilas/1/.next/trace b/public/formacoes/1/.next/trace similarity index 100% rename from public/apostilas/1/.next/trace rename to public/formacoes/1/.next/trace diff --git a/public/apostilas/1/esp32-s3-blocos.png b/public/formacoes/1/esp32-s3-blocos.png similarity index 100% rename from public/apostilas/1/esp32-s3-blocos.png rename to public/formacoes/1/esp32-s3-blocos.png diff --git a/public/apostilas/1/index.html b/public/formacoes/1/index.html similarity index 94% rename from public/apostilas/1/index.html rename to public/formacoes/1/index.html index c906df2..65af4d9 100644 --- a/public/apostilas/1/index.html +++ b/public/formacoes/1/index.html @@ -1960,12 +1960,147 @@ Temperatura: 26.1 C | Umidade: 62.0 %

+ +
+ + + + + + + Apostila: + + + +
+ +
+ diff --git a/public/apostilas/1/pratica.fzz b/public/formacoes/1/pratica.fzz similarity index 100% rename from public/apostilas/1/pratica.fzz rename to public/formacoes/1/pratica.fzz diff --git a/public/apostilas/1/pratica.png b/public/formacoes/1/pratica.png similarity index 100% rename from public/apostilas/1/pratica.png rename to public/formacoes/1/pratica.png diff --git a/public/apostilas/assets/apostila.css b/public/formacoes/assets/apostila.css similarity index 76% rename from public/apostilas/assets/apostila.css rename to public/formacoes/assets/apostila.css index 4243041..17fd3e2 100644 --- a/public/apostilas/assets/apostila.css +++ b/public/formacoes/assets/apostila.css @@ -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; + } +} + diff --git a/public/apostilas/assets/capa-curvas.svg b/public/formacoes/assets/capa-curvas.svg similarity index 100% rename from public/apostilas/assets/capa-curvas.svg rename to public/formacoes/assets/capa-curvas.svg diff --git a/public/apostilas/assets/laser-escuro.svg b/public/formacoes/assets/laser-escuro.svg similarity index 100% rename from public/apostilas/assets/laser-escuro.svg rename to public/formacoes/assets/laser-escuro.svg diff --git a/public/apostilas/assets/laser.svg b/public/formacoes/assets/laser.svg similarity index 100% rename from public/apostilas/assets/laser.svg rename to public/formacoes/assets/laser.svg diff --git a/public/apostilas/assets/paged.polyfill.js b/public/formacoes/assets/paged.polyfill.js similarity index 100% rename from public/apostilas/assets/paged.polyfill.js rename to public/formacoes/assets/paged.polyfill.js diff --git a/scripts/exportar-cmyk.mjs b/scripts/exportar-cmyk.mjs new file mode 100644 index 0000000..857d5e7 --- /dev/null +++ b/scripts/exportar-cmyk.mjs @@ -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();