export_deck_pdf.mjs 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. #!/usr/bin/env node
  2. /**
  3. * export_deck_pdf.mjs — 把多文件 slide deck 导出为单个矢量 PDF
  4. *
  5. * 用法:
  6. * node export_deck_pdf.mjs --slides <dir> --out <file.pdf> [--width 1920] [--height 1080]
  7. *
  8. * 特点:
  9. * - 文字保留矢量(可复制、可搜索)
  10. * - 背景/图形 1:1 保真(Playwright 内嵌 Chromium 渲染)
  11. * - 不需要对 HTML 做任何改造
  12. * - 视觉损失 = 0(PDF 就是浏览器打印出来的)
  13. *
  14. * trade-off:
  15. * - PDF 不可再编辑文字(要改回到 HTML 改)
  16. *
  17. * 依赖:playwright pdf-lib
  18. * npm install playwright pdf-lib
  19. *
  20. * 会按文件名排序(01-xxx.html → 02-xxx.html → ...)
  21. */
  22. import { chromium } from 'playwright';
  23. import { PDFDocument } from 'pdf-lib';
  24. import fs from 'fs/promises';
  25. import path from 'path';
  26. function parseArgs() {
  27. const args = { width: 1920, height: 1080 };
  28. const a = process.argv.slice(2);
  29. for (let i = 0; i < a.length; i += 2) {
  30. const k = a[i].replace(/^--/, '');
  31. args[k] = a[i + 1];
  32. }
  33. if (!args.slides || !args.out) {
  34. console.error('用法: node export_deck_pdf.mjs --slides <dir> --out <file.pdf> [--width 1920] [--height 1080]');
  35. process.exit(1);
  36. }
  37. args.width = parseInt(args.width);
  38. args.height = parseInt(args.height);
  39. return args;
  40. }
  41. async function main() {
  42. const { slides, out, width, height } = parseArgs();
  43. const slidesDir = path.resolve(slides);
  44. const outFile = path.resolve(out);
  45. const files = (await fs.readdir(slidesDir))
  46. .filter(f => f.endsWith('.html'))
  47. .sort();
  48. if (!files.length) {
  49. console.error(`No .html files found in ${slidesDir}`);
  50. process.exit(1);
  51. }
  52. console.log(`Found ${files.length} slides in ${slidesDir}`);
  53. const browser = await chromium.launch();
  54. const ctx = await browser.newContext({ viewport: { width, height } });
  55. // 1) Render each HTML to its own PDF buffer
  56. const pageBuffers = [];
  57. for (const f of files) {
  58. const page = await ctx.newPage();
  59. const url = 'file://' + path.join(slidesDir, f);
  60. await page.goto(url, { waitUntil: 'networkidle' }).catch(() => page.goto(url));
  61. await page.waitForTimeout(1200); // web-font paint
  62. // emulate "screen" so CSS colors/backgrounds render the same as browser
  63. await page.emulateMedia({ media: 'screen' });
  64. const buf = await page.pdf({
  65. width: `${width}px`,
  66. height: `${height}px`,
  67. printBackground: true,
  68. margin: { top: 0, right: 0, bottom: 0, left: 0 },
  69. preferCSSPageSize: false,
  70. });
  71. pageBuffers.push(buf);
  72. await page.close();
  73. console.log(` [${pageBuffers.length}/${files.length}] ${f}`);
  74. }
  75. await browser.close();
  76. // 2) Merge into a single PDF
  77. const merged = await PDFDocument.create();
  78. for (const buf of pageBuffers) {
  79. const src = await PDFDocument.load(buf);
  80. const copied = await merged.copyPages(src, src.getPageIndices());
  81. copied.forEach(p => merged.addPage(p));
  82. }
  83. const bytes = await merged.save();
  84. await fs.writeFile(outFile, bytes);
  85. const kb = (bytes.byteLength / 1024).toFixed(0);
  86. console.log(`\n✓ Wrote ${outFile} (${kb} KB, ${files.length} pages, vector)`);
  87. }
  88. main().catch(e => { console.error(e); process.exit(1); });