Skill: karinecsy-collab/awesome-deck-pdf-check (version 1.0.5)

--- name: html-ppt-to-pdf description: Turn any design style into a polished HTML slide deck and export it as a high-quality PDF. Triggers when users say things like "make me a nice PPT", "create slides based on X style", "build a deck", "generate presentation". Supports four input types: (1) Upload a .pptx template file — extract its colors, fonts and layout via python-pptx; (2) Share an image or screenshot of a design; (3) Provide a website URL to clone its visual style; (4) Describe a style in words (e.g. "Apple style", "Notion minimal", "Linear dark"). Output: pixel-sharp 1440×900 slides exported to PDF via Puppeteer, production-quality design. --- # HTML → PDF Slide Workflow ## ‼️ STOP — Read Before Doing Anything Before writing a single line of code or HTML, you MUST complete these two confirmation steps **in order**. Do not proceed until you receive explicit user approval for each. ### Confirmation 1 of 2: Design Style Analyze the user's input (image / URL / keyword / .pptx), then show a design spec summary and ask: > Here's the design style I extracted from [source]: > - Background: `#000` dark / Accent: `#2997FF` blue > - Font: SF Pro Display, oversized heading, minimal whitespace > - Style: dark Keynote, glow effects > > **Does this match what you want? Any changes before I start?** If the user provided a website URL and fetching fails (Fake IP, login wall, etc.): - Say exactly why it failed - Ask: **"Could you send a few screenshots of the site? I'll analyze the design from those."** - Do NOT assume or default to any style Wait for explicit approval → then move to Confirmation 2. ### Confirmation 2 of 2: Slide Content Ask the user if they have existing materials: > Do you have reference materials for the content? > (existing PPT, doc, data, bullet points — anything works) > If yes, send them over. If not, I'll draft an outline for you to review. Once you receive a reply, prepare a numbered list: slide title + one-line summary per slide. Show it and ask: > Here's the planned structure — does this look right? Anything to add, remove, or change? Wait for explicit approval → **only then** begin generating HTML. --- **If either confirmation is skipped, the workflow is broken. Start over from Confirmation 1.** --- **`export_pdf.js` auto-detects Chrome in this order:** 1. Puppeteer bundled Chromium (`npm install puppeteer`) 2. System Chrome/Chromium (macOS: `/Applications/Google Chrome.app`, Linux: `/usr/bin/chromium`) 3. If neither found → prints a clear error with install instructions, exits **If `npm install puppeteer` fails (sandbox/network restriction):** ```bash # Option A: skip Chromium download, install separately npm install puppeteer --ignore-scripts npx puppeteer browsers install chrome # Option B: use puppeteer-core (no bundled Chrome download) npm install puppeteer-core # script will auto-find system Chrome # Option C: install system Chrome first, then run script # macOS: brew install --cask google-chrome # Linux: sudo apt install chromium-browser npm install puppeteer-core ``` **Do NOT fall back to wkhtmltopdf, pdfkit, weasyprint, or any other tool.** For full installation instructions across OpenClaw, Claude Code, Codex, and Linux/macOS setup, see `references/install.md`. --- ## Workflow Overview ``` User input (image / website URL / style keyword / .pptx file) ↓ Step 1: Extract design spec → ⚠️ Confirm with user ↓ Step 2: Outline slide content → ⚠️ Confirm with user ↓ Step 3: Generate HTML → 💾 Save slides.html to disk ↓ Step 4: Run export_pdf.js → 💾 Output slides.pdf Both slides.html and slides.pdf must exist as final outputs. ``` --- ## ⚠️ Two Mandatory Confirmation Gates (never skip) ### Gate 1: Design Style After analyzing the input, show the extracted design spec summary and ask for confirmation: > Here's what I extracted from [source]: > - Background: `#000000` black · Accent: `#2997FF` apple blue > - Font: SF Pro Display, oversized centered headline > - Style: dark Keynote, glow effects > > Does this match the style you want? Any adjustments? **⚠️ If website fetch fails (e.g. Fake IP / login required):** - Tell the user clearly why it failed - Ask for screenshots: **"Could you send a few screenshots of the site? I'll analyze the design from those."** - **Never assume or default to any style** — wait for user confirmation before proceeding **Website analysis priority:** 1. Puppeteer screenshot → visual analysis (most accurate) 2. `web_fetch` page content → CSS/font extraction 3. Ask user to send screenshots ### Gate 2: Slide Content **First ask if the user has existing materials:** > Do you have any reference materials for the content? > e.g. existing PPT, doc, report, or a list of key points? > Send them over and I'll structure the slides — otherwise I'll draft an outline for you to review. After receiving a reply, prepare a one-line summary per slide and confirm before generating HTML. --- ## Step 1: Extract Design Spec → DESIGN.md Supported inputs: | Input | Approach | |---|---| | 📂 .pptx template | Parse with python-pptx: extract colors, fonts, backgrounds, placeholder layouts (most accurate) | | 📎 Image / screenshot | Visual analysis: colors, font style, spacing, component patterns | | 🌐 Website URL | Puppeteer screenshot + `getComputedStyle` font/color extraction | | 💬 Style keyword | Use brand knowledge: Apple → SF Pro, Notion → Inter, Linear → Inter, Google → Google Sans | **Extracting from .pptx:** ```python from pptx import Presentation prs = Presentation("template.pptx") slide = prs.slides[0] for shape in slide.shapes: if shape.has_text_frame: for para in shape.text_frame.paragraphs: for run in para.runs: print(run.font.name, run.font.size, run.font.color.rgb, run.font.bold) ``` Fill extracted values into DESIGN.md (see `references/DESIGN_TEMPLATE.md`): - **Colors**: primary bg, secondary bg, text colors, accent, border → CSS variables - **Fonts**: heading / body / decorative number — name, weight, size range per level - **Layout**: padding, max-width, grid columns, gap, border-radius - **Components**: card, badge/label, button, divider, background decoration - **Section list**: CSS class + content theme + dark/light tone per slide **Font extraction by input type:** | Input | Method | |---|---| | `.pptx` | `run.font.name` per text run | | Website | `getComputedStyle(el).fontFamily` via Puppeteer | | Image / screenshot | Visual analysis — describe closest match (e.g. "geometric sans, similar to Helvetica") | | Style keyword | Brand knowledge mapping | Fonts go into DESIGN.md as CSS variables. Append Chinese fallbacks at the end (never hardcode them as primary): ```css --font-heading: 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', sans-serif; --font-body: 'SF Pro Text', 'PingFang SC', 'Helvetica Neue', sans-serif; ``` --- ## Step 2: Generate HTML > ⚠️ **Save the HTML to a `.html` file first. Do not merge HTML generation and PDF export into one step.** The HTML file is a required intermediate artifact — it must exist on disk before running `export_pdf.js`. Generate a single `.html` file and write it to disk (e.g. `slides.html`): - Single file, all CSS inlined, zero external dependencies - Each slide = one `<section class="...">`, fixed width **1440px**, height fits content - Fonts from DESIGN.md — Chinese fallback appended, never hardcoded as primary - `@media print`: `page-break-after: always` on every section Font consistency check after saving: ```bash grep -n "font-family" slides.html ``` Only proceed to Step 3 once the `.html` file is saved and verified. --- ## Step 3 & 4: Export PDF via Puppeteer > ⚠️ **Only use `export_pdf.js` (Puppeteer screenshot-and-compose). Do NOT use any of these alternatives — they all produce broken output:** > - `page.pdf()` directly → A4 portrait, white space below every slide > - `wkhtmltopdf` → outdated WebKit engine, broken flexbox/grid/modern CSS > - Any other HTML-to-PDF CLI (Prince, WeasyPrint, pdfkit, etc.) → layout incorrect > > The screenshot-and-compose approach is the only method that faithfully preserves the rendered layout. ### Environment matters **Claude Code / Codex / OpenClaw / local agent** (can run shell commands): - Read `scripts/export_pdf.js`, copy it to the project directory, run it: ```bash node export_pdf.js # reads slides.html → slides.pdf (auto-detects <section> elements) node export_pdf.js my-deck.html # specify a different file ``` **Claude.ai chat / any chat-only interface** (cannot run shell commands): - Your role ends at Step 3: generate the HTML and provide it to the user - Tell the user: **"Here's your `slides.html`. To export the PDF, copy `scripts/export_pdf.js` from the skill folder to the same directory and run `node export_pdf.js`."** - Do NOT attempt to run Puppeteer, install npm packages, or use any PDF tool in chat ```bash node export_pdf.js # reads slides.html → slides.pdf (auto-detects all <section> elements) node export_pdf.js my-deck.html # specify a different HTML file ``` **Auto-detection:** The script automatically finds all `<section>` elements in DOM order — no need to manually set SELECTORS. Only configure `SELECTORS` manually if you need a custom slide order or to merge multiple elements into one slide: ```js // Optional manual override — leave as [] for auto-detect const SELECTORS = [ '.cover', '.section-2', ['.chart', '.stats'], // array = merge into one slide ]; ``` See `references/export_pdf_guide.md` for details. --- ## Troubleshooting | Issue | Fix | |---|---| | PDF has white space / broken layout | Do not use `page.pdf()`, `wkhtmltopdf`, or any CLI PDF tool. Run `export_pdf.js` only — it uses Puppeteer screenshot-and-compose to preserve layout exactly | | Chinese text renders as Song/Ming | Add `'PingFang SC'` to font stack (available on macOS by default) | | Inconsistent fonts across slides | Run `grep -n "font-family" slides.html` and normalize | | Wrong page count in PDF | Check that SELECTORS covers all sections | | Detached Frame error | Use the reusable `overlayPage` pattern — never use `newPage()/close()` in a loop | | Website URL fails to fetch | Likely Fake IP proxy — use Puppeteer screenshot instead, or ask user for screenshots |

path: README.md
content: # html-ppt-to-pdf

An AI skill that turns any design style into a polished HTML slide deck and exports it as a high-quality PDF.

**Works with:** OpenClaw · Claude Code · Codex · Any AI agent

---

## ✨ What it does

Say *"make me slides in Apple style about X"* — the agent will:

1. Analyze the design style (from a `.pptx` template, website URL, screenshot, or keywords)
2. Confirm the style and content with you before generating anything
3. Build a single-file HTML slide deck matching that design
4. Export a pixel-accurate 1440×900 PDF using Puppeteer screenshot-and-compose

## 📥 Input types

| Input | Example |
|---|---|
| `.pptx` template | Upload a PowerPoint file → extract colors, fonts, layouts |
| Website URL | `https://linear.app` → clone its visual style |
| Screenshot / image | Drop a design reference image |
| Style keyword | "Apple Keynote dark", "Notion minimal", "Stripe landing" |

## 🖼️ Output preview

> *(PDF slides generated from this skill)*

<!-- Add screenshots here after publishing -->

---

## 🚀 Installation

### OpenClaw
```bash
clawhub install html-ppt-to-pdf
```

### Manual (Claude Code / Codex / any agent)
```bash
git clone https://github.com/<your-username>/html-ppt-to-pdf.git ~/.agents/skills/html-ppt-to-pdf
```
Then reference `SKILL.md` in your agent's system prompt or `AGENTS.md`.

### Dependencies
```bash
npm install puppeteer     # PDF export (required)
pip install python-pptx   # .pptx parsing (required for template input)
```

See [`references/install.md`](references/install.md) for platform-specific setup (macOS, Linux, Claude Code, Codex).

---

## 📁 Skill structure

```
html-ppt-to-pdf/
├── SKILL.md                  ← Agent instructions (start here)
├── scripts/
│   └── export_pdf.js         ← Puppeteer PDF export script (auto-detects sections)
└── references/
    ├── install.md             ← Installation guide for all platforms
    ├── DESIGN_TEMPLATE.md     ← DESIGN.md template for design spec extraction
    └── export_pdf_guide.md    ← export_pdf.js usage details
```

---

## 💡 How it works

The skill enforces a structured workflow:

```
Input → Extract design spec → ✅ Confirm style
      → Outline slide content → ✅ Confirm content
      → Generate HTML (single file, all CSS inlined)
      → Run export_pdf.js → 1440×900 PDF
```

Two mandatory confirmation steps prevent the agent from generating the wrong style or wrong content.

---

## ⚠️ PDF export notes

The skill uses **Puppeteer screenshot-and-compose** for PDF export — not `wkhtmltopdf`, `page.pdf()`, or any other tool. This is the only method that faithfully preserves modern CSS layout (flexbox, grid, gradients, backdrop-filter, etc.).

---

## 📄 License

MIT


path: references/DESIGN_TEMPLATE.md
content: # DESIGN.md 模板

> 根据输入（.pptx / 截图 / 网站 / 关键词）提取并填写。所有带 `[待填]` 的字段必须替换为实际值，不保留占位符。

---

## 基础信息

- **项目名称**：[待填]
- **主题风格**：[待填，如：暗色科技、极简白、渐变赛博]
- **整体色调**：[暗色 / 亮色 / 混合交替]
- **幻灯片尺寸**：1440×900px（16:9）

---

## 颜色系统

```css
:root {
  /* 背景 */
  --bg-primary:   [待填];   /* 主背景色，如 #000000 或 #FFFFFF */
  --bg-secondary: [待填];   /* 次级背景/卡片背景 */

  /* 文字 */
  --text-primary:   [待填]; /* 主要文字色 */
  --text-secondary: [待填]; /* 次要文字色（副标题/说明文字） */

  /* 强调 */
  --accent:       [待填];   /* 主强调色（按钮/高亮/数字） */
  --accent-light: [待填];   /* 强调色浅色变体（背景/标签） */

  /* 边框/分隔 */
  --border:       [待填];   /* 卡片边框、分隔线 */
}
```

---

## 字体系统

> 从输入来源提取实际字体名，中文字体作为 fallback 追加在末尾，不写死。

```css
:root {
  --font-heading: '[待填]', 'PingFang SC', 'Helvetica Neue', sans-serif;
  --font-body:    '[待填]', 'PingFang SC', 'Helvetica Neue', sans-serif;
}
```

| 用途 | 字体名 | 字重 | 字号范围 |
|------|--------|------|----------|
| 主标题 | [待填] | [待填] | [待填，如 80-120px] |
| 副标题 | [待填] | [待填] | [待填，如 20-28px] |
| 正文/说明 | [待填] | [待填] | [待填，如 16-18px] |
| 标签/小字 | [待填] | [待填] | [待填，如 11-13px] |
| 装饰数字 | [待填] | [待填] | [待填，如 150-200px] |

---

## 布局规范

- **页面内边距**：[待填，如 80px 60px]
- **内容最大宽度**：[待填，如 1200px 或 100%]
- **网格列数**：[待填，如 3列 / 4列 / 自由布局]
- **卡片间距（gap）**：[待填，如 24px]
- **圆角**：[待填，如 12px / 20px / 0]

---

## 组件风格

### 卡片
```css
/* [待填，描述背景/边框/投影风格] */
background: ;
border: ;
border-radius: ;
box-shadow: ;
```

### 标签 / 徽章
```css
/* [待填] */
font-size: ;
letter-spacing: ;
border-radius: ;
background: ;
color: ;
```

### 按钮 / CTA
```css
/* [待填] */
background: ;
color: ;
border-radius: ;
padding: ;
```

### 分隔线
```css
/* [待填，如渐变线/纯色线/无] */
```

### 背景装饰
```css
/* [待填，如光晕/网格/大字/无] */
/* 示例：径向渐变光晕 */
/* background: radial-gradient(circle at 30% 50%, rgba(主色, .15), transparent 60%); */
```

---

## Section 清单

列出每个幻灯片页面的 CSS class 和内容主题：

| # | CSS Class | 内容主题 | 背景色调 |
|---|-----------|----------|----------|
| 1 | [待填] | [待填] | 暗/亮 |
| 2 | [待填] | [待填] | 暗/亮 |
| … | … | … | … |


path: references/export_pdf_guide.md
content: # export_pdf.js 使用指南

## 工作原理

```
HTML → Puppeteer 按 selector 截图 → 等比缩放 → 合成 PDF
```

脚本流程：
1. 启动 headless Chrome，加载 HTML（`deviceScaleFactor: 2` 高清）
2. 遍历 SELECTORS，用 `getBoundingClientRect` 获取真实尺寸
3. 截图 → 生成临时 slide HTML（黑底居中）→ 截图为 1440×900 PNG
4. 所有 PNG 合成为横向 PDF

## 关键参数

```js
const W = 1440, H = 900;  // 幻灯片尺寸（px），可调整

const SELECTORS = [
  '.cover',
  '.section-2',
  ['.chart', '.stats'],  // 数组 = 合并为一页
];
```

## 运行方式

```bash
# 默认读取 slides.html，输出 slides.pdf
node export_pdf.js

# 指定 HTML 文件（输出自动同名）
node export_pdf.js my-deck.html
```

## 调试技巧

**查看每页实际尺寸**：脚本会打印 `(宽x高 → scale=X.XX)`，scale < 1 说明内容超高，考虑拆分

**跳过某页**：注释掉对应 selector

**只生成某几页**：临时只保留需要的 selector 测试

## 依赖

- Node.js 18+
- puppeteer（`npm install puppeteer`）

## ⚠️ 重要：Detached Frame 陷阱

脚本使用 **复用 overlayPage** 方案（全程只有一个辅助页面）。

**不要**改为 `browser.newPage()` / `p2.close()` 循环——新版 Puppeteer 会报 `Attempted to use detached Frame` 错误。

## 输出

- 输出文件：与输入 HTML 同名（扩展名改为 `.pdf`）
- 临时文件 `_pdf_pages/` 自动清理


path: references/install.md
content: # Installation Guide

## OpenClaw

```bash
# Install via ClawHub
clawhub install html-ppt-to-pdf

# Or install manually
cp -r html-ppt-to-pdf ~/.agents/skills/
```

The skill is auto-discovered on next session start. No restart needed.

---

## Claude Code (claude.ai/code)

1. Download or clone the skill folder
2. Place it in your skills directory:
   ```bash
   cp -r html-ppt-to-pdf ~/.claude/skills/
   ```
3. Reference it in your project's `CLAUDE.md` or system prompt:
   ```
   Skills path: ~/.claude/skills/html-ppt-to-pdf/SKILL.md
   ```

---

## Codex (OpenAI)

1. Place the skill folder in your workspace:
   ```bash
   cp -r html-ppt-to-pdf ~/your-project/skills/
   ```
2. Add to your `AGENTS.md` or system prompt:
   ```
   Available skill: skills/html-ppt-to-pdf/SKILL.md
   ```

---

## Any Agent (Manual)

Paste the contents of `SKILL.md` into your agent's system prompt or context window. The agent will follow the workflow instructions automatically.

---

## System Dependencies

Install these once before first use:

```bash
# Node.js (v18+) — required for Puppeteer PDF export
node --version   # check if installed
# Install: https://nodejs.org

# Puppeteer (headless Chrome)
npm install puppeteer

# python-pptx (required for .pptx template parsing)
pip install python-pptx
```

### macOS (recommended)
All dependencies available via Homebrew:
```bash
brew install node
npm install puppeteer
pip3 install python-pptx
```

### Linux
```bash
# Puppeteer may need extra Chrome deps
sudo apt-get install -y libx11-xcb1 libxcomposite1 libxcursor1 libxdamage1 \
  libxi6 libxtst6 libnss3 libcups2 libxss1 libxrandr2 libasound2 libatk1.0-0 \
  libgtk-3-0
npm install puppeteer
pip install python-pptx
```

---

## Puppeteer Installation Troubleshooting

If `npm install puppeteer` fails (network/permission issues):

```bash
# Option 1: skip download, install Chrome separately
npm install puppeteer --ignore-scripts
npx puppeteer browsers install chrome

# Option 2: use puppeteer-core with system Chrome
npm install puppeteer-core
# Then in export_pdf.js, change launch() to:
# puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' })

# Option 3: check if Chrome/Chromium is already installed
which google-chrome chromium chromium-browser 2>/dev/null
```

> ⚠️ If Puppeteer cannot be installed, **stop and tell the user** — do not fall back to wkhtmltopdf, pdfkit, weasyprint, or any other PDF tool. These tools do not support modern CSS and will produce broken layouts.

## Verify Installation

Run a quick test:
```bash
node -e "const p = require('puppeteer'); console.log('puppeteer ok:', p.executablePath ? 'bundled' : 'system')"
python3 -c "import pptx; print('python-pptx ok')"
```


path: scripts/export_pdf.js
content: /**
 * export_pdf.js — html-ppt-to-pdf skill
 *
 * Usage:
 *   node export_pdf.js                  # reads slides.html → slides.pdf
 *   node export_pdf.js my-deck.html     # reads my-deck.html → my-deck.pdf
 *
 * ⚠️  Do NOT use page.pdf() directly, wkhtmltopdf, pdfkit, or any other tool.
 *     This script uses Puppeteer screenshot-and-compose — the only method that
 *     preserves modern CSS layout (flexbox, grid, gradients, backdrop-filter).
 *
 * Chrome detection: tries bundled Chromium first, then falls back to system Chrome.
 * Works in sandboxed environments (Claude Code, Codex) where Chromium download is blocked.
 */

const path = require('path');
const fs   = require('fs');

// ─── Config ──────────────────────────────────────────────────────────────────
const HTML_FILE = process.argv[2] || 'slides.html';
const OUT_FILE  = HTML_FILE.replace(/\.html?$/i, '.pdf');
const W = 1440, H = 900;

/**
 * SELECTORS — one entry per output slide.
 * Leave as [] to auto-detect all <section> elements in DOM order.
 * Each entry: string selector OR array of selectors (merged into one slide).
 */
const SELECTORS = [];
// ─────────────────────────────────────────────────────────────────────────────

/** Find a usable Chrome/Chromium executable */
function findChrome() {
  // 1. Try puppeteer bundled browser
  try {
    const p = require('puppeteer');
    if (p.executablePath && typeof p.executablePath === 'function') {
      const ep = p.executablePath();
      if (ep && fs.existsSync(ep)) return { pkg: 'puppeteer', executablePath: ep };
    }
  } catch (_) {}

  // 2. Try puppeteer-core
  try {
    const pc = require('puppeteer-core');
    return { pkg: 'puppeteer-core', executablePath: undefined }; // will need system path
  } catch (_) {}

  return null;
}

/** System Chrome paths by platform */
const SYSTEM_CHROME_PATHS = {
  darwin: [
    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
    '/Applications/Chromium.app/Contents/MacOS/Chromium',
    '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
  ],
  linux: [
    '/usr/bin/google-chrome',
    '/usr/bin/google-chrome-stable',
    '/usr/bin/chromium',
    '/usr/bin/chromium-browser',
    '/snap/bin/chromium',
  ],
  win32: [
    'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
    'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
  ],
};

function findSystemChrome() {
  const paths = SYSTEM_CHROME_PATHS[process.platform] || [];
  for (const p of paths) {
    if (fs.existsSync(p)) return p;
  }
  return null;
}

async function launch() {
  const chrome = findChrome();
  const systemPath = findSystemChrome();

  let puppeteer, executablePath;

  if (chrome?.pkg === 'puppeteer' && chrome.executablePath) {
    // Best case: puppeteer with bundled Chromium
    puppeteer = require('puppeteer');
    executablePath = chrome.executablePath;
    console.log('🟢 Using bundled Chromium:', executablePath);
  } else if (systemPath) {
    // Fallback: use system Chrome with puppeteer or puppeteer-core
    try { puppeteer = require('puppeteer'); } catch (_) {
      puppeteer = require('puppeteer-core');
    }
    executablePath = systemPath;
    console.log('🟡 Using system Chrome:', executablePath);
  } else {
    console.error(`
❌ No Chrome/Chromium found. Please install one of the following:

  macOS:   brew install --cask google-chrome
  Linux:   sudo apt install chromium-browser
  npm:     npm install puppeteer   (downloads bundled Chromium)
`);
    process.exit(1);
  }

  return puppeteer.launch({
    headless: true,
    executablePath,
    args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
  });
}

async function getBoundingBox(page, selectors) {
  const sels = Array.isArray(selectors) ? selectors : [selectors];
  return await page.evaluate((sels) => {
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    for (const s of sels) {
      const el = document.querySelector(s);
      if (!el) continue;
      const r = el.getBoundingClientRect();
      minX = Math.min(minX, r.left);  minY = Math.min(minY, r.top);
      maxX = Math.max(maxX, r.right); maxY = Math.max(maxY, r.bottom);
    }
    return minX === Infinity ? null : { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
  }, sels);
}

async function autoDetectSelectors(page) {
  return await page.evaluate(() =>
    Array.from(document.querySelectorAll('section')).map((el, i) => {
      const cls = el.className ? '.' + el.className.trim().split(/\s+/)[0] : null;
      return cls || `section:nth-of-type(${i + 1})`;
    })
  );
}

(async () => {
  const browser = await launch();

  const page = await browser.newPage();
  await page.setViewport({ width: W, height: H, deviceScaleFactor: 2 });
  const filePath = path.resolve(process.cwd(), HTML_FILE);
  await page.goto(`file://${filePath}`, { waitUntil: 'networkidle0' });
  await new Promise(r => setTimeout(r, 1500));

  const selectors = SELECTORS.length > 0 ? SELECTORS : await autoDetectSelectors(page);
  if (selectors.length === 0) {
    console.error('❌ No <section> elements found. Check your HTML file.');
    await browser.close(); process.exit(1);
  }
  console.log(`📐 ${selectors.length} slides detected`);

  const overlayPage = await browser.newPage();
  await overlayPage.setViewport({ width: W, height: H, deviceScaleFactor: 2 });

  const imgDir = path.resolve(process.cwd(), '_pdf_pages');
  if (fs.existsSync(imgDir)) fs.rmSync(imgDir, { recursive: true });
  fs.mkdirSync(imgDir);

  for (let i = 0; i < selectors.length; i++) {
    const sel = selectors[i];
    const selStr = Array.isArray(sel) ? sel.join('+') : sel;
    try {
      const box = await getBoundingBox(page, sel);
      if (!box) { console.log(`⚠️  skip (not found): ${selStr}`); continue; }

      const scale = Math.min(W / box.width, H / box.height);
      const scaledW = Math.round(box.width * scale);
      const scaledH = Math.round(box.height * scale);

      const rawPath = path.join(imgDir, `raw_${String(i+1).padStart(2,'0')}.png`);
      await page.screenshot({ path: rawPath, clip: { x: box.x, y: box.y, width: box.width, height: box.height } });

      const slidePath = path.join(imgDir, `slide_${String(i+1).padStart(2,'0')}.html`);
      fs.writeFileSync(slidePath,
        `<!DOCTYPE html><html><head><style>*{margin:0;padding:0}` +
        `body{width:${W}px;height:${H}px;background:#000;display:flex;align-items:center;justify-content:center;overflow:hidden}` +
        `img{width:${scaledW}px;height:${scaledH}px;display:block}</style></head>` +
        `<body><img src="${rawPath}"></body></html>`
      );

      await overlayPage.goto(`file://${slidePath}`, { waitUntil: 'networkidle0' });
      const imgPath = path.join(imgDir, `page_${String(i+1).padStart(2,'0')}.png`);
      await overlayPage.screenshot({ path: imgPath, clip: { x: 0, y: 0, width: W, height: H } });

      fs.unlinkSync(rawPath); fs.unlinkSync(slidePath);
      console.log(`✅ Slide ${i+1}: ${selStr} (${Math.round(box.width)}×${Math.round(box.height)}, scale=${scale.toFixed(2)})`);
    } catch (e) { console.log(`❌ ${selStr}: ${e.message}`); }
  }

  const imgs = fs.readdirSync(imgDir).filter(f => f.startsWith('page_') && f.endsWith('.png')).sort();
  const slides = imgs.map(img =>
    `<div style="width:${W}px;height:${H}px;page-break-after:always;overflow:hidden">` +
    `<img src="${path.join(imgDir, img)}" style="width:${W}px;height:${H}px;display:block"></div>`
  ).join('\n');

  const tmpHtml = path.resolve(process.cwd(), '_slides.html');
  fs.writeFileSync(tmpHtml,
    `<!DOCTYPE html><html><head><meta charset="utf-8"><style>` +
    `*{margin:0;padding:0;box-sizing:border-box}body{background:#000}` +
    `@media print{@page{size:${W}px ${H}px;margin:0}body{-webkit-print-color-adjust:exact;print-color-adjust:exact}}` +
    `</style></head><body>${slides}</body></html>`
  );

  await overlayPage.goto(`file://${tmpHtml}`, { waitUntil: 'networkidle0' });
  await new Promise(r => setTimeout(r, 800));
  await overlayPage.pdf({
    path: path.resolve(process.cwd(), OUT_FILE),
    width: `${W}px`, height: `${H}px`,
    printBackground: true,
    margin: { top: 0, right: 0, bottom: 0, left: 0 }
  });

  await browser.close();
  fs.rmSync(imgDir, { recursive: true });
  fs.unlinkSync(tmpHtml);
  console.log(`\n🎉 Done: ${OUT_FILE}  (${imgs.length} slides)`);
})();


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: clean (LOW)