PPT.vue 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923
  1. <!--
  2. * @Author: LiZhiWei
  3. * @Date: 2025-12-26 14:41:26
  4. * @LastEditors: LiZhiWei
  5. * @LastEditTime: 2025-12-26 17:49:03
  6. * @Description:
  7. -->
  8. <template>
  9. <div ref="rootRef" class="pptx-preview">
  10. <div v-if="!slides.length" class="pptx-empty">暂无可渲染的幻灯片</div>
  11. <div v-else class="pptx-slides">
  12. <div
  13. v-for="(slide, slideIndex) in slides"
  14. :key="slideIndex"
  15. class="pptx-slide-shell"
  16. :style="getSlideShellStyle()"
  17. >
  18. <div
  19. class="pptx-slide"
  20. :style="getSlideStyle(slide)"
  21. >
  22. <div
  23. v-for="(el, elementIndex) in slide.elements"
  24. :key="getElementKey(el, elementIndex)"
  25. class="pptx-element"
  26. :style="getElementStyle(el, elementIndex)"
  27. >
  28. <template v-if="isImageElement(el)">
  29. <div class="pptx-img-wrap" :style="getImageWrapStyle(el)">
  30. <img
  31. class="pptx-img"
  32. :src="getMediaSrc(el)"
  33. :style="getImageStyle(el)"
  34. alt=""
  35. draggable="false"
  36. />
  37. </div>
  38. </template>
  39. <template v-else-if="isVideoElement(el)">
  40. <video class="pptx-media" :src="getMediaSrc(el)" controls preload="metadata" />
  41. </template>
  42. <template v-else-if="isAudioElement(el)">
  43. <audio class="pptx-media" :src="getMediaSrc(el)" controls preload="metadata" />
  44. </template>
  45. <template v-else-if="isMathElement(el)">
  46. <img class="pptx-img" :src="getMediaSrc(el)" alt="" draggable="false" />
  47. </template>
  48. <template v-else-if="isChartElement(el)">
  49. <svg
  50. class="pptx-chart"
  51. :viewBox="`0 0 ${getNumber(el.width)} ${getNumber(el.height)}`"
  52. preserveAspectRatio="none"
  53. >
  54. <template v-if="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).kind === 'bar'">
  55. <rect
  56. v-for="(b, i) in getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).bars"
  57. :key="i"
  58. :x="b.x"
  59. :y="b.y"
  60. :width="b.w"
  61. :height="b.h"
  62. :fill="b.fill"
  63. :fill-opacity="b.opacity"
  64. />
  65. </template>
  66. <template v-else-if="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).kind === 'line'">
  67. <path
  68. v-for="(p, i) in getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).paths"
  69. :key="i"
  70. :d="p.d"
  71. fill="none"
  72. :stroke="p.stroke"
  73. :stroke-width="p.strokeWidth"
  74. stroke-linejoin="round"
  75. stroke-linecap="round"
  76. />
  77. <circle
  78. v-for="(pt, i) in getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).points"
  79. :key="i"
  80. :cx="pt.cx"
  81. :cy="pt.cy"
  82. :r="pt.r"
  83. :fill="pt.fill"
  84. :fill-opacity="pt.opacity"
  85. />
  86. </template>
  87. <template v-else-if="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).kind === 'pie'">
  88. <path
  89. v-for="(s, i) in getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).slices"
  90. :key="i"
  91. :d="s.d"
  92. :fill="s.fill"
  93. :fill-opacity="s.opacity"
  94. />
  95. <circle
  96. v-if="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).holeR"
  97. :cx="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).cx"
  98. :cy="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).cy"
  99. :r="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).holeR"
  100. fill="#fff"
  101. />
  102. </template>
  103. <template v-else-if="getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).kind === 'scatter'">
  104. <circle
  105. v-for="(pt, i) in getChartModel(el, slideIndex, getElementCacheId(el, elementIndex)).points"
  106. :key="i"
  107. :cx="pt.cx"
  108. :cy="pt.cy"
  109. :r="pt.r"
  110. :fill="pt.fill"
  111. :fill-opacity="pt.opacity"
  112. />
  113. </template>
  114. </svg>
  115. </template>
  116. <template v-else-if="isGroupElement(el) || isDiagramElement(el)">
  117. <PptxGroup
  118. :el="el"
  119. :slide-index="slideIndex"
  120. :cache-id="getElementCacheId(el, elementIndex)"
  121. :abs-left="getNumber(el.left)"
  122. :abs-top="getNumber(el.top)"
  123. />
  124. </template>
  125. <template v-else-if="isTableElement(el)">
  126. <div class="pptx-table-wrap">
  127. <table class="pptx-table">
  128. <colgroup v-if="getTableModel(el, slideIndex, getElementCacheId(el, elementIndex)).colWidths.length">
  129. <col
  130. v-for="(w, i) in getTableModel(el, slideIndex, getElementCacheId(el, elementIndex)).colWidths"
  131. :key="i"
  132. :style="{ width: w ? `${w}px` : undefined }"
  133. />
  134. </colgroup>
  135. <tbody>
  136. <tr
  137. v-for="(row, ri) in getTableModel(el, slideIndex, getElementCacheId(el, elementIndex)).rows"
  138. :key="ri"
  139. :style="getTableRowStyle(el, slideIndex, getElementCacheId(el, elementIndex), ri)"
  140. >
  141. <template v-for="(cell, ci) in row" :key="`${ri}-${ci}`">
  142. <td
  143. v-if="!cell.skip"
  144. :colspan="cell.colspan"
  145. :rowspan="cell.rowspan"
  146. :style="cell.style"
  147. >
  148. <div class="pptx-table-html" v-html="sanitizeHtml(cell.html)" />
  149. </td>
  150. </template>
  151. </tr>
  152. </tbody>
  153. </table>
  154. </div>
  155. </template>
  156. <template v-else-if="isShapeElement(el)">
  157. <svg
  158. class="pptx-shape-svg"
  159. :viewBox="`0 0 ${getNumber(el.width)} ${getNumber(el.height)}`"
  160. preserveAspectRatio="none"
  161. >
  162. <defs v-if="hasShapeDefs(el, slideIndex, getElementCacheId(el, elementIndex))">
  163. <linearGradient
  164. v-if="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))?.kind === 'linear'"
  165. :id="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.id"
  166. :x1="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.x1"
  167. :y1="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.y1"
  168. :x2="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.x2"
  169. :y2="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.y2"
  170. >
  171. <stop
  172. v-for="(s, i) in getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.stops"
  173. :key="i"
  174. :offset="s.offset"
  175. :stop-color="s.color"
  176. :stop-opacity="s.opacity"
  177. />
  178. </linearGradient>
  179. <radialGradient
  180. v-else-if="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))?.kind === 'radial'"
  181. :id="getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.id"
  182. cx="50%"
  183. cy="50%"
  184. r="50%"
  185. >
  186. <stop
  187. v-for="(s, i) in getShapeGradient(el, slideIndex, getElementCacheId(el, elementIndex))!.stops"
  188. :key="i"
  189. :offset="s.offset"
  190. :stop-color="s.color"
  191. :stop-opacity="s.opacity"
  192. />
  193. </radialGradient>
  194. <pattern
  195. v-if="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))"
  196. :id="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.id"
  197. patternUnits="userSpaceOnUse"
  198. :width="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.width"
  199. :height="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.height"
  200. >
  201. <rect
  202. x="0"
  203. y="0"
  204. :width="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.width"
  205. :height="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.height"
  206. :fill="getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.background"
  207. />
  208. <template
  209. v-for="(shape, i) in getShapePattern(el, slideIndex, getElementCacheId(el, elementIndex))!.shapes"
  210. :key="i"
  211. >
  212. <circle
  213. v-if="shape.kind === 'circle'"
  214. :cx="shape.cx"
  215. :cy="shape.cy"
  216. :r="shape.r"
  217. :fill="shape.fill"
  218. />
  219. <polygon
  220. v-else-if="shape.kind === 'polygon'"
  221. :points="shape.points"
  222. :fill="shape.fill"
  223. />
  224. <rect
  225. v-else-if="shape.kind === 'rect'"
  226. :x="shape.x"
  227. :y="shape.y"
  228. :width="shape.width"
  229. :height="shape.height"
  230. :fill="shape.fill"
  231. :transform="shape.transform"
  232. />
  233. </template>
  234. </pattern>
  235. <pattern
  236. v-if="getShapeImagePaint(el, slideIndex, getElementCacheId(el, elementIndex))"
  237. :id="getShapeImagePaint(el, slideIndex, getElementCacheId(el, elementIndex))!.id"
  238. patternUnits="objectBoundingBox"
  239. width="1"
  240. height="1"
  241. >
  242. <image
  243. x="0"
  244. y="0"
  245. width="1"
  246. height="1"
  247. preserveAspectRatio="none"
  248. :href="getShapeImagePaint(el, slideIndex, getElementCacheId(el, elementIndex))!.href"
  249. />
  250. </pattern>
  251. </defs>
  252. <path
  253. :d="getShapePath(el)"
  254. :fill="getShapeFill(el, slideIndex, getElementCacheId(el, elementIndex))"
  255. :stroke="getShapeStroke(el)"
  256. :stroke-width="getShapeStrokeWidth(el)"
  257. :stroke-dasharray="getShapeDasharray(el)"
  258. />
  259. </svg>
  260. <div
  261. v-if="typeof el.content === 'string' && el.content.trim()"
  262. class="pptx-html"
  263. :style="getHtmlBoxStyle(el)"
  264. >
  265. <div
  266. class="pptx-html-inner"
  267. :style="getHtmlInnerStyle(el)"
  268. v-html="sanitizeHtml(el.content)"
  269. />
  270. </div>
  271. </template>
  272. <template v-else>
  273. <div
  274. v-if="typeof el.content === 'string' && el.content.trim()"
  275. class="pptx-html"
  276. :style="getHtmlBoxStyle(el)"
  277. >
  278. <div
  279. class="pptx-html-inner"
  280. :style="getHtmlInnerStyle(el)"
  281. v-html="sanitizeHtml(el.content)"
  282. />
  283. </div>
  284. </template>
  285. </div>
  286. </div>
  287. </div>
  288. </div>
  289. </div>
  290. </template>
  291. <script setup lang="ts">
  292. import { computed, defineComponent, h, onBeforeUnmount, onMounted, ref } from "vue"
  293. import type { CSSProperties } from "vue"
  294. type PptxSize = {
  295. width: number
  296. height: number
  297. }
  298. type PptxFill = {
  299. type?: string
  300. value?: any
  301. }
  302. type PptxElement = {
  303. type?: string
  304. left?: number
  305. top?: number
  306. width?: number
  307. height?: number
  308. rotate?: number
  309. order?: number
  310. opacity?: number
  311. isFlipH?: boolean
  312. isFlipV?: boolean
  313. vAlign?: "up" | "mid" | "down" | string
  314. content?: string
  315. fill?: PptxFill | string
  316. path?: string
  317. borderColor?: string
  318. borderWidth?: number
  319. borderStrokeDasharray?: string
  320. __source?: "layout" | "slide"
  321. [key: string]: any
  322. }
  323. type PptxSlide = {
  324. fill?: PptxFill
  325. elements?: PptxElement[]
  326. layoutElements?: PptxElement[]
  327. [key: string]: any
  328. }
  329. type PptxJson = {
  330. size?: PptxSize
  331. slides?: PptxSlide[]
  332. [key: string]: any
  333. }
  334. const props = defineProps<{ pptxJson: PptxJson }>()
  335. const rootRef = ref<HTMLDivElement | null>(null)
  336. const containerWidth = ref(0)
  337. let resizeObserver: ResizeObserver | null = null
  338. const size = computed<PptxSize>(() => {
  339. const w = (props.pptxJson?.size?.width ?? 960) as number
  340. const h = (props.pptxJson?.size?.height ?? 540) as number
  341. return {
  342. width: Number.isFinite(w) ? w : 960,
  343. height: Number.isFinite(h) ? h : 540,
  344. }
  345. })
  346. const scale = computed(() => {
  347. const w = containerWidth.value
  348. if (!w) return 1
  349. const raw = w / size.value.width
  350. return Math.min(1, Math.max(0.1, raw))
  351. })
  352. const slides = computed(() => {
  353. const rawSlides = Array.isArray(props.pptxJson?.slides) ? props.pptxJson.slides : []
  354. return rawSlides.map((slide) => {
  355. const layoutElements = (Array.isArray(slide.layoutElements) ? slide.layoutElements : []).map((el) => ({
  356. ...(el as any),
  357. __source: "layout" as const,
  358. }))
  359. const slideElements = (Array.isArray(slide.elements) ? slide.elements : []).map((el) => ({
  360. ...(el as any),
  361. __source: "slide" as const,
  362. }))
  363. const merged = [...layoutElements, ...slideElements]
  364. const elements = merged
  365. .filter((el) => el && typeof el === "object")
  366. .slice()
  367. .sort((a, b) => (getNumber(a.order) || 0) - (getNumber(b.order) || 0))
  368. return {
  369. ...slide,
  370. elements,
  371. }
  372. })
  373. })
  374. onMounted(() => {
  375. if (!rootRef.value) return
  376. console.log('props.pptxJson', props.pptxJson)
  377. const updateWidth = () => {
  378. if (!rootRef.value) return
  379. containerWidth.value = rootRef.value.clientWidth
  380. }
  381. updateWidth()
  382. resizeObserver = new ResizeObserver(() => updateWidth())
  383. resizeObserver.observe(rootRef.value)
  384. })
  385. onBeforeUnmount(() => {
  386. resizeObserver?.disconnect()
  387. resizeObserver = null
  388. })
  389. function getNumber(value: unknown) {
  390. const n = Number(value)
  391. return Number.isFinite(n) ? n : 0
  392. }
  393. function sanitizeHtml(html: string) {
  394. if (!html) return ""
  395. try {
  396. const parser = new DOMParser()
  397. const doc = parser.parseFromString(html, "text/html")
  398. const forbidden = ["script", "style", "iframe", "object", "embed", "link", "meta"]
  399. forbidden.forEach((tag) => {
  400. doc.querySelectorAll(tag).forEach((node) => node.remove())
  401. })
  402. doc.querySelectorAll("*").forEach((node) => {
  403. Array.from(node.attributes).forEach((attr) => {
  404. const name = attr.name.toLowerCase()
  405. const value = String(attr.value || "").trim().toLowerCase()
  406. if (name.startsWith("on")) node.removeAttribute(attr.name)
  407. if ((name === "href" || name === "src") && value.startsWith("javascript:")) {
  408. node.removeAttribute(attr.name)
  409. }
  410. })
  411. const style = node.getAttribute("style")
  412. if (style) {
  413. const replaced = style.replace(/font-size\s*:\s*([\d.]+)pt/gi, (_m, v) => {
  414. const n = Number(v)
  415. if (!Number.isFinite(n)) return _m
  416. const px = (n * 96) / 72
  417. return `font-size: ${px.toFixed(3).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1")}px`
  418. })
  419. if (replaced !== style) node.setAttribute("style", replaced)
  420. }
  421. })
  422. return doc.body.innerHTML
  423. } catch {
  424. return html
  425. }
  426. }
  427. function getElementKey(el: PptxElement, elementIndex: number) {
  428. const stable = getNumber(el.order) || elementIndex
  429. return `${el.type || "el"}-${stable}`
  430. }
  431. function getElementCacheId(el: PptxElement, elementIndex: number, parentCacheId?: string) {
  432. const stable = getNumber(el.order) || elementIndex
  433. const t = String(el.type || "el")
  434. const self = `${t}-${stable}`
  435. return parentCacheId ? `${parentCacheId}/${self}` : self
  436. }
  437. function getSlideShellStyle() {
  438. return {
  439. width: `${Math.round(size.value.width * scale.value)}px`,
  440. height: `${Math.round(size.value.height * scale.value)}px`,
  441. } as CSSProperties
  442. }
  443. function getSlideStyle(slide: PptxSlide) {
  444. const background = getSlideBackground(slide.fill)
  445. return {
  446. width: `${size.value.width}px`,
  447. height: `${size.value.height}px`,
  448. transform: `scale(${scale.value})`,
  449. background,
  450. } as CSSProperties
  451. }
  452. function getSlideBackground(fill?: PptxFill) {
  453. if (!fill || !fill.type) return "#fff"
  454. if (fill.type === "color") return String(fill.value || "#fff")
  455. if (fill.type === "gradient") {
  456. const colors = Array.isArray(fill.value?.colors) ? fill.value.colors : []
  457. const rot = getNumber(fill.value?.rot)
  458. const path = String(fill.value?.path || "rect")
  459. const stops = colors
  460. .map((c: any) => {
  461. const pos = String(c?.pos ?? "")
  462. const color = String(c?.color ?? "")
  463. if (!color) return ""
  464. return pos ? `${color} ${pos}` : color
  465. })
  466. .filter(Boolean)
  467. .join(", ")
  468. if (!stops) return "#fff"
  469. if (path === "rect") {
  470. const angle = (90 - rot + 360) % 360
  471. return `linear-gradient(${angle}deg, ${stops})`
  472. }
  473. return `radial-gradient(circle, ${stops})`
  474. }
  475. if (fill.type === "image") {
  476. const src = String(fill.value?.picBase64 || fill.value?.src || "")
  477. if (!src) return "#fff"
  478. return `center / cover no-repeat url(${src})`
  479. }
  480. return "#fff"
  481. }
  482. function getElementStyle(el: PptxElement, elementIndex: number) {
  483. const left = getNumber(el.left)
  484. const top = getNumber(el.top)
  485. const width = Math.max(0, getNumber(el.width))
  486. const height = Math.max(0, getNumber(el.height))
  487. const rotate = getNumber(el.rotate)
  488. const flipH = !!el.isFlipH
  489. const flipV = !!el.isFlipV
  490. const zIndex = getElementZIndex(el, elementIndex)
  491. const opacity = el.opacity == null ? 1 : Math.max(0, Math.min(1, getNumber(el.opacity)))
  492. const transforms: string[] = []
  493. if (rotate) transforms.push(`rotate(${rotate}deg)`)
  494. if (flipH) transforms.push(`scaleX(-1)`)
  495. if (flipV) transforms.push(`scaleY(-1)`)
  496. const background = getElementBackground(el)
  497. const border = getElementBorderCss(el)
  498. const radius = getElementBorderRadius(el)
  499. const shadowFilter = getElementShadowFilter(el)
  500. return {
  501. position: "absolute" as const,
  502. left: `${left}px`,
  503. top: `${top}px`,
  504. width: `${width}px`,
  505. height: `${height}px`,
  506. zIndex,
  507. opacity,
  508. background,
  509. border,
  510. borderRadius: radius,
  511. filter: shadowFilter,
  512. transformOrigin: "center center",
  513. transform: transforms.length ? transforms.join(" ") : undefined,
  514. } as CSSProperties
  515. }
  516. function getElementZIndex(el: PptxElement, elementIndex: number) {
  517. const base = getNumber(el.order) || elementIndex
  518. const source = (el as any)?.__source
  519. const offset = source === "slide" ? 100000 : 0
  520. return base + offset
  521. }
  522. function getElementShadowFilter(el: PptxElement) {
  523. const shadow = (el as any)?.shadow
  524. if (!shadow || typeof shadow !== "object") return undefined
  525. const h = getNumber((shadow as any).h)
  526. const v = getNumber((shadow as any).v)
  527. const blur = Math.max(0, getNumber((shadow as any).blur))
  528. const color = String((shadow as any).color || "").trim()
  529. if (!color || (!h && !v && !blur)) return undefined
  530. return `drop-shadow(${h}px ${v}px ${blur}px ${color})`
  531. }
  532. function getElementBorderRadius(el: PptxElement) {
  533. const candidates = [el.radius, el.cornerRadius, el.rx]
  534. const r = candidates.map(getNumber).find((n) => n > 0) || 0
  535. return r ? `${r}px` : undefined
  536. }
  537. function getElementBorderCss(el: PptxElement) {
  538. if (isShapeElement(el)) return undefined
  539. const border = getBorderInfo(el)
  540. if (!border.color || !border.width) return undefined
  541. return `${border.width}px solid ${border.color}`
  542. }
  543. function getElementBackground(el: PptxElement) {
  544. if (isShapeElement(el) || isImageElement(el) || isVideoElement(el) || isAudioElement(el)) return undefined
  545. if (typeof el.fill === "string") {
  546. const raw = el.fill.trim()
  547. return raw ? raw : undefined
  548. }
  549. const fill = typeof el.fill === "object" && el.fill ? (el.fill as PptxFill) : null
  550. if (!fill || !fill.type) return undefined
  551. if (fill.type === "color") return String(fill.value || "") || undefined
  552. if (fill.type === "gradient") {
  553. const colors = Array.isArray(fill.value?.colors) ? fill.value.colors : []
  554. const rot = getNumber(fill.value?.rot)
  555. const path = String(fill.value?.path || "rect")
  556. const stops = colors
  557. .map((c: any) => {
  558. const pos = String(c?.pos ?? "")
  559. const color = String(c?.color ?? "")
  560. if (!color) return ""
  561. return pos ? `${color} ${pos}` : color
  562. })
  563. .filter(Boolean)
  564. .join(", ")
  565. if (!stops) return undefined
  566. if (path === "rect") {
  567. const angle = (90 - rot + 360) % 360
  568. return `linear-gradient(${angle}deg, ${stops})`
  569. }
  570. return `radial-gradient(circle, ${stops})`
  571. }
  572. return undefined
  573. }
  574. function getHtmlBoxStyle(el: PptxElement) {
  575. const vAlign = String(el.vAlign || "up")
  576. const justifyContent = vAlign === "down" ? "flex-end" : vAlign === "mid" ? "center" : "flex-start"
  577. return {
  578. width: "100%",
  579. height: "100%",
  580. display: "flex" as const,
  581. flexDirection: "column" as const,
  582. justifyContent,
  583. } as CSSProperties
  584. }
  585. function getHtmlInnerStyle(el: PptxElement) {
  586. const autoFit = (el as any)?.autoFit
  587. const rawScale = autoFit && typeof autoFit === "object" ? getNumber(autoFit.fontScale) : 1
  588. const fontScale = rawScale && rawScale !== 1 ? Math.max(0.1, Math.min(5, rawScale)) : 1
  589. const isVertical = !!(el as any)?.isVertical
  590. const writingMode = isVertical ? ("vertical-rl" as const) : undefined
  591. const textOrientation = isVertical ? ("mixed" as const) : undefined
  592. if (fontScale === 1) {
  593. return {
  594. width: "100%",
  595. height: "100%",
  596. writingMode,
  597. textOrientation,
  598. } as CSSProperties
  599. }
  600. const inv = 100 / fontScale
  601. return {
  602. width: `${inv}%`,
  603. height: `${inv}%`,
  604. transformOrigin: "top left",
  605. transform: `scale(${fontScale})`,
  606. writingMode,
  607. textOrientation,
  608. } as CSSProperties
  609. }
  610. function isShapeElement(el: PptxElement) {
  611. return String(el.type || "").toLowerCase() === "shape"
  612. }
  613. function isImageElement(el: PptxElement) {
  614. const t = String(el.type || "").toLowerCase()
  615. if (t === "image" || t === "pic" || t === "picture") return true
  616. const src = getMediaSrc(el)
  617. return !!src && /^data:image\//i.test(src)
  618. }
  619. function isVideoElement(el: PptxElement) {
  620. const t = String(el.type || "").toLowerCase()
  621. if (t === "video") return true
  622. const src = getMediaSrc(el)
  623. return !!src && /^data:video\//i.test(src)
  624. }
  625. function isAudioElement(el: PptxElement) {
  626. const t = String(el.type || "").toLowerCase()
  627. if (t === "audio") return true
  628. const src = getMediaSrc(el)
  629. return !!src && /^data:audio\//i.test(src)
  630. }
  631. function isTableElement(el: PptxElement) {
  632. return String(el.type || "").toLowerCase() === "table"
  633. }
  634. function isChartElement(el: PptxElement) {
  635. const t = String(el.type || "").toLowerCase()
  636. return t === "chart" || t === "charts"
  637. }
  638. function isGroupElement(el: PptxElement) {
  639. const t = String(el.type || "").toLowerCase()
  640. if (t === "group" || t === "groupshape" || t === "grpsp" || t === "grp") return true
  641. const hasChildren = Array.isArray((el as any)?.elements) && (el as any).elements.length
  642. return hasChildren && t.includes("group")
  643. }
  644. function isDiagramElement(el: PptxElement) {
  645. const t = String(el.type || "").toLowerCase()
  646. return t === "diagram" || t === "smartart" || t === "smart_art"
  647. }
  648. function isMathElement(el: PptxElement) {
  649. return String(el.type || "").toLowerCase() === "math"
  650. }
  651. function getGroupChildren(el: PptxElement) {
  652. const raw = (el as any)?.elements
  653. return Array.isArray(raw) ? (raw as PptxElement[]) : ([] as PptxElement[])
  654. }
  655. function getGroupChildStyle(parent: PptxElement, child: PptxElement, childIndex: number, parentAbsLeft: number, parentAbsTop: number) {
  656. const parentW = Math.max(0, getNumber(parent.width))
  657. const parentH = Math.max(0, getNumber(parent.height))
  658. const rawLeft = getNumber(child.left)
  659. const rawTop = getNumber(child.top)
  660. const childW = Math.max(0, getNumber(child.width))
  661. const childH = Math.max(0, getNumber(child.height))
  662. const isRelative =
  663. rawLeft >= -0.5 &&
  664. rawTop >= -0.5 &&
  665. rawLeft + childW <= parentW + 0.5 &&
  666. rawTop + childH <= parentH + 0.5
  667. const left = isRelative ? rawLeft : rawLeft - parentAbsLeft
  668. const top = isRelative ? rawTop : rawTop - parentAbsTop
  669. const width = childW
  670. const height = childH
  671. const rotate = getNumber(child.rotate)
  672. const flipH = !!child.isFlipH
  673. const flipV = !!child.isFlipV
  674. const zIndex = getNumber(child.order) || childIndex
  675. const opacity = child.opacity == null ? 1 : Math.max(0, Math.min(1, getNumber(child.opacity)))
  676. const transforms: string[] = []
  677. if (rotate) transforms.push(`rotate(${rotate}deg)`)
  678. if (flipH) transforms.push(`scaleX(-1)`)
  679. if (flipV) transforms.push(`scaleY(-1)`)
  680. const background = getElementBackground(child)
  681. const border = getElementBorderCss(child)
  682. const radius = getElementBorderRadius(child)
  683. const shadowFilter = getElementShadowFilter(child)
  684. return {
  685. position: "absolute" as const,
  686. left: `${left}px`,
  687. top: `${top}px`,
  688. width: `${width}px`,
  689. height: `${height}px`,
  690. zIndex,
  691. opacity,
  692. background,
  693. border,
  694. borderRadius: radius,
  695. filter: shadowFilter,
  696. transformOrigin: "center center",
  697. transform: transforms.length ? transforms.join(" ") : undefined,
  698. } as CSSProperties
  699. }
  700. type GroupRenderProps = {
  701. el: PptxElement
  702. slideIndex: number
  703. cacheId: string
  704. absLeft: number
  705. absTop: number
  706. }
  707. const PptxGroup = defineComponent<GroupRenderProps>({
  708. name: "PptxGroup",
  709. props: {
  710. el: { type: Object as any, required: true },
  711. slideIndex: { type: Number, required: true },
  712. cacheId: { type: String, required: true },
  713. absLeft: { type: Number, required: true },
  714. absTop: { type: Number, required: true },
  715. },
  716. setup(groupProps) {
  717. const renderShapeDefs = (el: PptxElement, slideIndex: number, cacheId: string) => {
  718. if (!hasShapeDefs(el, slideIndex, cacheId)) return null
  719. const grad = getShapeGradient(el, slideIndex, cacheId)
  720. const pattern = getShapePattern(el, slideIndex, cacheId)
  721. const paint = getShapeImagePaint(el, slideIndex, cacheId)
  722. const nodes: any[] = []
  723. if (grad?.kind === "linear") {
  724. nodes.push(
  725. h(
  726. "linearGradient",
  727. { id: grad.id, x1: grad.x1, y1: grad.y1, x2: grad.x2, y2: grad.y2 },
  728. grad.stops.map((s, i) => h("stop", { key: i, offset: s.offset, "stop-color": s.color, "stop-opacity": s.opacity }))
  729. )
  730. )
  731. } else if (grad?.kind === "radial") {
  732. nodes.push(
  733. h(
  734. "radialGradient",
  735. { id: grad.id, cx: "50%", cy: "50%", r: "50%" },
  736. grad.stops.map((s, i) => h("stop", { key: i, offset: s.offset, "stop-color": s.color, "stop-opacity": s.opacity }))
  737. )
  738. )
  739. }
  740. if (pattern) {
  741. nodes.push(
  742. h(
  743. "pattern",
  744. { id: pattern.id, patternUnits: "userSpaceOnUse", width: pattern.width, height: pattern.height },
  745. [
  746. h("rect", { x: 0, y: 0, width: pattern.width, height: pattern.height, fill: pattern.background }),
  747. ...pattern.shapes.map((shape, i) => {
  748. if (shape.kind === "circle") return h("circle", { key: i, cx: shape.cx, cy: shape.cy, r: shape.r, fill: shape.fill })
  749. if (shape.kind === "polygon") return h("polygon", { key: i, points: shape.points, fill: shape.fill })
  750. return h("rect", {
  751. key: i,
  752. x: (shape as any).x,
  753. y: (shape as any).y,
  754. width: (shape as any).width,
  755. height: (shape as any).height,
  756. fill: (shape as any).fill,
  757. transform: (shape as any).transform,
  758. })
  759. }),
  760. ]
  761. )
  762. )
  763. }
  764. if (paint) {
  765. nodes.push(
  766. h(
  767. "pattern",
  768. { id: paint.id, patternUnits: "objectBoundingBox", width: 1, height: 1 },
  769. [
  770. h("image", { x: 0, y: 0, width: 1, height: 1, preserveAspectRatio: "none", href: paint.href }),
  771. ]
  772. )
  773. )
  774. }
  775. return nodes.length ? h("defs", null, nodes) : null
  776. }
  777. const renderElement = (child: PptxElement, childIndex: number, parentEl: PptxElement, parentAbsLeft: number, parentAbsTop: number, parentCacheId: string) => {
  778. const style = getGroupChildStyle(parentEl, child, childIndex, parentAbsLeft, parentAbsTop)
  779. const cacheId = getElementCacheId(child, childIndex, parentCacheId)
  780. const left = getNumber((style as any)?.left)
  781. const top = getNumber((style as any)?.top)
  782. const absLeft = parentAbsLeft + left
  783. const absTop = parentAbsTop + top
  784. if (isImageElement(child)) {
  785. return h(
  786. "div",
  787. { key: cacheId, class: "pptx-element", style },
  788. [
  789. h(
  790. "div",
  791. { class: "pptx-img-wrap", style: getImageWrapStyle(child) },
  792. [h("img", { class: "pptx-img", src: getMediaSrc(child), style: getImageStyle(child), alt: "", draggable: false })]
  793. ),
  794. ]
  795. )
  796. }
  797. if (isVideoElement(child)) {
  798. return h("div", { key: cacheId, class: "pptx-element", style }, [h("video", { class: "pptx-media", src: getMediaSrc(child), controls: true, preload: "metadata" })])
  799. }
  800. if (isAudioElement(child)) {
  801. return h("div", { key: cacheId, class: "pptx-element", style }, [h("audio", { class: "pptx-media", src: getMediaSrc(child), controls: true, preload: "metadata" })])
  802. }
  803. if (isMathElement(child)) {
  804. return h("div", { key: cacheId, class: "pptx-element", style }, [h("img", { class: "pptx-img", src: getMediaSrc(child), alt: "", draggable: false })])
  805. }
  806. if (isChartElement(child)) {
  807. const model = getChartModel(child, groupProps.slideIndex, cacheId)
  808. const nodes: any[] = []
  809. if (model.kind === "bar") {
  810. nodes.push(...model.bars.map((b, i) => h("rect", { key: i, x: b.x, y: b.y, width: b.w, height: b.h, fill: b.fill, "fill-opacity": b.opacity })))
  811. } else if (model.kind === "line") {
  812. nodes.push(
  813. ...model.paths.map((p, i) =>
  814. h("path", { key: i, d: p.d, fill: "none", stroke: p.stroke, "stroke-width": p.strokeWidth, "stroke-linejoin": "round", "stroke-linecap": "round" })
  815. )
  816. )
  817. nodes.push(...model.points.map((pt, i) => h("circle", { key: i, cx: pt.cx, cy: pt.cy, r: pt.r, fill: pt.fill, "fill-opacity": pt.opacity })))
  818. } else if (model.kind === "pie") {
  819. nodes.push(...model.slices.map((s, i) => h("path", { key: i, d: s.d, fill: s.fill, "fill-opacity": s.opacity })))
  820. if (model.holeR) nodes.push(h("circle", { cx: model.cx, cy: model.cy, r: model.holeR, fill: "#fff" }))
  821. } else if (model.kind === "scatter") {
  822. nodes.push(...model.points.map((pt, i) => h("circle", { key: i, cx: pt.cx, cy: pt.cy, r: pt.r, fill: pt.fill, "fill-opacity": pt.opacity })))
  823. }
  824. return h(
  825. "div",
  826. { key: cacheId, class: "pptx-element", style },
  827. [h("svg", { class: "pptx-chart", viewBox: `0 0 ${getNumber(child.width)} ${getNumber(child.height)}`, preserveAspectRatio: "none" }, nodes)]
  828. )
  829. }
  830. if (isTableElement(child)) {
  831. const model = getTableModel(child, groupProps.slideIndex, cacheId)
  832. return h(
  833. "div",
  834. { key: cacheId, class: "pptx-element", style },
  835. [
  836. h(
  837. "div",
  838. { class: "pptx-table-wrap" },
  839. [
  840. h(
  841. "table",
  842. { class: "pptx-table" },
  843. [
  844. model.colWidths.length
  845. ? h(
  846. "colgroup",
  847. null,
  848. model.colWidths.map((w, i) => h("col", { key: i, style: { width: w ? `${w}px` : undefined } }))
  849. )
  850. : null,
  851. h(
  852. "tbody",
  853. null,
  854. model.rows.map((row, ri) =>
  855. h(
  856. "tr",
  857. { key: ri, style: getTableRowStyle(child, groupProps.slideIndex, cacheId, ri) },
  858. row
  859. .map((cell, ci) => {
  860. if (cell.skip) return null
  861. return h(
  862. "td",
  863. { key: `${ri}-${ci}`, colspan: cell.colspan, rowspan: cell.rowspan, style: cell.style },
  864. [h("div", { class: "pptx-table-html", innerHTML: sanitizeHtml(cell.html) })]
  865. )
  866. })
  867. .filter(Boolean)
  868. )
  869. )
  870. ),
  871. ].filter(Boolean) as any
  872. ),
  873. ]
  874. ),
  875. ]
  876. )
  877. }
  878. if (isShapeElement(child)) {
  879. const defs = renderShapeDefs(child, groupProps.slideIndex, cacheId)
  880. const svg = h(
  881. "svg",
  882. { class: "pptx-shape-svg", viewBox: `0 0 ${getNumber(child.width)} ${getNumber(child.height)}`, preserveAspectRatio: "none" },
  883. [
  884. defs,
  885. h("path", {
  886. d: getShapePath(child),
  887. fill: getShapeFill(child, groupProps.slideIndex, cacheId),
  888. stroke: getShapeStroke(child),
  889. "stroke-width": getShapeStrokeWidth(child),
  890. "stroke-dasharray": getShapeDasharray(child),
  891. }),
  892. ].filter(Boolean) as any
  893. )
  894. const html = typeof child.content === "string" && child.content.trim()
  895. const overlay = html
  896. ? h(
  897. "div",
  898. { class: "pptx-html", style: getHtmlBoxStyle(child) },
  899. [h("div", { class: "pptx-html-inner", style: getHtmlInnerStyle(child), innerHTML: sanitizeHtml(child.content as string) })]
  900. )
  901. : null
  902. return h("div", { key: cacheId, class: "pptx-element", style }, [svg, overlay].filter(Boolean) as any)
  903. }
  904. if (isGroupElement(child) || isDiagramElement(child)) {
  905. return h(
  906. "div",
  907. { key: cacheId, class: "pptx-element", style },
  908. [h(PptxGroup as any, { el: child, slideIndex: groupProps.slideIndex, cacheId, absLeft, absTop })]
  909. )
  910. }
  911. const html = typeof child.content === "string" && child.content.trim()
  912. return h(
  913. "div",
  914. { key: cacheId, class: "pptx-element", style },
  915. html
  916. ? [
  917. h(
  918. "div",
  919. { class: "pptx-html", style: getHtmlBoxStyle(child) },
  920. [h("div", { class: "pptx-html-inner", style: getHtmlInnerStyle(child), innerHTML: sanitizeHtml(child.content as string) })]
  921. ),
  922. ]
  923. : []
  924. )
  925. }
  926. return () => {
  927. const children = getGroupChildren(groupProps.el)
  928. return h(
  929. "div",
  930. { class: "pptx-group-inner" },
  931. children.map((child, idx) => renderElement(child, idx, groupProps.el, groupProps.absLeft, groupProps.absTop, groupProps.cacheId))
  932. )
  933. }
  934. },
  935. })
  936. function normalizeRatio(n: unknown) {
  937. const v = getNumber(n)
  938. if (!v) return 0
  939. if (v > 1) return Math.max(0, Math.min(1, v / 100))
  940. return Math.max(0, Math.min(1, v))
  941. }
  942. function getImageCropRect(el: PptxElement) {
  943. const rect = (el as any)?.rect
  944. const crop = (el as any)?.crop
  945. const raw = rect && typeof rect === "object" ? rect : crop && typeof crop === "object" ? crop : null
  946. if (!raw) return null
  947. const t = normalizeRatio(raw.t)
  948. const b = normalizeRatio(raw.b)
  949. const l = normalizeRatio(raw.l)
  950. const r = normalizeRatio(raw.r)
  951. if (!t && !b && !l && !r) return null
  952. const safeL = Math.max(0, Math.min(0.9, l))
  953. const safeR = Math.max(0, Math.min(0.9, r))
  954. const safeT = Math.max(0, Math.min(0.9, t))
  955. const safeB = Math.max(0, Math.min(0.9, b))
  956. if (safeL + safeR >= 0.98 || safeT + safeB >= 0.98) return null
  957. return { t: safeT, b: safeB, l: safeL, r: safeR }
  958. }
  959. function getImageGeom(el: PptxElement) {
  960. const g = String((el as any)?.geom || (el as any)?.shapeType || "").toLowerCase()
  961. return g
  962. }
  963. function getImageWrapStyle(el: PptxElement) {
  964. const geom = getImageGeom(el)
  965. const isEllipse = geom === "ellipse" || geom === "circle"
  966. const borderRadius = isEllipse ? "50%" : getElementBorderRadius(el)
  967. return {
  968. width: "100%",
  969. height: "100%",
  970. position: "relative" as const,
  971. overflow: "hidden",
  972. borderRadius,
  973. } as CSSProperties
  974. }
  975. function normalizeFilterFactor(value: unknown, base = 1) {
  976. if (value == null) return base
  977. const n = getNumber(value)
  978. if (!n) return base
  979. const v = n > 10 ? n / 100 : n
  980. return Math.max(0, Math.min(3, v))
  981. }
  982. function buildImageFilter(el: PptxElement) {
  983. const filters = (el as any)?.filters
  984. if (!filters || typeof filters !== "object") return undefined
  985. const brightness = normalizeFilterFactor(filters.brightness, 1)
  986. const contrast = normalizeFilterFactor(filters.contrast, 1)
  987. const saturation = normalizeFilterFactor(filters.saturation, 1)
  988. const colorTemperature = getNumber(filters.colorTemperature)
  989. const parts: string[] = []
  990. if (brightness !== 1) parts.push(`brightness(${brightness})`)
  991. if (contrast !== 1) parts.push(`contrast(${contrast})`)
  992. if (saturation !== 1) parts.push(`saturate(${saturation})`)
  993. if (colorTemperature) {
  994. const t = Math.max(-100, Math.min(100, colorTemperature))
  995. const hue = t * 0.6
  996. const sepia = Math.min(1, Math.abs(t) / 200)
  997. parts.push(`hue-rotate(${hue}deg)`)
  998. if (t > 0 && sepia) parts.push(`sepia(${sepia})`)
  999. }
  1000. return parts.length ? parts.join(" ") : undefined
  1001. }
  1002. function getImageStyle(el: PptxElement) {
  1003. const crop = getImageCropRect(el)
  1004. const filter = buildImageFilter(el)
  1005. if (!crop) {
  1006. return {
  1007. width: "100%",
  1008. height: "100%",
  1009. objectFit: "contain",
  1010. filter,
  1011. display: "block",
  1012. } as CSSProperties
  1013. }
  1014. const scaleX = 1 / (1 - crop.l - crop.r)
  1015. const scaleY = 1 / (1 - crop.t - crop.b)
  1016. const translateX = -crop.l * 100
  1017. const translateY = -crop.t * 100
  1018. return {
  1019. width: "100%",
  1020. height: "100%",
  1021. objectFit: "fill",
  1022. display: "block",
  1023. transformOrigin: "top left",
  1024. transform: `translate(${translateX}%, ${translateY}%) scale(${scaleX}, ${scaleY})`,
  1025. filter,
  1026. } as CSSProperties
  1027. }
  1028. function getMediaSrc(el: PptxElement) {
  1029. const fillObj = typeof el.fill === "object" && el.fill ? (el.fill as PptxFill) : null
  1030. const candidates = [
  1031. el.src,
  1032. el.url,
  1033. el.picBase64,
  1034. el.mediaBase64,
  1035. el.base64,
  1036. el.blob,
  1037. el.blobUrl,
  1038. el.value?.picBase64,
  1039. el.value?.src,
  1040. fillObj?.value?.picBase64,
  1041. fillObj?.value?.src,
  1042. ]
  1043. const found = candidates.find((v) => typeof v === "string" && v.trim())
  1044. return found ? String(found) : ""
  1045. }
  1046. type ChartBar = { x: number; y: number; w: number; h: number; fill: string; opacity?: number }
  1047. type ChartPath = { d: string; stroke: string; strokeWidth: number }
  1048. type ChartPoint = { cx: number; cy: number; r: number; fill: string; opacity?: number }
  1049. type ChartSlice = { d: string; fill: string; opacity?: number }
  1050. type ChartModel = {
  1051. kind: "bar" | "line" | "pie" | "scatter"
  1052. bars: ChartBar[]
  1053. paths: ChartPath[]
  1054. points: ChartPoint[]
  1055. slices: ChartSlice[]
  1056. cx: number
  1057. cy: number
  1058. holeR: number
  1059. }
  1060. const chartCache = new Map<string, ChartModel>()
  1061. function getChartModel(el: PptxElement, slideIndex: number, cacheId: string): ChartModel {
  1062. const key = `c-${getCacheKey(slideIndex, cacheId)}`
  1063. const cached = chartCache.get(key)
  1064. if (cached) return cached
  1065. const w = Math.max(1, getNumber(el.width))
  1066. const h = Math.max(1, getNumber(el.height))
  1067. const chartType = String((el as any)?.chartType || "").toLowerCase()
  1068. const colorsRaw = Array.isArray((el as any)?.colors) ? ((el as any).colors as string[]) : []
  1069. const colors = colorsRaw.length ? colorsRaw.map((c) => String(c || "").trim()).filter(Boolean) : []
  1070. const getColor = (i: number) => colors[i % Math.max(1, colors.length)] || "#4e79a7"
  1071. const opacity = el.opacity == null ? 1 : Math.max(0, Math.min(1, getNumber(el.opacity)))
  1072. const padding = Math.max(4, Math.min(24, Math.round(Math.min(w, h) * 0.08)))
  1073. const isPie = chartType.includes("pie") || chartType.includes("doughnut")
  1074. const isBar = chartType.includes("bar") || chartType.includes("col")
  1075. const isScatter = chartType.includes("scatter") || chartType.includes("bubble")
  1076. if (isScatter) {
  1077. const data = (el as any)?.data
  1078. const xs = Array.isArray(data?.[0]) ? (data[0] as number[]) : []
  1079. const ys = Array.isArray(data?.[1]) ? (data[1] as number[]) : []
  1080. const n = Math.min(xs.length, ys.length)
  1081. const minX = n ? Math.min(...xs.slice(0, n)) : 0
  1082. const maxX = n ? Math.max(...xs.slice(0, n)) : 1
  1083. const minY = n ? Math.min(...ys.slice(0, n)) : 0
  1084. const maxY = n ? Math.max(...ys.slice(0, n)) : 1
  1085. const spanX = maxX - minX || 1
  1086. const spanY = maxY - minY || 1
  1087. const innerW = Math.max(1, w - padding * 2)
  1088. const innerH = Math.max(1, h - padding * 2)
  1089. const points: ChartPoint[] = []
  1090. for (let i = 0; i < n; i++) {
  1091. const x = padding + ((xs[i] - minX) / spanX) * innerW
  1092. const y = padding + (1 - (ys[i] - minY) / spanY) * innerH
  1093. points.push({ cx: x, cy: y, r: 3, fill: getColor(0), opacity })
  1094. }
  1095. const model: ChartModel = { kind: "scatter", bars: [], paths: [], points, slices: [], cx: 0, cy: 0, holeR: 0 }
  1096. chartCache.set(key, model)
  1097. return model
  1098. }
  1099. const rawData = (el as any)?.data
  1100. if (isPie) {
  1101. const series = Array.isArray(rawData) ? (rawData as any[]) : []
  1102. const valuesRaw = Array.isArray(series?.[0]?.values) ? (series[0].values as any[]) : []
  1103. const values = valuesRaw
  1104. .map((v) => ({ y: getNumber(v?.y), x: String(v?.x ?? "") }))
  1105. .filter((v) => Number.isFinite(v.y) && v.y > 0)
  1106. const sum = values.reduce((acc, v) => acc + v.y, 0) || 1
  1107. const cx = w / 2
  1108. const cy = h / 2
  1109. const r = Math.max(2, Math.min(w, h) / 2 - padding)
  1110. const holeSize = String((el as any)?.holeSize || "").trim()
  1111. const holeRatio = holeSize.endsWith("%") ? getNumber(holeSize.replace("%", "")) / 100 : getNumber(holeSize)
  1112. const holeR = Math.max(0, Math.min(r - 1, r * Math.max(0, Math.min(0.9, holeRatio || 0))))
  1113. let start = -Math.PI / 2
  1114. const slices: ChartSlice[] = values.map((v, i) => {
  1115. const a = (v.y / sum) * Math.PI * 2
  1116. const end = start + a
  1117. const x1 = cx + r * Math.cos(start)
  1118. const y1 = cy + r * Math.sin(start)
  1119. const x2 = cx + r * Math.cos(end)
  1120. const y2 = cy + r * Math.sin(end)
  1121. const large = a > Math.PI ? 1 : 0
  1122. const d = `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`
  1123. start = end
  1124. return { d, fill: getColor(i), opacity }
  1125. })
  1126. const model: ChartModel = { kind: "pie", bars: [], paths: [], points: [], slices, cx, cy, holeR }
  1127. chartCache.set(key, model)
  1128. return model
  1129. }
  1130. const series = Array.isArray(rawData) ? (rawData as any[]) : []
  1131. const items = series
  1132. .map((s) => {
  1133. const values = Array.isArray(s?.values) ? (s.values as any[]) : []
  1134. return {
  1135. key: String(s?.key || ""),
  1136. values: values.map((v) => ({ x: String(v?.x ?? ""), y: getNumber(v?.y) })),
  1137. }
  1138. })
  1139. .filter((s) => s.values.length)
  1140. const categories = Array.from(
  1141. new Set(
  1142. items
  1143. .flatMap((s) => s.values.map((v) => v.x))
  1144. .filter((x) => x != null)
  1145. .map((x) => String(x))
  1146. )
  1147. )
  1148. const catCount = Math.max(1, categories.length)
  1149. const seriesCount = Math.max(1, items.length)
  1150. const maxY = Math.max(
  1151. 1,
  1152. ...items.flatMap((s) => s.values.map((v) => v.y)).filter((v) => Number.isFinite(v))
  1153. )
  1154. const innerW = Math.max(1, w - padding * 2)
  1155. const innerH = Math.max(1, h - padding * 2)
  1156. const barDir = String((el as any)?.barDir || "col").toLowerCase()
  1157. if (isBar) {
  1158. const bars: ChartBar[] = []
  1159. if (barDir === "bar") {
  1160. const bandH = innerH / catCount
  1161. const barH = bandH * 0.8
  1162. const gapY = (bandH - barH) / 2
  1163. const eachH = barH / seriesCount
  1164. for (let ci = 0; ci < catCount; ci++) {
  1165. for (let si = 0; si < items.length; si++) {
  1166. const v = items[si].values.find((vv) => vv.x === categories[ci])
  1167. const value = v ? v.y : 0
  1168. const bw = (value / maxY) * innerW
  1169. const x = padding
  1170. const y = padding + ci * bandH + gapY + si * eachH
  1171. bars.push({ x, y, w: Math.max(0, bw), h: Math.max(0, eachH * 0.9), fill: getColor(si), opacity })
  1172. }
  1173. }
  1174. } else {
  1175. const bandW = innerW / catCount
  1176. const barW = bandW * 0.8
  1177. const gapX = (bandW - barW) / 2
  1178. const eachW = barW / seriesCount
  1179. for (let ci = 0; ci < catCount; ci++) {
  1180. for (let si = 0; si < items.length; si++) {
  1181. const v = items[si].values.find((vv) => vv.x === categories[ci])
  1182. const value = v ? v.y : 0
  1183. const bh = (value / maxY) * innerH
  1184. const x = padding + ci * bandW + gapX + si * eachW
  1185. const y = padding + (innerH - bh)
  1186. bars.push({ x, y, w: Math.max(0, eachW * 0.9), h: Math.max(0, bh), fill: getColor(si), opacity })
  1187. }
  1188. }
  1189. }
  1190. const model: ChartModel = { kind: "bar", bars, paths: [], points: [], slices: [], cx: 0, cy: 0, holeR: 0 }
  1191. chartCache.set(key, model)
  1192. return model
  1193. }
  1194. const paths: ChartPath[] = []
  1195. const points: ChartPoint[] = []
  1196. const strokeWidth = Math.max(1, Math.round(Math.min(w, h) * 0.01))
  1197. const marker = !!(el as any)?.marker
  1198. const xAt = (i: number) => padding + (catCount === 1 ? innerW / 2 : (i / (catCount - 1)) * innerW)
  1199. const yAt = (value: number) => padding + (1 - value / maxY) * innerH
  1200. items.forEach((s, si) => {
  1201. let d = ""
  1202. categories.forEach((cat, ci) => {
  1203. const v = s.values.find((vv) => vv.x === cat)
  1204. const value = v ? v.y : 0
  1205. const x = xAt(ci)
  1206. const y = yAt(value)
  1207. d += ci === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`
  1208. if (marker) points.push({ cx: x, cy: y, r: strokeWidth * 0.9 + 1, fill: getColor(si), opacity })
  1209. })
  1210. paths.push({ d, stroke: getColor(si), strokeWidth })
  1211. })
  1212. const model: ChartModel = { kind: "line", bars: [], paths, points, slices: [], cx: 0, cy: 0, holeR: 0 }
  1213. chartCache.set(key, model)
  1214. return model
  1215. }
  1216. type TableBorderSide = {
  1217. borderColor?: string
  1218. borderWidth?: number
  1219. borderType?: string
  1220. strokeDasharray?: string
  1221. }
  1222. type TableCellRaw = {
  1223. text?: string
  1224. content?: string
  1225. html?: string
  1226. colSpan?: number
  1227. rowSpan?: number
  1228. hMerge?: number
  1229. vMerge?: number
  1230. fillColor?: string
  1231. fontColor?: string
  1232. fontBold?: boolean
  1233. fontItalic?: boolean
  1234. borders?: {
  1235. top?: TableBorderSide
  1236. right?: TableBorderSide
  1237. bottom?: TableBorderSide
  1238. left?: TableBorderSide
  1239. }
  1240. [key: string]: any
  1241. }
  1242. type TableCellView = {
  1243. html: string
  1244. colspan: number
  1245. rowspan: number
  1246. skip: boolean
  1247. style: CSSProperties
  1248. }
  1249. type TableModel = {
  1250. colWidths: number[]
  1251. rowHeights: number[]
  1252. rows: TableCellView[][]
  1253. }
  1254. const tableCache = new Map<string, TableModel>()
  1255. function getTableModel(el: PptxElement, slideIndex: number, cacheId: string): TableModel {
  1256. const key = `t-${getCacheKey(slideIndex, cacheId)}`
  1257. const cached = tableCache.get(key)
  1258. if (cached) return cached
  1259. const rawData = (el as any)?.data
  1260. const data: TableCellRaw[][] = Array.isArray(rawData) ? rawData : []
  1261. const colWidthsRaw = (el as any)?.colWidths
  1262. const rowHeightsRaw = (el as any)?.rowHeights
  1263. const colWidths = Array.isArray(colWidthsRaw) ? colWidthsRaw.map(getNumber) : []
  1264. const rowHeights = Array.isArray(rowHeightsRaw) ? rowHeightsRaw.map(getNumber) : []
  1265. const rows: TableCellView[][] = data.map((row) => {
  1266. const r = Array.isArray(row) ? row : []
  1267. return r.map((cellRaw) => normalizeTableCell(el, cellRaw))
  1268. })
  1269. const model: TableModel = {
  1270. colWidths,
  1271. rowHeights,
  1272. rows,
  1273. }
  1274. tableCache.set(key, model)
  1275. return model
  1276. }
  1277. function getTableRowStyle(el: PptxElement, slideIndex: number, cacheId: string, rowIndex: number) {
  1278. const model = getTableModel(el, slideIndex, cacheId)
  1279. const h = model.rowHeights[rowIndex]
  1280. if (!h) return undefined
  1281. return {
  1282. height: `${h}px`,
  1283. } as CSSProperties
  1284. }
  1285. function extractTextAlign(html: string) {
  1286. const m = html.match(/text-align\s*:\s*(left|right|center|justify)/i)
  1287. return m ? m[1].toLowerCase() : "left"
  1288. }
  1289. function getTableBorderStyle(side?: TableBorderSide) {
  1290. if (!side) return ""
  1291. const color = String(side.borderColor || "")
  1292. const width = Math.max(0, getNumber(side.borderWidth))
  1293. const borderType = String(side.borderType || "solid")
  1294. if (!color || !width) return ""
  1295. const style = borderType === "dashed" || borderType === "dash" ? "dashed" : "solid"
  1296. return `${width}px ${style} ${color}`
  1297. }
  1298. function normalizeTableCell(tableEl: PptxElement, cellRaw: TableCellRaw): TableCellView {
  1299. const raw = cellRaw && typeof cellRaw === "object" ? cellRaw : ({} as TableCellRaw)
  1300. const html = String(raw.text ?? raw.content ?? raw.html ?? "")
  1301. const colspan = Math.max(1, getNumber(raw.colSpan ?? raw.colspan ?? raw.gridSpan) || 1)
  1302. const rowspan = Math.max(1, getNumber(raw.rowSpan ?? raw.rowspan) || 1)
  1303. const skip = getNumber(raw.hMerge) === 1 || getNumber(raw.vMerge) === 1
  1304. const background = String(raw.fillColor || "") || undefined
  1305. const color = String(raw.fontColor || "") || undefined
  1306. const fontWeight = raw.fontBold ? 700 : undefined
  1307. const fontStyle = raw.fontItalic ? "italic" : undefined
  1308. const textAlign = html ? extractTextAlign(html) : undefined
  1309. const borders = raw.borders || (tableEl as any)?.borders || undefined
  1310. const borderTop = getTableBorderStyle(borders?.top)
  1311. const borderRight = getTableBorderStyle(borders?.right)
  1312. const borderBottom = getTableBorderStyle(borders?.bottom)
  1313. const borderLeft = getTableBorderStyle(borders?.left)
  1314. const style: CSSProperties = {
  1315. padding: "2px 4px",
  1316. verticalAlign: "middle",
  1317. background,
  1318. color,
  1319. fontWeight,
  1320. fontStyle,
  1321. textAlign: textAlign as any,
  1322. borderTop: borderTop || undefined,
  1323. borderRight: borderRight || undefined,
  1324. borderBottom: borderBottom || undefined,
  1325. borderLeft: borderLeft || undefined,
  1326. overflow: "hidden",
  1327. }
  1328. return {
  1329. html,
  1330. colspan,
  1331. rowspan,
  1332. skip,
  1333. style,
  1334. }
  1335. }
  1336. type BorderInfo = {
  1337. color: string
  1338. width: number
  1339. dasharray?: string
  1340. }
  1341. function getBorderInfo(el: PptxElement): BorderInfo {
  1342. const directColor = String(el.borderColor ?? el.stroke ?? el.lineColor ?? "")
  1343. const directWidth = getNumber(el.borderWidth ?? el.strokeWidth ?? el.lineWidth)
  1344. const directDash = String(el.borderStrokeDasharray ?? el.strokeDasharray ?? el.dasharray ?? "")
  1345. const borderObj = el.border && typeof el.border === "object" ? el.border : null
  1346. const lineObj = el.line && typeof el.line === "object" ? el.line : null
  1347. const fallbackColor = String(borderObj?.color ?? lineObj?.color ?? "")
  1348. const fallbackWidth = getNumber(borderObj?.width ?? lineObj?.width)
  1349. const fallbackDash = String(borderObj?.dasharray ?? lineObj?.dasharray ?? "")
  1350. const color = directColor || fallbackColor
  1351. const width = directWidth || fallbackWidth
  1352. const dasharray = (directDash && directDash !== "0" ? directDash : "") || (fallbackDash && fallbackDash !== "0" ? fallbackDash : "")
  1353. return {
  1354. color: color || "transparent",
  1355. width: Math.max(0, width),
  1356. dasharray: dasharray || undefined,
  1357. }
  1358. }
  1359. type PatternShape =
  1360. | { kind: "circle"; cx: number; cy: number; r: number; fill: string }
  1361. | { kind: "polygon"; points: string; fill: string }
  1362. | { kind: "rect"; x: number; y: number; width: number; height: number; fill: string; transform?: string }
  1363. type ShapePattern = {
  1364. id: string
  1365. width: number
  1366. height: number
  1367. background: string
  1368. shapes: PatternShape[]
  1369. }
  1370. type GradientStop = {
  1371. offset: string
  1372. color: string
  1373. opacity?: number
  1374. }
  1375. type ShapeGradient = {
  1376. id: string
  1377. kind: "linear" | "radial"
  1378. x1?: string
  1379. y1?: string
  1380. x2?: string
  1381. y2?: string
  1382. stops: GradientStop[]
  1383. }
  1384. type ShapeImagePaint = {
  1385. id: string
  1386. href: string
  1387. }
  1388. function toDomId(raw: string) {
  1389. return raw.replace(/[^a-zA-Z0-9_-]/g, "_")
  1390. }
  1391. function getShapePattern(el: PptxElement, slideIndex: number, cacheId: string): ShapePattern | null {
  1392. const fill = typeof el.fill === "object" ? (el.fill as PptxFill) : null
  1393. if (!fill || fill.type !== "pattern" || !fill.value) return null
  1394. const type = String(fill.value?.type || "")
  1395. const foreground = String(fill.value?.foregroundColor || "#000")
  1396. const background = String(fill.value?.backgroundColor || "#fff")
  1397. const id = `pptx-pattern-${slideIndex}-${toDomId(cacheId)}`
  1398. if (type === "pct5") {
  1399. return {
  1400. id,
  1401. width: 10,
  1402. height: 10,
  1403. background,
  1404. shapes: [{ kind: "circle", cx: 5, cy: 5, r: 1.2, fill: foreground }],
  1405. }
  1406. }
  1407. if (type === "solidDmnd") {
  1408. return {
  1409. id,
  1410. width: 12,
  1411. height: 12,
  1412. background,
  1413. shapes: [
  1414. {
  1415. kind: "polygon",
  1416. points: "6,0 12,6 6,12 0,6",
  1417. fill: foreground,
  1418. },
  1419. ],
  1420. }
  1421. }
  1422. return {
  1423. id,
  1424. width: 10,
  1425. height: 10,
  1426. background,
  1427. shapes: [{ kind: "circle", cx: 5, cy: 5, r: 1, fill: foreground }],
  1428. }
  1429. }
  1430. function getShapeImagePaint(el: PptxElement, slideIndex: number, cacheId: string): ShapeImagePaint | null {
  1431. const fill = typeof el.fill === "object" ? (el.fill as PptxFill) : null
  1432. if (!fill || fill.type !== "image") return null
  1433. const href = String(fill.value?.picBase64 || fill.value?.src || "")
  1434. if (!href) return null
  1435. return {
  1436. id: `pptx-shape-img-${slideIndex}-${toDomId(cacheId)}`,
  1437. href,
  1438. }
  1439. }
  1440. const shapeCache = {
  1441. pattern: new Map<string, ShapePattern | null>(),
  1442. image: new Map<string, ShapeImagePaint | null>(),
  1443. gradient: new Map<string, ShapeGradient | null>(),
  1444. }
  1445. function getCacheKey(slideIndex: number, cacheId: string) {
  1446. return `${slideIndex}-${cacheId}`
  1447. }
  1448. function getShapePatternCached(el: PptxElement, slideIndex: number, cacheId: string) {
  1449. const key = `p-${getCacheKey(slideIndex, cacheId)}`
  1450. if (shapeCache.pattern.has(key)) return shapeCache.pattern.get(key) as ShapePattern | null
  1451. const value = getShapePattern(el, slideIndex, cacheId)
  1452. shapeCache.pattern.set(key, value)
  1453. return value
  1454. }
  1455. function getShapeImagePaintCached(el: PptxElement, slideIndex: number, cacheId: string) {
  1456. const key = `i-${getCacheKey(slideIndex, cacheId)}`
  1457. if (shapeCache.image.has(key)) return shapeCache.image.get(key) as ShapeImagePaint | null
  1458. const value = getShapeImagePaint(el, slideIndex, cacheId)
  1459. shapeCache.image.set(key, value)
  1460. return value
  1461. }
  1462. function normalizeOffset(pos: unknown) {
  1463. const s = String(pos ?? "").trim()
  1464. if (!s) return ""
  1465. if (s.endsWith("%")) return s
  1466. const n = Number(s)
  1467. if (Number.isFinite(n)) return `${n}%`
  1468. return s
  1469. }
  1470. function getShapeGradient(el: PptxElement, slideIndex: number, cacheId: string): ShapeGradient | null {
  1471. const key = `g-${getCacheKey(slideIndex, cacheId)}`
  1472. if (shapeCache.gradient.has(key)) return shapeCache.gradient.get(key) as ShapeGradient | null
  1473. const fill = typeof el.fill === "object" ? (el.fill as PptxFill) : null
  1474. if (!fill || fill.type !== "gradient" || !fill.value) {
  1475. shapeCache.gradient.set(key, null)
  1476. return null
  1477. }
  1478. const colors = Array.isArray(fill.value?.colors) ? fill.value.colors : []
  1479. const rot = getNumber(fill.value?.rot)
  1480. const path = String(fill.value?.path || "rect")
  1481. const stops: GradientStop[] = colors
  1482. .map((c: any) => {
  1483. const offset = normalizeOffset(c?.pos)
  1484. const color = String(c?.color ?? "").trim()
  1485. const opacityRaw = c?.alpha ?? c?.opacity
  1486. const opacity = opacityRaw == null ? undefined : Math.max(0, Math.min(1, getNumber(opacityRaw)))
  1487. if (!color) return null
  1488. return {
  1489. offset: offset || undefined,
  1490. color,
  1491. opacity,
  1492. }
  1493. })
  1494. .filter(Boolean) as any
  1495. const id = `pptx-grad-${slideIndex}-${toDomId(cacheId)}`
  1496. if (!stops.length) {
  1497. shapeCache.gradient.set(key, null)
  1498. return null
  1499. }
  1500. if (path !== "rect") {
  1501. const result: ShapeGradient = {
  1502. id,
  1503. kind: "radial",
  1504. stops: stops.map((s) => ({ ...s, offset: s.offset || "0%" })),
  1505. }
  1506. shapeCache.gradient.set(key, result)
  1507. return result
  1508. }
  1509. const angle = ((90 - rot + 360) % 360) * (Math.PI / 180)
  1510. const dx = Math.cos(angle)
  1511. const dy = Math.sin(angle)
  1512. const x1 = 0.5 - dx / 2
  1513. const y1 = 0.5 + dy / 2
  1514. const x2 = 0.5 + dx / 2
  1515. const y2 = 0.5 - dy / 2
  1516. const result: ShapeGradient = {
  1517. id,
  1518. kind: "linear",
  1519. x1: `${Math.max(0, Math.min(1, x1)) * 100}%`,
  1520. y1: `${Math.max(0, Math.min(1, y1)) * 100}%`,
  1521. x2: `${Math.max(0, Math.min(1, x2)) * 100}%`,
  1522. y2: `${Math.max(0, Math.min(1, y2)) * 100}%`,
  1523. stops: stops.map((s) => ({ ...s, offset: s.offset || "0%" })),
  1524. }
  1525. shapeCache.gradient.set(key, result)
  1526. return result
  1527. }
  1528. function hasShapeDefs(el: PptxElement, slideIndex: number, cacheId: string) {
  1529. return !!(
  1530. getShapeGradient(el, slideIndex, cacheId) ||
  1531. getShapePatternCached(el, slideIndex, cacheId) ||
  1532. getShapeImagePaintCached(el, slideIndex, cacheId)
  1533. )
  1534. }
  1535. function getShapeFill(el: PptxElement, slideIndex: number, cacheId: string) {
  1536. const fill = typeof el.fill === "object" ? (el.fill as PptxFill) : null
  1537. if (!fill || !fill.type) return "transparent"
  1538. if (fill.type === "color") return String(fill.value || "transparent")
  1539. if (fill.type === "gradient") {
  1540. const grad = getShapeGradient(el, slideIndex, cacheId)
  1541. return grad ? `url(#${grad.id})` : "transparent"
  1542. }
  1543. if (fill.type === "pattern") {
  1544. const pattern = getShapePatternCached(el, slideIndex, cacheId)
  1545. return pattern ? `url(#${pattern.id})` : String(fill.value?.foregroundColor || "transparent")
  1546. }
  1547. if (fill.type === "image") {
  1548. const paint = getShapeImagePaintCached(el, slideIndex, cacheId)
  1549. return paint ? `url(#${paint.id})` : "transparent"
  1550. }
  1551. return "transparent"
  1552. }
  1553. function getShapeStroke(el: PptxElement) {
  1554. const border = getBorderInfo(el)
  1555. return border.color || "transparent"
  1556. }
  1557. function getShapeStrokeWidth(el: PptxElement) {
  1558. const border = getBorderInfo(el)
  1559. return Math.max(0, border.width)
  1560. }
  1561. function getShapeDasharray(el: PptxElement) {
  1562. const border = getBorderInfo(el)
  1563. return border.dasharray
  1564. }
  1565. function getShapePath(el: PptxElement) {
  1566. const raw = typeof el.path === "string" ? el.path.trim() : ""
  1567. if (raw) return raw
  1568. const w = Math.max(0, getNumber(el.width))
  1569. const h = Math.max(0, getNumber(el.height))
  1570. if (!w || !h) return ""
  1571. const shapeType = String(el.shapType || el.shapeType || el.geom || "").toLowerCase()
  1572. if (shapeType === "ellipse" || shapeType === "circle") {
  1573. const cx = w / 2
  1574. const cy = h / 2
  1575. const rx = w / 2
  1576. const ry = h / 2
  1577. return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy} Z`
  1578. }
  1579. if (shapeType === "triangle" || shapeType === "tri") {
  1580. return `M ${w / 2} 0 L ${w} ${h} L 0 ${h} Z`
  1581. }
  1582. if (shapeType === "diamond" || shapeType === "rhombus") {
  1583. return `M ${w / 2} 0 L ${w} ${h / 2} L ${w / 2} ${h} L 0 ${h / 2} Z`
  1584. }
  1585. const r = Math.max(0, Math.min(Math.min(w, h) / 2, getNumber(el.radius || el.cornerRadius || el.rx)))
  1586. if (r) {
  1587. const rr = Math.min(r, w / 2, h / 2)
  1588. return `M ${rr} 0 H ${w - rr} A ${rr} ${rr} 0 0 1 ${w} ${rr} V ${h - rr} A ${rr} ${rr} 0 0 1 ${w - rr} ${h} H ${rr} A ${rr} ${rr} 0 0 1 0 ${h - rr} V ${rr} A ${rr} ${rr} 0 0 1 ${rr} 0 Z`
  1589. }
  1590. return `M 0 0 H ${w} V ${h} H 0 Z`
  1591. }
  1592. </script>
  1593. <style scoped lang="less">
  1594. .pptx-preview {
  1595. width: 100%;
  1596. }
  1597. .pptx-empty {
  1598. padding: 40px 12px;
  1599. color: #909399;
  1600. text-align: center;
  1601. }
  1602. .pptx-slides {
  1603. display: flex;
  1604. flex-direction: column;
  1605. gap: 16px;
  1606. align-items: center;
  1607. }
  1608. .pptx-slide-shell {
  1609. position: relative;
  1610. display: flex;
  1611. align-items: flex-start;
  1612. justify-content: center;
  1613. }
  1614. .pptx-slide {
  1615. position: absolute;
  1616. left: 0;
  1617. top: 0;
  1618. transform-origin: top left;
  1619. border-radius: 6px;
  1620. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  1621. overflow: hidden;
  1622. }
  1623. .pptx-element {
  1624. overflow: visible;
  1625. }
  1626. .pptx-img,
  1627. .pptx-media {
  1628. width: 100%;
  1629. height: 100%;
  1630. }
  1631. .pptx-media {
  1632. object-fit: contain;
  1633. }
  1634. .pptx-shape-svg {
  1635. width: 100%;
  1636. height: 100%;
  1637. display: block;
  1638. }
  1639. .pptx-chart {
  1640. width: 100%;
  1641. height: 100%;
  1642. display: block;
  1643. }
  1644. .pptx-group-inner {
  1645. width: 100%;
  1646. height: 100%;
  1647. position: relative;
  1648. }
  1649. .pptx-table-wrap {
  1650. width: 100%;
  1651. height: 100%;
  1652. }
  1653. .pptx-table {
  1654. width: 100%;
  1655. height: 100%;
  1656. table-layout: fixed;
  1657. border-collapse: collapse;
  1658. }
  1659. .pptx-table-html {
  1660. width: 100%;
  1661. height: 100%;
  1662. overflow: hidden;
  1663. pointer-events: none;
  1664. }
  1665. .pptx-html {
  1666. width: 100%;
  1667. height: 100%;
  1668. overflow: visible;
  1669. pointer-events: none;
  1670. }
  1671. .pptx-html-inner {
  1672. white-space: pre-wrap;
  1673. overflow-wrap: anywhere;
  1674. word-break: break-word;
  1675. }
  1676. .pptx-html :deep(p) {
  1677. margin: 0;
  1678. padding: 0;
  1679. word-break: break-word;
  1680. }
  1681. .pptx-html :deep(ul),
  1682. .pptx-html :deep(ol) {
  1683. margin: 0;
  1684. padding: 0 0 0 20px;
  1685. word-break: break-word;
  1686. }
  1687. .pptx-table-html :deep(p) {
  1688. margin: 0;
  1689. padding: 0;
  1690. word-break: break-word;
  1691. }
  1692. .pptx-table-html :deep(ul),
  1693. .pptx-table-html :deep(ol) {
  1694. margin: 0;
  1695. padding: 0 0 0 20px;
  1696. word-break: break-word;
  1697. }
  1698. </style>