import { computed, ref } from 'vue' import { storeToRefs } from 'pinia' import { trim } from 'lodash' import { saveAs } from 'file-saver' import pptxgen from 'pptxgenjs' import tinycolor from 'tinycolor2' import { toPng, toJpeg } from 'html-to-image' import { useSlidesStore } from '@/store' import type { PPTElementOutline, PPTElementShadow, PPTElementLink, Slide } from '@/types/slides' import { getElementRange, getLineElementPath, getTableSubThemeColor } from '@/utils/element' import { type AST, toAST } from '@/utils/htmlParser' import { type SvgPoints, toPoints } from '@/utils/svgPathParser' import { encrypt } from '@/utils/crypto' import { svg2Base64 } from '@/utils/svg2Base64' import { renderElementToBase64, isCanvasRenderSupported, getElementDimensions } from '@/utils/canvasRenderer' import { renderWithHuggingfaceFix, isHuggingfaceEnvironment } from '@/utils/huggingfaceRenderer' import { vectorRenderManager, RenderStrategy } from '@/utils/VectorRenderManager' import { VECTOR_EXPORT_CONFIG } from '@/config/vectorExportConfig' import message from '@/utils/message' interface ExportImageConfig { quality: number width: number fontEmbedCSS?: string } export default () => { const slidesStore = useSlidesStore() const { slides, theme, viewportRatio, title, viewportSize } = storeToRefs(slidesStore) const defaultFontSize = 16 const ratioPx2Inch = computed(() => { return 96 * (viewportSize.value / 960) }) const ratioPx2Pt = computed(() => { return 96 / 72 * (viewportSize.value / 960) }) const exporting = ref(false) // 导出图片(生产环境优化版本) const exportImage = (domRef: HTMLElement, format: string, quality: number, ignoreWebfont = true) => { exporting.value = true // 环境检测 const isHF = VECTOR_EXPORT_CONFIG.ENVIRONMENT.isHuggingface() const isProd = VECTOR_EXPORT_CONFIG.ENVIRONMENT.isProduction() if (!isProd) { console.log('exportImage: Environment check:', { isHuggingface: isHF, format, production: isProd }) } // 使用VectorRenderManager预处理矢量图形元素 const preprocessVectorElements = async () => { const svgElements = domRef.querySelectorAll('svg'); const vectorShapes = domRef.querySelectorAll('.vector-shape'); // 处理SVG元素 - 使用VectorRenderManager优化 for (const svg of Array.from(svgElements)) { try { const renderResult = await vectorRenderManager.renderElement(svg); if (renderResult.success && renderResult.data) { // 用优化后的SVG替换原始SVG const tempDiv = document.createElement('div'); tempDiv.innerHTML = renderResult.data; const optimizedSvg = tempDiv.querySelector('svg'); if (optimizedSvg && svg.parentNode) { svg.parentNode.replaceChild(optimizedSvg, svg); } } else { // 降级处理:手动清理属性 svg.removeAttribute('vector-effect'); const vectorEffectElements = svg.querySelectorAll('[vector-effect]'); vectorEffectElements.forEach(el => el.removeAttribute('vector-effect')); if (!svg.hasAttribute('xmlns')) { svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); } } } catch (error) { if (!isProd) { console.warn('exportImage: VectorRenderManager failed, using fallback:', error); } // 降级处理 svg.removeAttribute('vector-effect'); if (!svg.hasAttribute('xmlns')) { svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); } } } // 处理矢量形状 - 使用VectorRenderManager优化 for (const shape of Array.from(vectorShapes)) { try { const svgChild = shape.querySelector('svg'); if (svgChild) { const renderResult = await vectorRenderManager.renderElement(svgChild); if (renderResult.success && renderResult.data) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = renderResult.data; const optimizedSvg = tempDiv.querySelector('svg'); if (optimizedSvg) { svgChild.parentNode?.replaceChild(optimizedSvg, svgChild); } } else { // 降级处理 svgChild.removeAttribute('vector-effect'); if (!svgChild.hasAttribute('xmlns')) { svgChild.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); } } } } catch (error) { if (!isProd) { console.warn('exportImage: Vector shape optimization failed:', error); } } } }; const foreignObjectSpans = domRef.querySelectorAll('foreignObject [xmlns]') foreignObjectSpans.forEach(spanRef => spanRef.removeAttribute('xmlns')) setTimeout(async () => { try { // 预处理矢量元素 await preprocessVectorElements(); let dataUrl: string if (isHF) { // Huggingface环境使用专用渲染器 console.log('exportImage: Using Huggingface renderer') dataUrl = await renderWithHuggingfaceFix(domRef) // 如果需要JPEG格式,转换PNG到JPEG if (format === 'jpeg' && dataUrl.startsWith('data:image/png')) { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') const img = new Image() await new Promise((resolve, reject) => { img.onload = () => { canvas.width = img.width canvas.height = img.height ctx?.drawImage(img, 0, 0) dataUrl = canvas.toDataURL('image/jpeg', quality) resolve(dataUrl) } img.onerror = reject img.src = dataUrl }) } } else { // 非Huggingface环境使用原有方法 const toImage = format === 'png' ? toPng : toJpeg const config: ExportImageConfig = { quality, width: 1600, } if (ignoreWebfont) config.fontEmbedCSS = '' dataUrl = await toImage(domRef, config) } exporting.value = false saveAs(dataUrl, `${title.value}.${format}`) if (isHF) { message.success('图片导出成功(Huggingface优化版本)') } } catch (error) { console.error('exportImage failed:', error) exporting.value = false if (isHF) { message.error('图片导出失败,Huggingface环境可能存在兼容性问题') } else { message.error('导出图片失败') } } }, 200) } // 导出pptist文件(特有 .pptist 后缀文件) const exportSpecificFile = (_slides: Slide[]) => { const blob = new Blob([encrypt(JSON.stringify(_slides))], { type: '' }) saveAs(blob, `${title.value}.pptist`) } // 导出JSON文件 const exportJSON = () => { const json = { title: title.value, width: viewportSize.value, height: viewportSize.value * viewportRatio.value, theme: theme.value, slides: slides.value, } const blob = new Blob([JSON.stringify(json)], { type: '' }) saveAs(blob, `${title.value}.json`) } // 新增:导出为HTML网页 const exportHTML = (_slides: Slide[], options: any = {}) => { exporting.value = true try { const { includeInteractivity = false, standalone = true, includeCSS = true } = options // 生成HTML内容 const htmlContent = generateHTMLPresentation(_slides, { title: title.value, theme: theme.value, viewportSize: viewportSize.value, viewportRatio: viewportRatio.value, includeInteractivity, standalone, includeCSS }) // 创建并下载HTML文件 const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' }) saveAs(blob, `${title.value}.html`) message.success('HTML网页导出成功') } catch (error: any) { message.error('HTML网页导出失败') } finally { exporting.value = false } } // 生成完整的HTML演示文稿 const generateHTMLPresentation = (slides: Slide[], config: any) => { const { title, theme, viewportSize, viewportRatio, includeInteractivity, includeCSS } = config const slideHeight = Math.round(viewportSize * viewportRatio) // CSS样式 const css = includeCSS ? ` ` : '' // JavaScript交互功能 const javascript = includeInteractivity ? ` ` : '' // 生成幻灯片HTML const slidesHTML = slides.map((slide, index) => { const slideBackground = formatSlideBackground(slide.background) const elementsHTML = slide.elements.map(element => formatElement(element)).join('') return `
${elementsHTML}
` }).join('') // 导航控件HTML const navigationHTML = includeInteractivity ? `
1 / ${slides.length}
` : '' // 组装完整HTML const html = ` ${title} ${css}
${slidesHTML}
${navigationHTML} ${javascript} ` return html } // 格式化幻灯片背景 const formatSlideBackground = (background: any) => { if (!background) { return 'background: #ffffff;' } if (background.type === 'solid') { return `background: ${background.color || '#ffffff'};` } if (background.type === 'gradient') { const { gradientType, colors } = background if (gradientType === 'linear') { return `background: linear-gradient(${background.gradientRotate || 0}deg, ${colors.map((c: any) => c.color).join(', ')});` } return `background: radial-gradient(${colors.map((c: any) => c.color).join(', ')});` } if (background.type === 'image' && background.image) { return `background-image: url(${background.image.src}); background-size: cover; background-position: center;` } return 'background: #ffffff;' } // 格式化元素 // 生产环境级别的矢量图形SVG生成函数 const generateSVGFromShape = (element: any): string => { try { const { width, height, path, fill, outline, viewBox, opacity = 1 } = element; const [vbX, vbY, vbWidth, vbHeight] = viewBox || [0, 0, width, height]; // 安全的颜色处理 const fillColor = fill || '#000000'; const strokeColor = outline?.color || 'none'; const strokeWidth = outline?.width || 0; // 构建SVG字符串,确保所有属性都被正确转义 const svgAttributes = [ `width="${width}"`, `height="${height}"`, `viewBox="${vbX} ${vbY} ${vbWidth} ${vbHeight}"`, 'xmlns="http://www.w3.org/2000/svg"', 'style="display: block; width: 100%; height: 100%;"' ].join(' '); const pathAttributes = [ `d="${path}"`, `fill="${fillColor}"`, strokeWidth > 0 ? `stroke="${strokeColor}"` : '', strokeWidth > 0 ? `stroke-width="${strokeWidth}"` : '', opacity < 1 ? `opacity="${opacity}"` : '', 'vector-effect="non-scaling-stroke"' ].filter(Boolean).join(' '); return ``; } catch (error) { console.error('generateSVGFromShape error:', error); // 生产环境降级处理:返回简单的占位符 return ` Vector `; } }; const formatElement = (element: any) => { const baseStyle = ` left: ${element.left || 0}px; top: ${element.top || 0}px; width: ${element.width || 100}px; height: ${element.height || 100}px; transform: rotate(${element.rotate || 0}deg); ` if (element.type === 'text') { const textStyle = ` font-size: ${element.fontSize || 16}px; font-family: ${element.fontName || 'Microsoft YaHei'}; color: ${element.color || '#000000'}; font-weight: ${element.bold ? 'bold' : 'normal'}; font-style: ${element.italic ? 'italic' : 'normal'}; text-decoration: ${element.underline ? 'underline' : 'none'}; text-align: ${element.align || 'left'}; line-height: ${element.lineHeight || 1.5}; ` return `
${element.content || ''}
` } if (element.type === 'image' && element.src) { return `
图片
` } if (element.type === 'shape') { // 处理特殊矢量图形(生产环境优化) if (element.special && element.path) { try { const svgContent = generateSVGFromShape(element); // 使用VectorRenderManager进一步优化SVG const tempDiv = document.createElement('div'); tempDiv.innerHTML = svgContent; const svgElement = tempDiv.querySelector('svg'); if (svgElement) { // 异步优化SVG(不阻塞主流程) vectorRenderManager.renderElement(svgElement).then(result => { if (result.success && result.data) { // 在后台优化成功,但不影响当前渲染 if (!VECTOR_EXPORT_CONFIG.ENVIRONMENT.isProduction()) { console.log('formatElement: SVG optimized successfully'); } } }).catch(error => { if (!VECTOR_EXPORT_CONFIG.ENVIRONMENT.isProduction()) { console.warn('formatElement: Background SVG optimization failed:', error); } }); } return `
${svgContent}
`; } catch (error) { // 生产环境降级处理 if (VECTOR_EXPORT_CONFIG.ERROR_CONFIG.LOGGING.VERBOSE) { console.warn('formatElement: Vector shape generation failed, using fallback:', error); } // 使用VectorRenderManager生成优化的降级SVG try { const placeholderElement = document.createElement('div'); placeholderElement.style.width = `${element.width}px`; placeholderElement.style.height = `${element.height}px`; // 尝试使用VectorRenderManager生成占位符 vectorRenderManager.renderElement(placeholderElement).then(result => { if (result.success && result.data) { // 异步替换占位符(不阻塞当前渲染) if (!VECTOR_EXPORT_CONFIG.ENVIRONMENT.isProduction()) { console.log('formatElement: Fallback SVG generated successfully'); } } }).catch(() => { // 静默处理降级失败 }); } catch (managerError) { // VectorRenderManager也失败时的最终降级 } // 使用简化的矢量图形表示 const fallbackSvg = ` Vector `; return `
${fallbackSvg}
`; } } // 处理普通形状 const shapeStyle = ` background: ${element.fill || '#ffffff'}; border: ${element.outline?.width || 0}px solid ${element.outline?.color || '#000000'}; border-radius: ${element.borderRadius || 0}px; ` return `
` } // 其他元素类型的基本处理 return `
` } // 格式化颜色值为 透明度 + HexString,供pptxgenjs使用 const formatColor = (_color: string) => { if (!_color) { return { alpha: 0, color: '#000000', } } const c = tinycolor(_color) const alpha = c.getAlpha() const color = alpha === 0 ? '#ffffff' : c.setAlpha(1).toHexString() return { alpha, color, } } type FormatColor = ReturnType // 将HTML字符串格式化为pptxgenjs所需的格式 // 核心思路:将HTML字符串按样式分片平铺,每个片段需要继承祖先元素的样式信息,遇到块级元素需要换行 const formatHTML = (html: string) => { const ast = toAST(html) let bulletFlag = false let indent = 0 const slices: pptxgen.TextProps[] = [] const parse = (obj: AST[], baseStyleObj: { [key: string]: string } = {}) => { for (const item of obj) { const isBlockTag = 'tagName' in item && ['div', 'li', 'p'].includes(item.tagName) if (isBlockTag && slices.length) { const lastSlice = slices[slices.length - 1] if (!lastSlice.options) lastSlice.options = {} lastSlice.options.breakLine = true } const styleObj = { ...baseStyleObj } const styleAttr = 'attributes' in item ? item.attributes.find(attr => attr.key === 'style') : null if (styleAttr && styleAttr.value) { const styleArr = styleAttr.value.split(';') for (const styleItem of styleArr) { const [_key, _value] = styleItem.split(': ') const [key, value] = [trim(_key), trim(_value)] if (key && value) styleObj[key] = value } } if ('tagName' in item) { if (item.tagName === 'em') { styleObj['font-style'] = 'italic' } if (item.tagName === 'strong') { styleObj['font-weight'] = 'bold' } if (item.tagName === 'sup') { styleObj['vertical-align'] = 'super' } if (item.tagName === 'sub') { styleObj['vertical-align'] = 'sub' } if (item.tagName === 'a') { const attr = item.attributes.find(attr => attr.key === 'href') styleObj['href'] = attr?.value || '' } if (item.tagName === 'ul') { styleObj['list-type'] = 'ul' } if (item.tagName === 'ol') { styleObj['list-type'] = 'ol' } if (item.tagName === 'li') { bulletFlag = true } if (item.tagName === 'p') { if ('attributes' in item) { const dataIndentAttr = item.attributes.find(attr => attr.key === 'data-indent') if (dataIndentAttr && dataIndentAttr.value) indent = +dataIndentAttr.value } } } if ('tagName' in item && item.tagName === 'br') { slices.push({ text: '', options: { breakLine: true } }) } else if ('content' in item) { const text = item.content.replace(/ /g, ' ').replace(/>/g, '>').replace(/</g, '<').replace(/&/g, '&').replace(/\n/g, '') const options: pptxgen.TextPropsOptions = {} if (styleObj['font-size']) { options.fontSize = parseInt(styleObj['font-size']) / ratioPx2Pt.value } if (styleObj['color']) { options.color = formatColor(styleObj['color']).color } if (styleObj['background-color']) { options.highlight = formatColor(styleObj['background-color']).color } if (styleObj['text-decoration-line']) { if (styleObj['text-decoration-line'].indexOf('underline') !== -1) { options.underline = { color: options.color || '#000000', style: 'sng', } } if (styleObj['text-decoration-line'].indexOf('line-through') !== -1) { options.strike = 'sngStrike' } } if (styleObj['text-decoration']) { if (styleObj['text-decoration'].indexOf('underline') !== -1) { options.underline = { color: options.color || '#000000', style: 'sng', } } if (styleObj['text-decoration'].indexOf('line-through') !== -1) { options.strike = 'sngStrike' } } if (styleObj['vertical-align']) { if (styleObj['vertical-align'] === 'super') options.superscript = true if (styleObj['vertical-align'] === 'sub') options.subscript = true } if (styleObj['text-align']) options.align = styleObj['text-align'] as pptxgen.HAlign if (styleObj['font-weight']) options.bold = styleObj['font-weight'] === 'bold' if (styleObj['font-style']) options.italic = styleObj['font-style'] === 'italic' if (styleObj['font-family']) options.fontFace = styleObj['font-family'] if (styleObj['href']) options.hyperlink = { url: styleObj['href'] } if (bulletFlag && styleObj['list-type'] === 'ol') { options.bullet = { type: 'number', indent: (options.fontSize || defaultFontSize) * 1.25 } options.paraSpaceBefore = 0.1 bulletFlag = false } if (bulletFlag && styleObj['list-type'] === 'ul') { options.bullet = { indent: (options.fontSize || defaultFontSize) * 1.25 } options.paraSpaceBefore = 0.1 bulletFlag = false } if (indent) { options.indentLevel = indent indent = 0 } slices.push({ text, options }) } else if ('children' in item) parse(item.children, styleObj) } } parse(ast) return slices } type Points = Array< | { x: number; y: number; moveTo?: boolean } | { x: number; y: number; curve: { type: 'arc'; hR: number; wR: number; stAng: number; swAng: number } } | { x: number; y: number; curve: { type: 'quadratic'; x1: number; y1: number } } | { x: number; y: number; curve: { type: 'cubic'; x1: number; y1: number; x2: number; y2: number } } | { close: true } > // 将SVG路径信息格式化为pptxgenjs所需要的格式 const formatPoints = (points: SvgPoints, scale = { x: 1, y: 1 }): Points => { return points.map(point => { if (point.close !== undefined) { return { close: true } } else if (point.type === 'M') { return { x: point.x / ratioPx2Inch.value * scale.x, y: point.y / ratioPx2Inch.value * scale.y, moveTo: true, } } else if (point.curve) { if (point.curve.type === 'cubic') { return { x: point.x / ratioPx2Inch.value * scale.x, y: point.y / ratioPx2Inch.value * scale.y, curve: { type: 'cubic', x1: (point.curve.x1 as number) / ratioPx2Inch.value * scale.x, y1: (point.curve.y1 as number) / ratioPx2Inch.value * scale.y, x2: (point.curve.x2 as number) / ratioPx2Inch.value * scale.x, y2: (point.curve.y2 as number) / ratioPx2Inch.value * scale.y, }, } } else if (point.curve.type === 'quadratic') { return { x: point.x / ratioPx2Inch.value * scale.x, y: point.y / ratioPx2Inch.value * scale.y, curve: { type: 'quadratic', x1: (point.curve.x1 as number) / ratioPx2Inch.value * scale.x, y1: (point.curve.y1 as number) / ratioPx2Inch.value * scale.y, }, } } } return { x: point.x / ratioPx2Inch.value * scale.x, y: point.y / ratioPx2Inch.value * scale.y, } }) } // 获取阴影配置 const getShadowOption = (shadow: PPTElementShadow): pptxgen.ShadowProps => { const c = formatColor(shadow.color) const { h, v } = shadow let offset = 4 let angle = 45 if (h === 0 && v === 0) { offset = 4 angle = 45 } else if (h === 0) { if (v > 0) { offset = v angle = 90 } else { offset = -v angle = 270 } } else if (v === 0) { if (h > 0) { offset = h angle = 1 } else { offset = -h angle = 180 } } else if (h > 0 && v > 0) { offset = Math.max(h, v) angle = 45 } else if (h > 0 && v < 0) { offset = Math.max(h, -v) angle = 315 } else if (h < 0 && v > 0) { offset = Math.max(-h, v) angle = 135 } else if (h < 0 && v < 0) { offset = Math.max(-h, -v) angle = 225 } return { type: 'outer', color: c.color.replace('#', ''), opacity: c.alpha, blur: shadow.blur / ratioPx2Pt.value, offset, angle, } } const dashTypeMap = { 'solid': 'solid', 'dashed': 'dash', 'dotted': 'sysDot', } // 获取边框配置 const getOutlineOption = (outline: PPTElementOutline): pptxgen.ShapeLineProps => { const c = formatColor(outline?.color || '#000000') return { color: c.color, transparency: (1 - c.alpha) * 100, width: (outline.width || 1) / ratioPx2Pt.value, dashType: outline.style ? dashTypeMap[outline.style] as 'solid' | 'dash' | 'sysDot' : 'solid', } } // 获取超链接配置 const getLinkOption = (link: PPTElementLink): pptxgen.HyperlinkProps | null => { const { type, target } = link if (type === 'web') return { url: target } if (type === 'slide') { const index = slides.value.findIndex(slide => slide.id === target) if (index !== -1) return { slide: index + 1 } } return null } // 判断是否为Base64图片地址 const isBase64Image = (url: string) => { const regex = /^data:image\/[^;]+;base64,/ return url.match(regex) !== null } // 判断是否为SVG图片地址 const isSVGImage = (url: string) => { const isSVGBase64 = /^data:image\/svg\+xml;base64,/.test(url) const isSVGUrl = /\.svg$/.test(url) return isSVGBase64 || isSVGUrl } // 确保元素已渲染的辅助函数 const ensureElementsRendered = async () => { return new Promise((resolve) => { // 强制重绘 document.body.offsetHeight; // 等待下一个动画帧 requestAnimationFrame(() => { requestAnimationFrame(() => { resolve(); }); }); }); }; // 导出PPTX文件 const exportPPTX = async (_slides: Slide[], masterOverwrite: boolean, ignoreMedia: boolean) => { exporting.value = true try { // 确保所有元素已渲染 await ensureElementsRendered(); } catch (error) { console.warn('Failed to ensure elements rendered:', error); } const pptx = new pptxgen() if (viewportRatio.value === 0.625) pptx.layout = 'LAYOUT_16x10' else if (viewportRatio.value === 0.75) pptx.layout = 'LAYOUT_4x3' else if (viewportRatio.value === 0.70710678) { pptx.defineLayout({ name: 'A3', width: 10, height: 7.0710678 }) pptx.layout = 'A3' } else if (viewportRatio.value === 1.41421356) { pptx.defineLayout({ name: 'A3_V', width: 10, height: 14.1421356 }) pptx.layout = 'A3_V' } else pptx.layout = 'LAYOUT_16x9' if (masterOverwrite) { const { color: bgColor, alpha: bgAlpha } = formatColor(theme.value.backgroundColor) pptx.defineSlideMaster({ title: 'PPTIST_MASTER', background: { color: bgColor, transparency: (1 - bgAlpha) * 100 }, }) } for (const slide of _slides) { const pptxSlide = pptx.addSlide() if (slide.background) { const background = slide.background if (background.type === 'image' && background.image) { if (isSVGImage(background.image.src)) { pptxSlide.addImage({ data: background.image.src, x: 0, y: 0, w: viewportSize.value / ratioPx2Inch.value, h: viewportSize.value * viewportRatio.value / ratioPx2Inch.value, }) } else if (isBase64Image(background.image.src)) { pptxSlide.background = { data: background.image.src } } else { pptxSlide.background = { path: background.image.src } } } else if (background.type === 'solid' && background.color) { const c = formatColor(background.color) pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 } } else if (background.type === 'gradient' && background.gradient) { const colors = background.gradient.colors const color1 = colors[0].color const color2 = colors[colors.length - 1].color const color = tinycolor.mix(color1, color2).toHexString() const c = formatColor(color) pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 } } } if (slide.remark) { const doc = new DOMParser().parseFromString(slide.remark, 'text/html') const pList = doc.body.querySelectorAll('p') const text = [] for (const p of pList) { const textContent = p.textContent text.push(textContent || '') } pptxSlide.addNotes(text.join('\n')) } if (!slide.elements) continue for (const el of slide.elements) { if (el.type === 'text') { const textProps = formatHTML(el.content) const options: pptxgen.TextPropsOptions = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, fontSize: defaultFontSize / ratioPx2Pt.value, fontFace: '微软雅黑', color: '#000000', valign: 'top', margin: 10 / ratioPx2Pt.value, paraSpaceBefore: 5 / ratioPx2Pt.value, lineSpacingMultiple: 1.5 / 1.25, autoFit: true, } if (el.rotate) options.rotate = el.rotate if (el.wordSpace) options.charSpacing = el.wordSpace / ratioPx2Pt.value if (el.lineHeight) options.lineSpacingMultiple = el.lineHeight / 1.25 if (el.fill) { const c = formatColor(el.fill) const opacity = el.opacity === undefined ? 1 : el.opacity options.fill = { color: c.color, transparency: (1 - c.alpha * opacity) * 100 } } if (el.defaultColor) options.color = formatColor(el.defaultColor).color if (el.defaultFontName) options.fontFace = el.defaultFontName if (el.shadow) options.shadow = getShadowOption(el.shadow) if (el.outline?.width) options.line = getOutlineOption(el.outline) if (el.opacity !== undefined) options.transparency = (1 - el.opacity) * 100 if (el.paragraphSpace !== undefined) options.paraSpaceBefore = el.paragraphSpace / ratioPx2Pt.value if (el.vertical) options.vert = 'eaVert' pptxSlide.addText(textProps, options) } else if (el.type === 'image') { const options: pptxgen.ImageProps = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, } if (isBase64Image(el.src)) options.data = el.src else options.path = el.src if (el.flipH) options.flipH = el.flipH if (el.flipV) options.flipV = el.flipV if (el.rotate) options.rotate = el.rotate if (el.link) { const linkOption = getLinkOption(el.link) if (linkOption) options.hyperlink = linkOption } if (el.filters?.opacity) options.transparency = 100 - parseInt(el.filters?.opacity) if (el.clip) { if (el.clip.shape === 'ellipse') options.rounding = true const [start, end] = el.clip.range const [startX, startY] = start const [endX, endY] = end const originW = el.width / ((endX - startX) / ratioPx2Inch.value) const originH = el.height / ((endY - startY) / ratioPx2Inch.value) options.w = originW / ratioPx2Inch.value options.h = originH / ratioPx2Inch.value options.sizing = { type: 'crop', x: startX / ratioPx2Inch.value * originW / ratioPx2Inch.value, y: startY / ratioPx2Inch.value * originH / ratioPx2Inch.value, w: (endX - startX) / ratioPx2Inch.value * originW / ratioPx2Inch.value, h: (endY - startY) / ratioPx2Inch.value * originH / ratioPx2Inch.value, } } pptxSlide.addImage(options) } else if (el.type === 'shape') { if (el.special) { console.log(`Processing special shape ${el.id} with Canvas rendering:`, { type: el.type, special: el.special, width: el.width, height: el.height, left: el.left, top: el.top }); // 检查Canvas渲染支持 if (!isCanvasRenderSupported()) { console.warn('Canvas rendering not supported, falling back to path-based export'); // 降级到普通形状处理的代码保持不变 const scale = { x: el.width / el.viewBox[0], y: el.height / el.viewBox[1], }; const points = formatPoints(toPoints(el.path), scale); let fillColor = formatColor(el.fill); if (el.gradient) { const colors = el.gradient.colors; const color1 = colors[0].color; const color2 = colors[colors.length - 1].color; const color = tinycolor.mix(color1, color2).toHexString(); fillColor = formatColor(color); } if (el.pattern) fillColor = formatColor('#00000000'); const opacity = el.opacity === undefined ? 1 : el.opacity; const fallbackOptions: pptxgen.ShapeProps = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, fill: { color: fillColor.color, transparency: (1 - fillColor.alpha * opacity) * 100 }, points, }; if (el.flipH) fallbackOptions.flipH = el.flipH; if (el.flipV) fallbackOptions.flipV = el.flipV; if (el.shadow) fallbackOptions.shadow = getShadowOption(el.shadow); if (el.outline?.width) fallbackOptions.line = getOutlineOption(el.outline); if (el.rotate) fallbackOptions.rotate = el.rotate; if (el.link) { const linkOption = getLinkOption(el.link); if (linkOption) fallbackOptions.hyperlink = linkOption; } pptxSlide.addShape('custGeom' as pptxgen.ShapeType, fallbackOptions); continue; } // 尝试多种选择器策略查找元素 let targetElement: HTMLElement | null = null; // 优先查找包含SVG的容器元素 const containerSelectors = [ `.thumbnail-list .base-element-${el.id}`, `[data-element-id="${el.id}"]`, `#element-${el.id}`, `[class*="${el.id}"]` ]; for (const selector of containerSelectors) { const container = document.querySelector(selector) as HTMLElement; if (container) { // 检查容器是否包含SVG const svgInContainer = container.querySelector('svg'); if (svgInContainer) { targetElement = container; console.log(`Found container with SVG for ${el.id} using selector: ${selector}`); break; } // 如果容器本身就是我们要的元素 if (container.tagName.toLowerCase() === 'svg' || container.children.length > 0) { targetElement = container; console.log(`Found target element for ${el.id} using selector: ${selector}`); break; } } } // 如果没找到容器,直接查找SVG元素 if (!targetElement) { const svgSelectors = [ `.thumbnail-list .base-element-${el.id} svg`, `[data-element-id="${el.id}"] svg`, `#element-${el.id} svg` ]; for (const selector of svgSelectors) { const svgElement = document.querySelector(selector) as HTMLElement; if (svgElement) { targetElement = svgElement; console.log(`Found SVG element for ${el.id} using selector: ${selector}`); break; } } } // 最后尝试通过父元素匹配查找 if (!targetElement) { const allSvgs = document.querySelectorAll('svg'); console.log(`Searching through ${allSvgs.length} SVG elements for ${el.id}`); for (const svg of allSvgs) { const parent = svg.closest(`[class*="${el.id}"]`); if (parent) { targetElement = svg as HTMLElement; console.log(`Found SVG for ${el.id} using parent matching`); break; } } } if (!targetElement) { console.warn(`No target element found for shape ${el.id}, falling back to path-based export`); // 降级处理代码... const scale = { x: el.width / el.viewBox[0], y: el.height / el.viewBox[1], }; const points = formatPoints(toPoints(el.path), scale); let fillColor = formatColor(el.fill); if (el.gradient) { const colors = el.gradient.colors; const color1 = colors[0].color; const color2 = colors[colors.length - 1].color; const color = tinycolor.mix(color1, color2).toHexString(); fillColor = formatColor(color); } if (el.pattern) fillColor = formatColor('#00000000'); const opacity = el.opacity === undefined ? 1 : el.opacity; const fallbackOptions: pptxgen.ShapeProps = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, fill: { color: fillColor.color, transparency: (1 - fillColor.alpha * opacity) * 100 }, points, }; if (el.flipH) fallbackOptions.flipH = el.flipH; if (el.flipV) fallbackOptions.flipV = el.flipV; if (el.shadow) fallbackOptions.shadow = getShadowOption(el.shadow); if (el.outline?.width) fallbackOptions.line = getOutlineOption(el.outline); if (el.rotate) fallbackOptions.rotate = el.rotate; if (el.link) { const linkOption = getLinkOption(el.link); if (linkOption) fallbackOptions.hyperlink = linkOption; } pptxSlide.addShape('custGeom' as pptxgen.ShapeType, fallbackOptions); continue; } // 检查元素尺寸 - 使用更智能的检查逻辑 const dimensions = getElementDimensions(targetElement); console.log(`Target element dimensions for ${el.id}:`, dimensions); // 更宽松的尺寸检查:只要有任何一种尺寸测量方法返回有效值就继续 const hasValidDimensions = ( dimensions.width > 0 || dimensions.height > 0 || dimensions.clientWidth > 0 || dimensions.clientHeight > 0 || dimensions.offsetWidth > 0 || dimensions.offsetHeight > 0 || dimensions.boundingRect.width > 0 || dimensions.boundingRect.height > 0 ); if (!hasValidDimensions) { console.warn(`No valid dimensions found for shape ${el.id}:`, dimensions); console.log('Falling back to path-based export'); // 降级处理代码... const scale = { x: el.width / el.viewBox[0], y: el.height / el.viewBox[1], }; const points = formatPoints(toPoints(el.path), scale); let fillColor = formatColor(el.fill); if (el.gradient) { const colors = el.gradient.colors; const color1 = colors[0].color; const color2 = colors[colors.length - 1].color; const color = tinycolor.mix(color1, color2).toHexString(); fillColor = formatColor(color); } if (el.pattern) fillColor = formatColor('#00000000'); const opacity = el.opacity === undefined ? 1 : el.opacity; const fallbackOptions: pptxgen.ShapeProps = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, fill: { color: fillColor.color, transparency: (1 - fillColor.alpha * opacity) * 100 }, points, }; if (el.flipH) fallbackOptions.flipH = el.flipH; if (el.flipV) fallbackOptions.flipV = el.flipV; if (el.shadow) fallbackOptions.shadow = getShadowOption(el.shadow); if (el.outline?.width) fallbackOptions.line = getOutlineOption(el.outline); if (el.rotate) fallbackOptions.rotate = el.rotate; if (el.link) { const linkOption = getLinkOption(el.link); if (linkOption) fallbackOptions.hyperlink = linkOption; } pptxSlide.addShape('custGeom' as pptxgen.ShapeType, fallbackOptions); continue; } // 使用多重策略渲染矢量图形 let base64Image; let renderSuccess = false; // 策略1: 优先使用SVG序列化(对矢量图形更可靠) try { console.log(`Attempting SVG serialization for ${el.id}`); const svgElement = targetElement.tagName.toLowerCase() === 'svg' ? targetElement : targetElement.querySelector('svg'); if (svgElement) { // 确保SVG元素已完全渲染 await new Promise(resolve => { requestAnimationFrame(() => { requestAnimationFrame(resolve); }); }); base64Image = svg2Base64(svgElement); console.log(`SVG serialization result for ${el.id}:`, { success: !!base64Image, length: base64Image ? base64Image.length : 0, preview: base64Image ? base64Image.substring(0, 100) + '...' : 'null' }); if (base64Image && base64Image.length > 100) { renderSuccess = true; console.log(`SVG serialization successful for ${el.id}`); } } } catch (svgError) { console.warn(`SVG serialization failed for ${el.id}:`, svgError); } // 策略2: 如果SVG序列化失败,使用Canvas渲染 if (!renderSuccess) { try { console.log(`Attempting Canvas rendering for ${el.id}`); // Canvas渲染选项 const renderOptions = { scale: 2, // 高分辨率渲染 backgroundColor: null, // 透明背景 useCORS: true, timeout: 15000, // 增加超时时间 format: 'png' as const, quality: 0.95 }; base64Image = await renderElementToBase64(targetElement, renderOptions); console.log(`Canvas rendering result for ${el.id}:`, { success: !!base64Image, length: base64Image ? base64Image.length : 0, preview: base64Image ? base64Image.substring(0, 100) + '...' : 'null' }); if (base64Image && base64Image.length > 100) { renderSuccess = true; console.log(`Canvas rendering successful for ${el.id}`); } } catch (canvasError) { console.error(`Canvas rendering failed for ${el.id}:`, canvasError); } } // 策略3: 如果前两种方法都失败,尝试简化的SVG渲染 if (!renderSuccess) { try { console.log(`Attempting simplified SVG rendering for ${el.id}`); const svgElement = targetElement.tagName.toLowerCase() === 'svg' ? targetElement : targetElement.querySelector('svg'); if (svgElement) { // 创建简化的SVG副本 const simplifiedSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const rect = svgElement.getBoundingClientRect(); simplifiedSVG.setAttribute('width', (rect.width || 100).toString()); simplifiedSVG.setAttribute('height', (rect.height || 100).toString()); simplifiedSVG.setAttribute('viewBox', `0 0 ${rect.width || 100} ${rect.height || 100}`); simplifiedSVG.innerHTML = svgElement.innerHTML; base64Image = svg2Base64(simplifiedSVG); if (base64Image && base64Image.length > 100) { renderSuccess = true; console.log(`Simplified SVG rendering successful for ${el.id}`); } } } catch (simplifiedError) { console.error(`Simplified SVG rendering failed for ${el.id}:`, simplifiedError); } } // 如果所有渲染策略都失败,使用路径导出作为最后备选 if (!renderSuccess) { console.warn(`All rendering strategies failed for ${el.id}, falling back to path export`); // 最终降级到路径导出 console.log(`Final fallback to path-based export for ${el.id}`); const scale = { x: el.width / el.viewBox[0], y: el.height / el.viewBox[1], }; const points = formatPoints(toPoints(el.path), scale); let fillColor = formatColor(el.fill); if (el.gradient) { const colors = el.gradient.colors; const color1 = colors[0].color; const color2 = colors[colors.length - 1].color; const color = tinycolor.mix(color1, color2).toHexString(); fillColor = formatColor(color); } if (el.pattern) fillColor = formatColor('#00000000'); const opacity = el.opacity === undefined ? 1 : el.opacity; const fallbackOptions: pptxgen.ShapeProps = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, fill: { color: fillColor.color, transparency: (1 - fillColor.alpha * opacity) * 100 }, points, }; if (el.flipH) fallbackOptions.flipH = el.flipH; if (el.flipV) fallbackOptions.flipV = el.flipV; if (el.shadow) fallbackOptions.shadow = getShadowOption(el.shadow); if (el.outline?.width) fallbackOptions.line = getOutlineOption(el.outline); if (el.rotate) fallbackOptions.rotate = el.rotate; if (el.link) { const linkOption = getLinkOption(el.link); if (linkOption) fallbackOptions.hyperlink = linkOption; } pptxSlide.addShape('custGeom' as pptxgen.ShapeType, fallbackOptions); continue; } // 添加渲染后的图像到幻灯片 const options: pptxgen.ImageProps = { data: base64Image, x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, } if (el.rotate) options.rotate = el.rotate if (el.flipH) options.flipH = el.flipH if (el.flipV) options.flipV = el.flipV if (el.link) { const linkOption = getLinkOption(el.link) if (linkOption) options.hyperlink = linkOption } console.log(`Successfully added Canvas-rendered image for shape ${el.id}`); pptxSlide.addImage(options) } else { const scale = { x: el.width / el.viewBox[0], y: el.height / el.viewBox[1], } const points = formatPoints(toPoints(el.path), scale) let fillColor = formatColor(el.fill) if (el.gradient) { const colors = el.gradient.colors const color1 = colors[0].color const color2 = colors[colors.length - 1].color const color = tinycolor.mix(color1, color2).toHexString() fillColor = formatColor(color) } if (el.pattern) fillColor = formatColor('#00000000') const opacity = el.opacity === undefined ? 1 : el.opacity const options: pptxgen.ShapeProps = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, fill: { color: fillColor.color, transparency: (1 - fillColor.alpha * opacity) * 100 }, points, } if (el.flipH) options.flipH = el.flipH if (el.flipV) options.flipV = el.flipV if (el.shadow) options.shadow = getShadowOption(el.shadow) if (el.outline?.width) options.line = getOutlineOption(el.outline) if (el.rotate) options.rotate = el.rotate if (el.link) { const linkOption = getLinkOption(el.link) if (linkOption) options.hyperlink = linkOption } pptxSlide.addShape('custGeom' as pptxgen.ShapeType, options) } if (el.text) { const textProps = formatHTML(el.text.content) const options: pptxgen.TextPropsOptions = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, fontSize: defaultFontSize / ratioPx2Pt.value, fontFace: '微软雅黑', color: '#000000', paraSpaceBefore: 5 / ratioPx2Pt.value, valign: el.text.align, } if (el.rotate) options.rotate = el.rotate if (el.text.defaultColor) options.color = formatColor(el.text.defaultColor).color if (el.text.defaultFontName) options.fontFace = el.text.defaultFontName pptxSlide.addText(textProps, options) } if (el.pattern) { const options: pptxgen.ImageProps = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, } if (isBase64Image(el.pattern)) options.data = el.pattern else options.path = el.pattern if (el.flipH) options.flipH = el.flipH if (el.flipV) options.flipV = el.flipV if (el.rotate) options.rotate = el.rotate if (el.link) { const linkOption = getLinkOption(el.link) if (linkOption) options.hyperlink = linkOption } pptxSlide.addImage(options) } } else if (el.type === 'line') { const path = getLineElementPath(el) const points = formatPoints(toPoints(path)) const { minX, maxX, minY, maxY } = getElementRange(el) const c = formatColor(el.color) const options: pptxgen.ShapeProps = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: (maxX - minX) / ratioPx2Inch.value, h: (maxY - minY) / ratioPx2Inch.value, line: { color: c.color, transparency: (1 - c.alpha) * 100, width: el.width / ratioPx2Pt.value, dashType: dashTypeMap[el.style] as 'solid' | 'dash' | 'sysDot', beginArrowType: el.points[0] ? 'arrow' : 'none', endArrowType: el.points[1] ? 'arrow' : 'none', }, points, } if (el.shadow) options.shadow = getShadowOption(el.shadow) pptxSlide.addShape('custGeom' as pptxgen.ShapeType, options) } else if (el.type === 'chart') { const chartData = [] for (let i = 0; i < el.data.series.length; i++) { const item = el.data.series[i] chartData.push({ name: `系列${i + 1}`, labels: el.data.labels, values: item, }) } let chartColors: string[] = [] if (el.themeColors.length === 10) chartColors = el.themeColors.map(color => formatColor(color).color) else if (el.themeColors.length === 1) chartColors = tinycolor(el.themeColors[0]).analogous(10).map(color => formatColor(color.toHexString()).color) else { const len = el.themeColors.length const supplement = tinycolor(el.themeColors[len - 1]).analogous(10 + 1 - len).map(color => color.toHexString()) chartColors = [...el.themeColors.slice(0, len - 1), ...supplement].map(color => formatColor(color).color) } const options: pptxgen.IChartOpts = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, chartColors: (el.chartType === 'pie' || el.chartType === 'ring') ? chartColors : chartColors.slice(0, el.data.series.length), } const textColor = formatColor(el.textColor || '#000000').color options.catAxisLabelColor = textColor options.valAxisLabelColor = textColor const fontSize = 14 / ratioPx2Pt.value options.catAxisLabelFontSize = fontSize options.valAxisLabelFontSize = fontSize if (el.fill || el.outline) { const plotArea: pptxgen.IChartPropsFillLine = {} if (el.fill) { plotArea.fill = { color: formatColor(el.fill).color } } if (el.outline) { plotArea.border = { pt: el.outline.width! / ratioPx2Pt.value, color: formatColor(el.outline.color!).color, } } options.plotArea = plotArea } if ((el.data.series.length > 1 && el.chartType !== 'scatter') || el.chartType === 'pie' || el.chartType === 'ring') { options.showLegend = true options.legendPos = 'b' options.legendColor = textColor options.legendFontSize = fontSize } let type = pptx.ChartType.bar if (el.chartType === 'bar') { type = pptx.ChartType.bar options.barDir = 'col' if (el.options?.stack) options.barGrouping = 'stacked' } else if (el.chartType === 'column') { type = pptx.ChartType.bar options.barDir = 'bar' if (el.options?.stack) options.barGrouping = 'stacked' } else if (el.chartType === 'line') { type = pptx.ChartType.line if (el.options?.lineSmooth) options.lineSmooth = true } else if (el.chartType === 'area') { type = pptx.ChartType.area } else if (el.chartType === 'radar') { type = pptx.ChartType.radar } else if (el.chartType === 'scatter') { type = pptx.ChartType.scatter options.lineSize = 0 } else if (el.chartType === 'pie') { type = pptx.ChartType.pie } else if (el.chartType === 'ring') { type = pptx.ChartType.doughnut options.holeSize = 60 } pptxSlide.addChart(type, chartData, options) } else if (el.type === 'table') { const hiddenCells = [] for (let i = 0; i < el.data.length; i++) { const rowData = el.data[i] for (let j = 0; j < rowData.length; j++) { const cell = rowData[j] if (cell.colspan > 1 || cell.rowspan > 1) { for (let row = i; row < i + cell.rowspan; row++) { for (let col = row === i ? j + 1 : j; col < j + cell.colspan; col++) hiddenCells.push(`${row}_${col}`) } } } } const tableData = [] const theme = el.theme let themeColor: FormatColor | null = null let subThemeColors: FormatColor[] = [] if (theme) { themeColor = formatColor(theme.color) subThemeColors = getTableSubThemeColor(theme.color).map(item => formatColor(item)) } for (let i = 0; i < el.data.length; i++) { const row = el.data[i] const _row = [] for (let j = 0; j < row.length; j++) { const cell = row[j] const cellOptions: pptxgen.TableCellProps = { colspan: cell.colspan, rowspan: cell.rowspan, bold: cell.style?.bold || false, italic: cell.style?.em || false, underline: { style: cell.style?.underline ? 'sng' : 'none' }, align: cell.style?.align || 'left', valign: 'middle', fontFace: cell.style?.fontname || '微软雅黑', fontSize: (cell.style?.fontsize ? parseInt(cell.style?.fontsize) : 14) / ratioPx2Pt.value, } if (theme && themeColor) { let c: FormatColor if (i % 2 === 0) c = subThemeColors[1] else c = subThemeColors[0] if (theme.rowHeader && i === 0) c = themeColor else if (theme.rowFooter && i === el.data.length - 1) c = themeColor else if (theme.colHeader && j === 0) c = themeColor else if (theme.colFooter && j === row.length - 1) c = themeColor cellOptions.fill = { color: c.color, transparency: (1 - c.alpha) * 100 } } if (cell.style?.backcolor) { const c = formatColor(cell.style.backcolor) cellOptions.fill = { color: c.color, transparency: (1 - c.alpha) * 100 } } if (cell.style?.color) cellOptions.color = formatColor(cell.style.color).color if (!hiddenCells.includes(`${i}_${j}`)) { _row.push({ text: cell.text, options: cellOptions, }) } } if (_row.length) tableData.push(_row) } const options: pptxgen.TableProps = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, colW: el.colWidths.map(item => el.width * item / ratioPx2Inch.value), } if (el.theme) options.fill = { color: '#ffffff' } if (el.outline.width && el.outline.color) { options.border = { type: el.outline.style === 'solid' ? 'solid' : 'dash', pt: el.outline.width / ratioPx2Pt.value, color: formatColor(el.outline.color).color, } } pptxSlide.addTable(tableData, options) } else if (el.type === 'latex') { const svgRef = document.querySelector(`.thumbnail-list .base-element-${el.id} svg`) as HTMLElement const base64SVG = svg2Base64(svgRef) const options: pptxgen.ImageProps = { data: base64SVG, x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, } if (el.link) { const linkOption = getLinkOption(el.link) if (linkOption) options.hyperlink = linkOption } pptxSlide.addImage(options) } else if (!ignoreMedia && (el.type === 'video' || el.type === 'audio')) { const options: pptxgen.MediaProps = { x: el.left / ratioPx2Inch.value, y: el.top / ratioPx2Inch.value, w: el.width / ratioPx2Inch.value, h: el.height / ratioPx2Inch.value, path: el.src, type: el.type, } if (el.type === 'video' && el.poster) options.cover = el.poster const extMatch = el.src.match(/\.([a-zA-Z0-9]+)(?:[\?#]|$)/) if (extMatch && extMatch[1]) options.extn = extMatch[1] else if (el.ext) options.extn = el.ext const videoExts = ['avi', 'mp4', 'm4v', 'mov', 'wmv'] const audioExts = ['mp3', 'm4a', 'mp4', 'wav', 'wma'] if (options.extn && [...videoExts, ...audioExts].includes(options.extn)) { pptxSlide.addMedia(options) } } } } setTimeout(() => { pptx.writeFile({ fileName: `${title.value}.pptx` }).then(() => exporting.value = false).catch(() => { exporting.value = false message.error('导出失败') }) }, 200) } return { exporting, exportImage, exportJSON, exportSpecificFile, exportHTML, exportPPTX, generateHTMLPresentation, } }