init: html2pptx project scaffold + requirements doc
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
output/
|
||||
*.pptx
|
||||
@@ -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
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* html2pptx — HTML to editable PPTX export engine
|
||||
*
|
||||
* 用法: node bin/html2pptx.js <input.html> <output.pptx>
|
||||
*
|
||||
* 流程:
|
||||
* 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 <input.html> <output.pptx>');
|
||||
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 容器:<section> 或 [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); });
|
||||
Generated
+220
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user