diff --git a/convert-w2p.js b/convert-w2p.js index d2ce2f6..e2b0fa7 100644 --- a/convert-w2p.js +++ b/convert-w2p.js @@ -261,7 +261,7 @@ function collectObjects(nodeId, ctx, processed) { * @param {Object} input web-to-pixso 格式的 JSON 对象 * @returns {Object} html2pptx Schema JSON 对象 */ -function convert(input) { +async function convert(input) { if (!input || !input.nodes) { throw new Error('无效输入:缺少 "nodes" 字段'); } @@ -272,6 +272,57 @@ function convert(input) { } const canvas = input.canvas || {}; + + // ===== 图片下载:RECTANGLE+IMG 标签且无 backgroundImages 时,从 src URL 下载 ===== + const pageUrl = input.source?.url || ''; + const https = require('https'); + const http = require('http'); + + function downloadImage(url) { + return new Promise(function(resolve) { + try { + var mod = url.startsWith('https') ? https : http; + mod.get(url, { timeout: 5000 }, function(res) { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + downloadImage(res.headers.location).then(resolve); + return; + } + var chunks = []; + res.on('data', function(c) { chunks.push(c); }); + res.on('end', function() { + var buf = Buffer.concat(chunks); + var b64 = 'data:image/png;base64,' + buf.toString('base64'); + resolve(b64); + }); + res.on('error', function() { resolve(null); }); + }).on('error', function() { resolve(null); }); + } catch (e) { resolve(null); } + }); + } + + async function fetchNodeImages(node) { + if (!node) return; + if (node.tag === 'IMG' && node.attributes?.src && (!node.backgroundImages || node.backgroundImages.length === 0)) { + var src = node.attributes.src; + // 拼接完整 URL:相对路径基于 origin,绝对路径基于 origin + var origin = pageUrl.replace(/^(https?:\/\/[^\/]+).*/, '$1'); + if (src.startsWith('/')) src = origin + src; + else if (!src.startsWith('http')) src = pageUrl.replace(/\/[^\/]*$/, '/') + src; + console.log('📥 下载图片: ' + src); + var data = await downloadImage(src); + if (data) { + if (!input.assets) input.assets = {}; + var key = 'img-' + (node.id || Math.random().toString(36).slice(2)); + input.assets[key] = { data: data }; + node.backgroundImages = [key]; + node.type = 'IMAGE'; // 标记为 IMAGE 类型 + } + } + if (node.children) for (var c of node.children) await fetchNodeImages(c); + } + + // 异步下载所有图片 + await fetchNodeImages(inputRoot); // 如果没有 canvas 信息则默认 1920×1080(16:9 常见尺寸) const canvasWidth = canvas.width || 1920; const canvasHeight = canvas.height || 1080; @@ -293,6 +344,7 @@ function convert(input) { rect: { x: nr.x - slideX, y: nr.y - slideY, w: rw, h: rh }, styles: node.styles || {}, src: node.src || node.text || '', layerGroup: node.layerGroup || '', + backgroundImages: node.backgroundImages, parentW: parentW || rw, _depth: depth }); @@ -578,15 +630,15 @@ function convert(input) { return za - zb; }); - // ===== 同坐标完全重叠去重:保留后者(高层) ===== + // ===== 同坐标完全重叠去重:保留后者(高层),仅同类型 ===== var seen = {}; var overlapRemove = new Set(); for (var i = sg.children.length - 1; i >= 0; i--) { var ci = sg.children[i]; if (!ci.rect) continue; - var key = Math.round(ci.rect.x) + ',' + Math.round(ci.rect.y) + ',' + Math.round(ci.rect.w) + ',' + Math.round(ci.rect.h); + var key = ci.type + ',' + Math.round(ci.rect.x) + ',' + Math.round(ci.rect.y) + ',' + Math.round(ci.rect.w) + ',' + Math.round(ci.rect.h); if (seen[key]) { - overlapRemove.add(ci.id); // 后出现的(更高层)保留,前面的移除 + overlapRemove.add(ci.id); } else { seen[key] = true; } @@ -881,6 +933,7 @@ function convert(input) { // ============================================================ if (require.main === module) { +(async function() { const args = process.argv.slice(2); if (args.length < 1) { @@ -912,7 +965,7 @@ if (require.main === module) { // 执行转换并写入 try { - const result = convert(inputJson); + const result = await convert(inputJson); console.log('result slides:', result.slides.length, 'first objects:', result.slides[0]?.objects?.length); // 渲染 PPTX(支持自定义布局) @@ -975,6 +1028,7 @@ if (require.main === module) { console.error('❌ 转换失败:', err.message); process.exit(1); } +})(); } module.exports = { convert };