/** * Exporta o material de uma formação para PDF: os slides em 16:9 exato e a * apostila em A4, já paginada pelo Paged.js. * * npm run slides-pdf → módulo 1 * npm run slides-pdf -- 2 → módulo 2 * * A página do PDF dos slides tem 1280 x 720 px (338,667 x 190,5 mm), a mesma * caixa do palco do slide.html, então nada é reescalado nem cortado. Os dois * arquivos saem ao lado do slide.html e da apostila.html da formação. * * Existe porque a caixa de impressão do navegador depende de o usuário lembrar * de marcar "gráficos de segundo plano" e "margens: nenhuma". Aqui isso é fixo. */ import http from "node:http"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import puppeteer from "puppeteer"; const raiz = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const modulo = (process.argv[2] || "1").replace(/\D/g, "") || "1"; const pastaModulo = path.join(raiz, "public", "formacoes", modulo); const saida = path.join(pastaModulo, `LASER-ESP32-Modulo-${modulo}-Slides.pdf`); const saidaApostila = path.join(pastaModulo, `LASER-ESP32-Modulo-${modulo}-Apostila.pdf`); if (!fs.existsSync(path.join(pastaModulo, "slide.html"))) { console.error(`Não achei public/formacoes/${modulo}/slide.html`); process.exit(1); } /* Um servidor estático efêmero: com file:// o Chrome bloqueia as fontes e as imagens de pastas irmãs, e o PDF sai com o tipo errado. */ const TIPOS = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".json": "application/json", ".woff2": "font/woff2", }; const servidor = http.createServer((req, res) => { const rel = decodeURIComponent(req.url.split("?")[0]); const arquivo = path.join(raiz, "public", path.normalize(rel).replace(/^(\.\.[/\\])+/, "")); if (!arquivo.startsWith(path.join(raiz, "public")) || !fs.existsSync(arquivo) || fs.statSync(arquivo).isDirectory()) { res.writeHead(404).end("não encontrado"); return; } res.writeHead(200, { "Content-Type": TIPOS[path.extname(arquivo)] || "application/octet-stream" }); fs.createReadStream(arquivo).pipe(res); }); await new Promise((r) => servidor.listen(0, "127.0.0.1", r)); const porta = servidor.address().port; const url = `http://127.0.0.1:${porta}/formacoes/${modulo}/slide.html`; console.log(`[1/4] Abrindo ${url}`); const navegador = await puppeteer.launch({ headless: "new", args: ["--no-sandbox"] }); try { const pagina = await navegador.newPage(); await pagina.goto(url, { waitUntil: "networkidle0", timeout: 60000 }); await pagina.evaluate(() => document.fonts.ready); console.log("[2/4] Conferindo o encaixe de cada slide no 16:9"); await pagina.emulateMediaType("print"); await new Promise((r) => setTimeout(r, 500)); const caixas = await pagina.evaluate(() => [...document.querySelectorAll("#palco .slide")].map((s) => { const b = s.getBoundingClientRect(); return { t: s.dataset.titulo, w: Math.round(b.width), h: Math.round(b.height) }; }) ); const fora = caixas.filter((c) => c.w !== 1280 || c.h !== 720); if (fora.length) { console.warn(` atenção: ${fora.length} slide(s) fora de 1280x720:`, fora.slice(0, 5)); } else { console.log(` ${caixas.length} slides, todos em 1280x720`); } console.log("[3/4] Gerando o PDF dos slides"); await pagina.pdf({ path: saida, printBackground: true, preferCSSPageSize: true, margin: { top: 0, right: 0, bottom: 0, left: 0 }, }); const kb = (fs.statSync(saida).size / 1024).toFixed(0); console.log(` ${path.relative(raiz, saida)} (${kb} KB, ${caixas.length} páginas em 16:9)`); /* A apostila é paginada no navegador pelo Paged.js, que não avisa quando termina. Espera-se o número de folhas parar de crescer. */ console.log("[4/4] Paginando e gerando o PDF da apostila"); const apostila = await navegador.newPage(); await apostila.goto(`http://127.0.0.1:${porta}/formacoes/${modulo}/apostila.html`, { waitUntil: "networkidle0", timeout: 120000 }); await apostila.waitForFunction(() => document.querySelectorAll(".pagedjs_page").length > 1, { timeout: 120000 }); let folhas = 0; for (;;) { await new Promise((r) => setTimeout(r, 1500)); const agora = await apostila.evaluate(() => document.querySelectorAll(".pagedjs_page").length); if (agora === folhas) break; folhas = agora; } await apostila.evaluate(() => document.fonts.ready); await apostila.pdf({ path: saidaApostila, printBackground: true, preferCSSPageSize: true, margin: { top: 0, right: 0, bottom: 0, left: 0 }, }); const kbA = (fs.statSync(saidaApostila).size / 1024).toFixed(0); console.log(` ${path.relative(raiz, saidaApostila)} (${kbA} KB, ${folhas} páginas em A4)`); } finally { await navegador.close(); servidor.close(); }