jvs-ppt-ui 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -1
- package/dist/lib/jvs-ppt-ui.es.js +21367 -21291
- package/dist/lib/jvs-ppt-ui.umd.js +85 -85
- package/dist/lib/style.css +1 -1
- package/package.json +7 -7
- package/scripts/check_connectors.cjs +25 -0
- package/scripts/check_slide1.cjs +11 -0
- package/scripts/check_slide123.cjs +25 -0
- package/scripts/convert-fonts-to-woff2.cjs +97 -0
- package/scripts/extract-slides.ts +149 -0
- package/scripts/find_candidate_pptx.cjs +34 -0
- package/scripts/find_orange_slide.cjs +29 -0
- package/scripts/generate-complete-ppt-preview.js +385 -0
- package/scripts/inspect_315_327.cjs +19 -0
- package/scripts/mcp-smoke-test.js +568 -0
- package/scripts/patch-pptxtojson.cjs +81 -0
- package/scripts/verify-fonts.cjs +20 -0
- package/src/App.vue +1 -1
- package/src/components/CanvasArea.vue +38 -16
- package/src/components/Header.vue +1587 -1564
- package/src/components/PptEditor.vue +5 -0
- package/src/components/SidebarLeft.vue +14 -3
- package/src/components/canvas/elements/CanvasShapeElement.vue +3 -3
- package/src/components/canvas/elements/CanvasTextElement.vue +4 -4
- package/src/composables/canvas/useCanvasShortcuts.ts +50 -1
- package/src/composables/canvas/useCanvasTable.ts +8 -3
- package/src/composables/canvas/useCanvasText.ts +7 -3
- package/src/composables/useSlides.ts +14 -5
|
@@ -16,7 +16,7 @@ import { inferElementTextType, type SlideTextType } from '../utils/textTypes'
|
|
|
16
16
|
// 快捷键弹层异步化;mock DSL 模板(~77KB)改为触发时动态加载
|
|
17
17
|
const ShortcutsDialog = defineAsyncComponent(() => import('./ShortcutsDialog.vue'))
|
|
18
18
|
|
|
19
|
-
const { presentationTitle, exportSlides, importSlides, currentZipMeta, startPresentation, activeIndex, slides, notify, renderLatexToSvg, globalFontFamily, globalFontColor, globalThemeColor, themeSchemeFillMap, themeClrScheme, logicalWidth, logicalHeight, dslInputDialogVisible, aiPptDialogVisible, shortcutsDialogVisible, registerPPTParser, isMasterEditingView, enterMasterView, exitMasterView, zoomRatio, resetZoom, normalizeSlideBackground, getEffectiveMasterElementsForSlide, getEffectiveSlideBackground, uploadMediaResourcesInSlides, getCustomUploadHandler } = useSlides()
|
|
19
|
+
const { presentationTitle, exportSlides, importSlides, currentZipMeta, startPresentation, activeIndex, slides, notify, renderLatexToSvg, globalFontFamily, globalFontColor, globalThemeColor, themeSchemeFillMap, themeClrScheme, logicalWidth, logicalHeight, dslInputDialogVisible, aiPptDialogVisible, shortcutsDialogVisible, registerPPTParser, isMasterEditingView, enterMasterView, exitMasterView, zoomRatio, resetZoom, normalizeSlideBackground, getEffectiveMasterElementsForSlide, getEffectiveSlideBackground, uploadMediaResourcesInSlides, getCustomUploadHandler, isReadonly } = useSlides()
|
|
20
20
|
import { sanitizeNbspInHtml, checkIsVertical } from '../composables/useSlides'
|
|
21
21
|
|
|
22
22
|
// 快捷键弹层挂载闩锁:首次打开后保持挂载,异步 chunk 只加载一次
|
|
@@ -42,6 +42,7 @@ const fileInputRef = ref<HTMLInputElement | null>(null)
|
|
|
42
42
|
const isEditingTitle = ref(false)
|
|
43
43
|
const titleInputRef = ref<HTMLInputElement | null>(null)
|
|
44
44
|
const startEditingTitle = () => {
|
|
45
|
+
if (isReadonly.value) return
|
|
45
46
|
isEditingTitle.value = true
|
|
46
47
|
nextTick(() => {
|
|
47
48
|
titleInputRef.value?.focus()
|
|
@@ -957,7 +958,7 @@ function parseHTMLTable(htmlStr: string, totalWidth?: number): { tableData: any[
|
|
|
957
958
|
const colgroupCols = tableEl.querySelectorAll('colgroup col')
|
|
958
959
|
const firstRowCells = tableEl.querySelectorAll('tr:first-child > td, tr:first-child > th')
|
|
959
960
|
const targetEls = colgroupCols.length >= totalCols ? colgroupCols : (firstRowCells.length > 0 ? firstRowCells : colgroupCols)
|
|
960
|
-
|
|
961
|
+
|
|
961
962
|
let colCounter = 0
|
|
962
963
|
targetEls.forEach((cEl) => {
|
|
963
964
|
if (colCounter >= totalCols) return
|
|
@@ -1785,960 +1786,960 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
|
|
|
1785
1786
|
|
|
1786
1787
|
// 2. Elements
|
|
1787
1788
|
const processElement = async (el: any, parentPixels: { left: number; top: number; width: number; height: number; rotate: number; flipH: boolean; flipV: boolean } | null = null) => {
|
|
1788
|
-
|
|
1789
|
+
if (el.visible === false) return
|
|
1789
1790
|
|
|
1790
|
-
|
|
1791
|
-
|
|
1791
|
+
let pixelWidth = 0
|
|
1792
|
+
let pixelHeight = 0
|
|
1792
1793
|
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1794
|
+
const rawWidth = el.style?.width !== undefined ? el.style.width : el.width
|
|
1795
|
+
const rawHeight = el.style?.height !== undefined ? el.style.height : el.height
|
|
1796
|
+
const rawLeft = el.style?.left !== undefined ? el.style.left : el.left
|
|
1797
|
+
const rawTop = el.style?.top !== undefined ? el.style.top : el.top
|
|
1797
1798
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1799
|
+
if (parentPixels) {
|
|
1800
|
+
pixelWidth = parseValToPx(rawWidth, parentPixels.width, 100)
|
|
1801
|
+
pixelHeight = parseValToPx(rawHeight, parentPixels.height, 40)
|
|
1802
|
+
} else {
|
|
1803
|
+
pixelWidth = parseCoordinate(rawWidth, logicalWidth.value)
|
|
1804
|
+
pixelHeight = parseCoordinate(rawHeight, logicalHeight.value)
|
|
1805
|
+
}
|
|
1805
1806
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
}
|
|
1812
|
-
if (!pixelWidth || el.style?.width === 'auto') {
|
|
1813
|
-
pixelWidth = domEl.offsetWidth
|
|
1814
|
-
}
|
|
1815
|
-
} else {
|
|
1816
|
-
const defaultFontSize = parseInt(el.style?.fontSize || '') || 18
|
|
1817
|
-
if (!pixelWidth || el.style?.width === 'auto') {
|
|
1818
|
-
pixelWidth = 400
|
|
1819
|
-
}
|
|
1820
|
-
if (!pixelHeight || el.style?.height === 'auto') {
|
|
1821
|
-
let topPadding = 4.8
|
|
1822
|
-
let bottomPadding = 4.8
|
|
1823
|
-
if (el.style?.padding) {
|
|
1824
|
-
const paddingParts = el.style.padding.trim().split(/\s+/)
|
|
1825
|
-
let parsedPadding = [4.8, 9.6, 4.8, 9.6]
|
|
1826
|
-
if (paddingParts.length === 4) {
|
|
1827
|
-
parsedPadding = paddingParts.map((p: string) => parseFloat(p) || 0)
|
|
1828
|
-
} else if (paddingParts.length === 1) {
|
|
1829
|
-
const val = parseFloat(paddingParts[0]) || 0
|
|
1830
|
-
parsedPadding = [val, val, val, val]
|
|
1831
|
-
}
|
|
1832
|
-
topPadding = parsedPadding[0]
|
|
1833
|
-
bottomPadding = parsedPadding[2]
|
|
1834
|
-
}
|
|
1835
|
-
pixelHeight = estimateTextHeight(el.content || '', pixelWidth, defaultFontSize) + topPadding + bottomPadding
|
|
1836
|
-
}
|
|
1807
|
+
if (el.type === 'text') {
|
|
1808
|
+
const domEl = document.getElementById(`canvas-el-${el.id}`)
|
|
1809
|
+
if (domEl && domEl.offsetHeight > 0) {
|
|
1810
|
+
if (!pixelHeight || el.style?.height === 'auto') {
|
|
1811
|
+
pixelHeight = domEl.offsetHeight
|
|
1837
1812
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
const elRotate = typeof el.style?.rotate === 'number'
|
|
1841
|
-
? el.style.rotate
|
|
1842
|
-
: (parseFloat(el.style?.rotate || el.rotate || '0') || 0)
|
|
1843
|
-
const elFlipH = Boolean(el.style?.flipH || el.flipH || el.isFlipH)
|
|
1844
|
-
const elFlipV = Boolean(el.style?.flipV || el.flipV || el.isFlipV)
|
|
1845
|
-
|
|
1846
|
-
let pixelLeft = 0
|
|
1847
|
-
let pixelTop = 0
|
|
1848
|
-
let rotate = 0
|
|
1849
|
-
let flipH = elFlipH
|
|
1850
|
-
let flipV = elFlipV
|
|
1851
|
-
|
|
1852
|
-
if (parentPixels) {
|
|
1853
|
-
const rawChildLeft = parseValToPx(rawLeft, parentPixels.width, 0)
|
|
1854
|
-
const rawChildTop = parseValToPx(rawTop, parentPixels.height, 0)
|
|
1855
|
-
|
|
1856
|
-
const pW = parentPixels.width
|
|
1857
|
-
const pH = parentPixels.height
|
|
1858
|
-
const cCenterRelX = (rawChildLeft + pixelWidth / 2) - pW / 2
|
|
1859
|
-
const cCenterRelY = (rawChildTop + pixelHeight / 2) - pH / 2
|
|
1860
|
-
|
|
1861
|
-
let flippedRelX = parentPixels.flipH ? -cCenterRelX : cCenterRelX
|
|
1862
|
-
let flippedRelY = parentPixels.flipV ? -cCenterRelY : cCenterRelY
|
|
1863
|
-
|
|
1864
|
-
let rotRelX = flippedRelX
|
|
1865
|
-
let rotRelY = flippedRelY
|
|
1866
|
-
if (parentPixels.rotate) {
|
|
1867
|
-
const rad = (parentPixels.rotate * Math.PI) / 180
|
|
1868
|
-
rotRelX = flippedRelX * Math.cos(rad) - flippedRelY * Math.sin(rad)
|
|
1869
|
-
rotRelY = flippedRelX * Math.sin(rad) + flippedRelY * Math.cos(rad)
|
|
1813
|
+
if (!pixelWidth || el.style?.width === 'auto') {
|
|
1814
|
+
pixelWidth = domEl.offsetWidth
|
|
1870
1815
|
}
|
|
1871
|
-
|
|
1872
|
-
const newCenterX = pW / 2 + rotRelX
|
|
1873
|
-
const newCenterY = pH / 2 + rotRelY
|
|
1874
|
-
|
|
1875
|
-
pixelLeft = parentPixels.left + (newCenterX - pixelWidth / 2)
|
|
1876
|
-
pixelTop = parentPixels.top + (newCenterY - pixelHeight / 2)
|
|
1877
|
-
|
|
1878
|
-
const parentOddFlips = (parentPixels.flipH !== parentPixels.flipV)
|
|
1879
|
-
const childEffectiveRot = parentOddFlips ? -elRotate : elRotate
|
|
1880
|
-
rotate = ((parentPixels.rotate + childEffectiveRot) % 360 + 360) % 360
|
|
1881
|
-
|
|
1882
|
-
flipH = parentPixels.flipH ? !elFlipH : elFlipH
|
|
1883
|
-
flipV = parentPixels.flipV ? !elFlipV : elFlipV
|
|
1884
1816
|
} else {
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
if (el.type === 'group' && Array.isArray(el.elements)) {
|
|
1891
|
-
const curGroupPixels = {
|
|
1892
|
-
left: pixelLeft,
|
|
1893
|
-
top: pixelTop,
|
|
1894
|
-
width: pixelWidth,
|
|
1895
|
-
height: pixelHeight,
|
|
1896
|
-
rotate,
|
|
1897
|
-
flipH,
|
|
1898
|
-
flipV
|
|
1899
|
-
}
|
|
1900
|
-
for (const child of el.elements) {
|
|
1901
|
-
await processElement(child, curGroupPixels)
|
|
1902
|
-
}
|
|
1903
|
-
return
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
const x = (pixelLeft / logicalWidth.value) * baseWidth
|
|
1907
|
-
const y = (pixelTop / logicalHeight.value) * baseHeight
|
|
1908
|
-
const w = (pixelWidth / logicalWidth.value) * baseWidth
|
|
1909
|
-
const h = (pixelHeight / logicalHeight.value) * baseHeight
|
|
1910
|
-
|
|
1911
|
-
if (el.type === 'text') {
|
|
1912
|
-
if (el.latex) {
|
|
1913
|
-
const svgString = renderLatexToSvg(el.latex)
|
|
1914
|
-
const base64SVG = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svgString)))
|
|
1915
|
-
pptxSlide.addImage({
|
|
1916
|
-
data: base64SVG,
|
|
1917
|
-
x, y, w, h,
|
|
1918
|
-
rotate,
|
|
1919
|
-
objectName: el.id
|
|
1920
|
-
})
|
|
1921
|
-
}
|
|
1922
|
-
else if ((el.content && (el.content.includes('<table') || el.content.includes('<table'))) || (el.data && Array.isArray(el.data) && el.data.length > 0)) {
|
|
1923
|
-
const res = (el.content && (el.content.includes('<table') || el.content.includes('<table')))
|
|
1924
|
-
? parseHTMLTable(el.content, w)
|
|
1925
|
-
: parseTableElementData(el, w)
|
|
1926
|
-
if (res.tableData && res.tableData.length > 0) {
|
|
1927
|
-
const tableOpts: any = { x, y, w, h, objectName: el.id }
|
|
1928
|
-
if (res.colW && res.colW.length > 0) {
|
|
1929
|
-
tableOpts.colW = res.colW
|
|
1930
|
-
}
|
|
1931
|
-
pptxSlide.addTable(res.tableData, tableOpts)
|
|
1932
|
-
}
|
|
1817
|
+
const defaultFontSize = parseInt(el.style?.fontSize || '') || 18
|
|
1818
|
+
if (!pixelWidth || el.style?.width === 'auto') {
|
|
1819
|
+
pixelWidth = 400
|
|
1933
1820
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
const defaultFontFamily = (el.style?.fontFamily && el.style.fontFamily !== 'inherit' && el.style.fontFamily !== '默认字体')
|
|
1938
|
-
? el.style.fontFamily
|
|
1939
|
-
: (globalFontFamily.value && globalFontFamily.value !== '默认字体' ? globalFontFamily.value : 'SourceHanSansCN')
|
|
1940
|
-
|
|
1941
|
-
const isBold = el.style?.fontWeight === 'bold' || el.style?.fontWeight === '700' || (parseInt(el.style?.fontWeight as string) || 0) > 500
|
|
1942
|
-
const isItalic = (el.style as any)?.fontStyle === 'italic'
|
|
1943
|
-
const isUnderline = !!(el.style?.textDecoration && el.style.textDecoration.includes('underline'))
|
|
1944
|
-
const isStrike = !!(el.style?.textDecoration && el.style.textDecoration.includes('line-through'))
|
|
1945
|
-
|
|
1946
|
-
const hasFillOrBorder = (el.style?.textBackground && el.style.textBackground !== 'transparent' && el.style.textBackground !== 'none') || (parseFloat(el.style?.borderWidth || '') || 0) > 0
|
|
1947
|
-
|
|
1948
|
-
const textShadowObj = (el as any).textShadow || (el.style && (el.style as any).textShadow) || (!hasFillOrBorder ? (el.shadow || (el.style && (el.style as any).shadow)) : undefined)
|
|
1949
|
-
let parsedTextShadow: any = undefined
|
|
1950
|
-
if (textShadowObj) {
|
|
1951
|
-
parsedTextShadow = convertElShadowToPptx(textShadowObj, ratioPx2Pt)
|
|
1952
|
-
}
|
|
1953
|
-
if (!parsedTextShadow) {
|
|
1954
|
-
let textShadowStr = el.style?.boxShadow
|
|
1955
|
-
if (!textShadowStr && el.content) {
|
|
1956
|
-
const match = el.content.match(/(?:text-shadow)\s*:\s*([^;"]+)/i)
|
|
1957
|
-
if (match) {
|
|
1958
|
-
textShadowStr = match[1].trim()
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
if (textShadowStr) {
|
|
1962
|
-
parsedTextShadow = parseCssShadow(textShadowStr, ratioPx2Pt)
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
const shapeShadowObj = hasFillOrBorder ? (el.shadow || (el.style && (el.style as any).shadow)) : undefined
|
|
1967
|
-
let parsedShapeShadow: any = undefined
|
|
1968
|
-
if (shapeShadowObj) {
|
|
1969
|
-
parsedShapeShadow = convertElShadowToPptx(shapeShadowObj, ratioPx2Pt)
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
const textRefl = (el as any).textReflection || (el.style as any)?.textReflection || (!hasFillOrBorder ? (el.reflection || (el.style && el.style.reflection)) : undefined)
|
|
1973
|
-
let parsedTextReflection: any = undefined
|
|
1974
|
-
if (textRefl) {
|
|
1975
|
-
parsedTextReflection = convertElReflectionToPptx(textRefl, ratioPx2Pt)
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
const shapeRefl = hasFillOrBorder ? (el.reflection || (el.style && el.style.reflection)) : undefined
|
|
1979
|
-
let parsedShapeReflection: any = undefined
|
|
1980
|
-
if (shapeRefl) {
|
|
1981
|
-
parsedShapeReflection = convertElReflectionToPptx(shapeRefl, ratioPx2Pt)
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
const textProps = formatHTMLToPPTX(el.content || '', {
|
|
1985
|
-
fontSize: defaultFontSize,
|
|
1986
|
-
color: defaultColor,
|
|
1987
|
-
fontFamily: defaultFontFamily,
|
|
1988
|
-
bold: isBold,
|
|
1989
|
-
italic: isItalic,
|
|
1990
|
-
underline: isUnderline,
|
|
1991
|
-
strike: isStrike,
|
|
1992
|
-
align: el.style?.textAlign || 'left',
|
|
1993
|
-
shadow: parsedTextShadow,
|
|
1994
|
-
reflection: parsedTextReflection
|
|
1995
|
-
})
|
|
1996
|
-
|
|
1997
|
-
let finalLineHeight: number | undefined = undefined
|
|
1998
|
-
if (el.style?.lineHeight !== undefined) {
|
|
1999
|
-
const val = typeof el.style.lineHeight === 'number'
|
|
2000
|
-
? el.style.lineHeight
|
|
2001
|
-
: parseFloat(el.style.lineHeight as string || '')
|
|
2002
|
-
if (!isNaN(val)) {
|
|
2003
|
-
finalLineHeight = val <= 1.2 ? 1.0 : val
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
let textValign = 'top'
|
|
2007
|
-
const currentValign = el.style?.verticalAlign || (el as any).vAlign
|
|
2008
|
-
if (currentValign === 'middle' || currentValign === 'mid') {
|
|
2009
|
-
textValign = 'middle'
|
|
2010
|
-
} else if (currentValign === 'bottom' || currentValign === 'down') {
|
|
2011
|
-
textValign = 'bottom'
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
let textMargin: [number, number, number, number] = [7.2, 7.2, 3.6, 3.6]
|
|
1821
|
+
if (!pixelHeight || el.style?.height === 'auto') {
|
|
1822
|
+
let topPadding = 4.8
|
|
1823
|
+
let bottomPadding = 4.8
|
|
2015
1824
|
if (el.style?.padding) {
|
|
2016
1825
|
const paddingParts = el.style.padding.trim().split(/\s+/)
|
|
2017
|
-
let parsedPadding = [
|
|
1826
|
+
let parsedPadding = [4.8, 9.6, 4.8, 9.6]
|
|
2018
1827
|
if (paddingParts.length === 4) {
|
|
2019
1828
|
parsedPadding = paddingParts.map((p: string) => parseFloat(p) || 0)
|
|
2020
1829
|
} else if (paddingParts.length === 1) {
|
|
2021
1830
|
const val = parseFloat(paddingParts[0]) || 0
|
|
2022
1831
|
parsedPadding = [val, val, val, val]
|
|
2023
1832
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
parsedPadding[1] / ratioPx2Pt, // right
|
|
2027
|
-
parsedPadding[2] / ratioPx2Pt, // bottom
|
|
2028
|
-
parsedPadding[0] / ratioPx2Pt // top
|
|
2029
|
-
]
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
const textOpts: any = {
|
|
2033
|
-
x, y, w, h,
|
|
2034
|
-
valign: textValign as any,
|
|
2035
|
-
align: (el.style?.textAlign || 'left') as any,
|
|
2036
|
-
margin: textMargin,
|
|
2037
|
-
rotate, flipH, flipV,
|
|
2038
|
-
objectName: el.id
|
|
2039
|
-
}
|
|
2040
|
-
if (finalLineHeight !== undefined) {
|
|
2041
|
-
textOpts.lineSpacingMultiple = finalLineHeight
|
|
1833
|
+
topPadding = parsedPadding[0]
|
|
1834
|
+
bottomPadding = parsedPadding[2]
|
|
2042
1835
|
}
|
|
1836
|
+
pixelHeight = estimateTextHeight(el.content || '', pixelWidth, defaultFontSize) + topPadding + bottomPadding
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
2043
1840
|
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
1841
|
+
const elRotate = typeof el.style?.rotate === 'number'
|
|
1842
|
+
? el.style.rotate
|
|
1843
|
+
: (parseFloat(el.style?.rotate || el.rotate || '0') || 0)
|
|
1844
|
+
const elFlipH = Boolean(el.style?.flipH || el.flipH || el.isFlipH)
|
|
1845
|
+
const elFlipV = Boolean(el.style?.flipV || el.flipV || el.isFlipV)
|
|
1846
|
+
|
|
1847
|
+
let pixelLeft = 0
|
|
1848
|
+
let pixelTop = 0
|
|
1849
|
+
let rotate = 0
|
|
1850
|
+
let flipH = elFlipH
|
|
1851
|
+
let flipV = elFlipV
|
|
1852
|
+
|
|
1853
|
+
if (parentPixels) {
|
|
1854
|
+
const rawChildLeft = parseValToPx(rawLeft, parentPixels.width, 0)
|
|
1855
|
+
const rawChildTop = parseValToPx(rawTop, parentPixels.height, 0)
|
|
1856
|
+
|
|
1857
|
+
const pW = parentPixels.width
|
|
1858
|
+
const pH = parentPixels.height
|
|
1859
|
+
const cCenterRelX = (rawChildLeft + pixelWidth / 2) - pW / 2
|
|
1860
|
+
const cCenterRelY = (rawChildTop + pixelHeight / 2) - pH / 2
|
|
1861
|
+
|
|
1862
|
+
let flippedRelX = parentPixels.flipH ? -cCenterRelX : cCenterRelX
|
|
1863
|
+
let flippedRelY = parentPixels.flipV ? -cCenterRelY : cCenterRelY
|
|
1864
|
+
|
|
1865
|
+
let rotRelX = flippedRelX
|
|
1866
|
+
let rotRelY = flippedRelY
|
|
1867
|
+
if (parentPixels.rotate) {
|
|
1868
|
+
const rad = (parentPixels.rotate * Math.PI) / 180
|
|
1869
|
+
rotRelX = flippedRelX * Math.cos(rad) - flippedRelY * Math.sin(rad)
|
|
1870
|
+
rotRelY = flippedRelX * Math.sin(rad) + flippedRelY * Math.cos(rad)
|
|
1871
|
+
}
|
|
2050
1872
|
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
const fillParsed = parseColor(el.style.textBackground)
|
|
2054
|
-
textOpts.fill = { color: fillParsed.color, transparency: fillParsed.transparency }
|
|
2055
|
-
}
|
|
1873
|
+
const newCenterX = pW / 2 + rotRelX
|
|
1874
|
+
const newCenterY = pH / 2 + rotRelY
|
|
2056
1875
|
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
const textBorderColorStr = el.style?.borderColor
|
|
2060
|
-
if (textBorderWidth > 0 && textBorderColorStr && textBorderColorStr !== 'transparent' && textBorderColorStr !== 'none') {
|
|
2061
|
-
const borderParsed = parseColor(textBorderColorStr)
|
|
2062
|
-
textOpts.line = {
|
|
2063
|
-
color: borderParsed.color,
|
|
2064
|
-
width: textBorderWidth / ratioPx2Pt,
|
|
2065
|
-
dashType: el.style?.borderStyle === 'dashed' ? 'dash' : 'solid'
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
1876
|
+
pixelLeft = parentPixels.left + (newCenterX - pixelWidth / 2)
|
|
1877
|
+
pixelTop = parentPixels.top + (newCenterY - pixelHeight / 2)
|
|
2068
1878
|
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
1879
|
+
const parentOddFlips = (parentPixels.flipH !== parentPixels.flipV)
|
|
1880
|
+
const childEffectiveRot = parentOddFlips ? -elRotate : elRotate
|
|
1881
|
+
rotate = ((parentPixels.rotate + childEffectiveRot) % 360 + 360) % 360
|
|
2072
1882
|
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
}
|
|
1883
|
+
flipH = parentPixels.flipH ? !elFlipH : elFlipH
|
|
1884
|
+
flipV = parentPixels.flipV ? !elFlipV : elFlipV
|
|
1885
|
+
} else {
|
|
1886
|
+
pixelLeft = parseCoordinate(rawLeft, logicalWidth.value)
|
|
1887
|
+
pixelTop = parseCoordinate(rawTop, logicalHeight.value)
|
|
1888
|
+
rotate = (elRotate % 360 + 360) % 360
|
|
1889
|
+
}
|
|
2081
1890
|
|
|
2082
|
-
|
|
2083
|
-
|
|
1891
|
+
if (el.type === 'group' && Array.isArray(el.elements)) {
|
|
1892
|
+
const curGroupPixels = {
|
|
1893
|
+
left: pixelLeft,
|
|
1894
|
+
top: pixelTop,
|
|
1895
|
+
width: pixelWidth,
|
|
1896
|
+
height: pixelHeight,
|
|
1897
|
+
rotate,
|
|
1898
|
+
flipH,
|
|
1899
|
+
flipV
|
|
2084
1900
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
1901
|
+
for (const child of el.elements) {
|
|
1902
|
+
await processElement(child, curGroupPixels)
|
|
1903
|
+
}
|
|
1904
|
+
return
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
const x = (pixelLeft / logicalWidth.value) * baseWidth
|
|
1908
|
+
const y = (pixelTop / logicalHeight.value) * baseHeight
|
|
1909
|
+
const w = (pixelWidth / logicalWidth.value) * baseWidth
|
|
1910
|
+
const h = (pixelHeight / logicalHeight.value) * baseHeight
|
|
2090
1911
|
|
|
2091
|
-
|
|
1912
|
+
if (el.type === 'text') {
|
|
1913
|
+
if (el.latex) {
|
|
1914
|
+
const svgString = renderLatexToSvg(el.latex)
|
|
1915
|
+
const base64SVG = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svgString)))
|
|
1916
|
+
pptxSlide.addImage({
|
|
1917
|
+
data: base64SVG,
|
|
2092
1918
|
x, y, w, h,
|
|
2093
|
-
rotate,
|
|
1919
|
+
rotate,
|
|
2094
1920
|
objectName: el.id
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
parsedImgShadow = parseCssShadow(el.style.boxShadow, ratioPx2Pt)
|
|
2106
|
-
}
|
|
2107
|
-
if (parsedImgShadow) {
|
|
2108
|
-
imgOpts.shadow = parsedImgShadow
|
|
2109
|
-
}
|
|
2110
|
-
const imgRefl = el.reflection || (el.style && el.style.reflection)
|
|
2111
|
-
if (imgRefl) {
|
|
2112
|
-
const parsedReflection = convertElReflectionToPptx(imgRefl, ratioPx2Pt)
|
|
2113
|
-
if (parsedReflection) {
|
|
2114
|
-
imgOpts.reflection = parsedReflection
|
|
1921
|
+
})
|
|
1922
|
+
}
|
|
1923
|
+
else if ((el.content && (el.content.includes('<table') || el.content.includes('<table'))) || (el.data && Array.isArray(el.data) && el.data.length > 0)) {
|
|
1924
|
+
const res = (el.content && (el.content.includes('<table') || el.content.includes('<table')))
|
|
1925
|
+
? parseHTMLTable(el.content, w)
|
|
1926
|
+
: parseTableElementData(el, w)
|
|
1927
|
+
if (res.tableData && res.tableData.length > 0) {
|
|
1928
|
+
const tableOpts: any = { x, y, w, h, objectName: el.id }
|
|
1929
|
+
if (res.colW && res.colW.length > 0) {
|
|
1930
|
+
tableOpts.colW = res.colW
|
|
2115
1931
|
}
|
|
1932
|
+
pptxSlide.addTable(res.tableData, tableOpts)
|
|
2116
1933
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
1934
|
+
}
|
|
1935
|
+
else {
|
|
1936
|
+
const defaultFontSize = Math.max(1, Math.round((parseInt(el.style?.fontSize || '') || 18) * fontSizeFactor))
|
|
1937
|
+
const defaultColor = el.style?.color || globalFontColor.value || '#475569'
|
|
1938
|
+
const defaultFontFamily = (el.style?.fontFamily && el.style.fontFamily !== 'inherit' && el.style.fontFamily !== '默认字体')
|
|
1939
|
+
? el.style.fontFamily
|
|
1940
|
+
: (globalFontFamily.value && globalFontFamily.value !== '默认字体' ? globalFontFamily.value : 'SourceHanSansCN')
|
|
1941
|
+
|
|
1942
|
+
const isBold = el.style?.fontWeight === 'bold' || el.style?.fontWeight === '700' || (parseInt(el.style?.fontWeight as string) || 0) > 500
|
|
1943
|
+
const isItalic = (el.style as any)?.fontStyle === 'italic'
|
|
1944
|
+
const isUnderline = !!(el.style?.textDecoration && el.style.textDecoration.includes('underline'))
|
|
1945
|
+
const isStrike = !!(el.style?.textDecoration && el.style.textDecoration.includes('line-through'))
|
|
1946
|
+
|
|
1947
|
+
const hasFillOrBorder = (el.style?.textBackground && el.style.textBackground !== 'transparent' && el.style.textBackground !== 'none') || (parseFloat(el.style?.borderWidth || '') || 0) > 0
|
|
1948
|
+
|
|
1949
|
+
const textShadowObj = (el as any).textShadow || (el.style && (el.style as any).textShadow) || (!hasFillOrBorder ? (el.shadow || (el.style && (el.style as any).shadow)) : undefined)
|
|
1950
|
+
let parsedTextShadow: any = undefined
|
|
1951
|
+
if (textShadowObj) {
|
|
1952
|
+
parsedTextShadow = convertElShadowToPptx(textShadowObj, ratioPx2Pt)
|
|
1953
|
+
}
|
|
1954
|
+
if (!parsedTextShadow) {
|
|
1955
|
+
let textShadowStr = el.style?.boxShadow
|
|
1956
|
+
if (!textShadowStr && el.content) {
|
|
1957
|
+
const match = el.content.match(/(?:text-shadow)\s*:\s*([^;"]+)/i)
|
|
1958
|
+
if (match) {
|
|
1959
|
+
textShadowStr = match[1].trim()
|
|
2139
1960
|
}
|
|
2140
1961
|
}
|
|
1962
|
+
if (textShadowStr) {
|
|
1963
|
+
parsedTextShadow = parseCssShadow(textShadowStr, ratioPx2Pt)
|
|
1964
|
+
}
|
|
2141
1965
|
}
|
|
2142
1966
|
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
// Set defaults based on shape types to match CanvasArea.vue styles
|
|
2149
|
-
let defaultFill = '#3b82f6'
|
|
2150
|
-
let defaultBorderColor = '#2563eb'
|
|
2151
|
-
let defaultBorderWidth = '2px'
|
|
2152
|
-
|
|
2153
|
-
if (el.content === 'triangle') {
|
|
2154
|
-
defaultFill = '#f59e0b'
|
|
2155
|
-
defaultBorderColor = '#d97706'
|
|
2156
|
-
defaultBorderWidth = '3px'
|
|
2157
|
-
} else if (el.content === 'circle') {
|
|
2158
|
-
defaultFill = '#3b82f6'
|
|
2159
|
-
defaultBorderColor = '#2563eb'
|
|
2160
|
-
defaultBorderWidth = '2px'
|
|
2161
|
-
} else if (typeof el.content === 'string' && (el.content.startsWith('M') || el.content.startsWith('m'))) {
|
|
2162
|
-
defaultFill = '#3b82f6'
|
|
2163
|
-
defaultBorderColor = '#2563eb'
|
|
2164
|
-
defaultBorderWidth = '2px'
|
|
2165
|
-
} else { // rect or default
|
|
2166
|
-
defaultFill = '#10b981'
|
|
2167
|
-
defaultBorderColor = '#059669'
|
|
2168
|
-
defaultBorderWidth = '2px'
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
const style: any = el.style || {}
|
|
2172
|
-
let shapeFillOpt: any = undefined
|
|
2173
|
-
|
|
2174
|
-
// Helper function to extract element gradient configuration for PPTX export
|
|
2175
|
-
const getElementGradientConfig = (item: any): any => {
|
|
2176
|
-
if (!item) return undefined
|
|
2177
|
-
let gradient = item.gradient || item.style?.gradient
|
|
2178
|
-
if (!gradient) {
|
|
2179
|
-
const rawFill = (item.style && item.style.fill !== undefined) ? item.style.fill : item.fill
|
|
2180
|
-
if (rawFill && typeof rawFill === 'object' && rawFill.type === 'gradient') {
|
|
2181
|
-
const val = rawFill.value || {}
|
|
2182
|
-
gradient = {
|
|
2183
|
-
path: val.path || 'line',
|
|
2184
|
-
type: val.path === 'line' || !val.path ? 'linear' : 'radial',
|
|
2185
|
-
rotate: val.rot !== undefined ? val.rot : (val.rotate !== undefined ? val.rotate : 90),
|
|
2186
|
-
colors: (val.colors || []).map((c: any) => ({
|
|
2187
|
-
color: c.color || c.value || '#3b82f6',
|
|
2188
|
-
pos: c.pos !== undefined ? (typeof c.pos === 'string' ? parseInt(c.pos) : c.pos) : 0
|
|
2189
|
-
}))
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
return gradient
|
|
1967
|
+
const shapeShadowObj = hasFillOrBorder ? (el.shadow || (el.style && (el.style as any).shadow)) : undefined
|
|
1968
|
+
let parsedShapeShadow: any = undefined
|
|
1969
|
+
if (shapeShadowObj) {
|
|
1970
|
+
parsedShapeShadow = convertElShadowToPptx(shapeShadowObj, ratioPx2Pt)
|
|
2194
1971
|
}
|
|
2195
1972
|
|
|
2196
|
-
const
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
if (rawFill && typeof rawFill === 'object' && (rawFill.type === 'image' || rawFill.type === 'pattern')) {
|
|
2201
|
-
return rawFill.value?.src || rawFill.value?.url || rawFill.value
|
|
2202
|
-
}
|
|
2203
|
-
return undefined
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
const patternUrl = getElementPatternUrl(el)
|
|
2207
|
-
const g = getElementGradientConfig(el)
|
|
2208
|
-
|
|
2209
|
-
// SVG 图片填充 PowerPoint 无法可靠渲染 a:blipFill,先按形状显示尺寸栅格化为 PNG
|
|
2210
|
-
// (画布上图案填充本身是 preserveAspectRatio="none" 拉伸铺满,与 a:stretch 语义一致)
|
|
2211
|
-
const rasterizeSvgFill = (svgUrl: string, widthPx: number, heightPx: number): Promise<string | undefined> => {
|
|
2212
|
-
return new Promise((resolve) => {
|
|
2213
|
-
const img = new Image()
|
|
2214
|
-
img.onload = () => {
|
|
2215
|
-
try {
|
|
2216
|
-
const canvas = document.createElement('canvas')
|
|
2217
|
-
canvas.width = Math.max(1, Math.min(4096, Math.round(widthPx)))
|
|
2218
|
-
canvas.height = Math.max(1, Math.min(4096, Math.round(heightPx)))
|
|
2219
|
-
const ctx = canvas.getContext('2d')
|
|
2220
|
-
if (!ctx) return resolve(undefined)
|
|
2221
|
-
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
2222
|
-
resolve(canvas.toDataURL('image/png'))
|
|
2223
|
-
} catch {
|
|
2224
|
-
resolve(undefined)
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
img.onerror = () => resolve(undefined)
|
|
2228
|
-
img.src = svgUrl
|
|
2229
|
-
})
|
|
1973
|
+
const textRefl = (el as any).textReflection || (el.style as any)?.textReflection || (!hasFillOrBorder ? (el.reflection || (el.style && el.style.reflection)) : undefined)
|
|
1974
|
+
let parsedTextReflection: any = undefined
|
|
1975
|
+
if (textRefl) {
|
|
1976
|
+
parsedTextReflection = convertElReflectionToPptx(textRefl, ratioPx2Pt)
|
|
2230
1977
|
}
|
|
2231
1978
|
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
}
|
|
1979
|
+
const shapeRefl = hasFillOrBorder ? (el.reflection || (el.style && el.style.reflection)) : undefined
|
|
1980
|
+
let parsedShapeReflection: any = undefined
|
|
1981
|
+
if (shapeRefl) {
|
|
1982
|
+
parsedShapeReflection = convertElReflectionToPptx(shapeRefl, ratioPx2Pt)
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
const textProps = formatHTMLToPPTX(el.content || '', {
|
|
1986
|
+
fontSize: defaultFontSize,
|
|
1987
|
+
color: defaultColor,
|
|
1988
|
+
fontFamily: defaultFontFamily,
|
|
1989
|
+
bold: isBold,
|
|
1990
|
+
italic: isItalic,
|
|
1991
|
+
underline: isUnderline,
|
|
1992
|
+
strike: isStrike,
|
|
1993
|
+
align: el.style?.textAlign || 'left',
|
|
1994
|
+
shadow: parsedTextShadow,
|
|
1995
|
+
reflection: parsedTextReflection
|
|
1996
|
+
})
|
|
1997
|
+
|
|
1998
|
+
let finalLineHeight: number | undefined = undefined
|
|
1999
|
+
if (el.style?.lineHeight !== undefined) {
|
|
2000
|
+
const val = typeof el.style.lineHeight === 'number'
|
|
2001
|
+
? el.style.lineHeight
|
|
2002
|
+
: parseFloat(el.style.lineHeight as string || '')
|
|
2003
|
+
if (!isNaN(val)) {
|
|
2004
|
+
finalLineHeight = val <= 1.2 ? 1.0 : val
|
|
2259
2005
|
}
|
|
2260
|
-
return undefined
|
|
2261
2006
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2007
|
+
let textValign = 'top'
|
|
2008
|
+
const currentValign = el.style?.verticalAlign || (el as any).vAlign
|
|
2009
|
+
if (currentValign === 'middle' || currentValign === 'mid') {
|
|
2010
|
+
textValign = 'middle'
|
|
2011
|
+
} else if (currentValign === 'bottom' || currentValign === 'down') {
|
|
2012
|
+
textValign = 'bottom'
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
let textMargin: [number, number, number, number] = [7.2, 7.2, 3.6, 3.6]
|
|
2016
|
+
if (el.style?.padding) {
|
|
2017
|
+
const paddingParts = el.style.padding.trim().split(/\s+/)
|
|
2018
|
+
let parsedPadding = [0, 0, 0, 0]
|
|
2019
|
+
if (paddingParts.length === 4) {
|
|
2020
|
+
parsedPadding = paddingParts.map((p: string) => parseFloat(p) || 0)
|
|
2021
|
+
} else if (paddingParts.length === 1) {
|
|
2022
|
+
const val = parseFloat(paddingParts[0]) || 0
|
|
2023
|
+
parsedPadding = [val, val, val, val]
|
|
2024
|
+
}
|
|
2025
|
+
textMargin = [
|
|
2026
|
+
parsedPadding[3] / ratioPx2Pt, // left
|
|
2027
|
+
parsedPadding[1] / ratioPx2Pt, // right
|
|
2028
|
+
parsedPadding[2] / ratioPx2Pt, // bottom
|
|
2029
|
+
parsedPadding[0] / ratioPx2Pt // top
|
|
2030
|
+
]
|
|
2270
2031
|
}
|
|
2271
2032
|
|
|
2272
|
-
const
|
|
2033
|
+
const textOpts: any = {
|
|
2273
2034
|
x, y, w, h,
|
|
2274
|
-
|
|
2035
|
+
valign: textValign as any,
|
|
2036
|
+
align: (el.style?.textAlign || 'left') as any,
|
|
2037
|
+
margin: textMargin,
|
|
2275
2038
|
rotate, flipH, flipV,
|
|
2276
2039
|
objectName: el.id
|
|
2277
2040
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
const borderColorStr = style.borderColor
|
|
2281
|
-
if (borderWidth > 0 && borderColorStr && borderColorStr !== 'transparent' && borderColorStr !== 'none') {
|
|
2282
|
-
const borderParsed = parseColor(borderColorStr)
|
|
2283
|
-
shapeOpts.line = {
|
|
2284
|
-
color: borderParsed.color,
|
|
2285
|
-
width: borderWidth / ratioPx2Pt,
|
|
2286
|
-
dashType: style.borderStyle === 'dashed' ? 'dash' : 'solid'
|
|
2287
|
-
}
|
|
2041
|
+
if (finalLineHeight !== undefined) {
|
|
2042
|
+
textOpts.lineSpacingMultiple = finalLineHeight
|
|
2288
2043
|
}
|
|
2289
2044
|
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
if (!parsedShadow && style.boxShadow) {
|
|
2293
|
-
parsedShadow = parseCssShadow(style.boxShadow, ratioPx2Pt)
|
|
2045
|
+
if (parsedShapeShadow) {
|
|
2046
|
+
textOpts.shadow = parsedShapeShadow
|
|
2294
2047
|
}
|
|
2295
|
-
if (
|
|
2296
|
-
|
|
2048
|
+
if (parsedShapeReflection) {
|
|
2049
|
+
textOpts.reflection = parsedShapeReflection
|
|
2297
2050
|
}
|
|
2298
2051
|
|
|
2299
|
-
|
|
2300
|
-
if (
|
|
2301
|
-
const
|
|
2302
|
-
|
|
2303
|
-
|
|
2052
|
+
// Apply background color to text element
|
|
2053
|
+
if (el.style?.textBackground && el.style.textBackground !== 'transparent' && el.style.textBackground !== 'none') {
|
|
2054
|
+
const fillParsed = parseColor(el.style.textBackground)
|
|
2055
|
+
textOpts.fill = { color: fillParsed.color, transparency: fillParsed.transparency }
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// Apply border line style to text element
|
|
2059
|
+
const textBorderWidth = parseFloat(el.style?.borderWidth || '') || 0
|
|
2060
|
+
const textBorderColorStr = el.style?.borderColor
|
|
2061
|
+
if (textBorderWidth > 0 && textBorderColorStr && textBorderColorStr !== 'transparent' && textBorderColorStr !== 'none') {
|
|
2062
|
+
const borderParsed = parseColor(textBorderColorStr)
|
|
2063
|
+
textOpts.line = {
|
|
2064
|
+
color: borderParsed.color,
|
|
2065
|
+
width: textBorderWidth / ratioPx2Pt,
|
|
2066
|
+
dashType: el.style?.borderStyle === 'dashed' ? 'dash' : 'solid'
|
|
2304
2067
|
}
|
|
2305
2068
|
}
|
|
2306
2069
|
|
|
2307
|
-
if (
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2070
|
+
if ((el.style as any)?.whiteSpace === 'nowrap') {
|
|
2071
|
+
textOpts.wrap = false
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
const elAny = el as any
|
|
2075
|
+
if (elAny.autoFit) {
|
|
2076
|
+
if (elAny.autoFit.type === 'shape') {
|
|
2077
|
+
textOpts.fit = 'resize'
|
|
2078
|
+
} else if (elAny.autoFit.type === 'text') {
|
|
2079
|
+
textOpts.fit = 'shrink'
|
|
2314
2080
|
}
|
|
2315
|
-
shapeOpts.points = formatPoints(rawPoints, scale, pixelToInch) as any
|
|
2316
2081
|
}
|
|
2317
2082
|
|
|
2318
|
-
|
|
2083
|
+
pptxSlide.addText(textProps, textOpts)
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
else if (el.type === 'image') {
|
|
2087
|
+
let imgPath = await resolveMediaSrc(el.content || (el as any).src || '', 'image')
|
|
2088
|
+
if (!imgPath) {
|
|
2089
|
+
imgPath = createFallbackImageBase64('图片缺失')
|
|
2090
|
+
}
|
|
2319
2091
|
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2092
|
+
const imgOpts: pptxgen.ImageProps = {
|
|
2093
|
+
x, y, w, h,
|
|
2094
|
+
rotate, flipH, flipV,
|
|
2095
|
+
objectName: el.id
|
|
2096
|
+
}
|
|
2097
|
+
if (imgPath.startsWith('data:')) {
|
|
2098
|
+
imgOpts.data = imgPath
|
|
2099
|
+
} else {
|
|
2100
|
+
imgOpts.path = imgPath
|
|
2101
|
+
}
|
|
2325
2102
|
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2103
|
+
const imgShadow = el.shadow || (el.style && (el.style as any).shadow)
|
|
2104
|
+
let parsedImgShadow = convertElShadowToPptx(imgShadow, ratioPx2Pt)
|
|
2105
|
+
if (!parsedImgShadow && el.style?.boxShadow) {
|
|
2106
|
+
parsedImgShadow = parseCssShadow(el.style.boxShadow, ratioPx2Pt)
|
|
2107
|
+
}
|
|
2108
|
+
if (parsedImgShadow) {
|
|
2109
|
+
imgOpts.shadow = parsedImgShadow
|
|
2110
|
+
}
|
|
2111
|
+
const imgRefl = el.reflection || (el.style && el.style.reflection)
|
|
2112
|
+
if (imgRefl) {
|
|
2113
|
+
const parsedReflection = convertElReflectionToPptx(imgRefl, ratioPx2Pt)
|
|
2114
|
+
if (parsedReflection) {
|
|
2115
|
+
imgOpts.reflection = parsedReflection
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2330
2118
|
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2119
|
+
if (el.clip && Array.isArray(el.clip.range) && el.clip.range.length >= 2) {
|
|
2120
|
+
const [start, end] = el.clip.range
|
|
2121
|
+
if (Array.isArray(start) && start.length >= 2 && Array.isArray(end) && end.length >= 2) {
|
|
2122
|
+
const [startX, startY] = start
|
|
2123
|
+
const [endX, endY] = end
|
|
2124
|
+
|
|
2125
|
+
const fractionX = (endX - startX) / 100
|
|
2126
|
+
const fractionY = (endY - startY) / 100
|
|
2127
|
+
if (fractionX > 0 && fractionY > 0) {
|
|
2128
|
+
const originW = w / fractionX
|
|
2129
|
+
const originH = h / fractionY
|
|
2130
|
+
|
|
2131
|
+
imgOpts.w = originW
|
|
2132
|
+
imgOpts.h = originH
|
|
2133
|
+
imgOpts.sizing = {
|
|
2134
|
+
type: 'crop',
|
|
2135
|
+
x: (startX / 100) * originW,
|
|
2136
|
+
y: (startY / 100) * originH,
|
|
2137
|
+
w: w,
|
|
2138
|
+
h: h,
|
|
2340
2139
|
}
|
|
2341
|
-
shapeMargin = [
|
|
2342
|
-
parsedPadding[3] / ratioPx2Pt, // left
|
|
2343
|
-
parsedPadding[1] / ratioPx2Pt, // right
|
|
2344
|
-
parsedPadding[2] / ratioPx2Pt, // bottom
|
|
2345
|
-
parsedPadding[0] / ratioPx2Pt // top
|
|
2346
|
-
]
|
|
2347
2140
|
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
pptxSlide.addImage(imgOpts)
|
|
2145
|
+
}
|
|
2146
|
+
else if (el.type === 'shape') {
|
|
2147
|
+
const shapeType = getPredefinedShapeType(el.content, el.pathFormula, (el as any).pptxShapeType)
|
|
2148
|
+
|
|
2149
|
+
// Set defaults based on shape types to match CanvasArea.vue styles
|
|
2150
|
+
let defaultFill = '#3b82f6'
|
|
2151
|
+
let defaultBorderColor = '#2563eb'
|
|
2152
|
+
let defaultBorderWidth = '2px'
|
|
2153
|
+
|
|
2154
|
+
if (el.content === 'triangle') {
|
|
2155
|
+
defaultFill = '#f59e0b'
|
|
2156
|
+
defaultBorderColor = '#d97706'
|
|
2157
|
+
defaultBorderWidth = '3px'
|
|
2158
|
+
} else if (el.content === 'circle') {
|
|
2159
|
+
defaultFill = '#3b82f6'
|
|
2160
|
+
defaultBorderColor = '#2563eb'
|
|
2161
|
+
defaultBorderWidth = '2px'
|
|
2162
|
+
} else if (typeof el.content === 'string' && (el.content.startsWith('M') || el.content.startsWith('m'))) {
|
|
2163
|
+
defaultFill = '#3b82f6'
|
|
2164
|
+
defaultBorderColor = '#2563eb'
|
|
2165
|
+
defaultBorderWidth = '2px'
|
|
2166
|
+
} else { // rect or default
|
|
2167
|
+
defaultFill = '#10b981'
|
|
2168
|
+
defaultBorderColor = '#059669'
|
|
2169
|
+
defaultBorderWidth = '2px'
|
|
2170
|
+
}
|
|
2348
2171
|
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
if (
|
|
2359
|
-
const val =
|
|
2360
|
-
|
|
2361
|
-
:
|
|
2362
|
-
|
|
2363
|
-
|
|
2172
|
+
const style: any = el.style || {}
|
|
2173
|
+
let shapeFillOpt: any = undefined
|
|
2174
|
+
|
|
2175
|
+
// Helper function to extract element gradient configuration for PPTX export
|
|
2176
|
+
const getElementGradientConfig = (item: any): any => {
|
|
2177
|
+
if (!item) return undefined
|
|
2178
|
+
let gradient = item.gradient || item.style?.gradient
|
|
2179
|
+
if (!gradient) {
|
|
2180
|
+
const rawFill = (item.style && item.style.fill !== undefined) ? item.style.fill : item.fill
|
|
2181
|
+
if (rawFill && typeof rawFill === 'object' && rawFill.type === 'gradient') {
|
|
2182
|
+
const val = rawFill.value || {}
|
|
2183
|
+
gradient = {
|
|
2184
|
+
path: val.path || 'line',
|
|
2185
|
+
type: val.path === 'line' || !val.path ? 'linear' : 'radial',
|
|
2186
|
+
rotate: val.rot !== undefined ? val.rot : (val.rotate !== undefined ? val.rotate : 90),
|
|
2187
|
+
colors: (val.colors || []).map((c: any) => ({
|
|
2188
|
+
color: c.color || c.value || '#3b82f6',
|
|
2189
|
+
pos: c.pos !== undefined ? (typeof c.pos === 'string' ? parseInt(c.pos) : c.pos) : 0
|
|
2190
|
+
}))
|
|
2364
2191
|
}
|
|
2365
2192
|
}
|
|
2193
|
+
}
|
|
2194
|
+
return gradient
|
|
2195
|
+
}
|
|
2366
2196
|
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2197
|
+
const getElementPatternUrl = (item: any): string | undefined => {
|
|
2198
|
+
if (!item) return undefined
|
|
2199
|
+
if (item.pattern || item.style?.pattern) return item.pattern || item.style?.pattern
|
|
2200
|
+
const rawFill = (item.style && item.style.fill !== undefined) ? item.style.fill : item.fill
|
|
2201
|
+
if (rawFill && typeof rawFill === 'object' && (rawFill.type === 'image' || rawFill.type === 'pattern')) {
|
|
2202
|
+
return rawFill.value?.src || rawFill.value?.url || rawFill.value
|
|
2203
|
+
}
|
|
2204
|
+
return undefined
|
|
2205
|
+
}
|
|
2372
2206
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2207
|
+
const patternUrl = getElementPatternUrl(el)
|
|
2208
|
+
const g = getElementGradientConfig(el)
|
|
2209
|
+
|
|
2210
|
+
// SVG 图片填充 PowerPoint 无法可靠渲染 a:blipFill,先按形状显示尺寸栅格化为 PNG
|
|
2211
|
+
// (画布上图案填充本身是 preserveAspectRatio="none" 拉伸铺满,与 a:stretch 语义一致)
|
|
2212
|
+
const rasterizeSvgFill = (svgUrl: string, widthPx: number, heightPx: number): Promise<string | undefined> => {
|
|
2213
|
+
return new Promise((resolve) => {
|
|
2214
|
+
const img = new Image()
|
|
2215
|
+
img.onload = () => {
|
|
2216
|
+
try {
|
|
2217
|
+
const canvas = document.createElement('canvas')
|
|
2218
|
+
canvas.width = Math.max(1, Math.min(4096, Math.round(widthPx)))
|
|
2219
|
+
canvas.height = Math.max(1, Math.min(4096, Math.round(heightPx)))
|
|
2220
|
+
const ctx = canvas.getContext('2d')
|
|
2221
|
+
if (!ctx) return resolve(undefined)
|
|
2222
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
2223
|
+
resolve(canvas.toDataURL('image/png'))
|
|
2224
|
+
} catch {
|
|
2225
|
+
resolve(undefined)
|
|
2226
|
+
}
|
|
2377
2227
|
}
|
|
2228
|
+
img.onerror = () => resolve(undefined)
|
|
2229
|
+
img.src = svgUrl
|
|
2230
|
+
})
|
|
2231
|
+
}
|
|
2378
2232
|
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
strike: isShapeTextStrike,
|
|
2387
|
-
align: shapeAlign,
|
|
2388
|
-
shadow: parsedShapeTextShadow,
|
|
2389
|
-
reflection: parsedShapeTextReflection
|
|
2390
|
-
})
|
|
2391
|
-
const shapeTextOpts: any = {
|
|
2392
|
-
x, y, w, h,
|
|
2393
|
-
shape: shapeType,
|
|
2394
|
-
valign: shapeValign as any,
|
|
2395
|
-
align: shapeAlign as any,
|
|
2396
|
-
margin: shapeMargin,
|
|
2397
|
-
rotate, flipH, flipV,
|
|
2398
|
-
objectName: el.id,
|
|
2399
|
-
fill: nativeFillOpt || shapeFillOpt
|
|
2400
|
-
}
|
|
2401
|
-
if (shapeOpts.points) {
|
|
2402
|
-
shapeTextOpts.points = shapeOpts.points
|
|
2233
|
+
const buildNativeShapeFill = async (): Promise<any> => {
|
|
2234
|
+
// 图片填充 → 原生 a:blipFill
|
|
2235
|
+
if (patternUrl) {
|
|
2236
|
+
const pUrl = await resolveMediaSrc(patternUrl, 'image')
|
|
2237
|
+
if (pUrl && /^data:image\/svg\+xml/i.test(pUrl)) {
|
|
2238
|
+
const pngUrl = await rasterizeSvgFill(pUrl, pixelWidth, pixelHeight)
|
|
2239
|
+
return pngUrl ? { type: 'image', data: pngUrl } : undefined
|
|
2403
2240
|
}
|
|
2404
|
-
if (
|
|
2405
|
-
|
|
2241
|
+
if (pUrl && pUrl.startsWith('data:')) {
|
|
2242
|
+
return { type: 'image', data: pUrl }
|
|
2406
2243
|
}
|
|
2407
|
-
if (
|
|
2408
|
-
|
|
2244
|
+
if (pUrl && /^https?:\/\//.test(pUrl)) {
|
|
2245
|
+
return { type: 'image', path: pUrl }
|
|
2409
2246
|
}
|
|
2410
|
-
|
|
2411
|
-
shapeTextOpts.reflection = shapeOpts.reflection
|
|
2412
|
-
}
|
|
2413
|
-
if (finalLineHeight !== undefined) {
|
|
2414
|
-
shapeTextOpts.lineSpacingMultiple = finalLineHeight
|
|
2415
|
-
}
|
|
2416
|
-
if (style.whiteSpace === 'nowrap') {
|
|
2417
|
-
shapeTextOpts.wrap = false
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
const elAny = el as any
|
|
2421
|
-
if (elAny.autoFit) {
|
|
2422
|
-
if (elAny.autoFit.type === 'shape') {
|
|
2423
|
-
shapeTextOpts.fit = 'resize'
|
|
2424
|
-
} else if (elAny.autoFit.type === 'text') {
|
|
2425
|
-
shapeTextOpts.fit = 'shrink'
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
pptxSlide.addText(textProps, shapeTextOpts)
|
|
2430
|
-
} else {
|
|
2431
|
-
pptxSlide.addShape(shapeType as any, shapeOpts)
|
|
2247
|
+
return undefined // 图片解析失败,回落纯色
|
|
2432
2248
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
if (rawType === 'pieChart' || rawType === 'pie3DChart') rawType = 'pie'
|
|
2444
|
-
if (rawType === 'barChart' || rawType === 'bar3DChart') rawType = 'bar'
|
|
2445
|
-
if (rawType === 'lineChart' || rawType === 'line3DChart') rawType = 'line'
|
|
2446
|
-
if (rawType === 'areaChart' || rawType === 'area3DChart') rawType = 'area'
|
|
2447
|
-
|
|
2448
|
-
let chartType = pptx.ChartType.bar
|
|
2449
|
-
if (rawType === 'line') {
|
|
2450
|
-
chartType = pptx.ChartType.line
|
|
2451
|
-
} else if (rawType === 'area') {
|
|
2452
|
-
chartType = pptx.ChartType.area
|
|
2453
|
-
} else if (rawType === 'scatter') {
|
|
2454
|
-
chartType = pptx.ChartType.scatter
|
|
2455
|
-
} else if (rawType === 'pie') {
|
|
2456
|
-
chartType = pptx.ChartType.pie
|
|
2457
|
-
} else if (rawType === 'donut') {
|
|
2458
|
-
chartType = pptx.ChartType.doughnut
|
|
2459
|
-
} else if (rawType === 'radar') {
|
|
2460
|
-
chartType = pptx.ChartType.radar
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
// 1. Resolve colors
|
|
2464
|
-
let colorsList: string[] = []
|
|
2465
|
-
if (Array.isArray(el.style?.chartColors) && el.style.chartColors.length > 0) {
|
|
2466
|
-
colorsList = el.style.chartColors
|
|
2467
|
-
} else if (Array.isArray((el as any).themeColors) && (el as any).themeColors.length > 0) {
|
|
2468
|
-
colorsList = (el as any).themeColors
|
|
2469
|
-
} else if (Array.isArray((el as any).colors) && (el as any).colors.length > 0) {
|
|
2470
|
-
colorsList = (el as any).colors
|
|
2471
|
-
} else if (Array.isArray(el.data?.datasets?.[0]?.backgroundColor) && el.data.datasets[0].backgroundColor.length > 0) {
|
|
2472
|
-
colorsList = el.data.datasets[0].backgroundColor
|
|
2473
|
-
} else if (Array.isArray(el.data?.datasets?.[0]?.colors) && el.data.datasets[0].colors.length > 0) {
|
|
2474
|
-
colorsList = el.data.datasets[0].colors
|
|
2475
|
-
} else {
|
|
2476
|
-
const dsColors = (el.data?.datasets || []).map((ds: any) => ds.color || ds.backgroundColor).filter((c: any) => c && typeof c === 'string')
|
|
2477
|
-
if (dsColors.length > 0) {
|
|
2478
|
-
colorsList = dsColors
|
|
2479
|
-
} else {
|
|
2480
|
-
colorsList = (globalThemeColor && globalThemeColor.value && globalThemeColor.value.length > 0)
|
|
2481
|
-
? globalThemeColor.value
|
|
2482
|
-
: ['#4f46e5', '#06b6d4', '#f59e0b', '#10b981', '#ec4899', '#8b5cf6', '#ef4444', '#14b8a6']
|
|
2249
|
+
// 渐变填充 → 原生 a:gradFill
|
|
2250
|
+
if (g && Array.isArray(g.colors) && g.colors.length >= 2) {
|
|
2251
|
+
return {
|
|
2252
|
+
type: 'gradient',
|
|
2253
|
+
stops: g.colors.map((c: any) => ({
|
|
2254
|
+
color: String(c.color || '#3b82f6').replace('#', ''),
|
|
2255
|
+
pos: typeof c.pos === 'number' && !isNaN(c.pos) ? c.pos : 0
|
|
2256
|
+
})),
|
|
2257
|
+
angle: g.rotate !== undefined ? g.rotate : (g.rot !== undefined ? g.rot : 90),
|
|
2258
|
+
radial: g.type === 'radial' || g.path === 'circle'
|
|
2483
2259
|
}
|
|
2484
2260
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
// 2. Legend visibility and position
|
|
2490
|
-
const isPieOrDonut = chartType === pptx.ChartType.doughnut || chartType === pptx.ChartType.pie
|
|
2491
|
-
let showLegend = false
|
|
2492
|
-
if (el.showLegend !== undefined) {
|
|
2493
|
-
showLegend = Boolean(el.showLegend)
|
|
2494
|
-
} else if ((el as any).options?.showLegend !== undefined) {
|
|
2495
|
-
showLegend = Boolean((el as any).options.showLegend)
|
|
2496
|
-
} else {
|
|
2497
|
-
showLegend = !isPieOrDonut
|
|
2498
|
-
}
|
|
2261
|
+
return undefined
|
|
2262
|
+
}
|
|
2263
|
+
const nativeFillOpt = await buildNativeShapeFill()
|
|
2499
2264
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2265
|
+
const rawFill = style.fill !== undefined ? style.fill : (el.fill !== undefined ? el.fill : defaultFill)
|
|
2266
|
+
if (rawFill === 'transparent' || rawFill === 'none') {
|
|
2267
|
+
shapeFillOpt = { type: 'none' }
|
|
2268
|
+
} else {
|
|
2269
|
+
const fillParsed = parseColor(rawFill)
|
|
2270
|
+
shapeFillOpt = { color: fillParsed.color, transparency: fillParsed.transparency }
|
|
2271
|
+
}
|
|
2502
2272
|
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2273
|
+
const shapeOpts: pptxgen.ShapeProps = {
|
|
2274
|
+
x, y, w, h,
|
|
2275
|
+
fill: nativeFillOpt || shapeFillOpt,
|
|
2276
|
+
rotate, flipH, flipV,
|
|
2277
|
+
objectName: el.id
|
|
2278
|
+
}
|
|
2509
2279
|
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
chartOpts.legendColor = textColorHex
|
|
2519
|
-
chartOpts.legendFontSize = 10
|
|
2520
|
-
chartOpts.legendFontFace = chartFont
|
|
2280
|
+
const borderWidth = style.borderWidth ? (parseFloat(style.borderWidth) || 0) : 0
|
|
2281
|
+
const borderColorStr = style.borderColor
|
|
2282
|
+
if (borderWidth > 0 && borderColorStr && borderColorStr !== 'transparent' && borderColorStr !== 'none') {
|
|
2283
|
+
const borderParsed = parseColor(borderColorStr)
|
|
2284
|
+
shapeOpts.line = {
|
|
2285
|
+
color: borderParsed.color,
|
|
2286
|
+
width: borderWidth / ratioPx2Pt,
|
|
2287
|
+
dashType: style.borderStyle === 'dashed' ? 'dash' : 'solid'
|
|
2521
2288
|
}
|
|
2289
|
+
}
|
|
2522
2290
|
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2291
|
+
const shapeShadow = el.shadow || style.shadow
|
|
2292
|
+
let parsedShadow: any = convertElShadowToPptx(shapeShadow, ratioPx2Pt)
|
|
2293
|
+
if (!parsedShadow && style.boxShadow) {
|
|
2294
|
+
parsedShadow = parseCssShadow(style.boxShadow, ratioPx2Pt)
|
|
2295
|
+
}
|
|
2296
|
+
if (parsedShadow) {
|
|
2297
|
+
shapeOpts.shadow = parsedShadow
|
|
2298
|
+
}
|
|
2529
2299
|
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2300
|
+
const shapeRefl = el.reflection || style.reflection
|
|
2301
|
+
if (shapeRefl) {
|
|
2302
|
+
const parsedReflection = convertElReflectionToPptx(shapeRefl, ratioPx2Pt)
|
|
2303
|
+
if (parsedReflection) {
|
|
2304
|
+
shapeOpts.reflection = parsedReflection
|
|
2535
2305
|
}
|
|
2306
|
+
}
|
|
2536
2307
|
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2308
|
+
if (shapeType === 'custGeom') {
|
|
2309
|
+
const rawPoints = toPoints(el.content || '')
|
|
2310
|
+
const viewBoxWidth = (Array.isArray(el.viewBox) && el.viewBox.length >= 2) ? el.viewBox[0] : pixelWidth
|
|
2311
|
+
const viewBoxHeight = (Array.isArray(el.viewBox) && el.viewBox.length >= 2) ? el.viewBox[1] : pixelHeight
|
|
2312
|
+
const scale = {
|
|
2313
|
+
x: pixelWidth / viewBoxWidth,
|
|
2314
|
+
y: viewBoxHeight > 0 ? pixelHeight / viewBoxHeight : 1
|
|
2543
2315
|
}
|
|
2316
|
+
shapeOpts.points = formatPoints(rawPoints, scale, pixelToInch) as any
|
|
2317
|
+
}
|
|
2544
2318
|
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
} else {
|
|
2568
|
-
chartOpts.showPercent = false
|
|
2569
|
-
chartOpts.showValue = false
|
|
2570
|
-
}
|
|
2571
|
-
} else {
|
|
2572
|
-
if (showDataLabels || (el as any).showValue) {
|
|
2573
|
-
chartOpts.showValue = true
|
|
2574
|
-
chartOpts.dataLabelColor = textColorHex
|
|
2575
|
-
chartOpts.dataLabelFontSize = 10
|
|
2576
|
-
chartOpts.dataLabelFontFace = chartFont
|
|
2319
|
+
const shapeText = typeof el.text === 'string' ? el.text : ((el as any).text?.content || (typeof el.content === 'string' && !el.content.startsWith('M') && !el.content.startsWith('m') && el.content !== 'circle' && el.content !== 'triangle' && el.content.replace(/<[^>]+>/g, '').trim().length > 0 ? el.content : ''))
|
|
2320
|
+
|
|
2321
|
+
if (shapeText) {
|
|
2322
|
+
const shapeTextDefaultColor = style.textColor || style.color || (style.fill === '#ffffff' || style.fill === '#FFFFFF' ? '#475569' : '#ffffff')
|
|
2323
|
+
const shapeTextDefaultFont = style.fontFamily || (globalFontFamily && globalFontFamily.value) || 'Arial'
|
|
2324
|
+
const resolvedShapeFont = (shapeTextDefaultFont !== 'inherit' && shapeTextDefaultFont !== '默认字体') ? shapeTextDefaultFont : 'Microsoft YaHei'
|
|
2325
|
+
const shapeTextDefaultSize = Math.max(1, Math.round((parseFloat(style.fontSize || '') || 16) * fontSizeFactor))
|
|
2326
|
+
|
|
2327
|
+
const isShapeTextBold = style.fontWeight === 'bold' || style.fontWeight === '700' || (parseInt(style.fontWeight as string) || 0) > 500
|
|
2328
|
+
const isShapeTextItalic = style.fontStyle === 'italic'
|
|
2329
|
+
const isShapeTextUnderline = !!(style.textDecoration && style.textDecoration.includes('underline'))
|
|
2330
|
+
const isShapeTextStrike = !!(style.textDecoration && style.textDecoration.includes('line-through'))
|
|
2331
|
+
|
|
2332
|
+
let shapeMargin: [number, number, number, number] = [7.2, 7.2, 3.6, 3.6]
|
|
2333
|
+
if (style.padding) {
|
|
2334
|
+
const paddingParts = style.padding.trim().split(/\s+/)
|
|
2335
|
+
let parsedPadding = [0, 0, 0, 0]
|
|
2336
|
+
if (paddingParts.length === 4) {
|
|
2337
|
+
parsedPadding = paddingParts.map((p: string) => parseFloat(p) || 0)
|
|
2338
|
+
} else if (paddingParts.length === 1) {
|
|
2339
|
+
const val = parseFloat(paddingParts[0]) || 0
|
|
2340
|
+
parsedPadding = [val, val, val, val]
|
|
2577
2341
|
}
|
|
2342
|
+
shapeMargin = [
|
|
2343
|
+
parsedPadding[3] / ratioPx2Pt, // left
|
|
2344
|
+
parsedPadding[1] / ratioPx2Pt, // right
|
|
2345
|
+
parsedPadding[2] / ratioPx2Pt, // bottom
|
|
2346
|
+
parsedPadding[0] / ratioPx2Pt // top
|
|
2347
|
+
]
|
|
2578
2348
|
}
|
|
2579
2349
|
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2350
|
+
let shapeValign = 'middle'
|
|
2351
|
+
const currentShapeValign = el.style?.verticalAlign || (el as any).vAlign
|
|
2352
|
+
if (currentShapeValign === 'top' || currentShapeValign === 'up') shapeValign = 'top'
|
|
2353
|
+
else if (currentShapeValign === 'bottom' || currentShapeValign === 'down') shapeValign = 'bottom'
|
|
2354
|
+
else if (currentShapeValign === 'middle' || currentShapeValign === 'mid') shapeValign = 'middle'
|
|
2585
2355
|
|
|
2586
|
-
|
|
2587
|
-
chartOpts.catGridLine = { style: 'none' }
|
|
2356
|
+
let shapeAlign = style.textAlign || 'center'
|
|
2588
2357
|
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2358
|
+
let finalLineHeight: number | undefined = undefined
|
|
2359
|
+
if (style.lineHeight !== undefined) {
|
|
2360
|
+
const val = typeof style.lineHeight === 'number'
|
|
2361
|
+
? style.lineHeight
|
|
2362
|
+
: parseFloat(style.lineHeight as string || '')
|
|
2363
|
+
if (!isNaN(val)) {
|
|
2364
|
+
finalLineHeight = val <= 1.2 ? 1.0 : val
|
|
2365
|
+
}
|
|
2593
2366
|
}
|
|
2594
2367
|
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2368
|
+
let shapeTextShadowObj = (el as any).textShadow || (el.style && (el.style as any).textShadow)
|
|
2369
|
+
let parsedShapeTextShadow: any = undefined
|
|
2370
|
+
if (shapeTextShadowObj) {
|
|
2371
|
+
parsedShapeTextShadow = convertElShadowToPptx(shapeTextShadowObj, ratioPx2Pt)
|
|
2599
2372
|
}
|
|
2600
2373
|
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
const
|
|
2620
|
-
|
|
2621
|
-
const lineOpts: any = {
|
|
2374
|
+
let shapeTextReflObj = (el as any).textReflection || (el.style as any)?.textReflection
|
|
2375
|
+
let parsedShapeTextReflection: any = undefined
|
|
2376
|
+
if (shapeTextReflObj && shapeTextReflObj.enabled !== false) {
|
|
2377
|
+
parsedShapeTextReflection = convertElReflectionToPptx(shapeTextReflObj, ratioPx2Pt)
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
const textProps = formatHTMLToPPTX(shapeText, {
|
|
2381
|
+
fontSize: shapeTextDefaultSize,
|
|
2382
|
+
color: shapeTextDefaultColor,
|
|
2383
|
+
fontFamily: resolvedShapeFont,
|
|
2384
|
+
bold: isShapeTextBold,
|
|
2385
|
+
italic: isShapeTextItalic,
|
|
2386
|
+
underline: isShapeTextUnderline,
|
|
2387
|
+
strike: isShapeTextStrike,
|
|
2388
|
+
align: shapeAlign,
|
|
2389
|
+
shadow: parsedShapeTextShadow,
|
|
2390
|
+
reflection: parsedShapeTextReflection
|
|
2391
|
+
})
|
|
2392
|
+
const shapeTextOpts: any = {
|
|
2622
2393
|
x, y, w, h,
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
beginArrowType: beginArrow as any,
|
|
2628
|
-
endArrowType: endArrow as any
|
|
2629
|
-
},
|
|
2630
|
-
points,
|
|
2394
|
+
shape: shapeType,
|
|
2395
|
+
valign: shapeValign as any,
|
|
2396
|
+
align: shapeAlign as any,
|
|
2397
|
+
margin: shapeMargin,
|
|
2631
2398
|
rotate, flipH, flipV,
|
|
2632
|
-
objectName: el.id
|
|
2399
|
+
objectName: el.id,
|
|
2400
|
+
fill: nativeFillOpt || shapeFillOpt
|
|
2633
2401
|
}
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
if (!parsedLineShadow && el.style?.boxShadow) {
|
|
2637
|
-
parsedLineShadow = parseCssShadow(el.style.boxShadow, ratioPx2Pt)
|
|
2402
|
+
if (shapeOpts.points) {
|
|
2403
|
+
shapeTextOpts.points = shapeOpts.points
|
|
2638
2404
|
}
|
|
2639
|
-
if (
|
|
2640
|
-
|
|
2641
|
-
if (lineRefl) {
|
|
2642
|
-
const parsedReflection = convertElReflectionToPptx(lineRefl, ratioPx2Pt)
|
|
2643
|
-
if (parsedReflection) lineOpts.reflection = parsedReflection
|
|
2405
|
+
if (shapeOpts.line) {
|
|
2406
|
+
shapeTextOpts.line = shapeOpts.line
|
|
2644
2407
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
} catch (e) {
|
|
2654
|
-
console.warn('Converting media Blob to DataURL failed:', e)
|
|
2655
|
-
}
|
|
2408
|
+
if (shapeOpts.shadow) {
|
|
2409
|
+
shapeTextOpts.shadow = shapeOpts.shadow
|
|
2410
|
+
}
|
|
2411
|
+
if (shapeOpts.reflection) {
|
|
2412
|
+
shapeTextOpts.reflection = shapeOpts.reflection
|
|
2413
|
+
}
|
|
2414
|
+
if (finalLineHeight !== undefined) {
|
|
2415
|
+
shapeTextOpts.lineSpacingMultiple = finalLineHeight
|
|
2656
2416
|
}
|
|
2657
|
-
if (
|
|
2658
|
-
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
} else if (mediaPath.startsWith('data:video/')) {
|
|
2668
|
-
if (mediaPath.includes('video/webm')) extn = 'webm'
|
|
2669
|
-
else if (mediaPath.includes('video/ogg')) extn = 'ogg'
|
|
2670
|
-
else extn = 'mp4'
|
|
2417
|
+
if (style.whiteSpace === 'nowrap') {
|
|
2418
|
+
shapeTextOpts.wrap = false
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
const elAny = el as any
|
|
2422
|
+
if (elAny.autoFit) {
|
|
2423
|
+
if (elAny.autoFit.type === 'shape') {
|
|
2424
|
+
shapeTextOpts.fit = 'resize'
|
|
2425
|
+
} else if (elAny.autoFit.type === 'text') {
|
|
2426
|
+
shapeTextOpts.fit = 'shrink'
|
|
2671
2427
|
}
|
|
2428
|
+
}
|
|
2672
2429
|
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2430
|
+
pptxSlide.addText(textProps, shapeTextOpts)
|
|
2431
|
+
} else {
|
|
2432
|
+
pptxSlide.addShape(shapeType as any, shapeOpts)
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
else if (el.type === 'chart') {
|
|
2436
|
+
const chartData = el.data?.datasets.map((ds: any, idx: number) => ({
|
|
2437
|
+
name: ds.label || `Series ${idx + 1}`,
|
|
2438
|
+
labels: el.data?.labels || [],
|
|
2439
|
+
values: ds.data || []
|
|
2440
|
+
})) || []
|
|
2441
|
+
|
|
2442
|
+
let rawType = (el.chartType as string) || 'bar'
|
|
2443
|
+
if (rawType === 'doughnutChart' || rawType === 'ring') rawType = 'donut'
|
|
2444
|
+
if (rawType === 'pieChart' || rawType === 'pie3DChart') rawType = 'pie'
|
|
2445
|
+
if (rawType === 'barChart' || rawType === 'bar3DChart') rawType = 'bar'
|
|
2446
|
+
if (rawType === 'lineChart' || rawType === 'line3DChart') rawType = 'line'
|
|
2447
|
+
if (rawType === 'areaChart' || rawType === 'area3DChart') rawType = 'area'
|
|
2448
|
+
|
|
2449
|
+
let chartType = pptx.ChartType.bar
|
|
2450
|
+
if (rawType === 'line') {
|
|
2451
|
+
chartType = pptx.ChartType.line
|
|
2452
|
+
} else if (rawType === 'area') {
|
|
2453
|
+
chartType = pptx.ChartType.area
|
|
2454
|
+
} else if (rawType === 'scatter') {
|
|
2455
|
+
chartType = pptx.ChartType.scatter
|
|
2456
|
+
} else if (rawType === 'pie') {
|
|
2457
|
+
chartType = pptx.ChartType.pie
|
|
2458
|
+
} else if (rawType === 'donut') {
|
|
2459
|
+
chartType = pptx.ChartType.doughnut
|
|
2460
|
+
} else if (rawType === 'radar') {
|
|
2461
|
+
chartType = pptx.ChartType.radar
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// 1. Resolve colors
|
|
2465
|
+
let colorsList: string[] = []
|
|
2466
|
+
if (Array.isArray(el.style?.chartColors) && el.style.chartColors.length > 0) {
|
|
2467
|
+
colorsList = el.style.chartColors
|
|
2468
|
+
} else if (Array.isArray((el as any).themeColors) && (el as any).themeColors.length > 0) {
|
|
2469
|
+
colorsList = (el as any).themeColors
|
|
2470
|
+
} else if (Array.isArray((el as any).colors) && (el as any).colors.length > 0) {
|
|
2471
|
+
colorsList = (el as any).colors
|
|
2472
|
+
} else if (Array.isArray(el.data?.datasets?.[0]?.backgroundColor) && el.data.datasets[0].backgroundColor.length > 0) {
|
|
2473
|
+
colorsList = el.data.datasets[0].backgroundColor
|
|
2474
|
+
} else if (Array.isArray(el.data?.datasets?.[0]?.colors) && el.data.datasets[0].colors.length > 0) {
|
|
2475
|
+
colorsList = el.data.datasets[0].colors
|
|
2476
|
+
} else {
|
|
2477
|
+
const dsColors = (el.data?.datasets || []).map((ds: any) => ds.color || ds.backgroundColor).filter((c: any) => c && typeof c === 'string')
|
|
2478
|
+
if (dsColors.length > 0) {
|
|
2479
|
+
colorsList = dsColors
|
|
2680
2480
|
} else {
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2481
|
+
colorsList = (globalThemeColor && globalThemeColor.value && globalThemeColor.value.length > 0)
|
|
2482
|
+
? globalThemeColor.value
|
|
2483
|
+
: ['#4f46e5', '#06b6d4', '#f59e0b', '#10b981', '#ec4899', '#8b5cf6', '#ef4444', '#14b8a6']
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
const cleanHexColors = colorsList
|
|
2487
|
+
.filter((c: any) => c && typeof c === 'string')
|
|
2488
|
+
.map((c: string) => parseColor(c).color)
|
|
2489
|
+
|
|
2490
|
+
// 2. Legend visibility and position
|
|
2491
|
+
const isPieOrDonut = chartType === pptx.ChartType.doughnut || chartType === pptx.ChartType.pie
|
|
2492
|
+
let showLegend = false
|
|
2493
|
+
if (el.showLegend !== undefined) {
|
|
2494
|
+
showLegend = Boolean(el.showLegend)
|
|
2495
|
+
} else if ((el as any).options?.showLegend !== undefined) {
|
|
2496
|
+
showLegend = Boolean((el as any).options.showLegend)
|
|
2497
|
+
} else {
|
|
2498
|
+
showLegend = !isPieOrDonut
|
|
2499
|
+
}
|
|
2696
2500
|
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2501
|
+
const chartFont = el.style?.fontFamily || el.fontFamily || (globalFontFamily && globalFontFamily.value) || 'Microsoft YaHei'
|
|
2502
|
+
const textColorHex = parseColor(el.style?.color || el.style?.textColor || el.textColor || '#64748b').color
|
|
2503
|
+
|
|
2504
|
+
const chartOpts: pptxgen.IChartOpts = {
|
|
2505
|
+
x, y, w, h,
|
|
2506
|
+
showLegend,
|
|
2507
|
+
chartColors: cleanHexColors.length > 0 ? cleanHexColors : undefined,
|
|
2508
|
+
objectName: el.id
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
if (showLegend) {
|
|
2512
|
+
let legendPos: 'b' | 'l' | 'r' | 't' = 'b'
|
|
2513
|
+
const rawPos = (el.legendPosition || (el as any).options?.legendPosition || 'bottom').toLowerCase()
|
|
2514
|
+
if (rawPos.startsWith('top')) legendPos = 't'
|
|
2515
|
+
else if (rawPos.startsWith('left')) legendPos = 'l'
|
|
2516
|
+
else if (rawPos.startsWith('right')) legendPos = 'r'
|
|
2517
|
+
else if (rawPos.startsWith('bottom')) legendPos = 'b'
|
|
2518
|
+
chartOpts.legendPos = legendPos
|
|
2519
|
+
chartOpts.legendColor = textColorHex
|
|
2520
|
+
chartOpts.legendFontSize = 10
|
|
2521
|
+
chartOpts.legendFontFace = chartFont
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// 3. Bar direction
|
|
2525
|
+
if (rawType === 'horizontal-bar') {
|
|
2526
|
+
chartOpts.barDir = 'bar'
|
|
2527
|
+
} else if (rawType === 'bar') {
|
|
2528
|
+
chartOpts.barDir = 'col'
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// 4. Donut specific options (hole size)
|
|
2532
|
+
if (chartType === pptx.ChartType.doughnut) {
|
|
2533
|
+
const rawHole = parseFloat((el as any).holeSize || (el as any).options?.holeSize)
|
|
2534
|
+
const holeSize = (!isNaN(rawHole) && rawHole > 0 && rawHole < 100) ? Math.round(rawHole) : 72
|
|
2535
|
+
chartOpts.holeSize = holeSize
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// 5. Slices border
|
|
2539
|
+
if (isPieOrDonut) {
|
|
2540
|
+
chartOpts.dataBorder = {
|
|
2541
|
+
pt: 1.5,
|
|
2542
|
+
color: 'FFFFFF'
|
|
2707
2543
|
}
|
|
2708
2544
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2545
|
+
|
|
2546
|
+
// 6. Data labels & Percent
|
|
2547
|
+
const hasPercentOpt = (el as any).showPercent !== undefined
|
|
2548
|
+
? Boolean((el as any).showPercent)
|
|
2549
|
+
: (el as any).options?.showPercent !== undefined
|
|
2550
|
+
? Boolean((el as any).options.showPercent)
|
|
2551
|
+
: isPieOrDonut
|
|
2552
|
+
|
|
2553
|
+
const showDataLabels = el.showDataLabels !== undefined
|
|
2554
|
+
? Boolean(el.showDataLabels)
|
|
2555
|
+
: (el as any).options?.showDataLabels !== undefined
|
|
2556
|
+
? Boolean((el as any).options.showDataLabels)
|
|
2557
|
+
: isPieOrDonut
|
|
2558
|
+
|
|
2559
|
+
if (isPieOrDonut) {
|
|
2560
|
+
if (hasPercentOpt || showDataLabels) {
|
|
2561
|
+
chartOpts.showPercent = true
|
|
2562
|
+
chartOpts.showValue = false
|
|
2563
|
+
chartOpts.dataLabelColor = 'FFFFFF'
|
|
2564
|
+
chartOpts.dataLabelFontSize = 11
|
|
2565
|
+
chartOpts.dataLabelFontBold = true
|
|
2566
|
+
chartOpts.dataLabelFormatCode = '0%'
|
|
2567
|
+
chartOpts.dataLabelFontFace = chartFont
|
|
2568
|
+
} else {
|
|
2569
|
+
chartOpts.showPercent = false
|
|
2570
|
+
chartOpts.showValue = false
|
|
2571
|
+
}
|
|
2572
|
+
} else {
|
|
2573
|
+
if (showDataLabels || (el as any).showValue) {
|
|
2574
|
+
chartOpts.showValue = true
|
|
2575
|
+
chartOpts.dataLabelColor = textColorHex
|
|
2576
|
+
chartOpts.dataLabelFontSize = 10
|
|
2577
|
+
chartOpts.dataLabelFontFace = chartFont
|
|
2719
2578
|
}
|
|
2720
2579
|
}
|
|
2580
|
+
|
|
2581
|
+
// 7. Cartesian axes & grid lines
|
|
2582
|
+
if (!isPieOrDonut && chartType !== pptx.ChartType.radar) {
|
|
2583
|
+
const gridHex = parseColor(el.style?.gridColor || el.gridColor || '#e2e8f0').color
|
|
2584
|
+
const showGrid = el.showGridlines !== false
|
|
2585
|
+
const showAxes = el.showAxes !== false
|
|
2586
|
+
|
|
2587
|
+
chartOpts.valGridLine = showGrid ? { color: gridHex, size: 0.75, style: 'dash' } : { style: 'none' }
|
|
2588
|
+
chartOpts.catGridLine = { style: 'none' }
|
|
2589
|
+
|
|
2590
|
+
chartOpts.catAxisLabelColor = textColorHex
|
|
2591
|
+
chartOpts.valAxisLabelColor = textColorHex
|
|
2592
|
+
chartOpts.catAxisHidden = !showAxes
|
|
2593
|
+
chartOpts.valAxisHidden = !showAxes
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
// 8. Line chart specific
|
|
2597
|
+
if (chartType === pptx.ChartType.line) {
|
|
2598
|
+
chartOpts.lineSize = 2.5
|
|
2599
|
+
chartOpts.lineSmooth = true
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
pptxSlide.addChart(chartType, chartData, chartOpts)
|
|
2603
|
+
}
|
|
2604
|
+
else if (el.type === 'line') {
|
|
2605
|
+
const mapArrowToPPTX = (uiType: string | undefined) => {
|
|
2606
|
+
if (!uiType) return 'none'
|
|
2607
|
+
const t = uiType.toLowerCase()
|
|
2608
|
+
if (t === 'arrow') return 'triangle'
|
|
2609
|
+
if (t === 'open-arrow') return 'arrow'
|
|
2610
|
+
if (t === 'stealth-arrow') return 'stealth'
|
|
2611
|
+
if (t === 'dot') return 'oval'
|
|
2612
|
+
if (t === 'diamond') return 'diamond'
|
|
2613
|
+
return 'none'
|
|
2614
|
+
}
|
|
2615
|
+
const beginArrow = mapArrowToPPTX(el.points?.[0])
|
|
2616
|
+
const endArrow = mapArrowToPPTX(el.points?.[1])
|
|
2617
|
+
|
|
2618
|
+
const path = getLineElementPath(el)
|
|
2619
|
+
const rawPoints = toPoints(path)
|
|
2620
|
+
const points = formatPoints(rawPoints, { x: 1, y: 1 }, pixelToInch) as any
|
|
2621
|
+
|
|
2622
|
+
const lineOpts: any = {
|
|
2623
|
+
x, y, w, h,
|
|
2624
|
+
line: {
|
|
2625
|
+
color: parseColor(el.style?.color || '#475569').color,
|
|
2626
|
+
width: ((el.style?.strokeWidth || 1) as number) / ratioPx2Pt,
|
|
2627
|
+
dashType: el.lineStyle === 'dashed' ? 'dash' : 'solid',
|
|
2628
|
+
beginArrowType: beginArrow as any,
|
|
2629
|
+
endArrowType: endArrow as any
|
|
2630
|
+
},
|
|
2631
|
+
points,
|
|
2632
|
+
rotate, flipH, flipV,
|
|
2633
|
+
objectName: el.id
|
|
2634
|
+
}
|
|
2635
|
+
const lineShadow = el.shadow || (el.style as any)?.shadow
|
|
2636
|
+
let parsedLineShadow = convertElShadowToPptx(lineShadow, ratioPx2Pt)
|
|
2637
|
+
if (!parsedLineShadow && el.style?.boxShadow) {
|
|
2638
|
+
parsedLineShadow = parseCssShadow(el.style.boxShadow, ratioPx2Pt)
|
|
2639
|
+
}
|
|
2640
|
+
if (parsedLineShadow) lineOpts.shadow = parsedLineShadow
|
|
2641
|
+
const lineRefl = el.reflection || el.style?.reflection
|
|
2642
|
+
if (lineRefl) {
|
|
2643
|
+
const parsedReflection = convertElReflectionToPptx(lineRefl, ratioPx2Pt)
|
|
2644
|
+
if (parsedReflection) lineOpts.reflection = parsedReflection
|
|
2645
|
+
}
|
|
2646
|
+
pptxSlide.addShape('custGeom' as any, lineOpts)
|
|
2721
2647
|
}
|
|
2648
|
+
else if (el.type === 'audio' || el.type === 'video') {
|
|
2649
|
+
const mediaInput = (el as any).blob || (el as any).src || el.content || (el as any).value || ''
|
|
2650
|
+
let mediaPath = ''
|
|
2651
|
+
if (typeof Blob !== 'undefined' && mediaInput instanceof Blob) {
|
|
2652
|
+
try {
|
|
2653
|
+
mediaPath = await blobToDataUrl(mediaInput)
|
|
2654
|
+
} catch (e) {
|
|
2655
|
+
console.warn('Converting media Blob to DataURL failed:', e)
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
if (!mediaPath) {
|
|
2659
|
+
mediaPath = await resolveMediaSrc(mediaInput, el.type as 'video' | 'audio')
|
|
2660
|
+
}
|
|
2661
|
+
if (mediaPath && mediaPath.startsWith('data:')) {
|
|
2662
|
+
let extn = el.type === 'audio' ? 'mp3' : 'mp4'
|
|
2663
|
+
if (mediaPath.startsWith('data:audio/')) {
|
|
2664
|
+
if (mediaPath.includes('audio/wav')) extn = 'wav'
|
|
2665
|
+
else if (mediaPath.includes('audio/m4a') || mediaPath.includes('audio/x-m4a') || mediaPath.includes('audio/mp4')) extn = 'm4a'
|
|
2666
|
+
else if (mediaPath.includes('audio/ogg')) extn = 'ogg'
|
|
2667
|
+
else extn = 'mp3'
|
|
2668
|
+
} else if (mediaPath.startsWith('data:video/')) {
|
|
2669
|
+
if (mediaPath.includes('video/webm')) extn = 'webm'
|
|
2670
|
+
else if (mediaPath.includes('video/ogg')) extn = 'ogg'
|
|
2671
|
+
else extn = 'mp4'
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
pptxSlide.addMedia({
|
|
2675
|
+
type: el.type,
|
|
2676
|
+
data: mediaPath,
|
|
2677
|
+
extn,
|
|
2678
|
+
x, y, w, h,
|
|
2679
|
+
objectName: el.id
|
|
2680
|
+
})
|
|
2681
|
+
} else {
|
|
2682
|
+
// 无法获取或跨域拦截二进制数据时的防护兜底:
|
|
2683
|
+
// 严禁调用带 path 的 addMedia,否则 pptxgenjs 会因为 XHR 失败向媒体文件写入损坏的图片数据,导致导出的 PPTX 无法打开!
|
|
2684
|
+
// 生成规范的优雅多媒体占位卡片并挂载在线超链接,确保 PPT 100% 正常打开且可点击跳转播放
|
|
2685
|
+
const isVideo = el.type === 'video'
|
|
2686
|
+
const mediaTitle = isVideo ? '视频' : '音频'
|
|
2687
|
+
const displayUrl = typeof mediaInput === 'string' ? mediaInput : ''
|
|
2688
|
+
|
|
2689
|
+
pptxSlide.addShape('rect' as any, {
|
|
2690
|
+
x, y, w, h,
|
|
2691
|
+
fill: { color: isVideo ? '1E293B' : '0F172A' },
|
|
2692
|
+
line: { color: '3B82F6', width: 1.5, dashType: 'dash' },
|
|
2693
|
+
rectRadius: 0.08,
|
|
2694
|
+
hyperlink: displayUrl ? { url: displayUrl, tooltip: `点击在线打开${mediaTitle}` } : undefined,
|
|
2695
|
+
objectName: el.id
|
|
2696
|
+
})
|
|
2722
2697
|
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2698
|
+
pptxSlide.addText([
|
|
2699
|
+
{ text: isVideo ? '▶ 视频多媒体' : '🎵 音频多媒体', options: { bold: true, fontSize: 13, color: 'FFFFFF' } },
|
|
2700
|
+
{ text: displayUrl ? `\n(在线链接: ${displayUrl.length > 36 ? displayUrl.slice(0, 36) + '...' : displayUrl})` : '', options: { fontSize: 9, color: '94A3B8' } },
|
|
2701
|
+
{ text: '\n[点击卡片在浏览器中播放]', options: { fontSize: 9, color: '60A5FA' } }
|
|
2702
|
+
], {
|
|
2703
|
+
x, y, w, h,
|
|
2704
|
+
align: 'center',
|
|
2705
|
+
valign: 'middle',
|
|
2706
|
+
hyperlink: displayUrl ? { url: displayUrl, tooltip: `点击在线打开${mediaTitle}` } : undefined
|
|
2707
|
+
})
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
else if (el.type === 'table') {
|
|
2711
|
+
const res = (el.content && (el.content.includes('<table') || el.content.includes('<table')))
|
|
2712
|
+
? parseHTMLTable(el.content, w)
|
|
2713
|
+
: parseTableElementData(el, w)
|
|
2714
|
+
if (res.tableData && res.tableData.length > 0) {
|
|
2715
|
+
const tableOpts: any = { x, y, w, h, objectName: el.id }
|
|
2716
|
+
if (res.colW && res.colW.length > 0) {
|
|
2717
|
+
tableOpts.colW = res.colW
|
|
2718
|
+
}
|
|
2719
|
+
pptxSlide.addTable(res.tableData, tableOpts)
|
|
2728
2720
|
}
|
|
2729
2721
|
}
|
|
2722
|
+
}
|
|
2730
2723
|
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2724
|
+
const hasNativeMaster = !!(currentZipMeta?.value?.masterFilesMap && Object.keys(currentZipMeta.value.masterFilesMap).length > 0)
|
|
2725
|
+
const masterElements = getEffectiveMasterElementsForSlide(slide, true)
|
|
2726
|
+
if (!hasNativeMaster && masterElements && masterElements.length > 0) {
|
|
2727
|
+
for (const mEl of masterElements) {
|
|
2728
|
+
await processElement(mEl, null)
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
const userElements = (slide.elements || []).filter((e: any) => {
|
|
2733
|
+
if (e.isMaster && !e.isPlaceholder && !e.placeholder && !e.id?.startsWith('ph-') && !e.id?.startsWith('ph_') && !e.name?.includes('占位符')) {
|
|
2734
|
+
if (masterElements && masterElements.some((m: any) => m.id === e.id)) {
|
|
2735
|
+
return false
|
|
2736
2736
|
}
|
|
2737
|
-
return true
|
|
2738
|
-
})
|
|
2739
|
-
for (const el of userElements) {
|
|
2740
|
-
await processElement(el, null)
|
|
2741
2737
|
}
|
|
2738
|
+
return true
|
|
2739
|
+
})
|
|
2740
|
+
for (const el of userElements) {
|
|
2741
|
+
await processElement(el, null)
|
|
2742
|
+
}
|
|
2742
2743
|
}
|
|
2743
2744
|
const rawBlob = (await pptx.write({ outputType: 'blob' })) as Blob
|
|
2744
2745
|
const animatedBlob = await injectAnimationToPptxBlob(
|
|
@@ -2791,6 +2792,14 @@ const handleExportCommand = (command: string) => {
|
|
|
2791
2792
|
}
|
|
2792
2793
|
}
|
|
2793
2794
|
|
|
2795
|
+
const handleImportCommand = (command: string) => {
|
|
2796
|
+
if (command === 'pptx') {
|
|
2797
|
+
triggerPPTXImport()
|
|
2798
|
+
} else if (command === 'json') {
|
|
2799
|
+
triggerJSONImport()
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2794
2803
|
function parseColorToRgb(c: string) {
|
|
2795
2804
|
c = c.trim().toLowerCase();
|
|
2796
2805
|
if (c.startsWith('#')) {
|
|
@@ -5767,12 +5776,12 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
5767
5776
|
points: connectorPoints,
|
|
5768
5777
|
elements: isNestedGroup
|
|
5769
5778
|
? (() => {
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5779
|
+
const nestedChildIdMap = new Map<string, any>(siblings)
|
|
5780
|
+
for (const ch of _el.elements || []) {
|
|
5781
|
+
if (ch && ch.id !== undefined) nestedChildIdMap.set(String(ch.id), ch)
|
|
5782
|
+
}
|
|
5783
|
+
return [...(_el.elements || [])].sort((a: any, b: any) => (a.order || 0) - (b.order || 0)).map((gChild: any) => convertChildElement(gChild, curW, curH, nestedChildIdMap))
|
|
5784
|
+
})()
|
|
5776
5785
|
: undefined
|
|
5777
5786
|
}
|
|
5778
5787
|
}
|
|
@@ -6761,186 +6770,268 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
6761
6770
|
})
|
|
6762
6771
|
console.log(json)
|
|
6763
6772
|
|
|
6764
|
-
|
|
6765
|
-
|
|
6773
|
+
let zipMeta: any = null
|
|
6774
|
+
let loadedZip: JSZip | null = null
|
|
6775
|
+
try {
|
|
6776
|
+
const zip = await JSZipLib.loadAsync(buffer)
|
|
6777
|
+
loadedZip = zip
|
|
6778
|
+
const layoutFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slideLayouts/slideLayout') && !k.includes('_rels') && !zip.files[k].dir)
|
|
6779
|
+
const sortSlideXml = (p1: string, p2: string) => {
|
|
6780
|
+
const m1 = /(\d+)\.xml/.exec(p1)
|
|
6781
|
+
const m2 = /(\d+)\.xml/.exec(p2)
|
|
6782
|
+
const n1 = m1 ? +m1[1] : 0
|
|
6783
|
+
const n2 = m2 ? +m2[1] : 0
|
|
6784
|
+
return n1 - n2
|
|
6785
|
+
}
|
|
6786
|
+
// 1. 提前解析主题配色 clrScheme(dk1/lt1/dk2/lt2/accent1-6),供母版、版式背景与装饰形状还原真实颜色
|
|
6787
|
+
const themeColors: Record<string, string> = {}
|
|
6766
6788
|
try {
|
|
6767
|
-
const
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
// 1. 提前解析主题配色 clrScheme(dk1/lt1/dk2/lt2/accent1-6),供母版、版式背景与装饰形状还原真实颜色
|
|
6778
|
-
const themeColors: Record<string, string> = {}
|
|
6779
|
-
try {
|
|
6780
|
-
const themeFiles = Object.keys(zip.files).filter(k => /^ppt\/theme\/theme\d+\.xml$/.test(k))
|
|
6781
|
-
if (themeFiles.length > 0) {
|
|
6782
|
-
const tTxt = await zip.file(themeFiles[0])?.async('text') || ''
|
|
6783
|
-
const schemeM = tTxt.match(/<a:clrScheme[\s\S]*?<\/a:clrScheme>/)
|
|
6784
|
-
if (schemeM) {
|
|
6785
|
-
const slotRe = /<a:(dk1|lt1|dk2|lt2|accent1|accent2|accent3|accent4|accent5|accent6|hlink|folHlink)>([\s\S]*?)<\/a:\1>/g
|
|
6786
|
-
let slotMatch: RegExpExecArray | null
|
|
6787
|
-
while ((slotMatch = slotRe.exec(schemeM[0]))) {
|
|
6788
|
-
const hexM = slotMatch[2].match(/<a:srgbClr val="([0-9A-Fa-f]{6})"/) || slotMatch[2].match(/<a:sysClr[^>]*lastClr="([0-9A-Fa-f]{6})"/)
|
|
6789
|
-
if (hexM) themeColors[slotMatch[1]] = '#' + hexM[1].toUpperCase()
|
|
6790
|
-
}
|
|
6789
|
+
const themeFiles = Object.keys(zip.files).filter(k => /^ppt\/theme\/theme\d+\.xml$/.test(k))
|
|
6790
|
+
if (themeFiles.length > 0) {
|
|
6791
|
+
const tTxt = await zip.file(themeFiles[0])?.async('text') || ''
|
|
6792
|
+
const schemeM = tTxt.match(/<a:clrScheme[\s\S]*?<\/a:clrScheme>/)
|
|
6793
|
+
if (schemeM) {
|
|
6794
|
+
const slotRe = /<a:(dk1|lt1|dk2|lt2|accent1|accent2|accent3|accent4|accent5|accent6|hlink|folHlink)>([\s\S]*?)<\/a:\1>/g
|
|
6795
|
+
let slotMatch: RegExpExecArray | null
|
|
6796
|
+
while ((slotMatch = slotRe.exec(schemeM[0]))) {
|
|
6797
|
+
const hexM = slotMatch[2].match(/<a:srgbClr val="([0-9A-Fa-f]{6})"/) || slotMatch[2].match(/<a:sysClr[^>]*lastClr="([0-9A-Fa-f]{6})"/)
|
|
6798
|
+
if (hexM) themeColors[slotMatch[1]] = '#' + hexM[1].toUpperCase()
|
|
6791
6799
|
}
|
|
6792
6800
|
}
|
|
6793
|
-
}
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
}
|
|
6801
|
+
}
|
|
6802
|
+
} catch (e) { }
|
|
6803
|
+
|
|
6804
|
+
// 通用背景 XML (<p:bg>) 解析器:全面支持 <a:blip> 图片、<a:gradFill> 渐变(含 circle/radial 径向渐变)、<a:srgbClr> 与 <a:schemeClr> 单色
|
|
6805
|
+
const parsePptxBgFromXml = async (bgTxt: string, xmlPath: string): Promise<string | null> => {
|
|
6806
|
+
if (!bgTxt) return null
|
|
6807
|
+
const mimeByExt: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', svg: 'image/svg+xml', tif: 'image/tiff', tiff: 'image/tiff' }
|
|
6808
|
+
|
|
6809
|
+
// 1. 图片填充 <a:blip>
|
|
6810
|
+
const blipMatch = bgTxt.match(/<a:blip\b[^>]*r:embed="([^"]+)"/)
|
|
6811
|
+
if (blipMatch) {
|
|
6812
|
+
const rId = blipMatch[1]
|
|
6813
|
+
const relsPath = xmlPath.replace(/(slideMasters|slideLayouts)\/([^/]+)\.xml$/, '$1/_rels/$2.xml.rels')
|
|
6814
|
+
const relsTxt = await zip.file(relsPath)?.async('text') || ''
|
|
6815
|
+
const relM = relsTxt.match(new RegExp(`Id="${rId}"[^>]*Target="([^"]+)"`)) || relsTxt.match(new RegExp(`Target="([^"]+)"[^>]*Id="${rId}"`))
|
|
6816
|
+
if (relM) {
|
|
6817
|
+
const stack = xmlPath.split('/').slice(0, -1)
|
|
6818
|
+
for (const part of relM[1].split('/')) {
|
|
6819
|
+
if (part === '..') stack.pop()
|
|
6820
|
+
else if (part && part !== '.') stack.push(part)
|
|
6821
|
+
}
|
|
6822
|
+
const mediaPath = stack.join('/')
|
|
6823
|
+
const ext = ((mediaPath.match(/\.(\w+)$/) || [])[1] || '').toLowerCase()
|
|
6824
|
+
const blob = await zip.file(mediaPath)?.async('blob')
|
|
6825
|
+
if (blob) {
|
|
6826
|
+
const blobUrl = URL.createObjectURL(blob)
|
|
6827
|
+
return `#ffffff url("${blobUrl}") center center / 100% 100% no-repeat`
|
|
6820
6828
|
}
|
|
6821
6829
|
}
|
|
6830
|
+
}
|
|
6822
6831
|
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
}
|
|
6832
|
+
// 2. 渐变填充 <a:gradFill>
|
|
6833
|
+
const gradMatch = bgTxt.match(/<a:gradFill\b[\s\S]*?<\/a:gradFill>/)
|
|
6834
|
+
if (gradMatch) {
|
|
6835
|
+
const gradTxt = gradMatch[0]
|
|
6836
|
+
const gsList = gradTxt.match(/<a:gs\b[\s\S]*?<\/a:gs>/g) || []
|
|
6837
|
+
if (gsList.length > 0) {
|
|
6838
|
+
const stops: string[] = []
|
|
6839
|
+
for (const gs of gsList) {
|
|
6840
|
+
const posM = gs.match(/pos="(\d+)"/)
|
|
6841
|
+
const posVal = posM ? Math.round(parseInt(posM[1], 10) / 1000) : 0
|
|
6842
|
+
let col = '#FFFFFF'
|
|
6843
|
+
const srgbM = gs.match(/<a:srgbClr\s+val="([0-9A-Fa-f]{6})"/)
|
|
6844
|
+
if (srgbM) {
|
|
6845
|
+
col = '#' + srgbM[1].toUpperCase()
|
|
6846
|
+
} else {
|
|
6847
|
+
const schemeM = gs.match(/<a:schemeClr\s+val="([^"]+)"([\s\S]*?)<\/a:schemeClr>/) || gs.match(/<a:schemeClr\s+val="([^"]+)"\s*\/>/)
|
|
6848
|
+
if (schemeM) {
|
|
6849
|
+
const val = schemeM[1]
|
|
6850
|
+
const inner = schemeM[2] || ''
|
|
6851
|
+
const lumModM = inner.match(/<a:lumMod val="(\d+)"/)
|
|
6852
|
+
const lumOffM = inner.match(/<a:lumOff val="(\d+)"/)
|
|
6853
|
+
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
6854
|
+
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
6855
|
+
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
6856
|
+
const baseHex = themeColors[slot] || themeColors[val] || (slot === 'dk1' || slot === 'tx1' ? '#000000' : '#FFFFFF')
|
|
6857
|
+
col = (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
6850
6858
|
}
|
|
6851
|
-
stops.push(`${col} ${posVal}%`)
|
|
6852
6859
|
}
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
return `radial-gradient(closest-side at ${cx}% ${cy}%, ${stops.join(', ')})`
|
|
6879
|
-
}
|
|
6880
|
-
return `radial-gradient(circle farthest-corner at ${cx}% ${cy}%, ${stops.join(', ')})`
|
|
6860
|
+
stops.push(`${col} ${posVal}%`)
|
|
6861
|
+
}
|
|
6862
|
+
const pathMatch = gradTxt.match(/<a:path\b[^>]*path="([^"]+)"/i)
|
|
6863
|
+
const pathKind = pathMatch ? pathMatch[1].toLowerCase() : ''
|
|
6864
|
+
const isPath = pathKind === 'shape'
|
|
6865
|
+
const isCircle = pathKind === 'circle' || pathKind === 'radial'
|
|
6866
|
+
const isRect = pathKind === 'rect'
|
|
6867
|
+
const isRadialOrRect = isCircle || isRect || /<a:fillToRect/i.test(gradTxt)
|
|
6868
|
+
|
|
6869
|
+
if (isRadialOrRect) {
|
|
6870
|
+
const fillToRectM = gradTxt.match(/<a:fillToRect\b([^>]*)\/?>/)
|
|
6871
|
+
let cx = 50
|
|
6872
|
+
let cy = 50
|
|
6873
|
+
if (fillToRectM) {
|
|
6874
|
+
const attrs = fillToRectM[1]
|
|
6875
|
+
const lM = attrs.match(/l="(-?\d+)"/)
|
|
6876
|
+
const tM = attrs.match(/t="(-?\d+)"/)
|
|
6877
|
+
const rM = attrs.match(/r="(-?\d+)"/)
|
|
6878
|
+
const bM = attrs.match(/b="(-?\d+)"/)
|
|
6879
|
+
const l = lM ? parseInt(lM[1], 10) / 100000 : 0.5
|
|
6880
|
+
const t = tM ? parseInt(tM[1], 10) / 100000 : 0.5
|
|
6881
|
+
const r = rM ? parseInt(rM[1], 10) / 100000 : 0.5
|
|
6882
|
+
const b = bM ? parseInt(bM[1], 10) / 100000 : 0.5
|
|
6883
|
+
cx = Math.round(((l + (1 - r)) / 2) * 100)
|
|
6884
|
+
cy = Math.round(((t + (1 - b)) / 2) * 100)
|
|
6881
6885
|
}
|
|
6882
|
-
if (
|
|
6883
|
-
return `radial-gradient(
|
|
6886
|
+
if (isRect) {
|
|
6887
|
+
return `radial-gradient(closest-side at ${cx}% ${cy}%, ${stops.join(', ')})`
|
|
6884
6888
|
}
|
|
6885
|
-
|
|
6886
|
-
const angVal = linM ? parseInt(linM[1], 10) / 60000 : 0
|
|
6887
|
-
const cssAng = (Math.round(angVal) + 90) % 360
|
|
6888
|
-
return `linear-gradient(${cssAng}deg, ${stops.join(', ')})`
|
|
6889
|
+
return `radial-gradient(circle farthest-corner at ${cx}% ${cy}%, ${stops.join(', ')})`
|
|
6889
6890
|
}
|
|
6891
|
+
if (isPath) {
|
|
6892
|
+
return `radial-gradient(ellipse farthest-corner at 50% 50%, ${stops.join(', ')})`
|
|
6893
|
+
}
|
|
6894
|
+
const linM = gradTxt.match(/<a:lin\b[^>]*ang="(-?\d+)"/)
|
|
6895
|
+
const angVal = linM ? parseInt(linM[1], 10) / 60000 : 0
|
|
6896
|
+
const cssAng = (Math.round(angVal) + 90) % 360
|
|
6897
|
+
return `linear-gradient(${cssAng}deg, ${stops.join(', ')})`
|
|
6890
6898
|
}
|
|
6899
|
+
}
|
|
6891
6900
|
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
}
|
|
6897
|
-
const schemeMatch = bgTxt.match(/<a:schemeClr\s+val="([^"]+)"([\s\S]*?)<\/a:schemeClr>/) || bgTxt.match(/<a:schemeClr\s+val="([^"]+)"\s*\/>/)
|
|
6898
|
-
if (schemeMatch) {
|
|
6899
|
-
const val = schemeMatch[1]
|
|
6900
|
-
const inner = schemeMatch[2] || ''
|
|
6901
|
-
const lumModM = inner.match(/<a:lumMod val="(\d+)"/)
|
|
6902
|
-
const lumOffM = inner.match(/<a:lumOff val="(\d+)"/)
|
|
6903
|
-
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
6904
|
-
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
6905
|
-
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
6906
|
-
const baseHex = themeColors[slot] || themeColors[val] || (slot === 'dk1' || slot === 'tx1' ? '#000000' : '#FFFFFF')
|
|
6907
|
-
return (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
6908
|
-
}
|
|
6909
|
-
|
|
6910
|
-
return null
|
|
6901
|
+
// 3. 纯色填充 <a:srgbClr> 或 <a:schemeClr>
|
|
6902
|
+
const srgbMatch = bgTxt.match(/<a:srgbClr\s+val="([0-9A-Fa-f]{6})"/)
|
|
6903
|
+
if (srgbMatch) {
|
|
6904
|
+
return '#' + srgbMatch[1].toUpperCase()
|
|
6911
6905
|
}
|
|
6906
|
+
const schemeMatch = bgTxt.match(/<a:schemeClr\s+val="([^"]+)"([\s\S]*?)<\/a:schemeClr>/) || bgTxt.match(/<a:schemeClr\s+val="([^"]+)"\s*\/>/)
|
|
6907
|
+
if (schemeMatch) {
|
|
6908
|
+
const val = schemeMatch[1]
|
|
6909
|
+
const inner = schemeMatch[2] || ''
|
|
6910
|
+
const lumModM = inner.match(/<a:lumMod val="(\d+)"/)
|
|
6911
|
+
const lumOffM = inner.match(/<a:lumOff val="(\d+)"/)
|
|
6912
|
+
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
6913
|
+
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
6914
|
+
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
6915
|
+
const baseHex = themeColors[slot] || themeColors[val] || (slot === 'dk1' || slot === 'tx1' ? '#000000' : '#FFFFFF')
|
|
6916
|
+
return (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
6917
|
+
}
|
|
6918
|
+
|
|
6919
|
+
return null
|
|
6920
|
+
}
|
|
6921
|
+
|
|
6922
|
+
layoutFiles.sort(sortSlideXml)
|
|
6923
|
+
|
|
6924
|
+
const allLayouts: any[] = []
|
|
6925
|
+
const layoutByFile = new Map<string, any>()
|
|
6926
|
+
|
|
6927
|
+
for (let i = 0; i < layoutFiles.length; i++) {
|
|
6928
|
+
const lf = layoutFiles[i]
|
|
6929
|
+
const txt = await zip.file(lf)?.async('text') || ''
|
|
6930
|
+
const mimeByExt: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', svg: 'image/svg+xml', tif: 'image/tiff', tiff: 'image/tiff' }
|
|
6912
6931
|
|
|
6913
|
-
|
|
6932
|
+
// 解析版式背景 (<p:bg>) 图片、渐变或颜色填充
|
|
6933
|
+
let layoutBg: string | null = null
|
|
6934
|
+
const bgMatch = txt.match(/<p:bg\b[\s\S]*?<\/p:bg>/)
|
|
6935
|
+
if (bgMatch) {
|
|
6936
|
+
layoutBg = await parsePptxBgFromXml(bgMatch[0], lf)
|
|
6937
|
+
}
|
|
6914
6938
|
|
|
6915
|
-
|
|
6916
|
-
|
|
6939
|
+
// 版式装饰图片(userDrawn <p:pic>,如整幅世界地图):未被任何页使用的版式走
|
|
6940
|
+
// XML 解析分支(parseLayoutShapesFromXml),sp 解析管不到 pic,需经 rels 把
|
|
6941
|
+
// media 转 base64 一并传入,否则该版式在母版编辑视图/缩略图整片空白
|
|
6942
|
+
const picSrcMap: Record<string, string> = {}
|
|
6943
|
+
const layoutPicMatches = txt.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
6944
|
+
if (layoutPicMatches.length > 0) {
|
|
6945
|
+
const relsTxt = await zip.file(lf.replace(/(slideLayouts\/)(slideLayout\d+\.xml)$/, '$1_rels/$2.rels'))?.async('text') || ''
|
|
6946
|
+
for (const pic of layoutPicMatches) {
|
|
6947
|
+
const rId = (pic.match(/r:embed="([^"]+)"/) || [])[1]
|
|
6948
|
+
if (!rId || picSrcMap[rId]) continue
|
|
6949
|
+
const relM = relsTxt.match(new RegExp(`Id="${rId}"[^>]*Target="([^"]+)"`)) || relsTxt.match(new RegExp(`Target="([^"]+)"[^>]*Id="${rId}"`))
|
|
6950
|
+
if (!relM) continue
|
|
6951
|
+
// target 相对 slideLayouts 目录(如 ../media/image2.jpeg)→ zip 内绝对路径
|
|
6952
|
+
const stack = lf.split('/').slice(0, -1)
|
|
6953
|
+
for (const part of relM[1].split('/')) {
|
|
6954
|
+
if (part === '..') stack.pop()
|
|
6955
|
+
else if (part && part !== '.') stack.push(part)
|
|
6956
|
+
}
|
|
6957
|
+
const mediaPath = stack.join('/')
|
|
6958
|
+
const ext = ((mediaPath.match(/\.(\w+)$/) || [])[1] || '').toLowerCase()
|
|
6959
|
+
const b64 = await zip.file(mediaPath)?.async('base64')
|
|
6960
|
+
if (b64) picSrcMap[rId] = `data:${mimeByExt[ext] || 'application/octet-stream'};base64,${b64}`
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
const nameM = txt.match(/<p:cSld[^>]*name="([^"]+)"/)
|
|
6964
|
+
const typeM = txt.match(/<p:sldLayout[^>]*type="([^"]+)"/)
|
|
6965
|
+
const isTitle = (typeM && typeM[1] === 'title') || txt.includes('type="ctrTitle"') || (nameM && (nameM[1].includes('Title') || nameM[1].includes('标题')))
|
|
6966
|
+
const isBlank = (typeM && typeM[1] === 'blank') || (nameM && (nameM[1].includes('Blank') || nameM[1].includes('空白')))
|
|
6967
|
+
const picPhCount = (txt.match(/type="pic"/g) || []).length
|
|
6968
|
+
|
|
6969
|
+
const lObj = {
|
|
6970
|
+
file: lf,
|
|
6971
|
+
id: `layout-${i + 1}`,
|
|
6972
|
+
name: nameM ? nameM[1] : `版式 ${i + 1}`,
|
|
6973
|
+
type: typeM ? typeM[1] : (isTitle ? 'title' : (isBlank ? 'blank' : 'content')),
|
|
6974
|
+
background: layoutBg || undefined,
|
|
6975
|
+
picPhCount,
|
|
6976
|
+
picSrcMap,
|
|
6977
|
+
xml: txt
|
|
6978
|
+
}
|
|
6979
|
+
allLayouts.push(lObj)
|
|
6980
|
+
layoutByFile.set(lf, lObj)
|
|
6981
|
+
const shortName = lf.replace('ppt/slideLayouts/', '')
|
|
6982
|
+
layoutByFile.set(shortName, lObj)
|
|
6983
|
+
layoutByFile.set(`../slideLayouts/${shortName}`, lObj)
|
|
6984
|
+
}
|
|
6917
6985
|
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6986
|
+
const slideRelsFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slides/_rels/slide') && k.endsWith('.rels'))
|
|
6987
|
+
slideRelsFiles.sort(sortSlideXml)
|
|
6988
|
+
|
|
6989
|
+
const slideLayoutMap: any[] = []
|
|
6990
|
+
const slideLayoutTargetList: string[] = []
|
|
6991
|
+
for (let i = 0; i < slideRelsFiles.length; i++) {
|
|
6992
|
+
const srf = slideRelsFiles[i]
|
|
6993
|
+
const txt = await zip.file(srf)?.async('text') || ''
|
|
6994
|
+
const layoutMatch = txt.match(/Type="[^"]*\/slideLayout"[^>]*Target="([^"]+)"/) || txt.match(/Target="([^"]+)"[^>]*Type="[^"]*\/slideLayout"/)
|
|
6995
|
+
const target = layoutMatch ? layoutMatch[1] : ''
|
|
6996
|
+
slideLayoutTargetList.push(target)
|
|
6997
|
+
const lObj = layoutByFile.get(target)
|
|
6998
|
+
slideLayoutMap.push(lObj || null)
|
|
6999
|
+
}
|
|
6922
7000
|
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
7001
|
+
let masterName = nameWithoutExt
|
|
7002
|
+
const masterShapesInfo: Array<{ name: string; text?: string; isPic?: boolean }> = []
|
|
7003
|
+
// 母版占位符 xfrm 位置表(idx/type → px 盒子):版式占位符常省略自身 xfrm(继承母版),
|
|
7004
|
+
// 解析版式提示框时按此回退,否则这类版式解析不出占位符、缩略图整片空白
|
|
7005
|
+
const masterPhXfrm: Record<string, { left: number; top: number; width: number; height: number }> = {}
|
|
7006
|
+
let primaryMasterXml = ''
|
|
7007
|
+
let masterXmlBg: string | null = null
|
|
7008
|
+
const masterPicSrcMap: Record<string, string> = {}
|
|
7009
|
+
try {
|
|
7010
|
+
const masterFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slideMasters/slideMaster') && !k.includes('_rels') && !zip.files[k].dir)
|
|
7011
|
+
masterFiles.sort(sortSlideXml)
|
|
7012
|
+
for (const mf of masterFiles) {
|
|
7013
|
+
const mTxt = await zip.file(mf)?.async('text') || ''
|
|
7014
|
+
if (!primaryMasterXml) primaryMasterXml = mTxt
|
|
7015
|
+
|
|
7016
|
+
// 提取母版背景 (<p:bg>)
|
|
7017
|
+
if (!masterXmlBg) {
|
|
7018
|
+
const mBgMatch = mTxt.match(/<p:bg\b[\s\S]*?<\/p:bg>/)
|
|
7019
|
+
if (mBgMatch) {
|
|
7020
|
+
masterXmlBg = await parsePptxBgFromXml(mBgMatch[0], mf)
|
|
7021
|
+
}
|
|
6928
7022
|
}
|
|
6929
7023
|
|
|
6930
|
-
//
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
const relsTxt = await zip.file(lf.replace(/(slideLayouts\/)(slideLayout\d+\.xml)$/, '$1_rels/$2.rels'))?.async('text') || ''
|
|
6937
|
-
for (const pic of layoutPicMatches) {
|
|
7024
|
+
// 防御策略 3:从 slideMasters/_rels/slideMaster*.xml.rels 提取母版图片引用,解析为 Base64 供 Logo/背景图显影
|
|
7025
|
+
const masterPicMatches = mTxt.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7026
|
+
if (masterPicMatches.length > 0) {
|
|
7027
|
+
const relsTxt = await zip.file(mf.replace(/(slideMasters\/)(slideMaster\d+\.xml)$/, '$1_rels/$2.rels'))?.async('text') || ''
|
|
7028
|
+
const mimeByExt: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', svg: 'image/svg+xml', tif: 'image/tiff', tiff: 'image/tiff' }
|
|
7029
|
+
for (const pic of masterPicMatches) {
|
|
6938
7030
|
const rId = (pic.match(/r:embed="([^"]+)"/) || [])[1]
|
|
6939
|
-
if (!rId ||
|
|
7031
|
+
if (!rId || masterPicSrcMap[rId]) continue
|
|
6940
7032
|
const relM = relsTxt.match(new RegExp(`Id="${rId}"[^>]*Target="([^"]+)"`)) || relsTxt.match(new RegExp(`Target="([^"]+)"[^>]*Id="${rId}"`))
|
|
6941
7033
|
if (!relM) continue
|
|
6942
|
-
|
|
6943
|
-
const stack = lf.split('/').slice(0, -1)
|
|
7034
|
+
const stack = mf.split('/').slice(0, -1)
|
|
6944
7035
|
for (const part of relM[1].split('/')) {
|
|
6945
7036
|
if (part === '..') stack.pop()
|
|
6946
7037
|
else if (part && part !== '.') stack.push(part)
|
|
@@ -6948,481 +7039,399 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
6948
7039
|
const mediaPath = stack.join('/')
|
|
6949
7040
|
const ext = ((mediaPath.match(/\.(\w+)$/) || [])[1] || '').toLowerCase()
|
|
6950
7041
|
const b64 = await zip.file(mediaPath)?.async('base64')
|
|
6951
|
-
if (b64)
|
|
7042
|
+
if (b64) masterPicSrcMap[rId] = `data:${mimeByExt[ext] || 'application/octet-stream'};base64,${b64}`
|
|
6952
7043
|
}
|
|
6953
7044
|
}
|
|
6954
|
-
const nameM = txt.match(/<p:cSld[^>]*name="([^"]+)"/)
|
|
6955
|
-
const typeM = txt.match(/<p:sldLayout[^>]*type="([^"]+)"/)
|
|
6956
|
-
const isTitle = (typeM && typeM[1] === 'title') || txt.includes('type="ctrTitle"') || (nameM && (nameM[1].includes('Title') || nameM[1].includes('标题')))
|
|
6957
|
-
const isBlank = (typeM && typeM[1] === 'blank') || (nameM && (nameM[1].includes('Blank') || nameM[1].includes('空白')))
|
|
6958
|
-
const picPhCount = (txt.match(/type="pic"/g) || []).length
|
|
6959
|
-
|
|
6960
|
-
const lObj = {
|
|
6961
|
-
file: lf,
|
|
6962
|
-
id: `layout-${i + 1}`,
|
|
6963
|
-
name: nameM ? nameM[1] : `版式 ${i + 1}`,
|
|
6964
|
-
type: typeM ? typeM[1] : (isTitle ? 'title' : (isBlank ? 'blank' : 'content')),
|
|
6965
|
-
background: layoutBg || undefined,
|
|
6966
|
-
picPhCount,
|
|
6967
|
-
picSrcMap,
|
|
6968
|
-
xml: txt
|
|
6969
|
-
}
|
|
6970
|
-
allLayouts.push(lObj)
|
|
6971
|
-
layoutByFile.set(lf, lObj)
|
|
6972
|
-
const shortName = lf.replace('ppt/slideLayouts/', '')
|
|
6973
|
-
layoutByFile.set(shortName, lObj)
|
|
6974
|
-
layoutByFile.set(`../slideLayouts/${shortName}`, lObj)
|
|
6975
|
-
}
|
|
6976
7045
|
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
const masterShapesInfo: Array<{ name: string; text?: string; isPic?: boolean }> = []
|
|
6994
|
-
// 母版占位符 xfrm 位置表(idx/type → px 盒子):版式占位符常省略自身 xfrm(继承母版),
|
|
6995
|
-
// 解析版式提示框时按此回退,否则这类版式解析不出占位符、缩略图整片空白
|
|
6996
|
-
const masterPhXfrm: Record<string, { left: number; top: number; width: number; height: number }> = {}
|
|
6997
|
-
let primaryMasterXml = ''
|
|
6998
|
-
let masterXmlBg: string | null = null
|
|
6999
|
-
const masterPicSrcMap: Record<string, string> = {}
|
|
7000
|
-
try {
|
|
7001
|
-
const masterFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slideMasters/slideMaster') && !k.includes('_rels') && !zip.files[k].dir)
|
|
7002
|
-
masterFiles.sort(sortSlideXml)
|
|
7003
|
-
for (const mf of masterFiles) {
|
|
7004
|
-
const mTxt = await zip.file(mf)?.async('text') || ''
|
|
7005
|
-
if (!primaryMasterXml) primaryMasterXml = mTxt
|
|
7006
|
-
|
|
7007
|
-
// 提取母版背景 (<p:bg>)
|
|
7008
|
-
if (!masterXmlBg) {
|
|
7009
|
-
const mBgMatch = mTxt.match(/<p:bg\b[\s\S]*?<\/p:bg>/)
|
|
7010
|
-
if (mBgMatch) {
|
|
7011
|
-
masterXmlBg = await parsePptxBgFromXml(mBgMatch[0], mf)
|
|
7012
|
-
}
|
|
7013
|
-
}
|
|
7014
|
-
|
|
7015
|
-
// 防御策略 3:从 slideMasters/_rels/slideMaster*.xml.rels 提取母版图片引用,解析为 Base64 供 Logo/背景图显影
|
|
7016
|
-
const masterPicMatches = mTxt.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7017
|
-
if (masterPicMatches.length > 0) {
|
|
7018
|
-
const relsTxt = await zip.file(mf.replace(/(slideMasters\/)(slideMaster\d+\.xml)$/, '$1_rels/$2.rels'))?.async('text') || ''
|
|
7019
|
-
const mimeByExt: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', svg: 'image/svg+xml', tif: 'image/tiff', tiff: 'image/tiff' }
|
|
7020
|
-
for (const pic of masterPicMatches) {
|
|
7021
|
-
const rId = (pic.match(/r:embed="([^"]+)"/) || [])[1]
|
|
7022
|
-
if (!rId || masterPicSrcMap[rId]) continue
|
|
7023
|
-
const relM = relsTxt.match(new RegExp(`Id="${rId}"[^>]*Target="([^"]+)"`)) || relsTxt.match(new RegExp(`Target="([^"]+)"[^>]*Id="${rId}"`))
|
|
7024
|
-
if (!relM) continue
|
|
7025
|
-
const stack = mf.split('/').slice(0, -1)
|
|
7026
|
-
for (const part of relM[1].split('/')) {
|
|
7027
|
-
if (part === '..') stack.pop()
|
|
7028
|
-
else if (part && part !== '.') stack.push(part)
|
|
7029
|
-
}
|
|
7030
|
-
const mediaPath = stack.join('/')
|
|
7031
|
-
const ext = ((mediaPath.match(/\.(\w+)$/) || [])[1] || '').toLowerCase()
|
|
7032
|
-
const b64 = await zip.file(mediaPath)?.async('base64')
|
|
7033
|
-
if (b64) masterPicSrcMap[rId] = `data:${mimeByExt[ext] || 'application/octet-stream'};base64,${b64}`
|
|
7034
|
-
}
|
|
7035
|
-
}
|
|
7036
|
-
|
|
7037
|
-
const spMatches = mTxt.match(/<p:sp\b[\s\S]*?<\/p:sp>/g) || []
|
|
7038
|
-
for (const sp of spMatches) {
|
|
7039
|
-
const name = sp.match(/<p:cNvPr[^>]*name="([^"]+)"/)?.[1] || sp.match(/name="([^"]+)"/)?.[1] || ''
|
|
7040
|
-
const text = (sp.match(/<a:t>([^<]+)<\/a:t>/g) || []).map(t => t.replace(/<\/?a:t>/g, '')).join('').trim()
|
|
7041
|
-
masterShapesInfo.push({ name, text })
|
|
7042
|
-
const phAttrsM = sp.match(/<p:ph\b([^>]*)\/?>/)
|
|
7043
|
-
if (phAttrsM) {
|
|
7044
|
-
const phAttrs = phAttrsM[1]
|
|
7045
|
-
const idxAttr = (phAttrs.match(/idx="(\d+)"/) || [])[1]
|
|
7046
|
-
const typeAttr = (phAttrs.match(/type="(\w+)"/) || [])[1]
|
|
7047
|
-
const off = sp.match(/<a:off\s+x="(-?\d+)"\s+y="(-?\d+)"/)
|
|
7048
|
-
const ext = sp.match(/<a:ext\s+cx="(\d+)"\s+cy="(\d+)"/)
|
|
7049
|
-
if (off && ext) {
|
|
7050
|
-
const box = { left: Math.round(parseInt(off[1]) / 9525), top: Math.round(parseInt(off[2]) / 9525), width: Math.round(parseInt(ext[1]) / 9525), height: Math.round(parseInt(ext[2]) / 9525) }
|
|
7051
|
-
if (idxAttr !== undefined && !masterPhXfrm['idx_' + idxAttr]) masterPhXfrm['idx_' + idxAttr] = box
|
|
7052
|
-
if (typeAttr && !masterPhXfrm['type_' + typeAttr]) masterPhXfrm['type_' + typeAttr] = box
|
|
7053
|
-
}
|
|
7046
|
+
const spMatches = mTxt.match(/<p:sp\b[\s\S]*?<\/p:sp>/g) || []
|
|
7047
|
+
for (const sp of spMatches) {
|
|
7048
|
+
const name = sp.match(/<p:cNvPr[^>]*name="([^"]+)"/)?.[1] || sp.match(/name="([^"]+)"/)?.[1] || ''
|
|
7049
|
+
const text = (sp.match(/<a:t>([^<]+)<\/a:t>/g) || []).map(t => t.replace(/<\/?a:t>/g, '')).join('').trim()
|
|
7050
|
+
masterShapesInfo.push({ name, text })
|
|
7051
|
+
const phAttrsM = sp.match(/<p:ph\b([^>]*)\/?>/)
|
|
7052
|
+
if (phAttrsM) {
|
|
7053
|
+
const phAttrs = phAttrsM[1]
|
|
7054
|
+
const idxAttr = (phAttrs.match(/idx="(\d+)"/) || [])[1]
|
|
7055
|
+
const typeAttr = (phAttrs.match(/type="(\w+)"/) || [])[1]
|
|
7056
|
+
const off = sp.match(/<a:off\s+x="(-?\d+)"\s+y="(-?\d+)"/)
|
|
7057
|
+
const ext = sp.match(/<a:ext\s+cx="(\d+)"\s+cy="(\d+)"/)
|
|
7058
|
+
if (off && ext) {
|
|
7059
|
+
const box = { left: Math.round(parseInt(off[1]) / 9525), top: Math.round(parseInt(off[2]) / 9525), width: Math.round(parseInt(ext[1]) / 9525), height: Math.round(parseInt(ext[2]) / 9525) }
|
|
7060
|
+
if (idxAttr !== undefined && !masterPhXfrm['idx_' + idxAttr]) masterPhXfrm['idx_' + idxAttr] = box
|
|
7061
|
+
if (typeAttr && !masterPhXfrm['type_' + typeAttr]) masterPhXfrm['type_' + typeAttr] = box
|
|
7054
7062
|
}
|
|
7055
7063
|
}
|
|
7056
|
-
const picMatches = mTxt.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7057
|
-
for (const pic of picMatches) {
|
|
7058
|
-
const name = pic.match(/<p:cNvPr[^>]*name="([^"]+)"/)?.[1] || pic.match(/name="([^"]+)"/)?.[1] || ''
|
|
7059
|
-
masterShapesInfo.push({ name, isPic: true })
|
|
7060
|
-
}
|
|
7061
7064
|
}
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
const
|
|
7065
|
-
|
|
7065
|
+
const picMatches = mTxt.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7066
|
+
for (const pic of picMatches) {
|
|
7067
|
+
const name = pic.match(/<p:cNvPr[^>]*name="([^"]+)"/)?.[1] || pic.match(/name="([^"]+)"/)?.[1] || ''
|
|
7068
|
+
masterShapesInfo.push({ name, isPic: true })
|
|
7066
7069
|
}
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7070
|
+
}
|
|
7071
|
+
if (masterFiles.length > 0) {
|
|
7072
|
+
const mTxt = await zip.file(masterFiles[0])?.async('text') || ''
|
|
7073
|
+
const mNameM = mTxt.match(/<p:cSld[^>]*name="([^"]+)"/)
|
|
7074
|
+
if (mNameM && mNameM[1]) masterName = mNameM[1]
|
|
7075
|
+
}
|
|
7076
|
+
if (!masterName || masterName === nameWithoutExt) {
|
|
7077
|
+
const themeFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/theme/theme') && !k.includes('_rels') && !zip.files[k].dir)
|
|
7078
|
+
if (themeFiles.length > 0) {
|
|
7079
|
+
const tTxt = await zip.file(themeFiles[0])?.async('text') || ''
|
|
7080
|
+
const tNameM = tTxt.match(/<a:theme[^>]*name="([^"]+)"/)
|
|
7081
|
+
if (tNameM && tNameM[1] && tNameM[1] !== 'Office 主题' && tNameM[1] !== 'Office Theme') {
|
|
7082
|
+
masterName = tNameM[1]
|
|
7075
7083
|
}
|
|
7076
7084
|
}
|
|
7077
|
-
}
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7085
|
+
}
|
|
7086
|
+
} catch (e) { }
|
|
7087
|
+
|
|
7088
|
+
|
|
7089
|
+
// 从母版 primaryMasterXml 解析标准文本色(txStyles: titleStyle/bodyStyle),支持 lumMod/lumOff 调制
|
|
7090
|
+
let masterTextColor = ''
|
|
7091
|
+
try {
|
|
7092
|
+
if (primaryMasterXml) {
|
|
7093
|
+
const titleMatch = primaryMasterXml.match(/<p:titleStyle>[\s\S]*?<a:schemeClr val="([^"]+)">([\s\S]*?)<\/a:schemeClr>/)
|
|
7094
|
+
const bodyMatch = primaryMasterXml.match(/<p:bodyStyle>[\s\S]*?<a:schemeClr val="([^"]+)">([\s\S]*?)<\/a:schemeClr>/)
|
|
7095
|
+
const m = bodyMatch || titleMatch
|
|
7096
|
+
if (m) {
|
|
7097
|
+
const val = m[1]
|
|
7098
|
+
const lumModM = m[2].match(/<a:lumMod val="(\d+)"/)
|
|
7099
|
+
const lumOffM = m[2].match(/<a:lumOff val="(\d+)"/)
|
|
7100
|
+
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
7101
|
+
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
7102
|
+
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
7103
|
+
const baseHex = themeColors[slot] || themeColors[val] || (slot === 'dk1' || slot === 'tx1' ? '#000000' : '#FFFFFF')
|
|
7104
|
+
masterTextColor = (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
7105
|
+
}
|
|
7106
|
+
}
|
|
7107
|
+
} catch (mErr) { }
|
|
7108
|
+
|
|
7109
|
+
// 提取并扫描全局 schemeClr 表达片段与计算颜色映射(仅针对幻灯片、母版、版式与主题核心XML)
|
|
7110
|
+
const schemeFillMap: Record<string, string> = {}
|
|
7111
|
+
try {
|
|
7112
|
+
for (const [name, file] of Object.entries(zip.files)) {
|
|
7113
|
+
if (name.endsWith('.xml') && !name.includes('_rels') && (name.startsWith('ppt/slides/') || name.startsWith('ppt/slideMasters/') || name.startsWith('ppt/slideLayouts/') || name.startsWith('ppt/theme/'))) {
|
|
7114
|
+
const txt = await file.async('text')
|
|
7115
|
+
const regex = /<a:schemeClr\b[^>]*\/>|<a:schemeClr\b[^>]*>(?:(?!<\/a:schemeClr>)[\s\S])*<\/a:schemeClr>/g
|
|
7116
|
+
const matches = txt.match(regex) || []
|
|
7117
|
+
for (const m of matches) {
|
|
7118
|
+
const clean = m.replace(/\s+/g, ' ').trim()
|
|
7119
|
+
const valM = clean.match(/val="([^"]+)"/)
|
|
7120
|
+
if (!valM) continue
|
|
7121
|
+
const val = valM[1]
|
|
7122
|
+
if (val === 'phClr') continue
|
|
7123
|
+
const lumModM = clean.match(/<a:lumMod val="(\d+)"/)
|
|
7124
|
+
const lumOffM = clean.match(/<a:lumOff val="(\d+)"/)
|
|
7091
7125
|
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
7092
7126
|
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
7093
7127
|
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
7094
|
-
const baseHex = themeColors[slot] || themeColors[val] ||
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
if (!valM) continue
|
|
7112
|
-
const val = valM[1]
|
|
7113
|
-
if (val === 'phClr') continue
|
|
7114
|
-
const lumModM = clean.match(/<a:lumMod val="(\d+)"/)
|
|
7115
|
-
const lumOffM = clean.match(/<a:lumOff val="(\d+)"/)
|
|
7116
|
-
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
7117
|
-
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
7118
|
-
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
7119
|
-
const baseHex = themeColors[slot] || themeColors[val] || ''
|
|
7120
|
-
if (baseHex) {
|
|
7121
|
-
const calcHex = (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
7122
|
-
const cleanHex = calcHex.replace('#', '').toUpperCase()
|
|
7123
|
-
schemeFillMap[cleanHex] = clean
|
|
7124
|
-
|
|
7125
|
-
// 处理 RGB 整数四舍五入 ±1 的容差 (如 8496B0 vs 8497B0)
|
|
7126
|
-
const r = parseInt(cleanHex.slice(0, 2), 16)
|
|
7127
|
-
const g = parseInt(cleanHex.slice(2, 4), 16)
|
|
7128
|
-
const b = parseInt(cleanHex.slice(4, 6), 16)
|
|
7129
|
-
const toHex = (num: number) => num.toString(16).padStart(2, '0').toUpperCase()
|
|
7130
|
-
for (let dg = -1; dg <= 1; dg++) {
|
|
7131
|
-
for (let db = -1; db <= 1; db++) {
|
|
7132
|
-
if (dg === 0 && db === 0) continue
|
|
7133
|
-
const nearHex = `${toHex(r)}${toHex(Math.min(255, Math.max(0, g + dg)))}${toHex(Math.min(255, Math.max(0, b + db)))}`
|
|
7134
|
-
if (!schemeFillMap[nearHex]) {
|
|
7135
|
-
schemeFillMap[nearHex] = clean
|
|
7136
|
-
}
|
|
7128
|
+
const baseHex = themeColors[slot] || themeColors[val] || ''
|
|
7129
|
+
if (baseHex) {
|
|
7130
|
+
const calcHex = (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
7131
|
+
const cleanHex = calcHex.replace('#', '').toUpperCase()
|
|
7132
|
+
schemeFillMap[cleanHex] = clean
|
|
7133
|
+
|
|
7134
|
+
// 处理 RGB 整数四舍五入 ±1 的容差 (如 8496B0 vs 8497B0)
|
|
7135
|
+
const r = parseInt(cleanHex.slice(0, 2), 16)
|
|
7136
|
+
const g = parseInt(cleanHex.slice(2, 4), 16)
|
|
7137
|
+
const b = parseInt(cleanHex.slice(4, 6), 16)
|
|
7138
|
+
const toHex = (num: number) => num.toString(16).padStart(2, '0').toUpperCase()
|
|
7139
|
+
for (let dg = -1; dg <= 1; dg++) {
|
|
7140
|
+
for (let db = -1; db <= 1; db++) {
|
|
7141
|
+
if (dg === 0 && db === 0) continue
|
|
7142
|
+
const nearHex = `${toHex(r)}${toHex(Math.min(255, Math.max(0, g + dg)))}${toHex(Math.min(255, Math.max(0, b + db)))}`
|
|
7143
|
+
if (!schemeFillMap[nearHex]) {
|
|
7144
|
+
schemeFillMap[nearHex] = clean
|
|
7137
7145
|
}
|
|
7138
7146
|
}
|
|
7139
7147
|
}
|
|
7140
7148
|
}
|
|
7141
7149
|
}
|
|
7142
7150
|
}
|
|
7143
|
-
}
|
|
7144
|
-
|
|
7145
|
-
// 提取版式占位符几何字典与每页占位符图片的几何形状映射
|
|
7146
|
-
const slidePicGeomMap: Array<Record<string, { geom?: string; polygonPoints?: string }>> = []
|
|
7147
|
-
const slideSpLstStyleAlignMap: Array<Record<string, string>> = []
|
|
7148
|
-
try {
|
|
7149
|
-
const layoutGeomMap = new Map<string, Map<string, { geom?: string; polygonPoints?: string; type?: string }>>()
|
|
7150
|
-
for (const layout of allLayouts) {
|
|
7151
|
-
const phMap = new Map<string, { geom?: string; polygonPoints?: string; type?: string }>()
|
|
7152
|
-
const spMatches = (layout.xml || '').match(/<p:(?:sp|pic)\b[\s\S]*?<\/p:(?:sp|pic)>/g) || []
|
|
7153
|
-
for (const sp of spMatches) {
|
|
7154
|
-
const phMatch = sp.match(/<p:ph\b[^>]*\/?>/)
|
|
7155
|
-
if (!phMatch) continue
|
|
7156
|
-
const phAttrs = phMatch[0]
|
|
7157
|
-
const idxM = phAttrs.match(/idx="(\d+)"/)
|
|
7158
|
-
const typeM = phAttrs.match(/type="(\w+)"/)
|
|
7159
|
-
const prstM = sp.match(/<a:prstGeom\b[^>]*prst="([^"]+)"/)
|
|
7160
|
-
const custM = sp.match(/<a:custGeom\b[\s\S]*?<\/a:custGeom>/)
|
|
7161
|
-
let geom = prstM ? prstM[1] : undefined
|
|
7162
|
-
let polygonPoints: string | undefined = undefined
|
|
7163
|
-
if (custM) {
|
|
7164
|
-
const poly = parseCustGeomToPolygon(custM[0])
|
|
7165
|
-
if (poly) {
|
|
7166
|
-
polygonPoints = poly
|
|
7167
|
-
geom = 'custom'
|
|
7168
|
-
}
|
|
7169
|
-
}
|
|
7170
|
-
const geomInfo = { geom, polygonPoints, type: typeM ? typeM[1] : undefined }
|
|
7171
|
-
if (idxM) phMap.set('idx_' + idxM[1], geomInfo)
|
|
7172
|
-
if (typeM) phMap.set('type_' + typeM[1], geomInfo)
|
|
7173
|
-
}
|
|
7174
|
-
layoutGeomMap.set(layout.file, phMap)
|
|
7175
|
-
const shortName = layout.file.replace('ppt/slideLayouts/', '')
|
|
7176
|
-
layoutGeomMap.set(shortName, phMap)
|
|
7177
|
-
layoutGeomMap.set(`../slideLayouts/${shortName}`, phMap)
|
|
7178
|
-
}
|
|
7151
|
+
}
|
|
7152
|
+
} catch (e) { }
|
|
7179
7153
|
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
const
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
spLstStyleAlignById[id] = standardAlign
|
|
7204
|
-
}
|
|
7205
|
-
}
|
|
7206
|
-
}
|
|
7207
|
-
slideSpLstStyleAlignMap.push(spLstStyleAlignById)
|
|
7208
|
-
|
|
7209
|
-
const picMatches = sXml.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7210
|
-
for (const pic of picMatches) {
|
|
7211
|
-
const idM = pic.match(/<p:cNvPr\b[^>]*id="(\d+)"/)
|
|
7212
|
-
if (!idM) continue
|
|
7213
|
-
const id = idM[1]
|
|
7214
|
-
const phMatch = pic.match(/<p:ph\b[^>]*\/?>/)
|
|
7215
|
-
if (phMatch && phMap) {
|
|
7216
|
-
const phAttrs = phMatch[0]
|
|
7217
|
-
const idxM = phAttrs.match(/idx="(\d+)"/)
|
|
7218
|
-
const typeM = phAttrs.match(/type="(\w+)"/)
|
|
7219
|
-
let geomInfo: { geom?: string; polygonPoints?: string } | undefined
|
|
7220
|
-
if (idxM && phMap.has('idx_' + idxM[1])) {
|
|
7221
|
-
geomInfo = phMap.get('idx_' + idxM[1])
|
|
7222
|
-
} else if (typeM && phMap.has('type_' + typeM[1])) {
|
|
7223
|
-
geomInfo = phMap.get('type_' + typeM[1])
|
|
7224
|
-
}
|
|
7225
|
-
if (geomInfo) {
|
|
7226
|
-
picGeomById[id] = geomInfo
|
|
7227
|
-
}
|
|
7154
|
+
// 提取版式占位符几何字典与每页占位符图片的几何形状映射
|
|
7155
|
+
const slidePicGeomMap: Array<Record<string, { geom?: string; polygonPoints?: string }>> = []
|
|
7156
|
+
const slideSpLstStyleAlignMap: Array<Record<string, string>> = []
|
|
7157
|
+
try {
|
|
7158
|
+
const layoutGeomMap = new Map<string, Map<string, { geom?: string; polygonPoints?: string; type?: string }>>()
|
|
7159
|
+
for (const layout of allLayouts) {
|
|
7160
|
+
const phMap = new Map<string, { geom?: string; polygonPoints?: string; type?: string }>()
|
|
7161
|
+
const spMatches = (layout.xml || '').match(/<p:(?:sp|pic)\b[\s\S]*?<\/p:(?:sp|pic)>/g) || []
|
|
7162
|
+
for (const sp of spMatches) {
|
|
7163
|
+
const phMatch = sp.match(/<p:ph\b[^>]*\/?>/)
|
|
7164
|
+
if (!phMatch) continue
|
|
7165
|
+
const phAttrs = phMatch[0]
|
|
7166
|
+
const idxM = phAttrs.match(/idx="(\d+)"/)
|
|
7167
|
+
const typeM = phAttrs.match(/type="(\w+)"/)
|
|
7168
|
+
const prstM = sp.match(/<a:prstGeom\b[^>]*prst="([^"]+)"/)
|
|
7169
|
+
const custM = sp.match(/<a:custGeom\b[\s\S]*?<\/a:custGeom>/)
|
|
7170
|
+
let geom = prstM ? prstM[1] : undefined
|
|
7171
|
+
let polygonPoints: string | undefined = undefined
|
|
7172
|
+
if (custM) {
|
|
7173
|
+
const poly = parseCustGeomToPolygon(custM[0])
|
|
7174
|
+
if (poly) {
|
|
7175
|
+
polygonPoints = poly
|
|
7176
|
+
geom = 'custom'
|
|
7228
7177
|
}
|
|
7229
7178
|
}
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
console.warn('提取占位符几何失败:', geomErr)
|
|
7234
|
-
}
|
|
7235
|
-
|
|
7236
|
-
// 捕获完整原生母版与版式文件及底层依赖媒体,供导出时无损注入原生 PPT 母版架构体系
|
|
7237
|
-
const masterFilesMap: Record<string, string> = {}
|
|
7238
|
-
for (const k of Object.keys(zip.files)) {
|
|
7239
|
-
if (k.startsWith('ppt/slideMasters/') && (k.endsWith('.xml') || k.endsWith('.rels')) && !zip.files[k].dir) {
|
|
7240
|
-
masterFilesMap[k] = await zip.file(k)?.async('text') || ''
|
|
7179
|
+
const geomInfo = { geom, polygonPoints, type: typeM ? typeM[1] : undefined }
|
|
7180
|
+
if (idxM) phMap.set('idx_' + idxM[1], geomInfo)
|
|
7181
|
+
if (typeM) phMap.set('type_' + typeM[1], geomInfo)
|
|
7241
7182
|
}
|
|
7183
|
+
layoutGeomMap.set(layout.file, phMap)
|
|
7184
|
+
const shortName = layout.file.replace('ppt/slideLayouts/', '')
|
|
7185
|
+
layoutGeomMap.set(shortName, phMap)
|
|
7186
|
+
layoutGeomMap.set(`../slideLayouts/${shortName}`, phMap)
|
|
7242
7187
|
}
|
|
7243
7188
|
|
|
7244
|
-
const
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7189
|
+
const slideFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slides/slide') && !k.includes('_rels') && !zip.files[k].dir)
|
|
7190
|
+
slideFiles.sort(sortSlideXml)
|
|
7191
|
+
|
|
7192
|
+
for (let i = 0; i < slideFiles.length; i++) {
|
|
7193
|
+
const sXml = await zip.file(slideFiles[i])?.async('text') || ''
|
|
7194
|
+
const lObj = slideLayoutMap[i]
|
|
7195
|
+
const phMap = lObj ? (layoutGeomMap.get(lObj.file) || layoutGeomMap.get(lObj.id)) : null
|
|
7196
|
+
const picGeomById: Record<string, { geom?: string; polygonPoints?: string }> = {}
|
|
7197
|
+
|
|
7198
|
+
// 提取各文本框在 a:lstStyle 中定义的继承对齐方式(当段落自身无显式 a:pPr algn 时兜底生效)
|
|
7199
|
+
const spLstStyleAlignById: Record<string, string> = {}
|
|
7200
|
+
const spMatches = sXml.match(/<p:sp\b[\s\S]*?<\/p:sp>/g) || []
|
|
7201
|
+
for (const sp of spMatches) {
|
|
7202
|
+
const idM = sp.match(/<p:cNvPr\b[^>]*id="(\d+)"/)
|
|
7203
|
+
if (!idM) continue
|
|
7204
|
+
const id = idM[1]
|
|
7205
|
+
const hasDirectPAlign = /<a:pPr\b[^>]*\balgn="/.test(sp)
|
|
7206
|
+
const lstStyleM = sp.match(/<a:lstStyle\b[\s\S]*?<\/a:lstStyle>/)
|
|
7207
|
+
if (!hasDirectPAlign && lstStyleM) {
|
|
7208
|
+
const algnM = lstStyleM[0].match(/algn="(ctr|center|r|right|l|left|just|dist)"/)
|
|
7209
|
+
if (algnM) {
|
|
7210
|
+
const a = algnM[1]
|
|
7211
|
+
const standardAlign = (a === 'ctr' || a === 'center') ? 'center' : (a === 'r' || a === 'right') ? 'right' : (a === 'dist') ? 'distribute' : (a === 'just') ? 'justify' : 'left'
|
|
7212
|
+
spLstStyleAlignById[id] = standardAlign
|
|
7213
|
+
}
|
|
7214
|
+
}
|
|
7248
7215
|
}
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
const
|
|
7260
|
-
const
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7216
|
+
slideSpLstStyleAlignMap.push(spLstStyleAlignById)
|
|
7217
|
+
|
|
7218
|
+
const picMatches = sXml.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7219
|
+
for (const pic of picMatches) {
|
|
7220
|
+
const idM = pic.match(/<p:cNvPr\b[^>]*id="(\d+)"/)
|
|
7221
|
+
if (!idM) continue
|
|
7222
|
+
const id = idM[1]
|
|
7223
|
+
const phMatch = pic.match(/<p:ph\b[^>]*\/?>/)
|
|
7224
|
+
if (phMatch && phMap) {
|
|
7225
|
+
const phAttrs = phMatch[0]
|
|
7226
|
+
const idxM = phAttrs.match(/idx="(\d+)"/)
|
|
7227
|
+
const typeM = phAttrs.match(/type="(\w+)"/)
|
|
7228
|
+
let geomInfo: { geom?: string; polygonPoints?: string } | undefined
|
|
7229
|
+
if (idxM && phMap.has('idx_' + idxM[1])) {
|
|
7230
|
+
geomInfo = phMap.get('idx_' + idxM[1])
|
|
7231
|
+
} else if (typeM && phMap.has('type_' + typeM[1])) {
|
|
7232
|
+
geomInfo = phMap.get('type_' + typeM[1])
|
|
7264
7233
|
}
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
const b64 = await zip.file(mediaPath)?.async('base64')
|
|
7268
|
-
if (b64) masterMediaMap[mediaPath] = b64
|
|
7234
|
+
if (geomInfo) {
|
|
7235
|
+
picGeomById[id] = geomInfo
|
|
7269
7236
|
}
|
|
7270
7237
|
}
|
|
7271
7238
|
}
|
|
7239
|
+
slidePicGeomMap.push(picGeomById)
|
|
7272
7240
|
}
|
|
7241
|
+
} catch (geomErr) {
|
|
7242
|
+
console.warn('提取占位符几何失败:', geomErr)
|
|
7243
|
+
}
|
|
7273
7244
|
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7245
|
+
// 捕获完整原生母版与版式文件及底层依赖媒体,供导出时无损注入原生 PPT 母版架构体系
|
|
7246
|
+
const masterFilesMap: Record<string, string> = {}
|
|
7247
|
+
for (const k of Object.keys(zip.files)) {
|
|
7248
|
+
if (k.startsWith('ppt/slideMasters/') && (k.endsWith('.xml') || k.endsWith('.rels')) && !zip.files[k].dir) {
|
|
7249
|
+
masterFilesMap[k] = await zip.file(k)?.async('text') || ''
|
|
7279
7250
|
}
|
|
7251
|
+
}
|
|
7280
7252
|
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
masterShapesInfo,
|
|
7286
|
-
themeColors,
|
|
7287
|
-
schemeFillMap,
|
|
7288
|
-
masterPhXfrm,
|
|
7289
|
-
primaryMasterXml,
|
|
7290
|
-
masterPicSrcMap,
|
|
7291
|
-
masterXmlBg,
|
|
7292
|
-
slidePicGeomMap,
|
|
7293
|
-
slideSpLstStyleAlignMap,
|
|
7294
|
-
masterTextColor,
|
|
7295
|
-
masterFilesMap,
|
|
7296
|
-
layoutFilesMap,
|
|
7297
|
-
masterMediaMap,
|
|
7298
|
-
tagFilesMap,
|
|
7299
|
-
slideLayoutTargetList
|
|
7253
|
+
const layoutFilesMap: Record<string, string> = {}
|
|
7254
|
+
for (const k of Object.keys(zip.files)) {
|
|
7255
|
+
if (k.startsWith('ppt/slideLayouts/') && (k.endsWith('.xml') || k.endsWith('.rels')) && !zip.files[k].dir) {
|
|
7256
|
+
layoutFilesMap[k] = await zip.file(k)?.async('text') || ''
|
|
7300
7257
|
}
|
|
7301
|
-
} catch (e) {
|
|
7302
|
-
console.warn('Failed to extract zipMeta:', e)
|
|
7303
7258
|
}
|
|
7304
7259
|
|
|
7305
|
-
const
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7260
|
+
const masterMediaMap: Record<string, string> = {}
|
|
7261
|
+
const relsFiles = [...Object.keys(masterFilesMap), ...Object.keys(layoutFilesMap)].filter(k => k.includes('_rels/'))
|
|
7262
|
+
for (const rf of relsFiles) {
|
|
7263
|
+
const rTxt = masterFilesMap[rf] || layoutFilesMap[rf] || ''
|
|
7264
|
+
const targetMatches = rTxt.match(/Target="([^"]+)"/g) || []
|
|
7265
|
+
for (const tm of targetMatches) {
|
|
7266
|
+
const rawTarget = tm.replace(/^Target="/, '').replace(/"$/, '')
|
|
7267
|
+
if (rawTarget.includes('media/') || rawTarget.includes('../media/')) {
|
|
7268
|
+
const sourcePart = rf.replace('/_rels/', '/').replace(/\.rels$/, '')
|
|
7269
|
+
const stack = sourcePart.split('/').slice(0, -1)
|
|
7270
|
+
for (const part of rawTarget.split('/')) {
|
|
7271
|
+
if (part === '..') stack.pop()
|
|
7272
|
+
else if (part && part !== '.') stack.push(part)
|
|
7273
|
+
}
|
|
7274
|
+
const mediaPath = stack.join('/')
|
|
7275
|
+
if (!masterMediaMap[mediaPath] && zip.files[mediaPath]) {
|
|
7276
|
+
const b64 = await zip.file(mediaPath)?.async('base64')
|
|
7277
|
+
if (b64) masterMediaMap[mediaPath] = b64
|
|
7278
|
+
}
|
|
7316
7279
|
}
|
|
7317
7280
|
}
|
|
7318
7281
|
}
|
|
7319
7282
|
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
await
|
|
7283
|
+
const tagFilesMap: Record<string, string> = {}
|
|
7284
|
+
for (const k of Object.keys(zip.files)) {
|
|
7285
|
+
if (k.startsWith('ppt/tags/') && k.endsWith('.xml') && !zip.files[k].dir) {
|
|
7286
|
+
tagFilesMap[k] = await zip.file(k)?.async('text') || ''
|
|
7324
7287
|
}
|
|
7325
|
-
presentationTitle.value = nameWithoutExt
|
|
7326
|
-
importSlides(parsedData)
|
|
7327
|
-
return true
|
|
7328
|
-
} else {
|
|
7329
|
-
notify('导入失败:文件中未发现有效幻灯片页')
|
|
7330
|
-
return false
|
|
7331
7288
|
}
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7289
|
+
|
|
7290
|
+
zipMeta = {
|
|
7291
|
+
allLayouts,
|
|
7292
|
+
slideLayoutMap,
|
|
7293
|
+
masterName: masterName || nameWithoutExt,
|
|
7294
|
+
masterShapesInfo,
|
|
7295
|
+
themeColors,
|
|
7296
|
+
schemeFillMap,
|
|
7297
|
+
masterPhXfrm,
|
|
7298
|
+
primaryMasterXml,
|
|
7299
|
+
masterPicSrcMap,
|
|
7300
|
+
masterXmlBg,
|
|
7301
|
+
slidePicGeomMap,
|
|
7302
|
+
slideSpLstStyleAlignMap,
|
|
7303
|
+
masterTextColor,
|
|
7304
|
+
masterFilesMap,
|
|
7305
|
+
layoutFilesMap,
|
|
7306
|
+
masterMediaMap,
|
|
7307
|
+
tagFilesMap,
|
|
7308
|
+
slideLayoutTargetList
|
|
7309
|
+
}
|
|
7310
|
+
} catch (e) {
|
|
7311
|
+
console.warn('Failed to extract zipMeta:', e)
|
|
7338
7312
|
}
|
|
7339
|
-
}
|
|
7340
7313
|
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7314
|
+
const parsedData = parsePPTXJSONData(json, zipMeta)
|
|
7315
|
+
let animsPerSlide: any[] = []
|
|
7316
|
+
try {
|
|
7317
|
+
animsPerSlide = await extractAnimationsFromPptxArrayBuffer(loadedZip || buffer, parsedData?.slides || [])
|
|
7318
|
+
} catch (animErr) {
|
|
7319
|
+
console.warn('提取 PPTX 动画失败:', animErr)
|
|
7320
|
+
}
|
|
7321
|
+
for (let i = 0; i < (parsedData?.slides?.length || 0); i++) {
|
|
7322
|
+
if (!parsedData.slides[i].animations || !parsedData.slides[i].animations.length) {
|
|
7323
|
+
if (animsPerSlide && animsPerSlide[i] && animsPerSlide[i].length) {
|
|
7324
|
+
parsedData.slides[i].animations = animsPerSlide[i]
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7345
7328
|
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
const buffer = event.target?.result as ArrayBuffer
|
|
7351
|
-
await importPptxArrayBuffer(buffer, file.name)
|
|
7352
|
-
} finally {
|
|
7353
|
-
target.value = ''
|
|
7329
|
+
if (parsedData.slides.length > 0) {
|
|
7330
|
+
if (typeof getCustomUploadHandler === 'function' && getCustomUploadHandler()) {
|
|
7331
|
+
loadingText.value = '正在上传媒体资源并导入...'
|
|
7332
|
+
await uploadMediaResourcesInSlides(parsedData)
|
|
7354
7333
|
}
|
|
7334
|
+
presentationTitle.value = nameWithoutExt
|
|
7335
|
+
importSlides(parsedData)
|
|
7336
|
+
return true
|
|
7337
|
+
} else {
|
|
7338
|
+
notify('导入失败:文件中未发现有效幻灯片页')
|
|
7339
|
+
return false
|
|
7355
7340
|
}
|
|
7356
|
-
|
|
7341
|
+
} catch (err: any) {
|
|
7342
|
+
console.error(err)
|
|
7343
|
+
notify('导入 PPTX 失败: ' + err.message)
|
|
7344
|
+
return false
|
|
7345
|
+
} finally {
|
|
7346
|
+
isExporting.value = false
|
|
7357
7347
|
}
|
|
7348
|
+
}
|
|
7358
7349
|
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
visible?: boolean
|
|
7364
|
-
}>(), {
|
|
7365
|
-
showMenu: true,
|
|
7366
|
-
toolbarMode: 'auto',
|
|
7367
|
-
visible: true
|
|
7368
|
-
})
|
|
7350
|
+
const onFileChange = async (e: Event) => {
|
|
7351
|
+
const target = e.target as HTMLInputElement
|
|
7352
|
+
const files = target.files
|
|
7353
|
+
if (!files || files.length === 0) return
|
|
7369
7354
|
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
presentationTitle: presentationTitle.value,
|
|
7379
|
-
isEditingTitle: isEditingTitle.value,
|
|
7380
|
-
startEditingTitle,
|
|
7381
|
-
stopEditingTitle,
|
|
7382
|
-
zoomRatio: zoomRatio.value,
|
|
7383
|
-
setZoomRatio: (val: number) => { zoomRatio.value = val },
|
|
7384
|
-
resetZoom,
|
|
7385
|
-
isMasterEditingView: isMasterEditingView.value,
|
|
7386
|
-
enterMasterView,
|
|
7387
|
-
exitMasterView,
|
|
7388
|
-
startPresentation,
|
|
7389
|
-
startFromBeginning,
|
|
7390
|
-
triggerPPTXImport,
|
|
7391
|
-
triggerJSONImport
|
|
7392
|
-
}
|
|
7393
|
-
for (const k of Object.keys(sp)) {
|
|
7394
|
-
if (k === 'deck' || k in merged) continue
|
|
7395
|
-
merged[k] = sp[k]
|
|
7396
|
-
}
|
|
7397
|
-
if ('deck' in sp) {
|
|
7398
|
-
Object.defineProperty(merged, 'deck', {
|
|
7399
|
-
get: () => sp.deck,
|
|
7400
|
-
enumerable: true,
|
|
7401
|
-
configurable: true
|
|
7402
|
-
})
|
|
7355
|
+
const file = files[0]
|
|
7356
|
+
const reader = new FileReader()
|
|
7357
|
+
reader.onload = async (event) => {
|
|
7358
|
+
try {
|
|
7359
|
+
const buffer = event.target?.result as ArrayBuffer
|
|
7360
|
+
await importPptxArrayBuffer(buffer, file.name)
|
|
7361
|
+
} finally {
|
|
7362
|
+
target.value = ''
|
|
7403
7363
|
}
|
|
7404
|
-
|
|
7405
|
-
|
|
7364
|
+
}
|
|
7365
|
+
reader.readAsArrayBuffer(file)
|
|
7366
|
+
}
|
|
7406
7367
|
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7368
|
+
const props = withDefaults(defineProps<{
|
|
7369
|
+
slotProps?: any
|
|
7370
|
+
showMenu?: boolean
|
|
7371
|
+
toolbarMode?: 'auto' | 'more' | 'full'
|
|
7372
|
+
visible?: boolean
|
|
7373
|
+
}>(), {
|
|
7374
|
+
showMenu: true,
|
|
7375
|
+
toolbarMode: 'auto',
|
|
7376
|
+
visible: true
|
|
7377
|
+
})
|
|
7378
|
+
|
|
7379
|
+
// 不能整体展开 slotProps:其 deck 是惰性 getter,展开会立刻执行 getDeck() 整册深拷贝
|
|
7380
|
+
// (110 页实测 ~500ms/次),且展开发生在本 computed 内会把本 computed 订阅到全册,
|
|
7381
|
+
// 拖拽/打字每步都失效重算。改为逐键拷贝(跳过 deck),deck 仍以 getter 形式透传,
|
|
7382
|
+
// 只在消费者真正读取时才现算
|
|
7383
|
+
const headerContext = computed(() => {
|
|
7384
|
+
const sp: any = props.slotProps || {}
|
|
7385
|
+
const merged: any = {
|
|
7386
|
+
showMenu: props.showMenu,
|
|
7387
|
+
presentationTitle: presentationTitle.value,
|
|
7388
|
+
isEditingTitle: isEditingTitle.value,
|
|
7389
|
+
startEditingTitle,
|
|
7390
|
+
stopEditingTitle,
|
|
7391
|
+
zoomRatio: zoomRatio.value,
|
|
7392
|
+
setZoomRatio: (val: number) => { zoomRatio.value = val },
|
|
7393
|
+
resetZoom,
|
|
7394
|
+
isMasterEditingView: isMasterEditingView.value,
|
|
7395
|
+
enterMasterView,
|
|
7396
|
+
exitMasterView,
|
|
7397
|
+
startPresentation,
|
|
7398
|
+
startFromBeginning,
|
|
7399
|
+
triggerPPTXImport,
|
|
7400
|
+
triggerJSONImport
|
|
7401
|
+
}
|
|
7402
|
+
for (const k of Object.keys(sp)) {
|
|
7403
|
+
if (k === 'deck' || k in merged) continue
|
|
7404
|
+
merged[k] = sp[k]
|
|
7405
|
+
}
|
|
7406
|
+
if ('deck' in sp) {
|
|
7407
|
+
Object.defineProperty(merged, 'deck', {
|
|
7408
|
+
get: () => sp.deck,
|
|
7409
|
+
enumerable: true,
|
|
7410
|
+
configurable: true
|
|
7411
|
+
})
|
|
7412
|
+
}
|
|
7413
|
+
return merged
|
|
7414
|
+
})
|
|
7415
|
+
|
|
7416
|
+
function handleToolbarWheel(e: WheelEvent) {
|
|
7417
|
+
const el = e.currentTarget as HTMLElement
|
|
7418
|
+
if (el && el.scrollWidth > el.clientWidth) {
|
|
7419
|
+
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
7420
|
+
el.scrollLeft += e.deltaY
|
|
7413
7421
|
}
|
|
7414
7422
|
}
|
|
7423
|
+
}
|
|
7415
7424
|
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7425
|
+
defineExpose({
|
|
7426
|
+
generatePptxBlob,
|
|
7427
|
+
generateImagesZipBlob,
|
|
7428
|
+
downloadAsPptx,
|
|
7429
|
+
downloadAllSlidesAsZip,
|
|
7430
|
+
downloadCurrentSlide,
|
|
7431
|
+
triggerPPTXImport,
|
|
7432
|
+
triggerJSONImport,
|
|
7433
|
+
importPptxArrayBuffer
|
|
7434
|
+
})
|
|
7426
7435
|
</script>
|
|
7427
7436
|
|
|
7428
7437
|
<template>
|
|
@@ -7438,167 +7447,178 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7438
7447
|
<!-- 外部左侧主体插槽 (默认呈现内置菜单和标题) -->
|
|
7439
7448
|
<slot name="left" v-bind="headerContext">
|
|
7440
7449
|
<el-popover v-if="showMenu" ref="menuPopoverRef" placement="bottom-start" :width="320" trigger="click"
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
<
|
|
7446
|
-
|
|
7447
|
-
</button>
|
|
7448
|
-
</template>
|
|
7449
|
-
|
|
7450
|
-
<div class="main-menu-popover">
|
|
7451
|
-
<!-- AIPPT Banner -->
|
|
7452
|
-
<div class="aippt-banner" @click="handleMenuAction(handleAIPPT)" v-if="false">
|
|
7453
|
-
<div class="aippt-icon-container">
|
|
7454
|
-
<svg class="aippt-arrow-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7455
|
-
<path d="M4.5 3L4 16.5L8.5 12.5L13.5 20L16.5 18L11.5 10.5L17 10L4.5 3Z" fill="#ffb800" stroke="#fff"
|
|
7456
|
-
stroke-width="1.5" stroke-linejoin="round" />
|
|
7457
|
-
<path d="M19 4L19.5 2.5L21 2L19.5 1.5L19 0L18.5 1.5L17 2L18.5 2.5L19 4Z" fill="#ffb800" />
|
|
7458
|
-
<path d="M21 14L21.5 12.5L23 12L21.5 11.5L21 10L20.5 11.5L19 12L20.5 12.5L21 14Z" fill="#ff9900" />
|
|
7459
|
-
<path d="M3 21L3.5 19.5L5 19L3.5 18.5L3 17L2.5 18.5L1 19L2.5 19.5L3 21Z" fill="#ffcc00" />
|
|
7450
|
+
popper-style="padding: 0; border-radius: 12px; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); border: 1px solid #e2e8f0; background: #fff; overflow: hidden;">
|
|
7451
|
+
<template #reference>
|
|
7452
|
+
<button class="btn-icon" style="padding: 4px; color: var(--text-primary); flex-shrink: 0;"
|
|
7453
|
+
data-tooltip="菜单">
|
|
7454
|
+
<svg class="icon" style="width: 18px; height: 18px" viewBox="0 0 24 24">
|
|
7455
|
+
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
|
|
7460
7456
|
</svg>
|
|
7461
|
-
</
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7457
|
+
</button>
|
|
7458
|
+
</template>
|
|
7459
|
+
|
|
7460
|
+
<div class="main-menu-popover">
|
|
7461
|
+
<!-- AIPPT Banner -->
|
|
7462
|
+
<div class="aippt-banner" @click="handleMenuAction(handleAIPPT)" v-if="false">
|
|
7463
|
+
<div class="aippt-icon-container">
|
|
7464
|
+
<svg class="aippt-arrow-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7465
|
+
<path d="M4.5 3L4 16.5L8.5 12.5L13.5 20L16.5 18L11.5 10.5L17 10L4.5 3Z" fill="#ffb800" stroke="#fff"
|
|
7466
|
+
stroke-width="1.5" stroke-linejoin="round" />
|
|
7467
|
+
<path d="M19 4L19.5 2.5L21 2L19.5 1.5L19 0L18.5 1.5L17 2L18.5 2.5L19 4Z" fill="#ffb800" />
|
|
7468
|
+
<path d="M21 14L21.5 12.5L23 12L21.5 11.5L21 10L20.5 11.5L19 12L20.5 12.5L21 14Z" fill="#ff9900" />
|
|
7469
|
+
<path d="M3 21L3.5 19.5L5 19L3.5 18.5L3 17L2.5 18.5L1 19L2.5 19.5L3 21Z" fill="#ffcc00" />
|
|
7470
|
+
</svg>
|
|
7471
|
+
</div>
|
|
7472
|
+
<div class="aippt-text-container">
|
|
7473
|
+
<div class="aippt-title">
|
|
7474
|
+
<span style="color: #3b82f6">AIPP</span><span style="color: #d946ef">T</span>
|
|
7475
|
+
</div>
|
|
7476
|
+
<div class="aippt-desc">输入一句话,智能生成演示文稿</div>
|
|
7465
7477
|
</div>
|
|
7466
|
-
<div class="aippt-desc">输入一句话,智能生成演示文稿</div>
|
|
7467
7478
|
</div>
|
|
7468
|
-
</div>
|
|
7469
7479
|
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7480
|
+
<div class="menu-body">
|
|
7481
|
+
<!-- Import File Section -->
|
|
7482
|
+
<div class="menu-section-title">导入文件</div>
|
|
7483
|
+
<div class="import-grid">
|
|
7484
|
+
<!-- PPTX Import -->
|
|
7485
|
+
<div class="import-card" @click="handleMenuAction(triggerPPTXImport)">
|
|
7486
|
+
<div class="import-icon-box pptx-icon">
|
|
7487
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7488
|
+
<path
|
|
7489
|
+
d="M4 4C4 2.89543 4.89543 2 6 2H14L20 8V20C20 21.1046 19.1046 22 18 22H6C4.89543 22 4 21.1046 4 20V4Z"
|
|
7490
|
+
fill="#ea4335" />
|
|
7491
|
+
<path d="M14 2V8H20L14 2Z" fill="#c5221f" />
|
|
7492
|
+
<text x="7" y="17" fill="#fff" font-size="9" font-weight="900" font-family="Arial">P</text>
|
|
7493
|
+
</svg>
|
|
7494
|
+
</div>
|
|
7495
|
+
<div class="import-label">PPTX</div>
|
|
7496
|
+
<div class="import-subtext">(仅供测试)</div>
|
|
7484
7497
|
</div>
|
|
7485
|
-
<div class="import-label">PPTX</div>
|
|
7486
|
-
<div class="import-subtext">(仅供测试)</div>
|
|
7487
|
-
</div>
|
|
7488
7498
|
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
+
<!-- JSON Import -->
|
|
7500
|
+
<div class="import-card" @click="handleMenuAction(triggerJSONImport)">
|
|
7501
|
+
<div class="import-icon-box json-icon">
|
|
7502
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7503
|
+
<path
|
|
7504
|
+
d="M4 4C4 2.89543 4.89543 2 6 2H14L20 8V20C20 21.1046 19.1046 22 18 22H6C4.89543 22 4 21.1046 4 20V4Z"
|
|
7505
|
+
fill="#f2994a" />
|
|
7506
|
+
<path d="M14 2V8H20L14 2Z" fill="#e07c24" />
|
|
7507
|
+
<text x="7" y="17" fill="#fff" font-size="9" font-weight="900" font-family="Arial">J</text>
|
|
7508
|
+
</svg>
|
|
7509
|
+
</div>
|
|
7510
|
+
<div class="import-label">JSON</div>
|
|
7511
|
+
<div class="import-subtext">(仅供测试)</div>
|
|
7512
|
+
</div>
|
|
7513
|
+
|
|
7514
|
+
<!-- JVS-PPT Import -->
|
|
7515
|
+
<div class="import-card" @click="handleMenuAction(handleJVSPPTImport)">
|
|
7516
|
+
<div class="import-icon-box pptist-icon">
|
|
7517
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7518
|
+
<path
|
|
7519
|
+
d="M4 4C4 2.89543 4.89543 2 6 2H14L20 8V20C20 21.1046 19.1046 22 18 22H6C4.89543 22 4 21.1046 4 20V4Z"
|
|
7520
|
+
fill="#eb5757" />
|
|
7521
|
+
<path d="M14 2V8H20L14 2Z" fill="#c03930" />
|
|
7522
|
+
<rect x="7" y="12" width="10" height="2" rx="1" fill="#fff" />
|
|
7523
|
+
<rect x="7" y="16" width="10" height="2" rx="1" fill="#fff" />
|
|
7524
|
+
</svg>
|
|
7525
|
+
</div>
|
|
7526
|
+
<div class="import-label">JVS-PPT</div>
|
|
7527
|
+
<div class="import-subtext">(专属格式)</div>
|
|
7499
7528
|
</div>
|
|
7500
|
-
<div class="import-label">JSON</div>
|
|
7501
|
-
<div class="import-subtext">(仅供测试)</div>
|
|
7502
7529
|
</div>
|
|
7503
7530
|
|
|
7504
|
-
<!--
|
|
7505
|
-
<div class="
|
|
7506
|
-
|
|
7507
|
-
|
|
7531
|
+
<!-- Divider -->
|
|
7532
|
+
<div class="menu-divider"></div>
|
|
7533
|
+
|
|
7534
|
+
<!-- Link Items List -->
|
|
7535
|
+
<div class="menu-links">
|
|
7536
|
+
<!-- Export -->
|
|
7537
|
+
<el-dropdown trigger="click" @command="handleExportCommand" :disabled="isExporting"
|
|
7538
|
+
style="width: 100%; display: block; text-align: left;">
|
|
7539
|
+
<div class="menu-link-item">
|
|
7540
|
+
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7541
|
+
<path
|
|
7542
|
+
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"
|
|
7543
|
+
fill="currentColor" />
|
|
7544
|
+
</svg>
|
|
7545
|
+
<span class="link-text">{{ isExporting ? '正在导出...' : '导出文件' }}</span>
|
|
7546
|
+
</div>
|
|
7547
|
+
<template #dropdown>
|
|
7548
|
+
<el-dropdown-menu>
|
|
7549
|
+
<el-dropdown-item command="png">下载当前页 (PNG)</el-dropdown-item>
|
|
7550
|
+
<el-dropdown-item command="jpeg">下载当前页 (JPEG)</el-dropdown-item>
|
|
7551
|
+
<el-dropdown-item command="zip">下载全部 (ZIP)</el-dropdown-item>
|
|
7552
|
+
<el-dropdown-item command="pptx">下载为 PPTX</el-dropdown-item>
|
|
7553
|
+
<el-dropdown-item command="json" divided>导出完整 JSON 配置</el-dropdown-item>
|
|
7554
|
+
<el-dropdown-item command="json-template">导出纯版式 JSON (排除图片/清空文本)</el-dropdown-item>
|
|
7555
|
+
</el-dropdown-menu>
|
|
7556
|
+
</template>
|
|
7557
|
+
</el-dropdown>
|
|
7558
|
+
|
|
7559
|
+
<!-- Reset -->
|
|
7560
|
+
<div v-if="!isReadonly" class="menu-link-item" @click="handleMenuAction(handleResetSlides)">
|
|
7561
|
+
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7508
7562
|
<path
|
|
7509
|
-
d="
|
|
7510
|
-
fill="
|
|
7511
|
-
<path d="M14 2V8H20L14 2Z" fill="#c03930" />
|
|
7512
|
-
<rect x="7" y="12" width="10" height="2" rx="1" fill="#fff" />
|
|
7513
|
-
<rect x="7" y="16" width="10" height="2" rx="1" fill="#fff" />
|
|
7563
|
+
d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"
|
|
7564
|
+
fill="currentColor" />
|
|
7514
7565
|
</svg>
|
|
7566
|
+
<span class="link-text">重置幻灯片</span>
|
|
7515
7567
|
</div>
|
|
7516
|
-
<div class="import-label">JVS-PPT</div>
|
|
7517
|
-
<div class="import-subtext">(专属格式)</div>
|
|
7518
|
-
</div>
|
|
7519
|
-
</div>
|
|
7520
7568
|
|
|
7521
|
-
|
|
7522
|
-
|
|
7569
|
+
<!-- Label -->
|
|
7570
|
+
<div class="menu-link-item" @click="handleMenuAction(handleLabelSlides)">
|
|
7571
|
+
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7572
|
+
<path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6h-5.6z" fill="currentColor" />
|
|
7573
|
+
</svg>
|
|
7574
|
+
<span class="link-text">幻灯片类型标注</span>
|
|
7575
|
+
</div>
|
|
7523
7576
|
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
<!-- Export -->
|
|
7527
|
-
<el-dropdown trigger="click" @command="handleExportCommand" :disabled="isExporting"
|
|
7528
|
-
style="width: 100%; display: block; text-align: left;">
|
|
7529
|
-
<div class="menu-link-item">
|
|
7577
|
+
<!-- Shortcuts -->
|
|
7578
|
+
<div class="menu-link-item" @click="handleMenuAction(handleShortcuts)">
|
|
7530
7579
|
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7531
7580
|
<path
|
|
7532
|
-
d="
|
|
7581
|
+
d="M4 15h2v3H4v-3zm0-4h2v3H4v-3zm0-4h2v3H4V7zm4 8h8v3H8v-3zm0-4h8v3H8v-3zm0-4h8v3H8V7zm12 8h2v3h-2v-3zm0-4h2v3h-2v-3zm0-4h2v3h-2V7z"
|
|
7533
7582
|
fill="currentColor" />
|
|
7534
7583
|
</svg>
|
|
7535
|
-
<span class="link-text"
|
|
7584
|
+
<span class="link-text">快捷操作</span>
|
|
7536
7585
|
</div>
|
|
7537
|
-
<template #dropdown>
|
|
7538
|
-
<el-dropdown-menu>
|
|
7539
|
-
<el-dropdown-item command="png">下载当前页 (PNG)</el-dropdown-item>
|
|
7540
|
-
<el-dropdown-item command="jpeg">下载当前页 (JPEG)</el-dropdown-item>
|
|
7541
|
-
<el-dropdown-item command="zip">下载全部 (ZIP)</el-dropdown-item>
|
|
7542
|
-
<el-dropdown-item command="pptx">下载为 PPTX</el-dropdown-item>
|
|
7543
|
-
<el-dropdown-item command="json" divided>导出完整 JSON 配置</el-dropdown-item>
|
|
7544
|
-
<el-dropdown-item command="json-template">导出纯版式 JSON (排除图片/清空文本)</el-dropdown-item>
|
|
7545
|
-
</el-dropdown-menu>
|
|
7546
|
-
</template>
|
|
7547
|
-
</el-dropdown>
|
|
7548
|
-
|
|
7549
|
-
<!-- Reset -->
|
|
7550
|
-
<div class="menu-link-item" @click="handleMenuAction(handleResetSlides)">
|
|
7551
|
-
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7552
|
-
<path
|
|
7553
|
-
d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"
|
|
7554
|
-
fill="currentColor" />
|
|
7555
|
-
</svg>
|
|
7556
|
-
<span class="link-text">重置幻灯片</span>
|
|
7557
|
-
</div>
|
|
7558
7586
|
|
|
7559
|
-
<!-- Label -->
|
|
7560
|
-
<div class="menu-link-item" @click="handleMenuAction(handleLabelSlides)">
|
|
7561
|
-
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7562
|
-
<path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6h-5.6z" fill="currentColor" />
|
|
7563
|
-
</svg>
|
|
7564
|
-
<span class="link-text">幻灯片类型标注</span>
|
|
7565
7587
|
</div>
|
|
7566
7588
|
|
|
7567
|
-
<!--
|
|
7568
|
-
<div class="menu-
|
|
7569
|
-
|
|
7570
|
-
<path
|
|
7571
|
-
d="M4 15h2v3H4v-3zm0-4h2v3H4v-3zm0-4h2v3H4V7zm4 8h8v3H8v-3zm0-4h8v3H8v-3zm0-4h8v3H8V7zm12 8h2v3h-2v-3zm0-4h2v3h-2v-3zm0-4h2v3h-2V7z"
|
|
7572
|
-
fill="currentColor" />
|
|
7573
|
-
</svg>
|
|
7574
|
-
<span class="link-text">快捷操作</span>
|
|
7589
|
+
<!-- Note Disclaimer -->
|
|
7590
|
+
<div class="menu-disclaimer">
|
|
7591
|
+
注:本站仅作测试/演示,不提供任何形式的服务
|
|
7575
7592
|
</div>
|
|
7576
|
-
|
|
7577
|
-
</div>
|
|
7578
|
-
|
|
7579
|
-
<!-- Note Disclaimer -->
|
|
7580
|
-
<div class="menu-disclaimer">
|
|
7581
|
-
注:本站仅作测试/演示,不提供任何形式的服务
|
|
7582
7593
|
</div>
|
|
7583
7594
|
</div>
|
|
7595
|
+
</el-popover>
|
|
7596
|
+
<div class="presentation-title-container"
|
|
7597
|
+
style="display: flex; align-items: center; gap: 6px; margin-left: 8px; flex-shrink: 0; min-width: 0; max-width: 280px;">
|
|
7598
|
+
<span v-if="!isEditingTitle || isReadonly" @click="startEditingTitle" class="presentation-title-text" :style="{
|
|
7599
|
+
fontSize: '15px',
|
|
7600
|
+
fontWeight: 600,
|
|
7601
|
+
color: '#0f172a',
|
|
7602
|
+
cursor: isReadonly ? 'default' : 'pointer',
|
|
7603
|
+
transition: 'color 0.15s ease',
|
|
7604
|
+
whiteSpace: 'nowrap',
|
|
7605
|
+
overflow: 'hidden',
|
|
7606
|
+
textOverflow: 'ellipsis',
|
|
7607
|
+
display: 'inline-block',
|
|
7608
|
+
maxWidth: '240px'
|
|
7609
|
+
}" :title="presentationTitle || '未命名演示文稿'">
|
|
7610
|
+
{{ presentationTitle || '未命名演示文稿' }}
|
|
7611
|
+
</span>
|
|
7612
|
+
<input v-else ref="titleInputRef" class="presentation-title-input-edit" v-model="presentationTitle"
|
|
7613
|
+
@blur="stopEditingTitle" @keyup.enter="stopEditingTitle"
|
|
7614
|
+
style="font-size: 15px; font-weight: 600; color: #0f172a; border: 1px solid #3b82f6; border-radius: 6px; padding: 4px 8px; outline: none; background: #fff; box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15); width: 200px;" />
|
|
7615
|
+
<svg v-if="!isEditingTitle && !isReadonly" @click="startEditingTitle" viewBox="0 0 24 24" fill="none"
|
|
7616
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
7617
|
+
style="width: 14px; height: 14px; color: #3b82f6; cursor: pointer; margin-left: 2px; flex-shrink: 0;">
|
|
7618
|
+
<path d="M12 20h9" />
|
|
7619
|
+
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
|
7620
|
+
</svg>
|
|
7584
7621
|
</div>
|
|
7585
|
-
</el-popover>
|
|
7586
|
-
<div class="presentation-title-container" style="display: flex; align-items: center; gap: 6px; margin-left: 8px; flex-shrink: 0; min-width: 0; max-width: 280px;">
|
|
7587
|
-
<span v-if="!isEditingTitle" @click="startEditingTitle" class="presentation-title-text"
|
|
7588
|
-
style="font-size: 15px; font-weight: 600; color: #0f172a; cursor: pointer; transition: color 0.15s ease; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline-block; max-width: 240px;"
|
|
7589
|
-
:title="presentationTitle || '未命名演示文稿'">
|
|
7590
|
-
{{ presentationTitle || '未命名演示文稿' }}
|
|
7591
|
-
</span>
|
|
7592
|
-
<input v-else ref="titleInputRef" class="presentation-title-input-edit" v-model="presentationTitle"
|
|
7593
|
-
@blur="stopEditingTitle" @keyup.enter="stopEditingTitle"
|
|
7594
|
-
style="font-size: 15px; font-weight: 600; color: #0f172a; border: 1px solid #3b82f6; border-radius: 6px; padding: 4px 8px; outline: none; background: #fff; box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15); width: 200px;" />
|
|
7595
|
-
<svg v-if="!isEditingTitle" @click="startEditingTitle" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
7596
|
-
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
7597
|
-
style="width: 14px; height: 14px; color: #3b82f6; cursor: pointer; margin-left: 2px; flex-shrink: 0;">
|
|
7598
|
-
<path d="M12 20h9" />
|
|
7599
|
-
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
|
7600
|
-
</svg>
|
|
7601
|
-
</div>
|
|
7602
7622
|
</slot>
|
|
7603
7623
|
|
|
7604
7624
|
<!-- 外部左侧后置插槽 (例如文档状态标签、副标题) -->
|
|
@@ -7606,11 +7626,13 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7606
7626
|
</div>
|
|
7607
7627
|
|
|
7608
7628
|
<!-- Center Section: Integrated Top Toolbar -->
|
|
7609
|
-
<div class="header-center-tools" @wheel.passive="handleToolbarWheel"
|
|
7629
|
+
<div v-if="!isReadonly" class="header-center-tools" @wheel.passive="handleToolbarWheel"
|
|
7630
|
+
style="flex: 1; display: flex; align-items: center; justify-content: flex-start; overflow-x: auto; overflow-y: hidden; min-width: 0; position: relative; z-index: 10; margin: 0 8px; scrollbar-width: none; -ms-overflow-style: none;">
|
|
7610
7631
|
<!-- 外部中间工具栏插槽 (默认呈现内置 Toolbar,支持前置/后置插槽扩展或整栏替换) -->
|
|
7611
7632
|
<slot name="center" v-bind="headerContext">
|
|
7612
7633
|
<slot name="toolbar" v-bind="headerContext">
|
|
7613
|
-
<div :style="{ margin: isMasterEditingView ? '0' : '0 auto' }"
|
|
7634
|
+
<div :style="{ margin: isMasterEditingView ? '0' : '0 auto' }"
|
|
7635
|
+
style="display: flex; align-items: center; flex-shrink: 0;">
|
|
7614
7636
|
<Toolbar :toolbar-mode="toolbarMode">
|
|
7615
7637
|
<template v-if="$slots['toolbar-prefix']" #prefix>
|
|
7616
7638
|
<slot name="toolbar-prefix" v-bind="headerContext"></slot>
|
|
@@ -7668,7 +7690,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7668
7690
|
</button>
|
|
7669
7691
|
|
|
7670
7692
|
<!-- Slide Master Mode Toggle Button -->
|
|
7671
|
-
<button v-if="!isMasterEditingView" class="btn-master-toggle" @click="enterMasterView()"
|
|
7693
|
+
<button v-if="!isMasterEditingView && !isReadonly" class="btn-master-toggle" @click="enterMasterView()"
|
|
7672
7694
|
style="height: 32px; padding: 0 12px; border: none; border-radius: 6px; background: #f1f5f9; color: #334155; font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: 6px; cursor: pointer; white-space: nowrap; flex-shrink: 0;">
|
|
7673
7695
|
<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor" style="flex-shrink: 0;">
|
|
7674
7696
|
<use xlink:href="#icon-muban"></use>
|
|
@@ -7696,7 +7718,8 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7696
7718
|
<div class="presentation-menu-item-content">
|
|
7697
7719
|
<div class="presentation-menu-item-left">
|
|
7698
7720
|
<svg viewBox="0 0 1024 1024" width="14" height="14" fill="currentColor">
|
|
7699
|
-
<path
|
|
7721
|
+
<path
|
|
7722
|
+
d="M213.333 170.667h85.334v682.666h-85.334V170.667zm170.667 0l426.667 341.333-426.667 341.333V170.667z" />
|
|
7700
7723
|
</svg>
|
|
7701
7724
|
<span>从头开始</span>
|
|
7702
7725
|
</div>
|