From 9744adbd52e4a70b1b8899ebb457f68d8b5236ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E8=BF=9B?= Date: Wed, 22 Jul 2026 17:54:42 +0800 Subject: [PATCH] init: html2pptx project scaffold + requirements doc --- .gitignore | 3 + README.md | 38 ++++++++ bin/html2pptx.js | 188 +++++++++++++++++++++++++++++++++++++++ package-lock.json | 220 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 18 ++++ 5 files changed, 467 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bin/html2pptx.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e68f331 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +output/ +*.pptx diff --git a/README.md b/README.md new file mode 100644 index 0000000..49f8163 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# html2pptx + +HTML to editable PPTX export engine. + +将 HTML 演示文稿导出为**文字可编辑、布局可还原**的 PPTX 文件。 + +## 原理 + +``` +HTML 文件 → HTTP 服务 → Chrome headless 渲染 → DOM 遍历提取位置/样式 → pptxgenjs 生成 PPTX +``` + +## 依赖 + +- Node.js 18+ +- Chrome / Chromium / Edge(用于渲染页面) + +## 使用 + +```bash +# 安装 +npm install + +# 导出 +node bin/html2pptx input.html output.pptx +``` + +## 核心依赖 + +| 包 | 用途 | +|---|------| +| playwright | 控制 Chrome headless 渲染页面 | +| pptxgenjs | 生成 PPTX 文件 | +| http-server | 本地静态文件服务(可选) | + +## License + +MIT diff --git a/bin/html2pptx.js b/bin/html2pptx.js new file mode 100644 index 0000000..bd99a2f --- /dev/null +++ b/bin/html2pptx.js @@ -0,0 +1,188 @@ +#!/usr/bin/env node +/** + * html2pptx — HTML to editable PPTX export engine + * + * 用法: node bin/html2pptx.js + * + * 流程: + * 1. 启动本地 HTTP 服务托管 HTML + * 2. Chrome headless 打开页面 + * 3. 遍历 DOM 提取每页元素的位置/文字/样式 + * 4. pptxgenjs 生成 PPTX 文件 + */ + +const { chromium } = require('playwright'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const PptxGenJS = require('pptxgenjs'); +const { execSync } = require('child_process'); + +const PORT = 8192; + +// ====== CLI 参数解析 ====== +const args = process.argv.slice(2); +if (args.length < 2) { + console.error('用法: node bin/html2pptx.js '); + process.exit(1); +} +const inputPath = path.resolve(args[0]); +const outputPath = path.resolve(args[1]); + +if (!fs.existsSync(inputPath)) { + console.error(`文件不存在: ${inputPath}`); + process.exit(1); +} + +async function main() { +const server = http.createServer((req, res) => { + const filePath = path.join(path.dirname(inputPath), req.url === '/' ? path.basename(inputPath) : req.url); + if (fs.existsSync(filePath)) { + const ext = path.extname(filePath); + const mime = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml' }; + res.writeHead(200, { 'Content-Type': mime[ext] || 'text/plain' }); + res.end(fs.readFileSync(filePath)); + } else { + res.writeHead(404); + res.end('Not Found'); + } +}); + +await new Promise(resolve => server.listen(PORT, '127.0.0.1', resolve)); +console.log(`HTTP 服务已启动: http://127.0.0.1:${PORT}/`); + +// ====== 2. Chrome headless 打开页面 ====== +const browser = await chromium.launch({ headless: true, channel: 'chrome' }); +const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); + +try { + await page.goto(`http://127.0.0.1:${PORT}/`, { waitUntil: 'networkidle' }); + console.log('页面加载完成'); + + // ====== 3. 提取每页的 DOM 信息 ====== + const slidesData = await page.evaluate(() => { + // 找 slide 容器:
或 [data-slide] 或直接 .slide + let slides = document.querySelectorAll('section[data-slide], .slide, section'); + if (slides.length === 0) { + // 如果是横向翻页,可能在一页里 + slides = [document.body]; + } + + return Array.from(slides).map(slide => { + const rect = slide.getBoundingClientRect(); + // 隐藏不可见的 slide + if (rect.width === 0 || rect.height === 0) return null; + + const elements = []; + const walker = document.createTreeWalker(slide, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, null, false); + + let node; + while (node = walker.nextNode()) { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent.trim(); + if (!text) continue; + const range = document.createRange(); + range.selectNodeContents(node); + const textRect = range.getBoundingClientRect(); + if (textRect.width === 0 || textRect.height === 0) continue; + + const parent = node.parentElement; + const style = parent ? window.getComputedStyle(parent) : null; + + elements.push({ + type: 'text', + text: text, + x: textRect.left - rect.left, + y: textRect.top - rect.top, + width: textRect.width, + height: textRect.height, + fontSize: style ? parseFloat(style.fontSize) : 14, + fontFamily: style ? style.fontFamily : 'Arial', + color: style ? style.color : '#000000', + fontWeight: style ? style.fontWeight : 'normal', + textAlign: style ? style.textAlign : 'left', + bold: style ? (parseInt(style.fontWeight) >= 700 || style.fontWeight === 'bold') : false + }); + } + } + + // 获取 slide 背景色 + const bg = window.getComputedStyle(slide).backgroundColor; + const bgColor = bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent' ? bg : '#FFFFFF'; + + return { + width: rect.width, + height: rect.height, + bgColor: bgColor, + elements: elements + }; + }).filter(Boolean); + }); + + console.log(`提取到 ${slidesData.length} 页幻灯片`); + + // ====== 4. pptxgenjs 生成 PPTX ====== + const pptx = new PptxGenJS(); + pptx.defineLayout({ name: 'CUSTOM', width: 10, height: 5.625 }); // 16:9 + pptx.layout = 'CUSTOM'; + + for (const slideData of slidesData) { + const slide = pptx.addSlide(); + + // 背景色 + slide.background = { fill: slideData.bgColor }; + + // 按 y 排序,从上到下 + slideData.elements.sort((a, b) => a.y - b.y || a.x - b.x); + + for (const el of slideData.elements) { + if (el.type === 'text' && el.text.trim()) { + // 缩放比例:HTML 像素 → PPT 英寸 + const scaleX = 10 / slideData.width; + const scaleY = 5.625 / slideData.height; + + const x = el.x * scaleX; + const y = el.y * scaleY; + const w = Math.max(el.width * scaleX, 0.5); + const h = Math.max(el.height * scaleY, 0.3); + + // 颜色格式转换: rgb(r,g,b) → hex + let color = el.color; + if (color.startsWith('rgb')) { + const m = color.match(/\d+/g); + if (m) color = '#' + m.slice(0, 3).map(c => parseInt(c).toString(16).padStart(2, '0')).join(''); + } + + try { + slide.addText(el.text, { + x: x, + y: y, + w: w, + h: h, + fontSize: Math.max(el.fontSize * scaleY, 8), + fontFace: el.fontFamily ? el.fontFamily.split(',')[0].trim().replace(/['"]/g, '') : 'Arial', + color: color, + bold: el.bold, + align: el.textAlign || 'left', + valign: 'top', + wrap: true, + margin: 0 + }); + } catch (e) { + // 跳过单个元素失败 + } + } + } + } + + await pptx.writeFile({ fileName: outputPath }); + console.log(`\nPPTX 已导出: ${outputPath}`); + +} catch (err) { + console.error('导出失败:', err.message); + process.exit(1); +} finally { + await browser.close(); + server.close(); +} +main().catch(err => { console.error('导出失败:', err.message); process.exit(1); }); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6f9a925 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,220 @@ +{ + "name": "html2pptx", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "html2pptx", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "playwright": "^1.60.0", + "pptxgenjs": "^4.0.1" + }, + "bin": { + "html2pptx": "bin/html2pptx.js" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pptxgenjs": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/pptxgenjs/-/pptxgenjs-4.0.1.tgz", + "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.8.1", + "https": "^1.0.0", + "image-size": "^1.2.1", + "jszip": "^3.10.1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..565713c --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "html2pptx", + "version": "0.1.0", + "description": "HTML to editable PPTX export engine", + "main": "bin/html2pptx.js", + "type": "commonjs", + "bin": { + "html2pptx": "./bin/html2pptx.js" + }, + "scripts": { + "test": "node bin/html2pptx.js test/fixtures/simple.html test/output.pptx" + }, + "dependencies": { + "playwright": "^1.60.0", + "pptxgenjs": "^4.0.1" + }, + "license": "MIT" +}