├── .eslintignore ├── favicon.ico ├── src ├── constants.js ├── shadow.js ├── position.js ├── readXmlFile.js ├── schemeColor.js ├── animation.js ├── align.js ├── border.js ├── utils.js ├── fontStyle.js ├── shape.js ├── math.js ├── text.js ├── color.js ├── chart.js ├── table.js ├── fill.js └── pptxtojson.js ├── .babelrc.cjs ├── .gitignore ├── LICENSE ├── rollup.config.js ├── package.json ├── .eslintrc.cjs ├── README.md ├── dist └── index.d.ts └── index.html /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /public -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipipi-pikachu/pptxtojson/HEAD/favicon.ico -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const RATIO_Inches_EMUs = 914400 // 1英寸 = 914400EMUs 2 | export const RATIO_Inches_Points = 72 // 1英寸 = 72pt 3 | export const RATIO_EMUs_Points = RATIO_Inches_Points / RATIO_Inches_EMUs // 1EMUs = (72 / 914400)pt -------------------------------------------------------------------------------- /.babelrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | modules: false, 7 | } 8 | ] 9 | ], 10 | plugins: [ 11 | [ 12 | '@babel/plugin-transform-runtime', 13 | ] 14 | ] 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /src/shadow.js: -------------------------------------------------------------------------------- 1 | import { getSolidFill } from './fill' 2 | import { RATIO_EMUs_Points } from './constants' 3 | 4 | export function getShadow(node, warpObj) { 5 | const chdwClrNode = getSolidFill(node, undefined, undefined, warpObj) 6 | const outerShdwAttrs = node['attrs'] 7 | const dir = outerShdwAttrs['dir'] ? (parseInt(outerShdwAttrs['dir']) / 60000) : 0 8 | const dist = outerShdwAttrs['dist'] ? parseInt(outerShdwAttrs['dist']) * RATIO_EMUs_Points : 0 9 | const blurRad = outerShdwAttrs['blurRad'] ? parseInt(outerShdwAttrs['blurRad']) * RATIO_EMUs_Points : '' 10 | const vx = dist * Math.sin(dir * Math.PI / 180) 11 | const hx = dist * Math.cos(dir * Math.PI / 180) 12 | 13 | return { 14 | h: hx, 15 | v: vx, 16 | blur: blurRad, 17 | color: chdwClrNode, 18 | } 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-PRESENT pipipi-pikachu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/position.js: -------------------------------------------------------------------------------- 1 | import { RATIO_EMUs_Points } from './constants' 2 | import { numberToFixed } from './utils' 3 | 4 | export function getPosition(slideSpNode, slideLayoutSpNode, slideMasterSpNode) { 5 | let off 6 | 7 | if (slideSpNode) off = slideSpNode['a:off']['attrs'] 8 | else if (slideLayoutSpNode) off = slideLayoutSpNode['a:off']['attrs'] 9 | else if (slideMasterSpNode) off = slideMasterSpNode['a:off']['attrs'] 10 | 11 | if (!off) return { top: 0, left: 0 } 12 | 13 | return { 14 | top: numberToFixed(parseInt(off['y']) * RATIO_EMUs_Points), 15 | left: numberToFixed(parseInt(off['x']) * RATIO_EMUs_Points), 16 | } 17 | } 18 | 19 | export function getSize(slideSpNode, slideLayoutSpNode, slideMasterSpNode) { 20 | let ext 21 | 22 | if (slideSpNode) ext = slideSpNode['a:ext']['attrs'] 23 | else if (slideLayoutSpNode) ext = slideLayoutSpNode['a:ext']['attrs'] 24 | else if (slideMasterSpNode) ext = slideMasterSpNode['a:ext']['attrs'] 25 | 26 | if (!ext) return { width: 0, height: 0 } 27 | 28 | return { 29 | width: numberToFixed(parseInt(ext['cx']) * RATIO_EMUs_Points), 30 | height: numberToFixed(parseInt(ext['cy']) * RATIO_EMUs_Points), 31 | } 32 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import { babel } from '@rollup/plugin-babel' 4 | import eslint from '@rollup/plugin-eslint' 5 | import terser from '@rollup/plugin-terser' 6 | import globals from 'rollup-plugin-node-globals' 7 | import builtins from 'rollup-plugin-node-builtins' 8 | 9 | const onwarn = warning => { 10 | if (warning.code === 'CIRCULAR_DEPENDENCY') return 11 | 12 | console.warn(`(!) ${warning.message}`) // eslint-disable-line 13 | } 14 | 15 | export default { 16 | input: 'src/pptxtojson.js', 17 | onwarn, 18 | output: [ 19 | { 20 | file: 'dist/index.umd.js', 21 | format: 'umd', 22 | name: 'pptxtojson', 23 | sourcemap: true, 24 | }, 25 | { 26 | file: 'dist/index.cjs', 27 | format: 'cjs', 28 | sourcemap: true, 29 | }, 30 | { 31 | file: 'dist/index.js', 32 | format: 'es', 33 | sourcemap: true, 34 | }, 35 | ], 36 | plugins: [ 37 | nodeResolve({ 38 | preferBuiltins: false, 39 | }), 40 | commonjs(), 41 | eslint(), 42 | babel({ 43 | babelHelpers: 'runtime', 44 | exclude: ['node_modules/**'], 45 | }), 46 | terser(), 47 | globals(), 48 | builtins(), 49 | ] 50 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pptxtojson", 3 | "version": "1.8.0", 4 | "description": "A javascript tool for parsing .pptx file", 5 | "type": "module", 6 | "main": "./dist/index.umd.js", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "scripts": { 10 | "clean:dist": "rimraf dist", 11 | "dev": "rollup -c -w", 12 | "lint": "eslint src --ext .js,.jsx,.ts,.tsx" 13 | }, 14 | "author": "pipipi_pikachu@163.com", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/pipipi-pikachu/pptxtojson" 18 | }, 19 | "keywords": [ 20 | "ppt", 21 | "pptx", 22 | "json" 23 | ], 24 | "homepage": "https://github.com/pipipi-pikachu/pptxtojson", 25 | "license": "MIT", 26 | "publishConfig": { 27 | "registry": "https://registry.npmjs.org" 28 | }, 29 | "dependencies": { 30 | "jszip": "^3.10.1", 31 | "tinycolor2": "1.6.0", 32 | "txml": "^5.1.1" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.20.2", 36 | "@babel/plugin-transform-runtime": "^7.19.6", 37 | "@babel/preset-env": "^7.20.2", 38 | "@babel/runtime": "^7.20.1", 39 | "@rollup/plugin-babel": "^6.0.2", 40 | "@rollup/plugin-commonjs": "^23.0.2", 41 | "@rollup/plugin-eslint": "^9.0.1", 42 | "@rollup/plugin-node-resolve": "^15.0.1", 43 | "@rollup/plugin-terser": "^0.1.0", 44 | "eslint": "^8.27.0", 45 | "rollup": "^3.3.0", 46 | "rollup-plugin-node-builtins": "^2.1.2", 47 | "rollup-plugin-node-globals": "^1.4.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/readXmlFile.js: -------------------------------------------------------------------------------- 1 | import * as txml from 'txml/dist/txml.mjs' 2 | 3 | let cust_attr_order = 0 4 | 5 | export function simplifyLostLess(children, parentAttributes = {}) { 6 | const out = {} 7 | if (!children.length) return out 8 | 9 | if (children.length === 1 && typeof children[0] === 'string') { 10 | return Object.keys(parentAttributes).length ? { 11 | attrs: { order: cust_attr_order++, ...parentAttributes }, 12 | value: children[0], 13 | } : children[0] 14 | } 15 | for (const child of children) { 16 | if (typeof child !== 'object') return 17 | if (child.tagName === '?xml') continue 18 | 19 | if (!out[child.tagName]) out[child.tagName] = [] 20 | 21 | const kids = simplifyLostLess(child.children || [], child.attributes) 22 | 23 | if (typeof kids === 'object') { 24 | if (!kids.attrs) kids.attrs = { order: cust_attr_order++ } 25 | else kids.attrs.order = cust_attr_order++ 26 | } 27 | if (Object.keys(child.attributes || {}).length) { 28 | kids.attrs = { ...kids.attrs, ...child.attributes } 29 | } 30 | out[child.tagName].push(kids) 31 | } 32 | for (const child in out) { 33 | if (out[child].length === 1) out[child] = out[child][0] 34 | } 35 | 36 | return out 37 | } 38 | 39 | export async function readXmlFile(zip, filename) { 40 | try { 41 | const data = await zip.file(filename).async('string') 42 | return simplifyLostLess(txml.parse(data)) 43 | } 44 | catch { 45 | return null 46 | } 47 | } -------------------------------------------------------------------------------- /src/schemeColor.js: -------------------------------------------------------------------------------- 1 | import { getTextByPathList } from './utils' 2 | 3 | export function getSchemeColorFromTheme(schemeClr, warpObj, clrMap, phClr) { 4 | let color 5 | let slideLayoutClrOvride 6 | if (clrMap) slideLayoutClrOvride = clrMap 7 | else { 8 | let sldClrMapOvr = getTextByPathList(warpObj['slideContent'], ['p:sld', 'p:clrMapOvr', 'a:overrideClrMapping', 'attrs']) 9 | if (sldClrMapOvr) slideLayoutClrOvride = sldClrMapOvr 10 | else { 11 | sldClrMapOvr = getTextByPathList(warpObj['slideLayoutContent'], ['p:sldLayout', 'p:clrMapOvr', 'a:overrideClrMapping', 'attrs']) 12 | if (sldClrMapOvr) slideLayoutClrOvride = sldClrMapOvr 13 | else { 14 | slideLayoutClrOvride = getTextByPathList(warpObj['slideMasterContent'], ['p:sldMaster', 'p:clrMap', 'attrs']) 15 | } 16 | } 17 | } 18 | const schmClrName = schemeClr.substr(2) 19 | if (schmClrName === 'phClr' && phClr) color = phClr 20 | else { 21 | if (slideLayoutClrOvride) { 22 | switch (schmClrName) { 23 | case 'tx1': 24 | case 'tx2': 25 | case 'bg1': 26 | case 'bg2': 27 | schemeClr = 'a:' + slideLayoutClrOvride[schmClrName] 28 | break 29 | default: 30 | break 31 | } 32 | } 33 | else { 34 | switch (schemeClr) { 35 | case 'tx1': 36 | schemeClr = 'a:dk1' 37 | break 38 | case 'tx2': 39 | schemeClr = 'a:dk2' 40 | break 41 | case 'bg1': 42 | schemeClr = 'a:lt1' 43 | break 44 | case 'bg2': 45 | schemeClr = 'a:lt2' 46 | break 47 | default: 48 | break 49 | } 50 | } 51 | const refNode = getTextByPathList(warpObj['themeContent'], ['a:theme', 'a:themeElements', 'a:clrScheme', schemeClr]) 52 | color = getTextByPathList(refNode, ['a:srgbClr', 'attrs', 'val']) 53 | if (!color && refNode) color = getTextByPathList(refNode, ['a:sysClr', 'attrs', 'lastClr']) 54 | } 55 | return color 56 | } -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | browser: true, 5 | es6: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 2020, 12 | sourceType: 'module', 13 | }, 14 | rules: { 15 | 'curly': ['error', 'multi-line'], 16 | 'eqeqeq': ['error', 'always'], 17 | 'semi': ['error', 'never'], 18 | 'indent': ['error', 2, { 19 | 'SwitchCase': 1, 20 | }], 21 | 'quotes': ['error', 'single', { 22 | 'avoidEscape': true, 23 | 'allowTemplateLiterals': true, 24 | }], 25 | 'key-spacing': ['error', { 26 | 'beforeColon': false, 27 | 'afterColon': true, 28 | 'mode': 'strict', 29 | }], 30 | 'no-empty': 'error', 31 | 'no-else-return': 'error', 32 | 'no-multi-spaces': 'error', 33 | 'require-await': 'error', 34 | 'brace-style': ['error', 'stroustrup'], 35 | 'spaced-comment': ['error', 'always'], 36 | 'arrow-spacing': 'error', 37 | 'no-duplicate-imports': 'error', 38 | 'comma-spacing': ['error', { 39 | 'before': false, 40 | 'after': true, 41 | }], 42 | 'default-case': 'error', 43 | 'consistent-this': ['error', '_this'], 44 | 'max-depth': ['error', 6], 45 | 'max-lines': ['error', 2000], 46 | 'no-multi-str': 'error', 47 | 'space-infix-ops': 'error', 48 | 'space-before-blocks': ['error', 'always'], 49 | 'space-before-function-paren': ['error', { 50 | 'named': 'never', 51 | 'anonymous': 'never', 52 | 'asyncArrow': 'always', 53 | }], 54 | 'keyword-spacing': ['error'], 55 | 'prefer-const': 'error', 56 | 'no-useless-return': 'error', 57 | 'array-bracket-spacing': 'error', 58 | 'no-useless-escape': 'off', 59 | 'no-case-declarations': 'off', 60 | 'no-eval': 'error', 61 | 'no-var': 'error', 62 | 'no-with': 'error', 63 | 'no-alert': 'warn', 64 | 'no-console': 'warn', 65 | 'no-debugger': 'warn', 66 | }, 67 | overrides: [ 68 | { 69 | files: [ 70 | '**/__tests__/*.{j,t}s?(x)', 71 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 72 | ], 73 | env: { 74 | jest: true, 75 | }, 76 | }, 77 | ], 78 | } -------------------------------------------------------------------------------- /src/animation.js: -------------------------------------------------------------------------------- 1 | import { getTextByPathList } from './utils' 2 | 3 | export function findTransitionNode(content, rootElement) { 4 | if (!content || !rootElement) return null 5 | 6 | const path1 = [rootElement, 'p:transition'] 7 | let transitionNode = getTextByPathList(content, path1) 8 | if (transitionNode) return transitionNode 9 | 10 | const path2 = [rootElement, 'mc:AlternateContent', 'mc:Choice', 'p:transition'] 11 | transitionNode = getTextByPathList(content, path2) 12 | if (transitionNode) return transitionNode 13 | 14 | const path3 = [rootElement, 'mc:AlternateContent', 'mc:Fallback', 'p:transition'] 15 | transitionNode = getTextByPathList(content, path3) 16 | 17 | return transitionNode 18 | } 19 | 20 | export function parseTransition(transitionNode) { 21 | if (!transitionNode) return null 22 | 23 | const transition = { 24 | type: 'none', 25 | duration: 1000, 26 | direction: null, 27 | } 28 | 29 | const attrs = transitionNode.attrs || {} 30 | 31 | let durationFound = false 32 | const durRegex = /^p\d{2}:dur$/ 33 | for (const key in attrs) { 34 | if (durRegex.test(key) && !isNaN(parseInt(attrs[key], 10))) { 35 | transition.duration = parseInt(attrs[key], 10) 36 | durationFound = true 37 | break 38 | } 39 | } 40 | 41 | if (!durationFound && attrs.spd) { 42 | switch (attrs.spd) { 43 | case 'slow': 44 | transition.duration = 1000 45 | break 46 | case 'med': 47 | transition.duration = 800 48 | break 49 | case 'fast': 50 | transition.duration = 500 51 | break 52 | default: 53 | transition.duration = 1000 54 | break 55 | } 56 | } 57 | 58 | if (attrs.advClick === '0' && attrs.advTm) { 59 | transition.autoNextAfter = parseInt(attrs.advTm, 10) 60 | } 61 | 62 | const effectRegex = /^(p|p\d{2}):/ 63 | for (const key in transitionNode) { 64 | if (key !== 'attrs' && effectRegex.test(key)) { 65 | const effectNode = transitionNode[key] 66 | transition.type = key.substring(key.indexOf(':') + 1) 67 | 68 | if (effectNode && effectNode.attrs) { 69 | const effectAttrs = effectNode.attrs 70 | 71 | if (effectAttrs.dur && !isNaN(parseInt(effectAttrs.dur, 10))) { 72 | if (!durationFound) transition.duration = parseInt(effectAttrs.dur, 10) 73 | } 74 | if (effectAttrs.dir) transition.direction = effectAttrs.dir 75 | } 76 | break 77 | } 78 | } 79 | 80 | return transition 81 | } -------------------------------------------------------------------------------- /src/align.js: -------------------------------------------------------------------------------- 1 | import { getTextByPathList } from './utils' 2 | 3 | export function getHorizontalAlign(node, pNode, type, warpObj) { 4 | let algn = getTextByPathList(node, ['a:pPr', 'attrs', 'algn']) 5 | if (!algn) algn = getTextByPathList(pNode, ['a:pPr', 'attrs', 'algn']) 6 | 7 | if (!algn) { 8 | if (type === 'title' || type === 'ctrTitle' || type === 'subTitle') { 9 | let lvlIdx = 1 10 | const lvlNode = getTextByPathList(pNode, ['a:pPr', 'attrs', 'lvl']) 11 | if (lvlNode) { 12 | lvlIdx = parseInt(lvlNode) + 1 13 | } 14 | const lvlStr = 'a:lvl' + lvlIdx + 'pPr' 15 | algn = getTextByPathList(warpObj, ['slideLayoutTables', 'typeTable', type, 'p:txBody', 'a:lstStyle', lvlStr, 'attrs', 'algn']) 16 | if (!algn) algn = getTextByPathList(warpObj, ['slideMasterTables', 'typeTable', type, 'p:txBody', 'a:lstStyle', lvlStr, 'attrs', 'algn']) 17 | if (!algn) algn = getTextByPathList(warpObj, ['slideMasterTextStyles', 'p:titleStyle', lvlStr, 'attrs', 'algn']) 18 | if (!algn && type === 'subTitle') { 19 | algn = getTextByPathList(warpObj, ['slideMasterTextStyles', 'p:bodyStyle', lvlStr, 'attrs', 'algn']) 20 | } 21 | } 22 | else if (type === 'body') { 23 | algn = getTextByPathList(warpObj, ['slideMasterTextStyles', 'p:bodyStyle', 'a:lvl1pPr', 'attrs', 'algn']) 24 | } 25 | else { 26 | algn = getTextByPathList(warpObj, ['slideMasterTables', 'typeTable', type, 'p:txBody', 'a:lstStyle', 'a:lvl1pPr', 'attrs', 'algn']) 27 | } 28 | } 29 | 30 | let align = 'left' 31 | if (algn) { 32 | switch (algn) { 33 | case 'l': 34 | align = 'left' 35 | break 36 | case 'r': 37 | align = 'right' 38 | break 39 | case 'ctr': 40 | align = 'center' 41 | break 42 | case 'just': 43 | align = 'justify' 44 | break 45 | case 'dist': 46 | align = 'justify' 47 | break 48 | default: 49 | align = 'inherit' 50 | } 51 | } 52 | return align 53 | } 54 | 55 | export function getVerticalAlign(node, slideLayoutSpNode, slideMasterSpNode) { 56 | let anchor = getTextByPathList(node, ['p:txBody', 'a:bodyPr', 'attrs', 'anchor']) 57 | if (!anchor) { 58 | anchor = getTextByPathList(slideLayoutSpNode, ['p:txBody', 'a:bodyPr', 'attrs', 'anchor']) 59 | if (!anchor) { 60 | anchor = getTextByPathList(slideMasterSpNode, ['p:txBody', 'a:bodyPr', 'attrs', 'anchor']) 61 | if (!anchor) anchor = 't' 62 | } 63 | } 64 | return (anchor === 'ctr') ? 'mid' : ((anchor === 'b') ? 'down' : 'up') 65 | } -------------------------------------------------------------------------------- /src/border.js: -------------------------------------------------------------------------------- 1 | import tinycolor from 'tinycolor2' 2 | import { getSchemeColorFromTheme } from './schemeColor' 3 | import { getTextByPathList } from './utils' 4 | 5 | export function getBorder(node, elType, warpObj) { 6 | let lineNode = getTextByPathList(node, ['p:spPr', 'a:ln']) 7 | if (!lineNode) { 8 | const lnRefNode = getTextByPathList(node, ['p:style', 'a:lnRef']) 9 | if (lnRefNode) { 10 | const lnIdx = getTextByPathList(lnRefNode, ['attrs', 'idx']) 11 | lineNode = warpObj['themeContent']['a:theme']['a:themeElements']['a:fmtScheme']['a:lnStyleLst']['a:ln'][Number(lnIdx) - 1] 12 | } 13 | } 14 | if (!lineNode) lineNode = node 15 | 16 | const isNoFill = getTextByPathList(lineNode, ['a:noFill']) 17 | 18 | let borderWidth = isNoFill ? 0 : (parseInt(getTextByPathList(lineNode, ['attrs', 'w'])) / 12700) 19 | if (isNaN(borderWidth)) { 20 | if (lineNode) borderWidth = 0 21 | else if (elType !== 'obj') borderWidth = 0 22 | else borderWidth = 1 23 | } 24 | 25 | let borderColor = getTextByPathList(lineNode, ['a:solidFill', 'a:srgbClr', 'attrs', 'val']) 26 | if (!borderColor) { 27 | const schemeClrNode = getTextByPathList(lineNode, ['a:solidFill', 'a:schemeClr']) 28 | const schemeClr = 'a:' + getTextByPathList(schemeClrNode, ['attrs', 'val']) 29 | borderColor = getSchemeColorFromTheme(schemeClr, warpObj) 30 | } 31 | 32 | if (!borderColor) { 33 | const schemeClrNode = getTextByPathList(node, ['p:style', 'a:lnRef', 'a:schemeClr']) 34 | const schemeClr = 'a:' + getTextByPathList(schemeClrNode, ['attrs', 'val']) 35 | borderColor = getSchemeColorFromTheme(schemeClr, warpObj) 36 | 37 | if (borderColor) { 38 | let shade = getTextByPathList(schemeClrNode, ['a:shade', 'attrs', 'val']) 39 | 40 | if (shade) { 41 | shade = parseInt(shade) / 100000 42 | 43 | const color = tinycolor('#' + borderColor).toHsl() 44 | borderColor = tinycolor({ h: color.h, s: color.s, l: color.l * shade, a: color.a }).toHex() 45 | } 46 | } 47 | } 48 | 49 | if (!borderColor) borderColor = '#000000' 50 | else borderColor = `#${borderColor}` 51 | 52 | const type = getTextByPathList(lineNode, ['a:prstDash', 'attrs', 'val']) 53 | let borderType = 'solid' 54 | let strokeDasharray = '0' 55 | switch (type) { 56 | case 'solid': 57 | borderType = 'solid' 58 | strokeDasharray = '0' 59 | break 60 | case 'dash': 61 | borderType = 'dashed' 62 | strokeDasharray = '5' 63 | break 64 | case 'dashDot': 65 | borderType = 'dashed' 66 | strokeDasharray = '5, 5, 1, 5' 67 | break 68 | case 'dot': 69 | borderType = 'dotted' 70 | strokeDasharray = '1, 5' 71 | break 72 | case 'lgDash': 73 | borderType = 'dashed' 74 | strokeDasharray = '10, 5' 75 | break 76 | case 'lgDashDotDot': 77 | borderType = 'dotted' 78 | strokeDasharray = '10, 5, 1, 5, 1, 5' 79 | break 80 | case 'sysDash': 81 | borderType = 'dashed' 82 | strokeDasharray = '5, 2' 83 | break 84 | case 'sysDashDot': 85 | borderType = 'dotted' 86 | strokeDasharray = '5, 2, 1, 5' 87 | break 88 | case 'sysDashDotDot': 89 | borderType = 'dotted' 90 | strokeDasharray = '5, 2, 1, 5, 1, 5' 91 | break 92 | case 'sysDot': 93 | borderType = 'dotted' 94 | strokeDasharray = '2, 5' 95 | break 96 | default: 97 | } 98 | 99 | return { 100 | borderColor, 101 | borderWidth, 102 | borderType, 103 | strokeDasharray, 104 | } 105 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function base64ArrayBuffer(arrayBuffer) { 2 | const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 3 | const bytes = new Uint8Array(arrayBuffer) 4 | const byteLength = bytes.byteLength 5 | const byteRemainder = byteLength % 3 6 | const mainLength = byteLength - byteRemainder 7 | 8 | let base64 = '' 9 | let a, b, c, d 10 | let chunk 11 | 12 | for (let i = 0; i < mainLength; i = i + 3) { 13 | chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2] 14 | a = (chunk & 16515072) >> 18 15 | b = (chunk & 258048) >> 12 16 | c = (chunk & 4032) >> 6 17 | d = chunk & 63 18 | base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] 19 | } 20 | 21 | if (byteRemainder === 1) { 22 | chunk = bytes[mainLength] 23 | a = (chunk & 252) >> 2 24 | b = (chunk & 3) << 4 25 | base64 += encodings[a] + encodings[b] + '==' 26 | } 27 | else if (byteRemainder === 2) { 28 | chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] 29 | a = (chunk & 64512) >> 10 30 | b = (chunk & 1008) >> 4 31 | c = (chunk & 15) << 2 32 | base64 += encodings[a] + encodings[b] + encodings[c] + '=' 33 | } 34 | 35 | return base64 36 | } 37 | 38 | export function extractFileExtension(filename) { 39 | return filename.substr((~-filename.lastIndexOf('.') >>> 0) + 2) 40 | } 41 | 42 | export function eachElement(node, func) { 43 | if (!node) return node 44 | 45 | let result = '' 46 | if (node.constructor === Array) { 47 | for (let i = 0; i < node.length; i++) { 48 | result += func(node[i], i) 49 | } 50 | } 51 | else result += func(node, 0) 52 | 53 | return result 54 | } 55 | 56 | export function getTextByPathList(node, path) { 57 | if (!node) return node 58 | 59 | for (const key of path) { 60 | node = node[key] 61 | if (!node) return node 62 | } 63 | 64 | return node 65 | } 66 | 67 | export function angleToDegrees(angle) { 68 | if (!angle) return 0 69 | return Math.round(angle / 60000) 70 | } 71 | 72 | export function escapeHtml(text) { 73 | const map = { 74 | '&': '&', 75 | '<': '<', 76 | '>': '>', 77 | '"': '"', 78 | "'": ''', 79 | } 80 | return text.replace(/[&<>"']/g, m => map[m]) 81 | } 82 | 83 | export function getMimeType(imgFileExt) { 84 | let mimeType = '' 85 | switch (imgFileExt.toLowerCase()) { 86 | case 'jpg': 87 | case 'jpeg': 88 | mimeType = 'image/jpeg' 89 | break 90 | case 'png': 91 | mimeType = 'image/png' 92 | break 93 | case 'gif': 94 | mimeType = 'image/gif' 95 | break 96 | case 'emf': 97 | mimeType = 'image/x-emf' 98 | break 99 | case 'wmf': 100 | mimeType = 'image/x-wmf' 101 | break 102 | case 'svg': 103 | mimeType = 'image/svg+xml' 104 | break 105 | case 'mp4': 106 | mimeType = 'video/mp4' 107 | break 108 | case 'webm': 109 | mimeType = 'video/webm' 110 | break 111 | case 'ogg': 112 | mimeType = 'video/ogg' 113 | break 114 | case 'avi': 115 | mimeType = 'video/avi' 116 | break 117 | case 'mpg': 118 | mimeType = 'video/mpg' 119 | break 120 | case 'wmv': 121 | mimeType = 'video/wmv' 122 | break 123 | case 'mp3': 124 | mimeType = 'audio/mpeg' 125 | break 126 | case 'wav': 127 | mimeType = 'audio/wav' 128 | break 129 | case 'tif': 130 | mimeType = 'image/tiff' 131 | break 132 | case 'tiff': 133 | mimeType = 'image/tiff' 134 | break 135 | default: 136 | } 137 | return mimeType 138 | } 139 | 140 | export function isVideoLink(vdoFile) { 141 | const urlRegex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 142 | return urlRegex.test(vdoFile) 143 | } 144 | 145 | export function toHex(n) { 146 | let hex = n.toString(16) 147 | while (hex.length < 2) { 148 | hex = '0' + hex 149 | } 150 | return hex 151 | } 152 | 153 | export function hasValidText(htmlString) { 154 | if (typeof DOMParser === 'undefined') { 155 | const text = htmlString.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ') 156 | return text.trim() !== '' 157 | } 158 | 159 | const parser = new DOMParser() 160 | const doc = parser.parseFromString(htmlString, 'text/html') 161 | const text = doc.body.textContent || doc.body.innerText 162 | return text.trim() !== '' 163 | } 164 | 165 | export function numberToFixed(num, fractionDigits = 4) { 166 | return parseFloat(num.toFixed(fractionDigits)) 167 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎨 pptxtojson 2 | 一个运行在浏览器中,可以将 .pptx 文件转为可读的 JSON 数据的 JavaScript 库。 3 | 4 | > 与其他的pptx文件解析工具的最大区别在于: 5 | > 1. 直接运行在浏览器端; 6 | > 2. 解析结果是**可读**的 JSON 数据,而不仅仅是把 XML 文件内容原样翻译成难以理解的 JSON。 7 | 8 | 在线DEMO:https://pipipi-pikachu.github.io/pptxtojson/ 9 | 10 | # 🎯 注意事项 11 | ### ⚒️ 使用场景 12 | 本仓库诞生于项目 [PPTist](https://github.com/pipipi-pikachu/PPTist) ,希望为其“导入 .pptx 文件功能”提供一个参考示例。不过就目前来说,解析出来的PPT信息与源文件在样式上还是存在差异。 13 | 14 | 但如果你只是需要提取PPT文件的文本内容、媒体资源信息、结构信息等,或者对排版/样式精准度没有特别高的要求,那么 pptxtojson 可能会对你有帮助。 15 | 16 | ### 📏 长度值单位 17 | 输出的JSON中,所有数值长度值单位都为`pt`(point) 18 | > 注意:在0.x版本中,所有输出的长度值单位都是px(像素) 19 | 20 | # 🔨安装 21 | ``` 22 | npm install pptxtojson 23 | ``` 24 | 25 | # 💿用法 26 | 27 | ### 浏览器 28 | ```html 29 | 30 | ``` 31 | 32 | ```javascript 33 | import { parse } from 'pptxtojson' 34 | 35 | document.querySelector('input').addEventListener('change', evt => { 36 | const file = evt.target.files[0] 37 | 38 | const reader = new FileReader() 39 | reader.onload = async e => { 40 | const json = await parse(e.target.result) 41 | console.log(json) 42 | } 43 | reader.readAsArrayBuffer(file) 44 | }) 45 | ``` 46 | 47 | ### Node.js(实验性,1.5.0以上版本) 48 | ```javascript 49 | const pptxtojson = require('pptxtojson/dist/index.cjs') 50 | const fs = require('fs') 51 | 52 | async function func() { 53 | const buffer = fs.readFileSync('test.pptx') 54 | 55 | const json = await pptxtojson.parse(buffer.buffer) 56 | console.log(json) 57 | } 58 | 59 | func() 60 | ``` 61 | 62 | ### 输出示例 63 | ```javascript 64 | { 65 | "slides": [ 66 | { 67 | "fill": { 68 | "type": "color", 69 | "value": "#FF0000" 70 | }, 71 | "elements": [ 72 | { 73 | "left": 0, 74 | "top": 0, 75 | "width": 72, 76 | "height": 72, 77 | "borderColor": "#1F4E79", 78 | "borderWidth": 1, 79 | "borderType": "solid", 80 | "borderStrokeDasharray": 0, 81 | "fill": { 82 | "type": "color", 83 | "value": "#FF0000" 84 | }, 85 | "content": "

TEST

", 86 | "isFlipV": false, 87 | "isFlipH": false, 88 | "rotate": 0, 89 | "vAlign": "mid", 90 | "name": "矩形 1", 91 | "type": "shape", 92 | "shapType": "rect" 93 | }, 94 | // more... 95 | ], 96 | "layoutElements": [ 97 | // more... 98 | ], 99 | "note": "演讲者备注内容..." 100 | }, 101 | // more... 102 | ], 103 | "themeColors": ['#4472C4', '#ED7D31', '#A5A5A5', '#FFC000', '#5B9BD5', '#70AD47'], 104 | "size": { 105 | "width": 960, 106 | "height": 540 107 | } 108 | } 109 | ``` 110 | 111 | # 📕 完整功能支持 112 | 113 | - 幻灯片主题色 `themeColors` 114 | 115 | - 幻灯片尺寸 `size` 116 | - 宽度 `width` 117 | - 高度 `height` 118 | 119 | - 幻灯片页面 `slides` 120 | 121 | - 页面备注 `note` 122 | 123 | - 页面背景填充(颜色、图片、渐变、图案) `fill` 124 | - 纯色填充 `type='color'` 125 | - 图片填充 `type='image'` 126 | - 渐变填充 `type='gradient'` 127 | - 图案填充 `type='pattern'` 128 | 129 | - 页面切换动画 `transition` 130 | - 类型 `type` 131 | - 持续时间 `duration` 132 | - 方向 `direction` 133 | 134 | - 页面内元素 `elements` / 母版元素 `layoutElements` 135 | - 文字 136 | - 类型 `type='text'` 137 | - 水平坐标 `left` 138 | - 垂直坐标 `top` 139 | - 宽度 `width` 140 | - 高度 `height` 141 | - 边框颜色 `borderColor` 142 | - 边框宽度 `borderWidth` 143 | - 边框类型(实线、点线、虚线) `borderType` 144 | - 非实线边框样式 `borderStrokeDasharray` 145 | - 阴影 `shadow` 146 | - 填充(颜色、图片、渐变、图案) `fill` 147 | - 内容文字(HTML富文本:字体、字号、颜色、渐变、下划线、删除线、斜体、加粗、阴影、角标、超链接) `content` 148 | - 垂直翻转 `isFlipV` 149 | - 水平翻转 `isFlipH` 150 | - 旋转角度 `rotate` 151 | - 垂直对齐方向 `vAlign` 152 | - 是否为竖向文本 `isVertical` 153 | - 元素名 `name` 154 | 155 | - 图片 156 | - 类型 `type='image'` 157 | - 水平坐标 `left` 158 | - 垂直坐标 `top` 159 | - 宽度 `width` 160 | - 高度 `height` 161 | - 边框颜色 `borderColor` 162 | - 边框宽度 `borderWidth` 163 | - 边框类型(实线、点线、虚线) `borderType` 164 | - 非实线边框样式 `borderStrokeDasharray` 165 | - 裁剪形状 `geom` 166 | - 裁剪范围 `rect` 167 | - 图片地址(base64) `src` 168 | - 旋转角度 `rotate` 169 | - 滤镜 `filters` 170 | 171 | - 形状 172 | - 类型 `type='shape'` 173 | - 水平坐标 `left` 174 | - 垂直坐标 `top` 175 | - 宽度 `width` 176 | - 高度 `height` 177 | - 边框颜色 `borderColor` 178 | - 边框宽度 `borderWidth` 179 | - 边框类型(实线、点线、虚线) `borderType` 180 | - 非实线边框样式 `borderStrokeDasharray` 181 | - 阴影 `shadow` 182 | - 填充(颜色、图片、渐变、图案) `fill` 183 | - 内容文字(HTML富文本,与文字元素一致) `content` 184 | - 垂直翻转 `isFlipV` 185 | - 水平翻转 `isFlipH` 186 | - 旋转角度 `rotate` 187 | - 形状类型 `shapType` 188 | - 垂直对齐方向 `vAlign` 189 | - 形状路径 `path` 190 | - 元素名 `name` 191 | 192 | - 表格 193 | - 类型 `type='table'` 194 | - 水平坐标 `left` 195 | - 垂直坐标 `top` 196 | - 宽度 `width` 197 | - 高度 `height` 198 | - 边框(4边) `borders` 199 | - 表格数据 `data` 200 | - 行高 `rowHeights` 201 | - 列宽 `colWidths` 202 | 203 | - 图表 204 | - 类型 `type='chart'` 205 | - 水平坐标 `left` 206 | - 垂直坐标 `top` 207 | - 宽度 `width` 208 | - 高度 `height` 209 | - 图表数据 `data` 210 | - 图表主题色 `colors` 211 | - 图表类型 `chartType` 212 | - 柱状图方向 `barDir` 213 | - 是否带数据标记 `marker` 214 | - 环形图尺寸 `holeSize` 215 | - 分组模式 `grouping` 216 | - 图表样式 `style` 217 | 218 | - 视频 219 | - 类型 `type='video'` 220 | - 水平坐标 `left` 221 | - 垂直坐标 `top` 222 | - 宽度 `width` 223 | - 高度 `height` 224 | - 视频blob `blob` 225 | - 视频src `src` 226 | 227 | - 音频 228 | - 类型 `type='audio'` 229 | - 水平坐标 `left` 230 | - 垂直坐标 `top` 231 | - 宽度 `width` 232 | - 高度 `height` 233 | - 音频blob `blob` 234 | 235 | - 公式 236 | - 类型 `type='math'` 237 | - 水平坐标 `left` 238 | - 垂直坐标 `top` 239 | - 宽度 `width` 240 | - 高度 `height` 241 | - 公式图片 `picBase64` 242 | - LaTeX表达式(仅支持常见结构) `latex` 243 | - 文本(文本和公式混排时存在) `text` 244 | 245 | - Smart图 246 | - 类型 `type='diagram'` 247 | - 水平坐标 `left` 248 | - 垂直坐标 `top` 249 | - 宽度 `width` 250 | - 高度 `height` 251 | - 子元素集合 `elements` 252 | 253 | - 多元素组合 254 | - 类型 `type='group'` 255 | - 水平坐标 `left` 256 | - 垂直坐标 `top` 257 | - 宽度 `width` 258 | - 高度 `height` 259 | - 子元素集合 `elements` 260 | 261 | ### 更多类型请参考 👇 262 | [https://github.com/pipipi-pikachu/pptxtojson/blob/master/dist/index.d.ts](https://github.com/pipipi-pikachu/pptxtojson/blob/master/dist/index.d.ts) 263 | 264 | # 🙏 感谢 265 | 本仓库大量参考了 [PPTX2HTML](https://github.com/g21589/PPTX2HTML) 和 [PPTXjs](https://github.com/meshesha/PPTXjs) 的实现。 266 | > 与它们不同的是:PPTX2HTML 和 PPTXjs 是将PPT文件转换为能够运行的 HTML 页面,而 pptxtojson 做的是将PPT文件转换为干净的 JSON 数据,且在原有基础上进行了大量优化补充(包括代码质量和提取信息的完整度和准确度)。 267 | 268 | # 📄 开源协议 269 | MIT License | Copyright © 2020-PRESENT [pipipi-pikachu](https://github.com/pipipi-pikachu) -------------------------------------------------------------------------------- /src/fontStyle.js: -------------------------------------------------------------------------------- 1 | import { getTextByPathList } from './utils' 2 | import { getShadow } from './shadow' 3 | import { getFillType, getGradientFill, getSolidFill } from './fill' 4 | 5 | export function getFontType(node, type, warpObj) { 6 | let typeface = getTextByPathList(node, ['a:rPr', 'a:latin', 'attrs', 'typeface']) 7 | 8 | if (!typeface) { 9 | const fontSchemeNode = getTextByPathList(warpObj['themeContent'], ['a:theme', 'a:themeElements', 'a:fontScheme']) 10 | 11 | if (type === 'title' || type === 'subTitle' || type === 'ctrTitle') { 12 | typeface = getTextByPathList(fontSchemeNode, ['a:majorFont', 'a:latin', 'attrs', 'typeface']) 13 | } 14 | else if (type === 'body') { 15 | typeface = getTextByPathList(fontSchemeNode, ['a:minorFont', 'a:latin', 'attrs', 'typeface']) 16 | } 17 | else { 18 | typeface = getTextByPathList(fontSchemeNode, ['a:minorFont', 'a:latin', 'attrs', 'typeface']) 19 | } 20 | } 21 | 22 | return typeface || '' 23 | } 24 | 25 | export function getFontColor(node, pNode, lstStyle, pFontStyle, lvl, warpObj) { 26 | const rPrNode = getTextByPathList(node, ['a:rPr']) 27 | let filTyp, color 28 | if (rPrNode) { 29 | filTyp = getFillType(rPrNode) 30 | if (filTyp === 'SOLID_FILL') { 31 | const solidFillNode = rPrNode['a:solidFill'] 32 | color = getSolidFill(solidFillNode, undefined, undefined, warpObj) 33 | } 34 | if (filTyp === 'GRADIENT_FILL') { 35 | const gradientFillNode = rPrNode['a:gradFill'] 36 | const gradient = getGradientFill(gradientFillNode, warpObj) 37 | return gradient 38 | } 39 | } 40 | if (!color && getTextByPathList(lstStyle, ['a:lvl' + lvl + 'pPr', 'a:defRPr'])) { 41 | const lstStyledefRPr = getTextByPathList(lstStyle, ['a:lvl' + lvl + 'pPr', 'a:defRPr']) 42 | filTyp = getFillType(lstStyledefRPr) 43 | if (filTyp === 'SOLID_FILL') { 44 | const solidFillNode = lstStyledefRPr['a:solidFill'] 45 | color = getSolidFill(solidFillNode, undefined, undefined, warpObj) 46 | } 47 | } 48 | if (!color) { 49 | const sPstyle = getTextByPathList(pNode, ['p:style', 'a:fontRef']) 50 | if (sPstyle) color = getSolidFill(sPstyle, undefined, undefined, warpObj) 51 | if (!color && pFontStyle) color = getSolidFill(pFontStyle, undefined, undefined, warpObj) 52 | } 53 | return color || '' 54 | } 55 | 56 | export function getFontSize(node, slideLayoutSpNode, type, slideMasterTextStyles, textBodyNode, pNode) { 57 | let fontSize 58 | 59 | if (getTextByPathList(node, ['a:rPr', 'attrs', 'sz'])) fontSize = getTextByPathList(node, ['a:rPr', 'attrs', 'sz']) / 100 60 | 61 | if ((isNaN(fontSize) || !fontSize) && pNode) { 62 | if (getTextByPathList(pNode, ['a:endParaRPr', 'attrs', 'sz'])) { 63 | fontSize = getTextByPathList(pNode, ['a:endParaRPr', 'attrs', 'sz']) / 100 64 | } 65 | } 66 | 67 | if ((isNaN(fontSize) || !fontSize) && textBodyNode) { 68 | const lstStyle = getTextByPathList(textBodyNode, ['a:lstStyle']) 69 | if (lstStyle) { 70 | let lvl = 1 71 | if (pNode) { 72 | const lvlNode = getTextByPathList(pNode, ['a:pPr', 'attrs', 'lvl']) 73 | if (lvlNode !== undefined) lvl = parseInt(lvlNode) + 1 74 | } 75 | 76 | const sz = getTextByPathList(lstStyle, [`a:lvl${lvl}pPr`, 'a:defRPr', 'attrs', 'sz']) 77 | if (sz) fontSize = parseInt(sz) / 100 78 | } 79 | } 80 | 81 | if ((isNaN(fontSize) || !fontSize)) { 82 | const sz = getTextByPathList(slideLayoutSpNode, ['p:txBody', 'a:lstStyle', 'a:lvl1pPr', 'a:defRPr', 'attrs', 'sz']) 83 | if (sz) fontSize = parseInt(sz) / 100 84 | } 85 | 86 | if ((isNaN(fontSize) || !fontSize) && slideLayoutSpNode) { 87 | let lvl = 1 88 | if (pNode) { 89 | const lvlNode = getTextByPathList(pNode, ['a:pPr', 'attrs', 'lvl']) 90 | if (lvlNode !== undefined) lvl = parseInt(lvlNode) + 1 91 | } 92 | const layoutSz = getTextByPathList(slideLayoutSpNode, ['p:txBody', 'a:lstStyle', `a:lvl${lvl}pPr`, 'a:defRPr', 'attrs', 'sz']) 93 | if (layoutSz) fontSize = parseInt(layoutSz) / 100 94 | } 95 | 96 | if ((isNaN(fontSize) || !fontSize) && pNode) { 97 | const paraSz = getTextByPathList(pNode, ['a:pPr', 'a:defRPr', 'attrs', 'sz']) 98 | if (paraSz) fontSize = parseInt(paraSz) / 100 99 | } 100 | 101 | if (isNaN(fontSize) || !fontSize) { 102 | let sz 103 | if (type === 'title' || type === 'subTitle' || type === 'ctrTitle') { 104 | sz = getTextByPathList(slideMasterTextStyles, ['p:titleStyle', 'a:lvl1pPr', 'a:defRPr', 'attrs', 'sz']) 105 | } 106 | else if (type === 'body') { 107 | sz = getTextByPathList(slideMasterTextStyles, ['p:bodyStyle', 'a:lvl1pPr', 'a:defRPr', 'attrs', 'sz']) 108 | } 109 | else if (type === 'dt' || type === 'sldNum') { 110 | sz = '1200' 111 | } 112 | else if (!type) { 113 | sz = getTextByPathList(slideMasterTextStyles, ['p:otherStyle', 'a:lvl1pPr', 'a:defRPr', 'attrs', 'sz']) 114 | } 115 | if (sz) fontSize = parseInt(sz) / 100 116 | } 117 | 118 | const baseline = getTextByPathList(node, ['a:rPr', 'attrs', 'baseline']) 119 | if (baseline && !isNaN(fontSize)) fontSize -= 10 120 | 121 | fontSize = (isNaN(fontSize) || !fontSize) ? 18 : fontSize 122 | 123 | return fontSize + 'pt' 124 | } 125 | 126 | export function getFontBold(node) { 127 | return getTextByPathList(node, ['a:rPr', 'attrs', 'b']) === '1' ? 'bold' : '' 128 | } 129 | 130 | export function getFontItalic(node) { 131 | return getTextByPathList(node, ['a:rPr', 'attrs', 'i']) === '1' ? 'italic' : '' 132 | } 133 | 134 | export function getFontDecoration(node) { 135 | return getTextByPathList(node, ['a:rPr', 'attrs', 'u']) === 'sng' ? 'underline' : '' 136 | } 137 | 138 | export function getFontDecorationLine(node) { 139 | return getTextByPathList(node, ['a:rPr', 'attrs', 'strike']) === 'sngStrike' ? 'line-through' : '' 140 | } 141 | 142 | export function getFontSpace(node) { 143 | const spc = getTextByPathList(node, ['a:rPr', 'attrs', 'spc']) 144 | return spc ? (parseInt(spc) / 100 + 'pt') : '' 145 | } 146 | 147 | export function getFontSubscript(node) { 148 | const baseline = getTextByPathList(node, ['a:rPr', 'attrs', 'baseline']) 149 | if (!baseline) return '' 150 | return parseInt(baseline) > 0 ? 'super' : 'sub' 151 | } 152 | 153 | export function getFontShadow(node, warpObj) { 154 | const txtShadow = getTextByPathList(node, ['a:rPr', 'a:effectLst', 'a:outerShdw']) 155 | if (txtShadow) { 156 | const shadow = getShadow(txtShadow, warpObj) 157 | if (shadow) { 158 | const { h, v, blur, color } = shadow 159 | if (!isNaN(v) && !isNaN(h)) { 160 | return h + 'pt ' + v + 'pt ' + (blur ? blur + 'pt' : '') + ' ' + color 161 | } 162 | } 163 | } 164 | return '' 165 | } -------------------------------------------------------------------------------- /src/shape.js: -------------------------------------------------------------------------------- 1 | import { getTextByPathList } from './utils' 2 | 3 | export function shapeArc(cX, cY, rX, rY, stAng, endAng, isClose) { 4 | let dData 5 | let angle = stAng 6 | if (endAng >= stAng) { 7 | while (angle <= endAng) { 8 | const radians = angle * (Math.PI / 180) 9 | const x = cX + Math.cos(radians) * rX 10 | const y = cY + Math.sin(radians) * rY 11 | if (angle === stAng) { 12 | dData = ' M' + x + ' ' + y 13 | } 14 | dData += ' L' + x + ' ' + y 15 | angle++ 16 | } 17 | } 18 | else { 19 | while (angle > endAng) { 20 | const radians = angle * (Math.PI / 180) 21 | const x = cX + Math.cos(radians) * rX 22 | const y = cY + Math.sin(radians) * rY 23 | if (angle === stAng) { 24 | dData = ' M ' + x + ' ' + y 25 | } 26 | dData += ' L ' + x + ' ' + y 27 | angle-- 28 | } 29 | } 30 | dData += (isClose ? ' z' : '') 31 | return dData 32 | } 33 | 34 | export function getCustomShapePath(custShapType, w, h) { 35 | const pathLstNode = getTextByPathList(custShapType, ['a:pathLst']) 36 | let pathNodes = getTextByPathList(pathLstNode, ['a:path']) 37 | 38 | if (Array.isArray(pathNodes)) pathNodes = pathNodes.shift() 39 | 40 | const maxX = parseInt(pathNodes['attrs']['w']) 41 | const maxY = parseInt(pathNodes['attrs']['h']) 42 | const cX = maxX === 0 ? 0 : (1 / maxX) * w 43 | const cY = maxY === 0 ? 0 : (1 / maxY) * h 44 | let d = '' 45 | 46 | let moveToNode = getTextByPathList(pathNodes, ['a:moveTo']) 47 | 48 | let lnToNodes = pathNodes['a:lnTo'] 49 | let cubicBezToNodes = pathNodes['a:cubicBezTo'] 50 | const arcToNodes = pathNodes['a:arcTo'] 51 | let closeNode = getTextByPathList(pathNodes, ['a:close']) 52 | if (!Array.isArray(moveToNode)) moveToNode = [moveToNode] 53 | 54 | const multiSapeAry = [] 55 | if (moveToNode.length > 0) { 56 | Object.keys(moveToNode).forEach(key => { 57 | const moveToPtNode = moveToNode[key]['a:pt'] 58 | if (moveToPtNode) { 59 | Object.keys(moveToPtNode).forEach(key => { 60 | const moveToNoPt = moveToPtNode[key] 61 | const spX = moveToNoPt['attrs', 'x'] 62 | const spY = moveToNoPt['attrs', 'y'] 63 | const order = moveToNoPt['attrs', 'order'] 64 | multiSapeAry.push({ 65 | type: 'movto', 66 | x: spX, 67 | y: spY, 68 | order, 69 | }) 70 | }) 71 | } 72 | }) 73 | if (lnToNodes) { 74 | if (!Array.isArray(lnToNodes)) lnToNodes = [lnToNodes] 75 | Object.keys(lnToNodes).forEach(key => { 76 | const lnToPtNode = lnToNodes[key]['a:pt'] 77 | if (lnToPtNode) { 78 | Object.keys(lnToPtNode).forEach(key => { 79 | const lnToNoPt = lnToPtNode[key] 80 | const ptX = lnToNoPt['attrs', 'x'] 81 | const ptY = lnToNoPt['attrs', 'y'] 82 | const order = lnToNoPt['attrs', 'order'] 83 | multiSapeAry.push({ 84 | type: 'lnto', 85 | x: ptX, 86 | y: ptY, 87 | order, 88 | }) 89 | }) 90 | } 91 | }) 92 | } 93 | if (cubicBezToNodes) { 94 | const cubicBezToPtNodesAry = [] 95 | if (!Array.isArray(cubicBezToNodes)) cubicBezToNodes = [cubicBezToNodes] 96 | Object.keys(cubicBezToNodes).forEach(key => { 97 | cubicBezToPtNodesAry.push(cubicBezToNodes[key]['a:pt']) 98 | }) 99 | 100 | cubicBezToPtNodesAry.forEach(key => { 101 | const pts_ary = [] 102 | key.forEach(pt => { 103 | const pt_obj = { 104 | x: pt['attrs']['x'], 105 | y: pt['attrs']['y'], 106 | } 107 | pts_ary.push(pt_obj) 108 | }) 109 | const order = key[0]['attrs']['order'] 110 | multiSapeAry.push({ 111 | type: 'cubicBezTo', 112 | cubBzPt: pts_ary, 113 | order, 114 | }) 115 | }) 116 | } 117 | if (arcToNodes) { 118 | const arcToNodesAttrs = arcToNodes['attrs'] 119 | const order = arcToNodesAttrs['order'] 120 | const hR = arcToNodesAttrs['hR'] 121 | const wR = arcToNodesAttrs['wR'] 122 | const stAng = arcToNodesAttrs['stAng'] 123 | const swAng = arcToNodesAttrs['swAng'] 124 | let shftX = 0 125 | let shftY = 0 126 | const arcToPtNode = getTextByPathList(arcToNodes, ['a:pt', 'attrs']) 127 | if (arcToPtNode) { 128 | shftX = arcToPtNode['x'] 129 | shftY = arcToPtNode['y'] 130 | } 131 | multiSapeAry.push({ 132 | type: 'arcTo', 133 | hR: hR, 134 | wR: wR, 135 | stAng: stAng, 136 | swAng: swAng, 137 | shftX: shftX, 138 | shftY: shftY, 139 | order, 140 | }) 141 | } 142 | if (closeNode) { 143 | if (!Array.isArray(closeNode)) closeNode = [closeNode] 144 | Object.keys(closeNode).forEach(() => { 145 | multiSapeAry.push({ 146 | type: 'close', 147 | order: Infinity, 148 | }) 149 | }) 150 | } 151 | 152 | multiSapeAry.sort((a, b) => a.order - b.order) 153 | 154 | let k = 0 155 | while (k < multiSapeAry.length) { 156 | if (multiSapeAry[k].type === 'movto') { 157 | const spX = parseInt(multiSapeAry[k].x) * cX 158 | const spY = parseInt(multiSapeAry[k].y) * cY 159 | d += ' M' + spX + ',' + spY 160 | } 161 | else if (multiSapeAry[k].type === 'lnto') { 162 | const Lx = parseInt(multiSapeAry[k].x) * cX 163 | const Ly = parseInt(multiSapeAry[k].y) * cY 164 | d += ' L' + Lx + ',' + Ly 165 | } 166 | else if (multiSapeAry[k].type === 'cubicBezTo') { 167 | const Cx1 = parseInt(multiSapeAry[k].cubBzPt[0].x) * cX 168 | const Cy1 = parseInt(multiSapeAry[k].cubBzPt[0].y) * cY 169 | const Cx2 = parseInt(multiSapeAry[k].cubBzPt[1].x) * cX 170 | const Cy2 = parseInt(multiSapeAry[k].cubBzPt[1].y) * cY 171 | const Cx3 = parseInt(multiSapeAry[k].cubBzPt[2].x) * cX 172 | const Cy3 = parseInt(multiSapeAry[k].cubBzPt[2].y) * cY 173 | d += ' C' + Cx1 + ',' + Cy1 + ' ' + Cx2 + ',' + Cy2 + ' ' + Cx3 + ',' + Cy3 174 | } 175 | else if (multiSapeAry[k].type === 'arcTo') { 176 | const hR = parseInt(multiSapeAry[k].hR) * cX 177 | const wR = parseInt(multiSapeAry[k].wR) * cY 178 | const stAng = parseInt(multiSapeAry[k].stAng) / 60000 179 | const swAng = parseInt(multiSapeAry[k].swAng) / 60000 180 | const endAng = stAng + swAng 181 | d += shapeArc(wR, hR, wR, hR, stAng, endAng, false) 182 | } 183 | else if (multiSapeAry[k].type === 'close') d += 'z' 184 | k++ 185 | } 186 | } 187 | 188 | return d 189 | } -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface Shadow { 2 | h: number 3 | v: number 4 | blur: number 5 | color: string 6 | } 7 | 8 | export interface ColorFill { 9 | type: 'color' 10 | value: string 11 | } 12 | 13 | export interface ImageFill { 14 | type: 'image' 15 | value: { 16 | picBase64: string 17 | opacity: number 18 | } 19 | } 20 | 21 | export interface GradientFill { 22 | type: 'gradient' 23 | value: { 24 | path: 'line' | 'circle' | 'rect' | 'shape' 25 | rot: number 26 | colors: { 27 | pos: string 28 | color: string 29 | }[] 30 | } 31 | } 32 | 33 | export interface PatternFill { 34 | type: 'pattern' 35 | value: { 36 | type: string 37 | foregroundColor: string 38 | backgroundColor: string 39 | } 40 | } 41 | 42 | export type Fill = ColorFill | ImageFill | GradientFill | PatternFill 43 | 44 | export interface Border { 45 | borderColor: string 46 | borderWidth: number 47 | borderType:'solid' | 'dashed' | 'dotted' 48 | } 49 | 50 | export interface Shape { 51 | type: 'shape' 52 | left: number 53 | top: number 54 | width: number 55 | height: number 56 | borderColor: string 57 | borderWidth: number 58 | borderType: 'solid' | 'dashed' | 'dotted' 59 | borderStrokeDasharray: string 60 | shadow?: Shadow 61 | fill: Fill 62 | content: string 63 | isFlipV: boolean 64 | isFlipH: boolean 65 | rotate: number 66 | shapType: string 67 | vAlign: string 68 | path?: string 69 | name: string 70 | order: number 71 | } 72 | 73 | export interface Text { 74 | type: 'text' 75 | left: number 76 | top: number 77 | width: number 78 | height: number 79 | borderColor: string 80 | borderWidth: number 81 | borderType: 'solid' | 'dashed' | 'dotted' 82 | borderStrokeDasharray: string 83 | shadow?: Shadow 84 | fill: Fill 85 | isFlipV: boolean 86 | isFlipH: boolean 87 | isVertical: boolean 88 | rotate: number 89 | content: string 90 | vAlign: string 91 | name: string 92 | order: number 93 | } 94 | 95 | export interface Image { 96 | type: 'image' 97 | left: number 98 | top: number 99 | width: number 100 | height: number 101 | src: string 102 | rotate: number 103 | isFlipH: boolean 104 | isFlipV: boolean 105 | order: number 106 | rect?: { 107 | t?: number 108 | b?: number 109 | l?: number 110 | r?: number 111 | } 112 | geom: string 113 | borderColor: string 114 | borderWidth: number 115 | borderType: 'solid' | 'dashed' | 'dotted' 116 | borderStrokeDasharray: string 117 | filters?: { 118 | sharpen?: number 119 | colorTemperature?: number 120 | saturation?: number 121 | brightness?: number 122 | contrast?: number 123 | } 124 | } 125 | 126 | export interface TableCell { 127 | text: string 128 | rowSpan?: number 129 | colSpan?: number 130 | vMerge?: number 131 | hMerge?: number 132 | fillColor?: string 133 | fontColor?: string 134 | fontBold?: boolean 135 | borders: { 136 | top?: Border 137 | bottom?: Border 138 | left?: Border 139 | right?: Border 140 | } 141 | } 142 | export interface Table { 143 | type: 'table' 144 | left: number 145 | top: number 146 | width: number 147 | height: number 148 | data: TableCell[][] 149 | borders: { 150 | top?: Border 151 | bottom?: Border 152 | left?: Border 153 | right?: Border 154 | } 155 | order: number 156 | rowHeights: number[] 157 | colWidths: number[] 158 | } 159 | 160 | export type ChartType = 'lineChart' | 161 | 'line3DChart' | 162 | 'barChart' | 163 | 'bar3DChart' | 164 | 'pieChart' | 165 | 'pie3DChart' | 166 | 'doughnutChart' | 167 | 'areaChart' | 168 | 'area3DChart' | 169 | 'scatterChart' | 170 | 'bubbleChart' | 171 | 'radarChart' | 172 | 'surfaceChart' | 173 | 'surface3DChart' | 174 | 'stockChart' 175 | 176 | export interface ChartValue { 177 | x: string 178 | y: number 179 | } 180 | export interface ChartXLabel { 181 | [key: string]: string 182 | } 183 | export interface ChartItem { 184 | key: string 185 | values: ChartValue[] 186 | xlabels: ChartXLabel 187 | } 188 | export type ScatterChartData = [number[], number[]] 189 | export interface CommonChart { 190 | type: 'chart' 191 | left: number 192 | top: number 193 | width: number 194 | height: number 195 | data: ChartItem[] 196 | colors: string[] 197 | chartType: Exclude 198 | barDir?: 'bar' | 'col' 199 | marker?: boolean 200 | holeSize?: string 201 | grouping?: string 202 | style?: string 203 | order: number 204 | } 205 | export interface ScatterChart { 206 | type: 'chart' 207 | left: number 208 | top: number 209 | width: number 210 | height: number 211 | data: ScatterChartData 212 | colors: string[] 213 | chartType: 'scatterChart' | 'bubbleChart' 214 | order: number 215 | } 216 | export type Chart = CommonChart | ScatterChart 217 | 218 | export interface Video { 219 | type: 'video' 220 | left: number 221 | top: number 222 | width: number 223 | height: number 224 | blob?: string 225 | src?: string 226 | order: number 227 | } 228 | 229 | export interface Audio { 230 | type: 'audio' 231 | left: number 232 | top: number 233 | width: number 234 | height: number 235 | blob: string 236 | order: number 237 | } 238 | 239 | export interface Diagram { 240 | type: 'diagram' 241 | left: number 242 | top: number 243 | width: number 244 | height: number 245 | elements: (Shape | Text)[] 246 | order: number 247 | } 248 | 249 | export interface Math { 250 | type: 'math' 251 | left: number 252 | top: number 253 | width: number 254 | height: number 255 | latex: string 256 | picBase64: string 257 | order: number 258 | text?: string 259 | } 260 | 261 | export type BaseElement = Shape | Text | Image | Table | Chart | Video | Audio | Diagram | Math 262 | 263 | export interface Group { 264 | type: 'group' 265 | left: number 266 | top: number 267 | width: number 268 | height: number 269 | rotate: number 270 | elements: BaseElement[] 271 | order: number 272 | isFlipH: boolean 273 | isFlipV: boolean 274 | } 275 | export type Element = BaseElement | Group 276 | 277 | export interface SlideTransition { 278 | type: string 279 | duration: number 280 | direction: string | null 281 | } 282 | 283 | export interface Slide { 284 | fill: Fill 285 | elements: Element[] 286 | layoutElements: Element[] 287 | note: string 288 | transition?: SlideTransition | null 289 | } 290 | 291 | export interface Options { 292 | slideFactor?: number 293 | fontsizeFactor?: number 294 | } 295 | 296 | export const parse: (file: ArrayBuffer, options?: Options) => Promise<{ 297 | slides: Slide[] 298 | themeColors: string[] 299 | size: { 300 | width: number 301 | height: number 302 | } 303 | }> -------------------------------------------------------------------------------- /src/math.js: -------------------------------------------------------------------------------- 1 | import { getTextByPathList } from './utils' 2 | 3 | export function findOMath(obj) { 4 | let results = [] 5 | if (typeof obj !== 'object') return results 6 | if (obj['m:oMath']) results = results.concat(obj['m:oMath']) 7 | 8 | Object.values(obj).forEach(value => { 9 | if (Array.isArray(value) || typeof value === 'object') { 10 | results = results.concat(findOMath(value)) 11 | } 12 | }) 13 | return results 14 | } 15 | 16 | export function parseFraction(fraction) { 17 | const numerator = parseOMath(fraction['m:num']) 18 | const denominator = parseOMath(fraction['m:den']) 19 | return `\\frac{${numerator}}{${denominator}}` 20 | } 21 | export function parseSuperscript(superscript) { 22 | const base = parseOMath(superscript['m:e']) 23 | const sup = parseOMath(superscript['m:sup']) 24 | return `${base}^{${sup}}` 25 | } 26 | export function parseSubscript(subscript) { 27 | const base = parseOMath(subscript['m:e']) 28 | const sub = parseOMath(subscript['m:sub']) 29 | return `${base}_{${sub}}` 30 | } 31 | export function parseRadical(radical) { 32 | const degree = parseOMath(radical['m:deg']) 33 | const expression = parseOMath(radical['m:e']) 34 | return degree ? `\\sqrt[${degree}]{${expression}}` : `\\sqrt{${expression}}` 35 | } 36 | export function parseMatrix(matrix) { 37 | const rows = matrix['m:mr'] 38 | const matrixRows = rows.map((row) => { 39 | return row['m:e'].map((element) => parseOMath(element)).join(' & ') 40 | }) 41 | return `\\begin{matrix} ${matrixRows.join(' \\\\ ')} \\end{matrix}` 42 | } 43 | export function parseNary(nary) { 44 | const op = getTextByPathList(nary, ['m:naryPr', 'm:chr', 'attrs', 'm:val']) || '∫' 45 | const sub = parseOMath(nary['m:sub']) 46 | const sup = parseOMath(nary['m:sup']) 47 | const e = parseOMath(nary['m:e']) 48 | return `${op}_{${sub}}^{${sup}}{${e}}` 49 | } 50 | export function parseLimit(limit, type) { 51 | const base = parseOMath(limit['m:e']) 52 | const lim = parseOMath(limit['m:lim']) 53 | return type === 'low' ? `${base}_{${lim}}` : `${base}^{${lim}}` 54 | } 55 | export function parseDelimiter(delimiter) { 56 | let left = getTextByPathList(delimiter, ['m:dPr', 'm:begChr', 'attrs', 'm:val']) 57 | let right = getTextByPathList(delimiter, ['m:dPr', 'm:endChr', 'attrs', 'm:val']) 58 | if (!left && !right) { 59 | left = '(' 60 | right = ')' 61 | } 62 | if (left && right) { 63 | left = `\\left${left}` 64 | right = `\\right${right}` 65 | } 66 | const e = parseOMath(delimiter['m:e']) 67 | return `${left}${e}${right}` 68 | } 69 | export function parseFunction(func) { 70 | const name = parseOMath(func['m:fName']) 71 | const arg = parseOMath(func['m:e']) 72 | return `\\${name}{${arg}}` 73 | } 74 | export function parseGroupChr(groupChr) { 75 | const chr = getTextByPathList(groupChr, ['m:groupChrPr', 'm:chr', 'attrs', 'm:val']) 76 | const e = parseOMath(groupChr['m:e']) 77 | return `${chr}${e}${chr}` 78 | } 79 | export function parseEqArr(eqArr) { 80 | const equations = eqArr['m:e'].map((eq) => parseOMath(eq)).join(' \\\\ ') 81 | return `\\begin{cases} ${equations} \\end{cases}` 82 | } 83 | export function parseBar(bar) { 84 | const e = parseOMath(bar['m:e']) 85 | const pos = getTextByPathList(bar, ['m:barPr', 'm:pos', 'attrs', 'm:val']) 86 | return pos === 'top' ? `\\overline{${e}}` : `\\underline{${e}}` 87 | } 88 | export function parseAccent(accent) { 89 | const chr = getTextByPathList(accent, ['m:accPr', 'm:chr', 'attrs', 'm:val']) || '^' 90 | const e = parseOMath(accent['m:e']) 91 | switch (chr) { 92 | case '\u0301': 93 | return `\\acute{${e}}` 94 | case '\u0300': 95 | return `\\grave{${e}}` 96 | case '\u0302': 97 | return `\\hat{${e}}` 98 | case '\u0303': 99 | return `\\tilde{${e}}` 100 | case '\u0304': 101 | return `\\bar{${e}}` 102 | case '\u0306': 103 | return `\\breve{${e}}` 104 | case '\u0307': 105 | return `\\dot{${e}}` 106 | case '\u0308': 107 | return `\\ddot{${e}}` 108 | case '\u030A': 109 | return `\\mathring{${e}}` 110 | case '\u030B': 111 | return `\\H{${e}}` 112 | case '\u030C': 113 | return `\\check{${e}}` 114 | case '\u0327': 115 | return `\\c{${e}}` 116 | default: 117 | return `\\${chr}{${e}}` 118 | } 119 | } 120 | export function parseBox(box) { 121 | const e = parseOMath(box['m:e']) 122 | return `\\boxed{${e}}` 123 | } 124 | 125 | 126 | export function parseOMath(oMath) { 127 | if (!oMath) return '' 128 | 129 | if (Array.isArray(oMath)) { 130 | return oMath.map(item => parseOMath(item)).join('') 131 | } 132 | 133 | const oMathList = [] 134 | const keys = Object.keys(oMath) 135 | for (const key of keys) { 136 | if (Array.isArray(oMath[key])) { 137 | oMathList.push(...oMath[key].map(item => ({ key, value: item }))) 138 | } 139 | else oMathList.push({ key, value: oMath[key] }) 140 | } 141 | 142 | oMathList.sort((a, b) => { 143 | let oA = 0 144 | if (a.key === 'm:r' && a.value && a.value['a:rPr']) oA = a.value['a:rPr']['attrs']['order'] 145 | else if (a.value[`${a.key}Pr`] && a.value[`${a.key}Pr`]['m:ctrlPr'] && a.value[`${a.key}Pr`]['m:ctrlPr']['a:rPr']) { 146 | oA = a.value[`${a.key}Pr`] && a.value[`${a.key}Pr`]['m:ctrlPr'] && a.value[`${a.key}Pr`]['m:ctrlPr']['a:rPr'] && a.value[`${a.key}Pr`]['m:ctrlPr']['a:rPr']['attrs']['order'] 147 | } 148 | let oB = 0 149 | if (b.key === 'm:r' && b.value && b.value['a:rPr']) oB = b.value['a:rPr']['attrs']['order'] 150 | else if (b.value[`${b.key}Pr`] && b.value[`${b.key}Pr`]['m:ctrlPr'] && b.value[`${b.key}Pr`]['m:ctrlPr']['a:rPr']) { 151 | oB = b.value[`${b.key}Pr`] && b.value[`${b.key}Pr`]['m:ctrlPr'] && b.value[`${b.key}Pr`]['m:ctrlPr']['a:rPr'] && b.value[`${b.key}Pr`]['m:ctrlPr']['a:rPr']['attrs']['order'] 152 | } 153 | return oA - oB 154 | }) 155 | 156 | return oMathList.map(({ key, value }) => { 157 | if (key === 'm:f') return parseFraction(value) 158 | if (key === 'm:sSup') return parseSuperscript(value) 159 | if (key === 'm:sSub') return parseSubscript(value) 160 | if (key === 'm:rad') return parseRadical(value) 161 | if (key === 'm:nary') return parseNary(value) 162 | if (key === 'm:limLow') return parseLimit(value, 'low') 163 | if (key === 'm:limUpp') return parseLimit(value, 'upp') 164 | if (key === 'm:d') return parseDelimiter(value) 165 | if (key === 'm:func') return parseFunction(value) 166 | if (key === 'm:groupChr') return parseGroupChr(value) 167 | if (key === 'm:eqArr') return parseEqArr(value) 168 | if (key === 'm:bar') return parseBar(value) 169 | if (key === 'm:acc') return parseAccent(value) 170 | if (key === 'm:borderBox') return parseBox(value) 171 | if (key === 'm:m') return parseMatrix(value) 172 | if (key === 'm:r') return parseOMath(value) 173 | if (key === 'm:t') return value 174 | return '' 175 | }).join('') 176 | } 177 | 178 | export function latexFormart(latex) { 179 | return latex.replaceAll(/</g, '<') 180 | .replaceAll(/>/g, '>') 181 | .replaceAll(/&/g, '&') 182 | .replaceAll(/'/g, "'") 183 | .replaceAll(/"/g, '"') 184 | } -------------------------------------------------------------------------------- /src/text.js: -------------------------------------------------------------------------------- 1 | import { getHorizontalAlign } from './align' 2 | import { getTextByPathList } from './utils' 3 | 4 | import { 5 | getFontType, 6 | getFontColor, 7 | getFontSize, 8 | getFontBold, 9 | getFontItalic, 10 | getFontDecoration, 11 | getFontDecorationLine, 12 | getFontSpace, 13 | getFontSubscript, 14 | getFontShadow, 15 | } from './fontStyle' 16 | 17 | export function genTextBody(textBodyNode, spNode, slideLayoutSpNode, type, warpObj) { 18 | if (!textBodyNode) return '' 19 | 20 | let text = '' 21 | 22 | const pFontStyle = getTextByPathList(spNode, ['p:style', 'a:fontRef']) 23 | 24 | const pNode = textBodyNode['a:p'] 25 | const pNodes = pNode.constructor === Array ? pNode : [pNode] 26 | 27 | let isList = '' 28 | 29 | for (const pNode of pNodes) { 30 | let rNode = pNode['a:r'] 31 | let fldNode = pNode['a:fld'] 32 | let brNode = pNode['a:br'] 33 | if (rNode) { 34 | rNode = (rNode.constructor === Array) ? rNode : [rNode] 35 | 36 | if (fldNode) { 37 | fldNode = (fldNode.constructor === Array) ? fldNode : [fldNode] 38 | rNode = rNode.concat(fldNode) 39 | } 40 | if (brNode) { 41 | brNode = (brNode.constructor === Array) ? brNode : [brNode] 42 | brNode.forEach(item => item.type = 'br') 43 | 44 | if (brNode.length > 1) brNode.shift() 45 | rNode = rNode.concat(brNode) 46 | rNode.sort((a, b) => { 47 | if (!a.attrs || !b.attrs) return true 48 | return a.attrs.order - b.attrs.order 49 | }) 50 | } 51 | } 52 | 53 | const align = getHorizontalAlign(pNode, spNode, type, warpObj) 54 | 55 | const listType = getListType(pNode) 56 | if (listType) { 57 | if (!isList) { 58 | text += `<${listType}>` 59 | isList = listType 60 | } 61 | else if (isList && isList !== listType) { 62 | text += `` 63 | text += `<${listType}>` 64 | isList = listType 65 | } 66 | text += `
  • ` 67 | } 68 | else { 69 | if (isList) { 70 | text += `` 71 | isList = '' 72 | } 73 | text += `

    ` 74 | } 75 | 76 | if (!rNode) { 77 | text += genSpanElement(pNode, spNode, textBodyNode, pFontStyle, slideLayoutSpNode, type, warpObj) 78 | } 79 | else { 80 | let prevStyleInfo = null 81 | let accumulatedText = '' 82 | 83 | for (const rNodeItem of rNode) { 84 | const styleInfo = getSpanStyleInfo(rNodeItem, pNode, textBodyNode, pFontStyle, slideLayoutSpNode, type, warpObj) 85 | 86 | if (!prevStyleInfo || prevStyleInfo.styleText !== styleInfo.styleText || prevStyleInfo.hasLink !== styleInfo.hasLink || styleInfo.hasLink) { 87 | if (accumulatedText) { 88 | const processedText = accumulatedText.replace(/\t/g, '    ').replace(/\s/g, ' ') 89 | text += `${processedText}` 90 | accumulatedText = '' 91 | } 92 | 93 | if (styleInfo.hasLink) { 94 | const processedText = styleInfo.text.replace(/\t/g, '    ').replace(/\s/g, ' ') 95 | text += `${processedText}` 96 | prevStyleInfo = null 97 | } 98 | else { 99 | prevStyleInfo = styleInfo 100 | accumulatedText = styleInfo.text 101 | } 102 | } 103 | else accumulatedText += styleInfo.text 104 | } 105 | 106 | if (accumulatedText && prevStyleInfo) { 107 | const processedText = accumulatedText.replace(/\t/g, '    ').replace(/\s/g, ' ') 108 | text += `${processedText}` 109 | } 110 | } 111 | 112 | if (listType) text += '

  • ' 113 | else text += '

    ' 114 | } 115 | return text 116 | } 117 | 118 | export function getListType(node) { 119 | const pPrNode = node['a:pPr'] 120 | if (!pPrNode) return '' 121 | 122 | if (pPrNode['a:buChar']) return 'ul' 123 | if (pPrNode['a:buAutoNum']) return 'ol' 124 | 125 | return '' 126 | } 127 | 128 | export function genSpanElement(node, pNode, textBodyNode, pFontStyle, slideLayoutSpNode, type, warpObj) { 129 | const { styleText, text, hasLink, linkURL } = getSpanStyleInfo(node, pNode, textBodyNode, pFontStyle, slideLayoutSpNode, type, warpObj) 130 | const processedText = text.replace(/\t/g, '    ').replace(/\s/g, ' ') 131 | 132 | if (hasLink) { 133 | return `${processedText}` 134 | } 135 | return `${processedText}` 136 | } 137 | 138 | export function getSpanStyleInfo(node, pNode, textBodyNode, pFontStyle, slideLayoutSpNode, type, warpObj) { 139 | const lstStyle = textBodyNode['a:lstStyle'] 140 | const slideMasterTextStyles = warpObj['slideMasterTextStyles'] 141 | 142 | let lvl = 1 143 | const pPrNode = pNode['a:pPr'] 144 | const lvlNode = getTextByPathList(pPrNode, ['attrs', 'lvl']) 145 | if (lvlNode !== undefined) lvl = parseInt(lvlNode) + 1 146 | 147 | let text = node['a:t'] 148 | if (typeof text !== 'string') text = getTextByPathList(node, ['a:fld', 'a:t']) 149 | if (typeof text !== 'string') text = ' ' 150 | 151 | let styleText = '' 152 | const fontColor = getFontColor(node, pNode, lstStyle, pFontStyle, lvl, warpObj) 153 | const fontSize = getFontSize(node, slideLayoutSpNode, type, slideMasterTextStyles, textBodyNode, pNode) 154 | const fontType = getFontType(node, type, warpObj) 155 | const fontBold = getFontBold(node) 156 | const fontItalic = getFontItalic(node) 157 | const fontDecoration = getFontDecoration(node) 158 | const fontDecorationLine = getFontDecorationLine(node) 159 | const fontSpace = getFontSpace(node) 160 | const shadow = getFontShadow(node, warpObj) 161 | const subscript = getFontSubscript(node) 162 | 163 | if (fontColor) { 164 | if (typeof fontColor === 'string') styleText += `color: ${fontColor};` 165 | else if (fontColor.colors) { 166 | const { colors, rot } = fontColor 167 | const stops = colors.map(item => `${item.color} ${item.pos}`).join(', ') 168 | const gradientStyle = `linear-gradient(${rot + 90}deg, ${stops})` 169 | styleText += `background: ${gradientStyle}; background-clip: text; color: transparent;` 170 | } 171 | } 172 | if (fontSize) styleText += `font-size: ${fontSize};` 173 | if (fontType) styleText += `font-family: ${fontType};` 174 | if (fontBold) styleText += `font-weight: ${fontBold};` 175 | if (fontItalic) styleText += `font-style: ${fontItalic};` 176 | if (fontDecoration) styleText += `text-decoration: ${fontDecoration};` 177 | if (fontDecorationLine) styleText += `text-decoration-line: ${fontDecorationLine};` 178 | if (fontSpace) styleText += `letter-spacing: ${fontSpace};` 179 | if (subscript) styleText += `vertical-align: ${subscript};` 180 | if (shadow) styleText += `text-shadow: ${shadow};` 181 | 182 | const linkID = getTextByPathList(node, ['a:rPr', 'a:hlinkClick', 'attrs', 'r:id']) 183 | const hasLink = linkID && warpObj['slideResObj'][linkID] 184 | 185 | return { 186 | styleText, 187 | text, 188 | hasLink, 189 | linkURL: hasLink ? warpObj['slideResObj'][linkID]['target'] : null 190 | } 191 | } -------------------------------------------------------------------------------- /src/color.js: -------------------------------------------------------------------------------- 1 | import tinycolor from 'tinycolor2' 2 | 3 | export function hueToRgb(t1, t2, hue) { 4 | if (hue < 0) hue += 6 5 | if (hue >= 6) hue -= 6 6 | if (hue < 1) return (t2 - t1) * hue + t1 7 | else if (hue < 3) return t2 8 | else if (hue < 4) return (t2 - t1) * (4 - hue) + t1 9 | return t1 10 | } 11 | 12 | export function hslToRgb(hue, sat, light) { 13 | let t2 14 | hue = hue / 60 15 | if (light <= 0.5) { 16 | t2 = light * (sat + 1) 17 | } 18 | else { 19 | t2 = light + sat - (light * sat) 20 | } 21 | const t1 = light * 2 - t2 22 | const r = hueToRgb(t1, t2, hue + 2) * 255 23 | const g = hueToRgb(t1, t2, hue) * 255 24 | const b = hueToRgb(t1, t2, hue - 2) * 255 25 | return { r, g, b } 26 | } 27 | 28 | export function applyShade(rgbStr, shadeValue, isAlpha) { 29 | const color = tinycolor(rgbStr).toHsl() 30 | if (shadeValue >= 1) shadeValue = 1 31 | const cacl_l = Math.min(color.l * shadeValue, 1) 32 | if (isAlpha) { 33 | return tinycolor({ 34 | h: color.h, 35 | s: color.s, 36 | l: cacl_l, 37 | a: color.a 38 | }).toHex8() 39 | } 40 | 41 | return tinycolor({ 42 | h: color.h, 43 | s: color.s, 44 | l: cacl_l, 45 | a: color.a, 46 | }).toHex() 47 | } 48 | 49 | export function applyTint(rgbStr, tintValue, isAlpha) { 50 | const color = tinycolor(rgbStr).toHsl() 51 | if (tintValue >= 1) tintValue = 1 52 | const cacl_l = color.l * tintValue + (1 - tintValue) 53 | if (isAlpha) { 54 | return tinycolor({ 55 | h: color.h, 56 | s: color.s, 57 | l: cacl_l, 58 | a: color.a 59 | }).toHex8() 60 | } 61 | 62 | return tinycolor({ 63 | h: color.h, 64 | s: color.s, 65 | l: cacl_l, 66 | a: color.a 67 | }).toHex() 68 | } 69 | 70 | export function applyLumOff(rgbStr, offset, isAlpha) { 71 | const color = tinycolor(rgbStr).toHsl() 72 | const lum = offset + color.l 73 | if (lum >= 1) { 74 | if (isAlpha) { 75 | return tinycolor({ 76 | h: color.h, 77 | s: color.s, 78 | l: 1, 79 | a: color.a 80 | }).toHex8() 81 | } 82 | 83 | return tinycolor({ 84 | h: color.h, 85 | s: color.s, 86 | l: 1, 87 | a: color.a 88 | }).toHex() 89 | } 90 | if (isAlpha) { 91 | return tinycolor({ 92 | h: color.h, 93 | s: color.s, 94 | l: lum, 95 | a: color.a 96 | }).toHex8() 97 | } 98 | 99 | return tinycolor({ 100 | h: color.h, 101 | s: color.s, 102 | l: lum, 103 | a: color.a 104 | }).toHex() 105 | } 106 | 107 | export function applyLumMod(rgbStr, multiplier, isAlpha) { 108 | const color = tinycolor(rgbStr).toHsl() 109 | let cacl_l = color.l * multiplier 110 | if (cacl_l >= 1) cacl_l = 1 111 | if (isAlpha) { 112 | return tinycolor({ 113 | h: color.h, 114 | s: color.s, 115 | l: cacl_l, 116 | a: color.a 117 | }).toHex8() 118 | } 119 | 120 | return tinycolor({ 121 | h: color.h, 122 | s: color.s, 123 | l: cacl_l, 124 | a: color.a 125 | }).toHex() 126 | } 127 | 128 | export function applyHueMod(rgbStr, multiplier, isAlpha) { 129 | const color = tinycolor(rgbStr).toHsl() 130 | let cacl_h = color.h * multiplier 131 | if (cacl_h >= 360) cacl_h = cacl_h - 360 132 | if (isAlpha) { 133 | return tinycolor({ 134 | h: cacl_h, 135 | s: color.s, 136 | l: color.l, 137 | a: color.a 138 | }).toHex8() 139 | } 140 | 141 | return tinycolor({ 142 | h: cacl_h, 143 | s: color.s, 144 | l: color.l, 145 | a: color.a 146 | }).toHex() 147 | } 148 | 149 | export function applySatMod(rgbStr, multiplier, isAlpha) { 150 | const color = tinycolor(rgbStr).toHsl() 151 | let cacl_s = color.s * multiplier 152 | if (cacl_s >= 1) cacl_s = 1 153 | if (isAlpha) { 154 | return tinycolor({ 155 | h: color.h, 156 | s: cacl_s, 157 | l: color.l, 158 | a: color.a 159 | }).toHex8() 160 | } 161 | 162 | return tinycolor({ 163 | h: color.h, 164 | s: cacl_s, 165 | l: color.l, 166 | a: color.a 167 | }).toHex() 168 | } 169 | 170 | export function getColorName2Hex(name) { 171 | let hex 172 | const colorName = ['white', 'AliceBlue', 'AntiqueWhite', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 'black', 'BlanchedAlmond', 'Blue', 'BlueViolet', 'Brown', 'BurlyWood', 'CadetBlue', 'Chartreuse', 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 'DarkBlue', 'DarkCyan', 'DarkGoldenRod', 'DarkGray', 'DarkGrey', 'DarkGreen', 'DarkKhaki', 'DarkMagenta', 'DarkOliveGreen', 'DarkOrange', 'DarkOrchid', 'DarkRed', 'DarkSalmon', 'DarkSeaGreen', 'DarkSlateBlue', 'DarkSlateGray', 'DarkSlateGrey', 'DarkTurquoise', 'DarkViolet', 'DeepPink', 'DeepSkyBlue', 'DimGray', 'DimGrey', 'DodgerBlue', 'FireBrick', 'FloralWhite', 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'GoldenRod', 'Gray', 'Grey', 'Green', 'GreenYellow', 'HoneyDew', 'HotPink', 'IndianRed', 'Indigo', 'Ivory', 'Khaki', 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 'LightCoral', 'LightCyan', 'LightGoldenRodYellow', 'LightGray', 'LightGrey', 'LightGreen', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 'LightSlateGray', 'LightSlateGrey', 'LightSteelBlue', 'LightYellow', 'Lime', 'LimeGreen', 'Linen', 'Magenta', 'Maroon', 'MediumAquaMarine', 'MediumBlue', 'MediumOrchid', 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 'MediumTurquoise', 'MediumVioletRed', 'MidnightBlue', 'MintCream', 'MistyRose', 'Moccasin', 'NavajoWhite', 'Navy', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 'Orchid', 'PaleGoldenRod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 'RebeccaPurple', 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Salmon', 'SandyBrown', 'SeaGreen', 'SeaShell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', 'SteelBlue', 'Tan', 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 'WhiteSmoke', 'Yellow', 'YellowGreen'] 173 | const colorHex = ['ffffff', 'f0f8ff', 'faebd7', '00ffff', '7fffd4', 'f0ffff', 'f5f5dc', 'ffe4c4', '000000', 'ffebcd', '0000ff', '8a2be2', 'a52a2a', 'deb887', '5f9ea0', '7fff00', 'd2691e', 'ff7f50', '6495ed', 'fff8dc', 'dc143c', '00ffff', '00008b', '008b8b', 'b8860b', 'a9a9a9', 'a9a9a9', '006400', 'bdb76b', '8b008b', '556b2f', 'ff8c00', '9932cc', '8b0000', 'e9967a', '8fbc8f', '483d8b', '2f4f4f', '2f4f4f', '00ced1', '9400d3', 'ff1493', '00bfff', '696969', '696969', '1e90ff', 'b22222', 'fffaf0', '228b22', 'ff00ff', 'dcdcdc', 'f8f8ff', 'ffd700', 'daa520', '808080', '808080', '008000', 'adff2f', 'f0fff0', 'ff69b4', 'cd5c5c', '4b0082', 'fffff0', 'f0e68c', 'e6e6fa', 'fff0f5', '7cfc00', 'fffacd', 'add8e6', 'f08080', 'e0ffff', 'fafad2', 'd3d3d3', 'd3d3d3', '90ee90', 'ffb6c1', 'ffa07a', '20b2aa', '87cefa', '778899', '778899', 'b0c4de', 'ffffe0', '00ff00', '32cd32', 'faf0e6', 'ff00ff', '800000', '66cdaa', '0000cd', 'ba55d3', '9370db', '3cb371', '7b68ee', '00fa9a', '48d1cc', 'c71585', '191970', 'f5fffa', 'ffe4e1', 'ffe4b5', 'ffdead', '000080', 'fdf5e6', '808000', '6b8e23', 'ffa500', 'ff4500', 'da70d6', 'eee8aa', '98fb98', 'afeeee', 'db7093', 'ffefd5', 'ffdab9', 'cd853f', 'ffc0cb', 'dda0dd', 'b0e0e6', '800080', '663399', 'ff0000', 'bc8f8f', '4169e1', '8b4513', 'fa8072', 'f4a460', '2e8b57', 'fff5ee', 'a0522d', 'c0c0c0', '87ceeb', '6a5acd', '708090', '708090', 'fffafa', '00ff7f', '4682b4', 'd2b48c', '008080', 'd8bfd8', 'ff6347', '40e0d0', 'ee82ee', 'f5deb3', 'ffffff', 'f5f5f5', 'ffff00', '9acd32'] 174 | const findIndx = colorName.indexOf(name) 175 | if (findIndx !== -1) hex = colorHex[findIndx] 176 | return hex 177 | } -------------------------------------------------------------------------------- /src/chart.js: -------------------------------------------------------------------------------- 1 | import { eachElement, getTextByPathList } from './utils' 2 | import { applyTint } from './color' 3 | 4 | function extractChartColors(serNode, warpObj) { 5 | if (serNode.constructor !== Array) serNode = [serNode] 6 | const schemeClrs = [] 7 | for (const node of serNode) { 8 | let schemeClr = getTextByPathList(node, ['c:spPr', 'a:solidFill', 'a:schemeClr']) 9 | if (!schemeClr) schemeClr = getTextByPathList(node, ['c:spPr', 'a:ln', 'a:solidFill', 'a:schemeClr']) 10 | if (!schemeClr) schemeClr = getTextByPathList(node, ['c:marker', 'c:spPr', 'a:ln', 'a:solidFill', 'a:schemeClr']) 11 | 12 | let clr = getTextByPathList(schemeClr, ['attrs', 'val']) 13 | if (clr) { 14 | clr = getTextByPathList(warpObj['themeContent'], ['a:theme', 'a:themeElements', 'a:clrScheme', `a:${clr}`, 'a:srgbClr', 'attrs', 'val']) 15 | const tint = getTextByPathList(schemeClr, ['a:tint', 'attrs', 'val']) / 100000 16 | if (clr && !isNaN(tint)) { 17 | clr = applyTint(clr, tint) 18 | } 19 | } 20 | else clr = getTextByPathList(node, ['c:spPr', 'a:solidFill', 'a:srgbClr', 'attrs', 'val']) 21 | 22 | if (clr) clr = '#' + clr 23 | schemeClrs.push(clr) 24 | } 25 | return schemeClrs 26 | } 27 | 28 | function extractChartData(serNode) { 29 | const dataMat = [] 30 | if (!serNode) return dataMat 31 | 32 | if (serNode['c:xVal']) { 33 | let dataRow = [] 34 | eachElement(serNode['c:xVal']['c:numRef']['c:numCache']['c:pt'], innerNode => { 35 | dataRow.push(parseFloat(innerNode['c:v'])) 36 | return '' 37 | }) 38 | dataMat.push(dataRow) 39 | dataRow = [] 40 | eachElement(serNode['c:yVal']['c:numRef']['c:numCache']['c:pt'], innerNode => { 41 | dataRow.push(parseFloat(innerNode['c:v'])) 42 | return '' 43 | }) 44 | dataMat.push(dataRow) 45 | } 46 | else { 47 | eachElement(serNode, (innerNode, index) => { 48 | const dataRow = [] 49 | const colName = getTextByPathList(innerNode, ['c:tx', 'c:strRef', 'c:strCache', 'c:pt', 'c:v']) || index 50 | 51 | const rowNames = {} 52 | if (getTextByPathList(innerNode, ['c:cat', 'c:strRef', 'c:strCache', 'c:pt'])) { 53 | eachElement(innerNode['c:cat']['c:strRef']['c:strCache']['c:pt'], innerNode => { 54 | rowNames[innerNode['attrs']['idx']] = innerNode['c:v'] 55 | return '' 56 | }) 57 | } 58 | else if (getTextByPathList(innerNode, ['c:cat', 'c:numRef', 'c:numCache', 'c:pt'])) { 59 | eachElement(innerNode['c:cat']['c:numRef']['c:numCache']['c:pt'], innerNode => { 60 | rowNames[innerNode['attrs']['idx']] = innerNode['c:v'] 61 | return '' 62 | }) 63 | } 64 | 65 | if (getTextByPathList(innerNode, ['c:val', 'c:numRef', 'c:numCache', 'c:pt'])) { 66 | eachElement(innerNode['c:val']['c:numRef']['c:numCache']['c:pt'], innerNode => { 67 | dataRow.push({ 68 | x: innerNode['attrs']['idx'], 69 | y: parseFloat(innerNode['c:v']), 70 | }) 71 | return '' 72 | }) 73 | } 74 | 75 | dataMat.push({ 76 | key: colName, 77 | values: dataRow, 78 | xlabels: rowNames, 79 | }) 80 | return '' 81 | }) 82 | } 83 | 84 | return dataMat 85 | } 86 | 87 | export function getChartInfo(plotArea, warpObj) { 88 | let chart = null 89 | for (const key in plotArea) { 90 | switch (key) { 91 | case 'c:lineChart': 92 | chart = { 93 | type: 'lineChart', 94 | data: extractChartData(plotArea[key]['c:ser']), 95 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 96 | grouping: getTextByPathList(plotArea[key], ['c:grouping', 'attrs', 'val']), 97 | marker: plotArea[key]['c:marker'] ? true : false, 98 | } 99 | break 100 | case 'c:line3DChart': 101 | chart = { 102 | type: 'line3DChart', 103 | data: extractChartData(plotArea[key]['c:ser']), 104 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 105 | grouping: getTextByPathList(plotArea[key], ['c:grouping', 'attrs', 'val']), 106 | } 107 | break 108 | case 'c:barChart': 109 | chart = { 110 | type: 'barChart', 111 | data: extractChartData(plotArea[key]['c:ser']), 112 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 113 | grouping: getTextByPathList(plotArea[key], ['c:grouping', 'attrs', 'val']), 114 | barDir: getTextByPathList(plotArea[key], ['c:barDir', 'attrs', 'val']), 115 | } 116 | break 117 | case 'c:bar3DChart': 118 | chart = { 119 | type: 'bar3DChart', 120 | data: extractChartData(plotArea[key]['c:ser']), 121 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 122 | grouping: getTextByPathList(plotArea[key], ['c:grouping', 'attrs', 'val']), 123 | barDir: getTextByPathList(plotArea[key], ['c:barDir', 'attrs', 'val']), 124 | } 125 | break 126 | case 'c:pieChart': 127 | chart = { 128 | type: 'pieChart', 129 | data: extractChartData(plotArea[key]['c:ser']), 130 | colors: extractChartColors(plotArea[key]['c:ser']['c:dPt'], warpObj), 131 | } 132 | break 133 | case 'c:pie3DChart': 134 | chart = { 135 | type: 'pie3DChart', 136 | data: extractChartData(plotArea[key]['c:ser']), 137 | colors: extractChartColors(plotArea[key]['c:ser']['c:dPt'], warpObj), 138 | } 139 | break 140 | case 'c:doughnutChart': 141 | chart = { 142 | type: 'doughnutChart', 143 | data: extractChartData(plotArea[key]['c:ser']), 144 | colors: extractChartColors(plotArea[key]['c:ser']['c:dPt'], warpObj), 145 | holeSize: getTextByPathList(plotArea[key], ['c:holeSize', 'attrs', 'val']), 146 | } 147 | break 148 | case 'c:areaChart': 149 | chart = { 150 | type: 'areaChart', 151 | data: extractChartData(plotArea[key]['c:ser']), 152 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 153 | grouping: getTextByPathList(plotArea[key], ['c:grouping', 'attrs', 'val']), 154 | } 155 | break 156 | case 'c:area3DChart': 157 | chart = { 158 | type: 'area3DChart', 159 | data: extractChartData(plotArea[key]['c:ser']), 160 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 161 | grouping: getTextByPathList(plotArea[key], ['c:grouping', 'attrs', 'val']), 162 | } 163 | break 164 | case 'c:scatterChart': 165 | chart = { 166 | type: 'scatterChart', 167 | data: extractChartData(plotArea[key]['c:ser']), 168 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 169 | style: getTextByPathList(plotArea[key], ['c:scatterStyle', 'attrs', 'val']), 170 | } 171 | break 172 | case 'c:bubbleChart': 173 | chart = { 174 | type: 'bubbleChart', 175 | data: extractChartData(plotArea[key]['c:ser']), 176 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 177 | } 178 | break 179 | case 'c:radarChart': 180 | chart = { 181 | type: 'radarChart', 182 | data: extractChartData(plotArea[key]['c:ser']), 183 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 184 | style: getTextByPathList(plotArea[key], ['c:radarStyle', 'attrs', 'val']), 185 | } 186 | break 187 | case 'c:surfaceChart': 188 | chart = { 189 | type: 'surfaceChart', 190 | data: extractChartData(plotArea[key]['c:ser']), 191 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 192 | } 193 | break 194 | case 'c:surface3DChart': 195 | chart = { 196 | type: 'surface3DChart', 197 | data: extractChartData(plotArea[key]['c:ser']), 198 | colors: extractChartColors(plotArea[key]['c:ser'], warpObj), 199 | } 200 | break 201 | case 'c:stockChart': 202 | chart = { 203 | type: 'stockChart', 204 | data: extractChartData(plotArea[key]['c:ser']), 205 | colors: [], 206 | } 207 | break 208 | default: 209 | } 210 | } 211 | 212 | return chart 213 | } -------------------------------------------------------------------------------- /src/table.js: -------------------------------------------------------------------------------- 1 | import { getShapeFill, getSolidFill } from './fill' 2 | import { getTextByPathList } from './utils' 3 | import { getBorder } from './border' 4 | 5 | export function getTableBorders(node, warpObj) { 6 | const borders = {} 7 | if (node['a:bottom']) { 8 | const obj = { 9 | 'p:spPr': { 10 | 'a:ln': node['a:bottom']['a:ln'] 11 | } 12 | } 13 | const border = getBorder(obj, undefined, warpObj) 14 | borders.bottom = border 15 | } 16 | if (node['a:top']) { 17 | const obj = { 18 | 'p:spPr': { 19 | 'a:ln': node['a:top']['a:ln'] 20 | } 21 | } 22 | const border = getBorder(obj, undefined, warpObj) 23 | borders.top = border 24 | } 25 | if (node['a:right']) { 26 | const obj = { 27 | 'p:spPr': { 28 | 'a:ln': node['a:right']['a:ln'] 29 | } 30 | } 31 | const border = getBorder(obj, undefined, warpObj) 32 | borders.right = border 33 | } 34 | if (node['a:left']) { 35 | const obj = { 36 | 'p:spPr': { 37 | 'a:ln': node['a:left']['a:ln'] 38 | } 39 | } 40 | const border = getBorder(obj, undefined, warpObj) 41 | borders.left = border 42 | } 43 | return borders 44 | } 45 | 46 | export async function getTableCellParams(tcNode, thisTblStyle, cellSource, warpObj) { 47 | const rowSpan = getTextByPathList(tcNode, ['attrs', 'rowSpan']) 48 | const colSpan = getTextByPathList(tcNode, ['attrs', 'gridSpan']) 49 | const vMerge = getTextByPathList(tcNode, ['attrs', 'vMerge']) 50 | const hMerge = getTextByPathList(tcNode, ['attrs', 'hMerge']) 51 | let fillColor 52 | let fontColor 53 | let fontBold 54 | 55 | const getCelFill = getTextByPathList(tcNode, ['a:tcPr']) 56 | if (getCelFill) { 57 | const cellObj = { 'p:spPr': getCelFill } 58 | const fill = await getShapeFill(cellObj, undefined, false, warpObj, 'slide') 59 | 60 | if (fill && fill.type === 'color' && fill.value) { 61 | fillColor = fill.value 62 | } 63 | } 64 | if (!fillColor) { 65 | let bgFillschemeClr 66 | if (cellSource) bgFillschemeClr = getTextByPathList(thisTblStyle, [cellSource, 'a:tcStyle', 'a:fill', 'a:solidFill']) 67 | if (bgFillschemeClr) { 68 | fillColor = getSolidFill(bgFillschemeClr, undefined, undefined, warpObj) 69 | } 70 | } 71 | 72 | let rowTxtStyl 73 | if (cellSource) rowTxtStyl = getTextByPathList(thisTblStyle, [cellSource, 'a:tcTxStyle']) 74 | if (rowTxtStyl) { 75 | fontColor = getSolidFill(rowTxtStyl, undefined, undefined, warpObj) 76 | if (getTextByPathList(rowTxtStyl, ['attrs', 'b']) === 'on') fontBold = true 77 | } 78 | 79 | let lin_bottm = getTextByPathList(tcNode, ['a:tcPr', 'a:lnB']) 80 | if (!lin_bottm) { 81 | if (cellSource) lin_bottm = getTextByPathList(thisTblStyle[cellSource], ['a:tcStyle', 'a:tcBdr', 'a:bottom', 'a:ln']) 82 | if (!lin_bottm) lin_bottm = getTextByPathList(thisTblStyle, ['a:wholeTbl', 'a:tcStyle', 'a:tcBdr', 'a:bottom', 'a:ln']) 83 | } 84 | let lin_top = getTextByPathList(tcNode, ['a:tcPr', 'a:lnT']) 85 | if (!lin_top) { 86 | if (cellSource) lin_top = getTextByPathList(thisTblStyle[cellSource], ['a:tcStyle', 'a:tcBdr', 'a:top', 'a:ln']) 87 | if (!lin_top) lin_top = getTextByPathList(thisTblStyle, ['a:wholeTbl', 'a:tcStyle', 'a:tcBdr', 'a:top', 'a:ln']) 88 | } 89 | let lin_left = getTextByPathList(tcNode, ['a:tcPr', 'a:lnL']) 90 | if (!lin_left) { 91 | if (cellSource) lin_left = getTextByPathList(thisTblStyle[cellSource], ['a:tcStyle', 'a:tcBdr', 'a:left', 'a:ln']) 92 | if (!lin_left) lin_left = getTextByPathList(thisTblStyle, ['a:wholeTbl', 'a:tcStyle', 'a:tcBdr', 'a:left', 'a:ln']) 93 | } 94 | let lin_right = getTextByPathList(tcNode, ['a:tcPr', 'a:lnR']) 95 | if (!lin_right) { 96 | if (cellSource) lin_right = getTextByPathList(thisTblStyle[cellSource], ['a:tcStyle', 'a:tcBdr', 'a:right', 'a:ln']) 97 | if (!lin_right) lin_right = getTextByPathList(thisTblStyle, ['a:wholeTbl', 'a:tcStyle', 'a:tcBdr', 'a:right', 'a:ln']) 98 | } 99 | 100 | const borders = {} 101 | if (lin_bottm) borders.bottom = getBorder(lin_bottm, undefined, warpObj) 102 | if (lin_top) borders.top = getBorder(lin_top, undefined, warpObj) 103 | if (lin_left) borders.left = getBorder(lin_left, undefined, warpObj) 104 | if (lin_right) borders.right = getBorder(lin_right, undefined, warpObj) 105 | 106 | return { 107 | fillColor, 108 | fontColor, 109 | fontBold, 110 | borders, 111 | rowSpan: rowSpan ? +rowSpan : undefined, 112 | colSpan: colSpan ? +colSpan : undefined, 113 | vMerge: vMerge ? +vMerge : undefined, 114 | hMerge: hMerge ? +hMerge : undefined, 115 | } 116 | } 117 | 118 | export function getTableRowParams(trNodes, i, tblStylAttrObj, thisTblStyle, warpObj) { 119 | let fillColor 120 | let fontColor 121 | let fontBold 122 | 123 | if (thisTblStyle && thisTblStyle['a:wholeTbl']) { 124 | const bgFillschemeClr = getTextByPathList(thisTblStyle, ['a:wholeTbl', 'a:tcStyle', 'a:fill', 'a:solidFill']) 125 | if (bgFillschemeClr) { 126 | const local_fillColor = getSolidFill(bgFillschemeClr, undefined, undefined, warpObj) 127 | if (local_fillColor) fillColor = local_fillColor 128 | } 129 | const rowTxtStyl = getTextByPathList(thisTblStyle, ['a:wholeTbl', 'a:tcTxStyle']) 130 | if (rowTxtStyl) { 131 | const local_fontColor = getSolidFill(rowTxtStyl, undefined, undefined, warpObj) 132 | if (local_fontColor) fontColor = local_fontColor 133 | if (getTextByPathList(rowTxtStyl, ['attrs', 'b']) === 'on') fontBold = true 134 | } 135 | } 136 | if (i === 0 && tblStylAttrObj['isFrstRowAttr'] === 1 && thisTblStyle) { 137 | const bgFillschemeClr = getTextByPathList(thisTblStyle, ['a:firstRow', 'a:tcStyle', 'a:fill', 'a:solidFill']) 138 | if (bgFillschemeClr) { 139 | const local_fillColor = getSolidFill(bgFillschemeClr, undefined, undefined, warpObj) 140 | if (local_fillColor) fillColor = local_fillColor 141 | } 142 | const rowTxtStyl = getTextByPathList(thisTblStyle, ['a:firstRow', 'a:tcTxStyle']) 143 | if (rowTxtStyl) { 144 | const local_fontColor = getSolidFill(rowTxtStyl, undefined, undefined, warpObj) 145 | if (local_fontColor) fontColor = local_fontColor 146 | if (getTextByPathList(rowTxtStyl, ['attrs', 'b']) === 'on') fontBold = true 147 | } 148 | } 149 | else if (i > 0 && tblStylAttrObj['isBandRowAttr'] === 1 && thisTblStyle) { 150 | fillColor = '' 151 | if ((i % 2) === 0 && thisTblStyle['a:band2H']) { 152 | const bgFillschemeClr = getTextByPathList(thisTblStyle, ['a:band2H', 'a:tcStyle', 'a:fill', 'a:solidFill']) 153 | if (bgFillschemeClr) { 154 | const local_fillColor = getSolidFill(bgFillschemeClr, undefined, undefined, warpObj) 155 | if (local_fillColor) fillColor = local_fillColor 156 | } 157 | const rowTxtStyl = getTextByPathList(thisTblStyle, ['a:band2H', 'a:tcTxStyle']) 158 | if (rowTxtStyl) { 159 | const local_fontColor = getSolidFill(rowTxtStyl, undefined, undefined, warpObj) 160 | if (local_fontColor) fontColor = local_fontColor 161 | } 162 | if (getTextByPathList(rowTxtStyl, ['attrs', 'b']) === 'on') fontBold = true 163 | } 164 | if ((i % 2) !== 0 && thisTblStyle['a:band1H']) { 165 | const bgFillschemeClr = getTextByPathList(thisTblStyle, ['a:band1H', 'a:tcStyle', 'a:fill', 'a:solidFill']) 166 | if (bgFillschemeClr) { 167 | const local_fillColor = getSolidFill(bgFillschemeClr, undefined, undefined, warpObj) 168 | if (local_fillColor) fillColor = local_fillColor 169 | } 170 | const rowTxtStyl = getTextByPathList(thisTblStyle, ['a:band1H', 'a:tcTxStyle']) 171 | if (rowTxtStyl) { 172 | const local_fontColor = getSolidFill(rowTxtStyl, undefined, undefined, warpObj) 173 | if (local_fontColor) fontColor = local_fontColor 174 | if (getTextByPathList(rowTxtStyl, ['attrs', 'b']) === 'on') fontBold = true 175 | } 176 | } 177 | } 178 | if (i === (trNodes.length - 1) && tblStylAttrObj['isLstRowAttr'] === 1 && thisTblStyle) { 179 | const bgFillschemeClr = getTextByPathList(thisTblStyle, ['a:lastRow', 'a:tcStyle', 'a:fill', 'a:solidFill']) 180 | if (bgFillschemeClr) { 181 | const local_fillColor = getSolidFill(bgFillschemeClr, undefined, undefined, warpObj) 182 | if (local_fillColor) { 183 | fillColor = local_fillColor 184 | } 185 | } 186 | const rowTxtStyl = getTextByPathList(thisTblStyle, ['a:lastRow', 'a:tcTxStyle']) 187 | if (rowTxtStyl) { 188 | const local_fontColor = getSolidFill(rowTxtStyl, undefined, undefined, warpObj) 189 | if (local_fontColor) fontColor = local_fontColor 190 | if (getTextByPathList(rowTxtStyl, ['attrs', 'b']) === 'on') fontBold = true 191 | } 192 | } 193 | 194 | return { 195 | fillColor, 196 | fontColor, 197 | fontBold, 198 | } 199 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | pptxtojson - PPTX转JSON 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 281 | 282 | 283 | 284 |
    285 | 286 |
    287 |
    288 | 289 |

    将 PPTX 文件转为可读的 JSON 数据

    290 |
    291 | 292 |
    293 |
    📤
    294 |

    上传PPTX文件

    295 |

    「点击选择文件」或「将文件拖放到区域内」上传

    296 | 297 |
    298 | 299 |
    300 |

    功能介绍

    301 |
    302 | 303 |

    解析PPTX数据,包括幻灯片主题、尺寸、背景、备注、母版、切页动画、页内元素及布局信息等

    304 |
    305 |
    306 | 307 |

    支持页内元素包括:文字(HTML富文本)、图片、形状、线条、表格、图表、音视频、公式等复杂元素

    308 |
    309 |
    310 | 311 |

    易读易写的结构化 JSON 数据格式

    312 |
    313 |
    314 | 315 |

    不依赖任何服务器端环境,直接在浏览器端运行

    316 |
    317 |
    318 | 319 |
    320 |

    相关链接

    321 | 325 | 329 |
    330 |
    331 | 332 | 333 |
    334 |
    335 |

    解析结果

    336 |
    337 | 338 | 339 |
    340 |
    341 | 342 |
    343 | 344 |
    345 |
    346 |
    347 | 348 | 351 | 352 | 475 | 476 | -------------------------------------------------------------------------------- /src/fill.js: -------------------------------------------------------------------------------- 1 | import tinycolor from 'tinycolor2' 2 | import { getSchemeColorFromTheme } from './schemeColor' 3 | import { 4 | applyShade, 5 | applyTint, 6 | applyLumOff, 7 | applyLumMod, 8 | applyHueMod, 9 | applySatMod, 10 | hslToRgb, 11 | getColorName2Hex, 12 | } from './color' 13 | 14 | import { 15 | base64ArrayBuffer, 16 | getTextByPathList, 17 | angleToDegrees, 18 | escapeHtml, 19 | getMimeType, 20 | toHex, 21 | } from './utils' 22 | 23 | export function getFillType(node) { 24 | let fillType = '' 25 | if (node['a:noFill']) fillType = 'NO_FILL' 26 | if (node['a:solidFill']) fillType = 'SOLID_FILL' 27 | if (node['a:gradFill']) fillType = 'GRADIENT_FILL' 28 | if (node['a:pattFill']) fillType = 'PATTERN_FILL' 29 | if (node['a:blipFill']) fillType = 'PIC_FILL' 30 | if (node['a:grpFill']) fillType = 'GROUP_FILL' 31 | 32 | return fillType 33 | } 34 | 35 | export async function getPicFill(type, node, warpObj) { 36 | if (!node) return '' 37 | 38 | let img 39 | const rId = getTextByPathList(node, ['a:blip', 'attrs', 'r:embed']) 40 | let imgPath 41 | if (type === 'slideBg' || type === 'slide') { 42 | imgPath = getTextByPathList(warpObj, ['slideResObj', rId, 'target']) 43 | } 44 | else if (type === 'slideLayoutBg') { 45 | imgPath = getTextByPathList(warpObj, ['layoutResObj', rId, 'target']) 46 | } 47 | else if (type === 'slideMasterBg') { 48 | imgPath = getTextByPathList(warpObj, ['masterResObj', rId, 'target']) 49 | } 50 | else if (type === 'themeBg') { 51 | imgPath = getTextByPathList(warpObj, ['themeResObj', rId, 'target']) 52 | } 53 | else if (type === 'diagramBg') { 54 | imgPath = getTextByPathList(warpObj, ['diagramResObj', rId, 'target']) 55 | } 56 | if (!imgPath) return imgPath 57 | 58 | img = getTextByPathList(warpObj, ['loaded-images', imgPath]) 59 | if (!img) { 60 | imgPath = escapeHtml(imgPath) 61 | 62 | const imgExt = imgPath.split('.').pop() 63 | if (imgExt === 'xml') return '' 64 | 65 | const imgArrayBuffer = await warpObj['zip'].file(imgPath).async('arraybuffer') 66 | const imgMimeType = getMimeType(imgExt) 67 | img = `data:${imgMimeType};base64,${base64ArrayBuffer(imgArrayBuffer)}` 68 | 69 | const loadedImages = warpObj['loaded-images'] || {} 70 | loadedImages[imgPath] = img 71 | warpObj['loaded-images'] = loadedImages 72 | } 73 | return img 74 | } 75 | 76 | export function getPicFillOpacity(node) { 77 | const aBlipNode = node['a:blip'] 78 | 79 | const aphaModFixNode = getTextByPathList(aBlipNode, ['a:alphaModFix', 'attrs']) 80 | let opacity = 1 81 | if (aphaModFixNode && aphaModFixNode['amt'] && aphaModFixNode['amt'] !== '') { 82 | opacity = parseInt(aphaModFixNode['amt']) / 100000 83 | } 84 | 85 | return opacity 86 | } 87 | 88 | export function getPicFilters(node) { 89 | if (!node) return null 90 | 91 | const aBlipNode = node['a:blip'] 92 | if (!aBlipNode) return null 93 | 94 | const filters = {} 95 | 96 | // 从a:extLst中获取滤镜效果(Microsoft Office 2010+扩展) 97 | const extLstNode = aBlipNode['a:extLst'] 98 | if (extLstNode && extLstNode['a:ext']) { 99 | const extNodes = Array.isArray(extLstNode['a:ext']) ? extLstNode['a:ext'] : [extLstNode['a:ext']] 100 | 101 | for (const extNode of extNodes) { 102 | if (!extNode['a14:imgProps'] || !extNode['a14:imgProps']['a14:imgLayer']) continue 103 | 104 | const imgLayerNode = extNode['a14:imgProps']['a14:imgLayer'] 105 | const imgEffects = imgLayerNode['a14:imgEffect'] 106 | 107 | if (!imgEffects) continue 108 | 109 | const effectArray = Array.isArray(imgEffects) ? imgEffects : [imgEffects] 110 | 111 | for (const effect of effectArray) { 112 | // 饱和度 113 | if (effect['a14:saturation']) { 114 | const satAttr = getTextByPathList(effect, ['a14:saturation', 'attrs', 'sat']) 115 | if (satAttr) { 116 | filters.saturation = parseInt(satAttr) / 100000 117 | } 118 | } 119 | 120 | // 亮度、对比度 121 | if (effect['a14:brightnessContrast']) { 122 | const brightAttr = getTextByPathList(effect, ['a14:brightnessContrast', 'attrs', 'bright']) 123 | const contrastAttr = getTextByPathList(effect, ['a14:brightnessContrast', 'attrs', 'contrast']) 124 | 125 | if (brightAttr) { 126 | filters.brightness = parseInt(brightAttr) / 100000 127 | } 128 | if (contrastAttr) { 129 | filters.contrast = parseInt(contrastAttr) / 100000 130 | } 131 | } 132 | 133 | // 锐化/柔化 134 | if (effect['a14:sharpenSoften']) { 135 | const amountAttr = getTextByPathList(effect, ['a14:sharpenSoften', 'attrs', 'amount']) 136 | if (amountAttr) { 137 | const amount = parseInt(amountAttr) / 100000 138 | if (amount > 0) { 139 | filters.sharpen = amount 140 | } 141 | else { 142 | filters.soften = Math.abs(amount) 143 | } 144 | } 145 | } 146 | 147 | // 色温 148 | if (effect['a14:colorTemperature']) { 149 | const tempAttr = getTextByPathList(effect, ['a14:colorTemperature', 'attrs', 'colorTemp']) 150 | if (tempAttr) { 151 | filters.colorTemperature = parseInt(tempAttr) 152 | } 153 | } 154 | } 155 | } 156 | } 157 | 158 | return Object.keys(filters).length > 0 ? filters : null 159 | } 160 | 161 | export async function getBgPicFill(bgPr, sorce, warpObj) { 162 | const picBase64 = await getPicFill(sorce, bgPr['a:blipFill'], warpObj) 163 | const aBlipNode = bgPr['a:blipFill']['a:blip'] 164 | 165 | const aphaModFixNode = getTextByPathList(aBlipNode, ['a:alphaModFix', 'attrs']) 166 | let opacity = 1 167 | if (aphaModFixNode && aphaModFixNode['amt'] && aphaModFixNode['amt'] !== '') { 168 | opacity = parseInt(aphaModFixNode['amt']) / 100000 169 | } 170 | 171 | return { 172 | picBase64, 173 | opacity, 174 | } 175 | } 176 | 177 | export function getGradientFill(node, warpObj) { 178 | const gsLst = node['a:gsLst']['a:gs'] 179 | const colors = [] 180 | for (let i = 0; i < gsLst.length; i++) { 181 | const lo_color = getSolidFill(gsLst[i], undefined, undefined, warpObj) 182 | const pos = getTextByPathList(gsLst[i], ['attrs', 'pos']) 183 | 184 | colors[i] = { 185 | pos: pos ? (pos / 1000 + '%') : '', 186 | color: lo_color, 187 | } 188 | } 189 | const lin = node['a:lin'] 190 | let rot = 0 191 | let pathType = 'line' 192 | if (lin) rot = angleToDegrees(lin['attrs']['ang']) 193 | else { 194 | const path = node['a:path'] 195 | if (path && path['attrs'] && path['attrs']['path']) pathType = path['attrs']['path'] 196 | } 197 | return { 198 | rot, 199 | path: pathType, 200 | colors: colors.sort((a, b) => parseInt(a.pos) - parseInt(b.pos)), 201 | } 202 | } 203 | 204 | export function getPatternFill(node, warpObj) { 205 | if (!node) return null 206 | 207 | const pattFill = node['a:pattFill'] 208 | if (!pattFill) return null 209 | 210 | const type = getTextByPathList(pattFill, ['attrs', 'prst']) 211 | 212 | const fgColorNode = pattFill['a:fgClr'] 213 | const bgColorNode = pattFill['a:bgClr'] 214 | 215 | let foregroundColor = '#000000' 216 | let backgroundColor = '#FFFFFF' 217 | 218 | if (fgColorNode) { 219 | foregroundColor = getSolidFill(fgColorNode, undefined, undefined, warpObj) 220 | } 221 | 222 | if (bgColorNode) { 223 | backgroundColor = getSolidFill(bgColorNode, undefined, undefined, warpObj) 224 | } 225 | 226 | return { 227 | type, 228 | foregroundColor, 229 | backgroundColor, 230 | } 231 | } 232 | 233 | export function getBgGradientFill(bgPr, phClr, slideMasterContent, warpObj) { 234 | if (bgPr) { 235 | const grdFill = bgPr['a:gradFill'] 236 | const gsLst = grdFill['a:gsLst']['a:gs'] 237 | const colors = [] 238 | 239 | for (let i = 0; i < gsLst.length; i++) { 240 | const lo_color = getSolidFill(gsLst[i], slideMasterContent['p:sldMaster']['p:clrMap']['attrs'], phClr, warpObj) 241 | const pos = getTextByPathList(gsLst[i], ['attrs', 'pos']) 242 | 243 | colors[i] = { 244 | pos: pos ? (pos / 1000 + '%') : '', 245 | color: lo_color, 246 | } 247 | } 248 | const lin = grdFill['a:lin'] 249 | let rot = 0 250 | let pathType = 'line' 251 | if (lin) rot = angleToDegrees(lin['attrs']['ang']) + 0 252 | else { 253 | const path = grdFill['a:path'] 254 | if (path && path['attrs'] && path['attrs']['path']) pathType = path['attrs']['path'] 255 | } 256 | return { 257 | rot, 258 | path: pathType, 259 | colors: colors.sort((a, b) => parseInt(a.pos) - parseInt(b.pos)), 260 | } 261 | } 262 | else if (phClr) { 263 | return phClr.indexOf('#') === -1 ? `#${phClr}` : phClr 264 | } 265 | return null 266 | } 267 | 268 | export async function getSlideBackgroundFill(warpObj) { 269 | const slideContent = warpObj['slideContent'] 270 | const slideLayoutContent = warpObj['slideLayoutContent'] 271 | const slideMasterContent = warpObj['slideMasterContent'] 272 | 273 | let bgPr = getTextByPathList(slideContent, ['p:sld', 'p:cSld', 'p:bg', 'p:bgPr']) 274 | let bgRef = getTextByPathList(slideContent, ['p:sld', 'p:cSld', 'p:bg', 'p:bgRef']) 275 | 276 | let background = '#fff' 277 | let backgroundType = 'color' 278 | 279 | if (bgPr) { 280 | const bgFillTyp = getFillType(bgPr) 281 | 282 | if (bgFillTyp === 'SOLID_FILL') { 283 | const sldFill = bgPr['a:solidFill'] 284 | let clrMapOvr 285 | const sldClrMapOvr = getTextByPathList(slideContent, ['p:sld', 'p:clrMapOvr', 'a:overrideClrMapping', 'attrs']) 286 | if (sldClrMapOvr) clrMapOvr = sldClrMapOvr 287 | else { 288 | const sldClrMapOvr = getTextByPathList(slideLayoutContent, ['p:sldLayout', 'p:clrMapOvr', 'a:overrideClrMapping', 'attrs']) 289 | if (sldClrMapOvr) clrMapOvr = sldClrMapOvr 290 | else clrMapOvr = getTextByPathList(slideMasterContent, ['p:sldMaster', 'p:clrMap', 'attrs']) 291 | } 292 | const sldBgClr = getSolidFill(sldFill, clrMapOvr, undefined, warpObj) 293 | background = sldBgClr 294 | } 295 | else if (bgFillTyp === 'GRADIENT_FILL') { 296 | const gradientFill = getBgGradientFill(bgPr, undefined, slideMasterContent, warpObj) 297 | if (typeof gradientFill === 'string') { 298 | background = gradientFill 299 | } 300 | else if (gradientFill) { 301 | background = gradientFill 302 | backgroundType = 'gradient' 303 | } 304 | } 305 | else if (bgFillTyp === 'PIC_FILL') { 306 | background = await getBgPicFill(bgPr, 'slideBg', warpObj) 307 | backgroundType = 'image' 308 | } 309 | else if (bgFillTyp === 'PATTERN_FILL') { 310 | const patternFill = getPatternFill(bgPr, warpObj) 311 | if (patternFill) { 312 | background = patternFill 313 | backgroundType = 'pattern' 314 | } 315 | } 316 | } 317 | else if (bgRef) { 318 | let clrMapOvr 319 | const sldClrMapOvr = getTextByPathList(slideContent, ['p:sld', 'p:clrMapOvr', 'a:overrideClrMapping', 'attrs']) 320 | if (sldClrMapOvr) clrMapOvr = sldClrMapOvr 321 | else { 322 | const sldClrMapOvr = getTextByPathList(slideLayoutContent, ['p:sldLayout', 'p:clrMapOvr', 'a:overrideClrMapping', 'attrs']) 323 | if (sldClrMapOvr) clrMapOvr = sldClrMapOvr 324 | else clrMapOvr = getTextByPathList(slideMasterContent, ['p:sldMaster', 'p:clrMap', 'attrs']) 325 | } 326 | const phClr = getSolidFill(bgRef, clrMapOvr, undefined, warpObj) 327 | const idx = Number(bgRef['attrs']['idx']) 328 | 329 | if (idx > 1000) { 330 | const trueIdx = idx - 1000 331 | const bgFillLst = warpObj['themeContent']['a:theme']['a:themeElements']['a:fmtScheme']['a:bgFillStyleLst'] 332 | const sortblAry = [] 333 | Object.keys(bgFillLst).forEach(key => { 334 | const bgFillLstTyp = bgFillLst[key] 335 | if (key !== 'attrs') { 336 | if (bgFillLstTyp.constructor === Array) { 337 | for (let i = 0; i < bgFillLstTyp.length; i++) { 338 | const obj = {} 339 | obj[key] = bgFillLstTyp[i] 340 | if (bgFillLstTyp[i]['attrs']) { 341 | obj['idex'] = bgFillLstTyp[i]['attrs']['order'] 342 | obj['attrs'] = { 343 | 'order': bgFillLstTyp[i]['attrs']['order'] 344 | } 345 | } 346 | sortblAry.push(obj) 347 | } 348 | } 349 | else { 350 | const obj = {} 351 | obj[key] = bgFillLstTyp 352 | if (bgFillLstTyp['attrs']) { 353 | obj['idex'] = bgFillLstTyp['attrs']['order'] 354 | obj['attrs'] = { 355 | 'order': bgFillLstTyp['attrs']['order'] 356 | } 357 | } 358 | sortblAry.push(obj) 359 | } 360 | } 361 | }) 362 | const sortByOrder = sortblAry.slice(0) 363 | sortByOrder.sort((a, b) => a.idex - b.idex) 364 | const bgFillLstIdx = sortByOrder[trueIdx - 1] 365 | const bgFillTyp = getFillType(bgFillLstIdx) 366 | if (bgFillTyp === 'SOLID_FILL') { 367 | const sldFill = bgFillLstIdx['a:solidFill'] 368 | const sldBgClr = getSolidFill(sldFill, clrMapOvr, undefined, warpObj) 369 | background = sldBgClr 370 | } 371 | else if (bgFillTyp === 'GRADIENT_FILL') { 372 | const gradientFill = getBgGradientFill(bgFillLstIdx, phClr, slideMasterContent, warpObj) 373 | if (typeof gradientFill === 'string') { 374 | background = gradientFill 375 | } 376 | else if (gradientFill) { 377 | background = gradientFill 378 | backgroundType = 'gradient' 379 | } 380 | } 381 | } 382 | } 383 | else { 384 | bgPr = getTextByPathList(slideLayoutContent, ['p:sldLayout', 'p:cSld', 'p:bg', 'p:bgPr']) 385 | bgRef = getTextByPathList(slideLayoutContent, ['p:sldLayout', 'p:cSld', 'p:bg', 'p:bgRef']) 386 | 387 | let clrMapOvr 388 | const sldClrMapOvr = getTextByPathList(slideLayoutContent, ['p:sldLayout', 'p:clrMapOvr', 'a:overrideClrMapping', 'attrs']) 389 | if (sldClrMapOvr) clrMapOvr = sldClrMapOvr 390 | else clrMapOvr = getTextByPathList(slideMasterContent, ['p:sldMaster', 'p:clrMap', 'attrs']) 391 | 392 | if (bgPr) { 393 | const bgFillTyp = getFillType(bgPr) 394 | if (bgFillTyp === 'SOLID_FILL') { 395 | const sldFill = bgPr['a:solidFill'] 396 | const sldBgClr = getSolidFill(sldFill, clrMapOvr, undefined, warpObj) 397 | background = sldBgClr 398 | } 399 | else if (bgFillTyp === 'GRADIENT_FILL') { 400 | const gradientFill = getBgGradientFill(bgPr, undefined, slideMasterContent, warpObj) 401 | if (typeof gradientFill === 'string') { 402 | background = gradientFill 403 | } 404 | else if (gradientFill) { 405 | background = gradientFill 406 | backgroundType = 'gradient' 407 | } 408 | } 409 | else if (bgFillTyp === 'PIC_FILL') { 410 | background = await getBgPicFill(bgPr, 'slideLayoutBg', warpObj) 411 | backgroundType = 'image' 412 | } 413 | else if (bgFillTyp === 'PATTERN_FILL') { 414 | const patternFill = getPatternFill(bgPr, warpObj) 415 | if (patternFill) { 416 | background = patternFill 417 | backgroundType = 'pattern' 418 | } 419 | } 420 | } 421 | else if (bgRef) { 422 | const phClr = getSolidFill(bgRef, clrMapOvr, undefined, warpObj) 423 | const idx = Number(bgRef['attrs']['idx']) 424 | 425 | if (idx > 1000) { 426 | const trueIdx = idx - 1000 427 | const bgFillLst = warpObj['themeContent']['a:theme']['a:themeElements']['a:fmtScheme']['a:bgFillStyleLst'] 428 | const sortblAry = [] 429 | Object.keys(bgFillLst).forEach(key => { 430 | const bgFillLstTyp = bgFillLst[key] 431 | if (key !== 'attrs') { 432 | if (bgFillLstTyp.constructor === Array) { 433 | for (let i = 0; i < bgFillLstTyp.length; i++) { 434 | const obj = {} 435 | obj[key] = bgFillLstTyp[i] 436 | if (bgFillLstTyp[i]['attrs']) { 437 | obj['idex'] = bgFillLstTyp[i]['attrs']['order'] 438 | obj['attrs'] = { 439 | 'order': bgFillLstTyp[i]['attrs']['order'] 440 | } 441 | } 442 | sortblAry.push(obj) 443 | } 444 | } 445 | else { 446 | const obj = {} 447 | obj[key] = bgFillLstTyp 448 | if (bgFillLstTyp['attrs']) { 449 | obj['idex'] = bgFillLstTyp['attrs']['order'] 450 | obj['attrs'] = { 451 | 'order': bgFillLstTyp['attrs']['order'] 452 | } 453 | } 454 | sortblAry.push(obj) 455 | } 456 | } 457 | }) 458 | const sortByOrder = sortblAry.slice(0) 459 | sortByOrder.sort((a, b) => a.idex - b.idex) 460 | const bgFillLstIdx = sortByOrder[trueIdx - 1] 461 | const bgFillTyp = getFillType(bgFillLstIdx) 462 | if (bgFillTyp === 'SOLID_FILL') { 463 | const sldFill = bgFillLstIdx['a:solidFill'] 464 | const sldBgClr = getSolidFill(sldFill, clrMapOvr, undefined, warpObj) 465 | background = sldBgClr 466 | } 467 | else if (bgFillTyp === 'GRADIENT_FILL') { 468 | const gradientFill = getBgGradientFill(bgFillLstIdx, phClr, slideMasterContent, warpObj) 469 | if (typeof gradientFill === 'string') { 470 | background = gradientFill 471 | } 472 | else if (gradientFill) { 473 | background = gradientFill 474 | backgroundType = 'gradient' 475 | } 476 | } 477 | else if (bgFillTyp === 'PIC_FILL') { 478 | background = await getBgPicFill(bgFillLstIdx, 'themeBg', warpObj) 479 | backgroundType = 'image' 480 | } 481 | else if (bgFillTyp === 'PATTERN_FILL') { 482 | const patternFill = getPatternFill(bgFillLstIdx, warpObj) 483 | if (patternFill) { 484 | background = patternFill 485 | backgroundType = 'pattern' 486 | } 487 | } 488 | } 489 | } 490 | else { 491 | bgPr = getTextByPathList(slideMasterContent, ['p:sldMaster', 'p:cSld', 'p:bg', 'p:bgPr']) 492 | bgRef = getTextByPathList(slideMasterContent, ['p:sldMaster', 'p:cSld', 'p:bg', 'p:bgRef']) 493 | 494 | const clrMap = getTextByPathList(slideMasterContent, ['p:sldMaster', 'p:clrMap', 'attrs']) 495 | if (bgPr) { 496 | const bgFillTyp = getFillType(bgPr) 497 | if (bgFillTyp === 'SOLID_FILL') { 498 | const sldFill = bgPr['a:solidFill'] 499 | const sldBgClr = getSolidFill(sldFill, clrMap, undefined, warpObj) 500 | background = sldBgClr 501 | } 502 | else if (bgFillTyp === 'GRADIENT_FILL') { 503 | const gradientFill = getBgGradientFill(bgPr, undefined, slideMasterContent, warpObj) 504 | if (typeof gradientFill === 'string') { 505 | background = gradientFill 506 | } 507 | else if (gradientFill) { 508 | background = gradientFill 509 | backgroundType = 'gradient' 510 | } 511 | } 512 | else if (bgFillTyp === 'PIC_FILL') { 513 | background = await getBgPicFill(bgPr, 'slideMasterBg', warpObj) 514 | backgroundType = 'image' 515 | } 516 | else if (bgFillTyp === 'PATTERN_FILL') { 517 | const patternFill = getPatternFill(bgPr, warpObj) 518 | if (patternFill) { 519 | background = patternFill 520 | backgroundType = 'pattern' 521 | } 522 | } 523 | } 524 | else if (bgRef) { 525 | const phClr = getSolidFill(bgRef, clrMap, undefined, warpObj) 526 | const idx = Number(bgRef['attrs']['idx']) 527 | 528 | if (idx > 1000) { 529 | const trueIdx = idx - 1000 530 | const bgFillLst = warpObj['themeContent']['a:theme']['a:themeElements']['a:fmtScheme']['a:bgFillStyleLst'] 531 | const sortblAry = [] 532 | Object.keys(bgFillLst).forEach(key => { 533 | const bgFillLstTyp = bgFillLst[key] 534 | if (key !== 'attrs') { 535 | if (bgFillLstTyp.constructor === Array) { 536 | for (let i = 0; i < bgFillLstTyp.length; i++) { 537 | const obj = {} 538 | obj[key] = bgFillLstTyp[i] 539 | if (bgFillLstTyp[i]['attrs']) { 540 | obj['idex'] = bgFillLstTyp[i]['attrs']['order'] 541 | obj['attrs'] = { 542 | 'order': bgFillLstTyp[i]['attrs']['order'] 543 | } 544 | } 545 | sortblAry.push(obj) 546 | } 547 | } 548 | else { 549 | const obj = {} 550 | obj[key] = bgFillLstTyp 551 | if (bgFillLstTyp['attrs']) { 552 | obj['idex'] = bgFillLstTyp['attrs']['order'] 553 | obj['attrs'] = { 554 | 'order': bgFillLstTyp['attrs']['order'] 555 | } 556 | } 557 | sortblAry.push(obj) 558 | } 559 | } 560 | }) 561 | const sortByOrder = sortblAry.slice(0) 562 | sortByOrder.sort((a, b) => a.idex - b.idex) 563 | const bgFillLstIdx = sortByOrder[trueIdx - 1] 564 | const bgFillTyp = getFillType(bgFillLstIdx) 565 | if (bgFillTyp === 'SOLID_FILL') { 566 | const sldFill = bgFillLstIdx['a:solidFill'] 567 | const sldBgClr = getSolidFill(sldFill, clrMapOvr, undefined, warpObj) 568 | background = sldBgClr 569 | } 570 | else if (bgFillTyp === 'GRADIENT_FILL') { 571 | const gradientFill = getBgGradientFill(bgFillLstIdx, phClr, slideMasterContent, warpObj) 572 | if (typeof gradientFill === 'string') { 573 | background = gradientFill 574 | } 575 | else if (gradientFill) { 576 | background = gradientFill 577 | backgroundType = 'gradient' 578 | } 579 | } 580 | else if (bgFillTyp === 'PIC_FILL') { 581 | background = await getBgPicFill(bgFillLstIdx, 'themeBg', warpObj) 582 | backgroundType = 'image' 583 | } 584 | else if (bgFillTyp === 'PATTERN_FILL') { 585 | const patternFill = getPatternFill(bgFillLstIdx, warpObj) 586 | if (patternFill) { 587 | background = patternFill 588 | backgroundType = 'pattern' 589 | } 590 | } 591 | } 592 | } 593 | } 594 | } 595 | return { 596 | type: backgroundType, 597 | value: background, 598 | } 599 | } 600 | 601 | export async function getShapeFill(node, pNode, isSvgMode, warpObj, source, groupHierarchy = []) { 602 | const fillType = getFillType(getTextByPathList(node, ['p:spPr'])) 603 | let type = 'color' 604 | let fillValue = '' 605 | if (fillType === 'NO_FILL') { 606 | return isSvgMode ? 'none' : '' 607 | } 608 | else if (fillType === 'SOLID_FILL') { 609 | const shpFill = node['p:spPr']['a:solidFill'] 610 | fillValue = getSolidFill(shpFill, undefined, undefined, warpObj) 611 | type = 'color' 612 | } 613 | else if (fillType === 'GRADIENT_FILL') { 614 | const shpFill = node['p:spPr']['a:gradFill'] 615 | fillValue = getGradientFill(shpFill, warpObj) 616 | type = 'gradient' 617 | } 618 | else if (fillType === 'PIC_FILL') { 619 | const shpFill = node['p:spPr']['a:blipFill'] 620 | const picBase64 = await getPicFill(source, shpFill, warpObj) 621 | const opacity = getPicFillOpacity(shpFill) 622 | fillValue = { 623 | picBase64, 624 | opacity, 625 | } 626 | type = 'image' 627 | } 628 | else if (fillType === 'PATTERN_FILL') { 629 | const shpFill = node['p:spPr']['a:pattFill'] 630 | fillValue = getPatternFill({ 'a:pattFill': shpFill }, warpObj) 631 | type = 'pattern' 632 | } 633 | else if (fillType === 'GROUP_FILL') { 634 | return findFillInGroupHierarchy(groupHierarchy, warpObj, source) 635 | } 636 | if (!fillValue) { 637 | const clrName = getTextByPathList(node, ['p:style', 'a:fillRef']) 638 | fillValue = getSolidFill(clrName, undefined, undefined, warpObj) 639 | type = 'color' 640 | } 641 | if (!fillValue && pNode && fillType === 'NO_FILL') { 642 | return isSvgMode ? 'none' : '' 643 | } 644 | 645 | return { 646 | type, 647 | value: fillValue, 648 | } 649 | } 650 | 651 | async function findFillInGroupHierarchy(groupHierarchy, warpObj, source) { 652 | for (const groupNode of groupHierarchy) { 653 | if (!groupNode || !groupNode['p:grpSpPr']) continue 654 | 655 | const grpSpPr = groupNode['p:grpSpPr'] 656 | const fillType = getFillType(grpSpPr) 657 | 658 | if (fillType === 'SOLID_FILL') { 659 | const shpFill = grpSpPr['a:solidFill'] 660 | const fillValue = getSolidFill(shpFill, undefined, undefined, warpObj) 661 | if (fillValue) { 662 | return { 663 | type: 'color', 664 | value: fillValue, 665 | } 666 | } 667 | } 668 | else if (fillType === 'GRADIENT_FILL') { 669 | const shpFill = grpSpPr['a:gradFill'] 670 | const fillValue = getGradientFill(shpFill, warpObj) 671 | if (fillValue) { 672 | return { 673 | type: 'gradient', 674 | value: fillValue, 675 | } 676 | } 677 | } 678 | else if (fillType === 'PIC_FILL') { 679 | const shpFill = grpSpPr['a:blipFill'] 680 | const picBase64 = await getPicFill(source, shpFill, warpObj) 681 | const opacity = getPicFillOpacity(shpFill) 682 | if (picBase64) { 683 | return { 684 | type: 'image', 685 | value: { 686 | picBase64, 687 | opacity, 688 | }, 689 | } 690 | } 691 | } 692 | else if (fillType === 'PATTERN_FILL') { 693 | const shpFill = grpSpPr['a:pattFill'] 694 | const fillValue = getPatternFill({ 'a:pattFill': shpFill }, warpObj) 695 | if (fillValue) { 696 | return { 697 | type: 'pattern', 698 | value: fillValue, 699 | } 700 | } 701 | } 702 | } 703 | 704 | return null 705 | } 706 | 707 | export function getSolidFill(solidFill, clrMap, phClr, warpObj) { 708 | if (!solidFill) return '' 709 | 710 | let color = '' 711 | let clrNode 712 | 713 | if (solidFill['a:srgbClr']) { 714 | clrNode = solidFill['a:srgbClr'] 715 | color = getTextByPathList(clrNode, ['attrs', 'val']) 716 | } 717 | else if (solidFill['a:schemeClr']) { 718 | clrNode = solidFill['a:schemeClr'] 719 | const schemeClr = 'a:' + getTextByPathList(clrNode, ['attrs', 'val']) 720 | color = getSchemeColorFromTheme(schemeClr, warpObj, clrMap, phClr) || '' 721 | } 722 | else if (solidFill['a:scrgbClr']) { 723 | clrNode = solidFill['a:scrgbClr'] 724 | const defBultColorVals = clrNode['attrs'] 725 | const red = (defBultColorVals['r'].indexOf('%') !== -1) ? defBultColorVals['r'].split('%').shift() : defBultColorVals['r'] 726 | const green = (defBultColorVals['g'].indexOf('%') !== -1) ? defBultColorVals['g'].split('%').shift() : defBultColorVals['g'] 727 | const blue = (defBultColorVals['b'].indexOf('%') !== -1) ? defBultColorVals['b'].split('%').shift() : defBultColorVals['b'] 728 | color = toHex(255 * (Number(red) / 100)) + toHex(255 * (Number(green) / 100)) + toHex(255 * (Number(blue) / 100)) 729 | } 730 | else if (solidFill['a:prstClr']) { 731 | clrNode = solidFill['a:prstClr'] 732 | const prstClr = getTextByPathList(clrNode, ['attrs', 'val']) 733 | color = getColorName2Hex(prstClr) 734 | } 735 | else if (solidFill['a:hslClr']) { 736 | clrNode = solidFill['a:hslClr'] 737 | const defBultColorVals = clrNode['attrs'] 738 | const hue = Number(defBultColorVals['hue']) / 100000 739 | const sat = Number((defBultColorVals['sat'].indexOf('%') !== -1) ? defBultColorVals['sat'].split('%').shift() : defBultColorVals['sat']) / 100 740 | const lum = Number((defBultColorVals['lum'].indexOf('%') !== -1) ? defBultColorVals['lum'].split('%').shift() : defBultColorVals['lum']) / 100 741 | const hsl2rgb = hslToRgb(hue, sat, lum) 742 | color = toHex(hsl2rgb.r) + toHex(hsl2rgb.g) + toHex(hsl2rgb.b) 743 | } 744 | else if (solidFill['a:sysClr']) { 745 | clrNode = solidFill['a:sysClr'] 746 | const sysClr = getTextByPathList(clrNode, ['attrs', 'lastClr']) 747 | if (sysClr) color = sysClr 748 | } 749 | 750 | let isAlpha = false 751 | const alpha = parseInt(getTextByPathList(clrNode, ['a:alpha', 'attrs', 'val'])) / 100000 752 | if (!isNaN(alpha)) { 753 | const al_color = tinycolor(color) 754 | al_color.setAlpha(alpha) 755 | color = al_color.toHex8() 756 | isAlpha = true 757 | } 758 | 759 | const hueMod = parseInt(getTextByPathList(clrNode, ['a:hueMod', 'attrs', 'val'])) / 100000 760 | if (!isNaN(hueMod)) { 761 | color = applyHueMod(color, hueMod, isAlpha) 762 | } 763 | const lumMod = parseInt(getTextByPathList(clrNode, ['a:lumMod', 'attrs', 'val'])) / 100000 764 | if (!isNaN(lumMod)) { 765 | color = applyLumMod(color, lumMod, isAlpha) 766 | } 767 | const lumOff = parseInt(getTextByPathList(clrNode, ['a:lumOff', 'attrs', 'val'])) / 100000 768 | if (!isNaN(lumOff)) { 769 | color = applyLumOff(color, lumOff, isAlpha) 770 | } 771 | const satMod = parseInt(getTextByPathList(clrNode, ['a:satMod', 'attrs', 'val'])) / 100000 772 | if (!isNaN(satMod)) { 773 | color = applySatMod(color, satMod, isAlpha) 774 | } 775 | const shade = parseInt(getTextByPathList(clrNode, ['a:shade', 'attrs', 'val'])) / 100000 776 | if (!isNaN(shade)) { 777 | color = applyShade(color, shade, isAlpha) 778 | } 779 | const tint = parseInt(getTextByPathList(clrNode, ['a:tint', 'attrs', 'val'])) / 100000 780 | if (!isNaN(tint)) { 781 | color = applyTint(color, tint, isAlpha) 782 | } 783 | 784 | if (color && color.indexOf('#') === -1) color = '#' + color 785 | 786 | return color 787 | } -------------------------------------------------------------------------------- /src/pptxtojson.js: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip' 2 | import { readXmlFile } from './readXmlFile' 3 | import { getBorder } from './border' 4 | import { getSlideBackgroundFill, getShapeFill, getSolidFill, getPicFill, getPicFilters } from './fill' 5 | import { getChartInfo } from './chart' 6 | import { getVerticalAlign } from './align' 7 | import { getPosition, getSize } from './position' 8 | import { genTextBody } from './text' 9 | import { getCustomShapePath } from './shape' 10 | import { extractFileExtension, base64ArrayBuffer, getTextByPathList, angleToDegrees, getMimeType, isVideoLink, escapeHtml, hasValidText, numberToFixed } from './utils' 11 | import { getShadow } from './shadow' 12 | import { getTableBorders, getTableCellParams, getTableRowParams } from './table' 13 | import { RATIO_EMUs_Points } from './constants' 14 | import { findOMath, latexFormart, parseOMath } from './math' 15 | import { getShapePath } from './shapePath' 16 | import { parseTransition, findTransitionNode } from './animation' 17 | 18 | export async function parse(file) { 19 | const slides = [] 20 | 21 | const zip = await JSZip.loadAsync(file) 22 | 23 | const filesInfo = await getContentTypes(zip) 24 | const { width, height, defaultTextStyle } = await getSlideInfo(zip) 25 | const { themeContent, themeColors } = await getTheme(zip) 26 | 27 | for (const filename of filesInfo.slides) { 28 | const singleSlide = await processSingleSlide(zip, filename, themeContent, defaultTextStyle) 29 | slides.push(singleSlide) 30 | } 31 | 32 | return { 33 | slides, 34 | themeColors, 35 | size: { 36 | width, 37 | height, 38 | }, 39 | } 40 | } 41 | 42 | async function getContentTypes(zip) { 43 | const ContentTypesJson = await readXmlFile(zip, '[Content_Types].xml') 44 | const subObj = ContentTypesJson['Types']['Override'] 45 | let slidesLocArray = [] 46 | let slideLayoutsLocArray = [] 47 | 48 | for (const item of subObj) { 49 | switch (item['attrs']['ContentType']) { 50 | case 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml': 51 | slidesLocArray.push(item['attrs']['PartName'].substr(1)) 52 | break 53 | case 'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml': 54 | slideLayoutsLocArray.push(item['attrs']['PartName'].substr(1)) 55 | break 56 | default: 57 | } 58 | } 59 | 60 | const sortSlideXml = (p1, p2) => { 61 | const n1 = +/(\d+)\.xml/.exec(p1)[1] 62 | const n2 = +/(\d+)\.xml/.exec(p2)[1] 63 | return n1 - n2 64 | } 65 | slidesLocArray = slidesLocArray.sort(sortSlideXml) 66 | slideLayoutsLocArray = slideLayoutsLocArray.sort(sortSlideXml) 67 | 68 | return { 69 | slides: slidesLocArray, 70 | slideLayouts: slideLayoutsLocArray, 71 | } 72 | } 73 | 74 | async function getSlideInfo(zip) { 75 | const content = await readXmlFile(zip, 'ppt/presentation.xml') 76 | const sldSzAttrs = content['p:presentation']['p:sldSz']['attrs'] 77 | const defaultTextStyle = content['p:presentation']['p:defaultTextStyle'] 78 | return { 79 | width: parseInt(sldSzAttrs['cx']) * RATIO_EMUs_Points, 80 | height: parseInt(sldSzAttrs['cy']) * RATIO_EMUs_Points, 81 | defaultTextStyle, 82 | } 83 | } 84 | 85 | async function getTheme(zip) { 86 | const preResContent = await readXmlFile(zip, 'ppt/_rels/presentation.xml.rels') 87 | const relationshipArray = preResContent['Relationships']['Relationship'] 88 | let themeURI 89 | 90 | if (relationshipArray.constructor === Array) { 91 | for (const relationshipItem of relationshipArray) { 92 | if (relationshipItem['attrs']['Type'] === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme') { 93 | themeURI = relationshipItem['attrs']['Target'] 94 | break 95 | } 96 | } 97 | } 98 | else if (relationshipArray['attrs']['Type'] === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme') { 99 | themeURI = relationshipArray['attrs']['Target'] 100 | } 101 | 102 | const themeContent = await readXmlFile(zip, 'ppt/' + themeURI) 103 | 104 | const themeColors = [] 105 | const clrScheme = getTextByPathList(themeContent, ['a:theme', 'a:themeElements', 'a:clrScheme']) 106 | if (clrScheme) { 107 | for (let i = 1; i <= 6; i++) { 108 | if (clrScheme[`a:accent${i}`] === undefined) break 109 | const color = getTextByPathList(clrScheme, [`a:accent${i}`, 'a:srgbClr', 'attrs', 'val']) 110 | if (color) themeColors.push('#' + color) 111 | } 112 | } 113 | 114 | return { themeContent, themeColors } 115 | } 116 | 117 | async function processSingleSlide(zip, sldFileName, themeContent, defaultTextStyle) { 118 | const resName = sldFileName.replace('slides/slide', 'slides/_rels/slide') + '.rels' 119 | const resContent = await readXmlFile(zip, resName) 120 | let relationshipArray = resContent['Relationships']['Relationship'] 121 | if (relationshipArray.constructor !== Array) relationshipArray = [relationshipArray] 122 | 123 | let noteFilename = '' 124 | let layoutFilename = '' 125 | let masterFilename = '' 126 | let themeFilename = '' 127 | let diagramFilename = '' 128 | const slideResObj = {} 129 | const layoutResObj = {} 130 | const masterResObj = {} 131 | const themeResObj = {} 132 | const diagramResObj = {} 133 | 134 | for (const relationshipArrayItem of relationshipArray) { 135 | switch (relationshipArrayItem['attrs']['Type']) { 136 | case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout': 137 | layoutFilename = relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/') 138 | slideResObj[relationshipArrayItem['attrs']['Id']] = { 139 | type: relationshipArrayItem['attrs']['Type'].replace('http://schemas.openxmlformats.org/officeDocument/2006/relationships/', ''), 140 | target: relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/') 141 | } 142 | break 143 | case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide': 144 | noteFilename = relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/') 145 | slideResObj[relationshipArrayItem['attrs']['Id']] = { 146 | type: relationshipArrayItem['attrs']['Type'].replace('http://schemas.openxmlformats.org/officeDocument/2006/relationships/', ''), 147 | target: relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/') 148 | } 149 | break 150 | case 'http://schemas.microsoft.com/office/2007/relationships/diagramDrawing': 151 | diagramFilename = relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/') 152 | slideResObj[relationshipArrayItem['attrs']['Id']] = { 153 | type: relationshipArrayItem['attrs']['Type'].replace('http://schemas.openxmlformats.org/officeDocument/2006/relationships/', ''), 154 | target: relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/') 155 | } 156 | break 157 | case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image': 158 | case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart': 159 | case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink': 160 | default: 161 | slideResObj[relationshipArrayItem['attrs']['Id']] = { 162 | type: relationshipArrayItem['attrs']['Type'].replace('http://schemas.openxmlformats.org/officeDocument/2006/relationships/', ''), 163 | target: relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/'), 164 | } 165 | } 166 | } 167 | 168 | const slideNotesContent = await readXmlFile(zip, noteFilename) 169 | const note = getNote(slideNotesContent) 170 | 171 | const slideLayoutContent = await readXmlFile(zip, layoutFilename) 172 | const slideLayoutTables = await indexNodes(slideLayoutContent) 173 | const slideLayoutResFilename = layoutFilename.replace('slideLayouts/slideLayout', 'slideLayouts/_rels/slideLayout') + '.rels' 174 | const slideLayoutResContent = await readXmlFile(zip, slideLayoutResFilename) 175 | relationshipArray = slideLayoutResContent['Relationships']['Relationship'] 176 | if (relationshipArray.constructor !== Array) relationshipArray = [relationshipArray] 177 | 178 | for (const relationshipArrayItem of relationshipArray) { 179 | switch (relationshipArrayItem['attrs']['Type']) { 180 | case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster': 181 | masterFilename = relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/') 182 | break 183 | default: 184 | layoutResObj[relationshipArrayItem['attrs']['Id']] = { 185 | type: relationshipArrayItem['attrs']['Type'].replace('http://schemas.openxmlformats.org/officeDocument/2006/relationships/', ''), 186 | target: relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/'), 187 | } 188 | } 189 | } 190 | 191 | const slideMasterContent = await readXmlFile(zip, masterFilename) 192 | const slideMasterTextStyles = getTextByPathList(slideMasterContent, ['p:sldMaster', 'p:txStyles']) 193 | const slideMasterTables = indexNodes(slideMasterContent) 194 | const slideMasterResFilename = masterFilename.replace('slideMasters/slideMaster', 'slideMasters/_rels/slideMaster') + '.rels' 195 | const slideMasterResContent = await readXmlFile(zip, slideMasterResFilename) 196 | relationshipArray = slideMasterResContent['Relationships']['Relationship'] 197 | if (relationshipArray.constructor !== Array) relationshipArray = [relationshipArray] 198 | 199 | for (const relationshipArrayItem of relationshipArray) { 200 | switch (relationshipArrayItem['attrs']['Type']) { 201 | case 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme': 202 | themeFilename = relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/') 203 | break 204 | default: 205 | masterResObj[relationshipArrayItem['attrs']['Id']] = { 206 | type: relationshipArrayItem['attrs']['Type'].replace('http://schemas.openxmlformats.org/officeDocument/2006/relationships/', ''), 207 | target: relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/'), 208 | } 209 | } 210 | } 211 | 212 | if (themeFilename) { 213 | const themeName = themeFilename.split('/').pop() 214 | const themeResFileName = themeFilename.replace(themeName, '_rels/' + themeName) + '.rels' 215 | const themeResContent = await readXmlFile(zip, themeResFileName) 216 | if (themeResContent) { 217 | relationshipArray = themeResContent['Relationships']['Relationship'] 218 | if (relationshipArray) { 219 | if (relationshipArray.constructor !== Array) relationshipArray = [relationshipArray] 220 | for (const relationshipArrayItem of relationshipArray) { 221 | themeResObj[relationshipArrayItem['attrs']['Id']] = { 222 | 'type': relationshipArrayItem['attrs']['Type'].replace('http://schemas.openxmlformats.org/officeDocument/2006/relationships/', ''), 223 | 'target': relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/') 224 | } 225 | } 226 | } 227 | } 228 | } 229 | 230 | let digramFileContent = {} 231 | if (diagramFilename) { 232 | const diagName = diagramFilename.split('/').pop() 233 | const diagramResFileName = diagramFilename.replace(diagName, '_rels/' + diagName) + '.rels' 234 | digramFileContent = await readXmlFile(zip, diagramFilename) 235 | if (digramFileContent) { 236 | const digramFileContentObjToStr = JSON.stringify(digramFileContent).replace(/dsp:/g, 'p:') 237 | digramFileContent = JSON.parse(digramFileContentObjToStr) 238 | } 239 | const digramResContent = await readXmlFile(zip, diagramResFileName) 240 | if (digramResContent) { 241 | relationshipArray = digramResContent['Relationships']['Relationship'] 242 | if (relationshipArray.constructor !== Array) relationshipArray = [relationshipArray] 243 | for (const relationshipArrayItem of relationshipArray) { 244 | diagramResObj[relationshipArrayItem['attrs']['Id']] = { 245 | 'type': relationshipArrayItem['attrs']['Type'].replace('http://schemas.openxmlformats.org/officeDocument/2006/relationships/', ''), 246 | 'target': relationshipArrayItem['attrs']['Target'].replace('../', 'ppt/') 247 | } 248 | } 249 | } 250 | } 251 | 252 | const tableStyles = await readXmlFile(zip, 'ppt/tableStyles.xml') 253 | 254 | const slideContent = await readXmlFile(zip, sldFileName) 255 | const nodes = slideContent['p:sld']['p:cSld']['p:spTree'] 256 | const warpObj = { 257 | zip, 258 | slideLayoutContent, 259 | slideLayoutTables, 260 | slideMasterContent, 261 | slideMasterTables, 262 | slideContent, 263 | tableStyles, 264 | slideResObj, 265 | slideMasterTextStyles, 266 | layoutResObj, 267 | masterResObj, 268 | themeContent, 269 | themeResObj, 270 | digramFileContent, 271 | diagramResObj, 272 | defaultTextStyle, 273 | } 274 | const layoutElements = await getLayoutElements(warpObj) 275 | const fill = await getSlideBackgroundFill(warpObj) 276 | 277 | const elements = [] 278 | for (const nodeKey in nodes) { 279 | if (nodes[nodeKey].constructor !== Array) nodes[nodeKey] = [nodes[nodeKey]] 280 | for (const node of nodes[nodeKey]) { 281 | const ret = await processNodesInSlide(nodeKey, node, nodes, warpObj, 'slide') 282 | if (ret) elements.push(ret) 283 | } 284 | } 285 | 286 | let transitionNode = findTransitionNode(slideContent, 'p:sld') 287 | if (!transitionNode) transitionNode = findTransitionNode(slideLayoutContent, 'p:sldLayout') 288 | if (!transitionNode) transitionNode = findTransitionNode(slideMasterContent, 'p:sldMaster') 289 | 290 | const transition = parseTransition(transitionNode) 291 | 292 | return { 293 | fill, 294 | elements, 295 | layoutElements, 296 | note, 297 | transition, 298 | } 299 | } 300 | 301 | function getNote(noteContent) { 302 | let text = '' 303 | let spNodes = getTextByPathList(noteContent, ['p:notes', 'p:cSld', 'p:spTree', 'p:sp']) 304 | if (!spNodes) return '' 305 | 306 | if (spNodes.constructor !== Array) spNodes = [spNodes] 307 | for (const spNode of spNodes) { 308 | let rNodes = getTextByPathList(spNode, ['p:txBody', 'a:p', 'a:r']) 309 | if (!rNodes) continue 310 | 311 | if (rNodes.constructor !== Array) rNodes = [rNodes] 312 | for (const rNode of rNodes) { 313 | const t = getTextByPathList(rNode, ['a:t']) 314 | if (t && typeof t === 'string') text += t 315 | } 316 | } 317 | return text 318 | } 319 | 320 | async function getLayoutElements(warpObj) { 321 | const elements = [] 322 | const slideLayoutContent = warpObj['slideLayoutContent'] 323 | const slideMasterContent = warpObj['slideMasterContent'] 324 | const nodesSldLayout = getTextByPathList(slideLayoutContent, ['p:sldLayout', 'p:cSld', 'p:spTree']) 325 | const nodesSldMaster = getTextByPathList(slideMasterContent, ['p:sldMaster', 'p:cSld', 'p:spTree']) 326 | 327 | const showMasterSp = getTextByPathList(slideLayoutContent, ['p:sldLayout', 'attrs', 'showMasterSp']) 328 | if (nodesSldLayout) { 329 | for (const nodeKey in nodesSldLayout) { 330 | if (nodesSldLayout[nodeKey].constructor === Array) { 331 | for (let i = 0; i < nodesSldLayout[nodeKey].length; i++) { 332 | const ph = getTextByPathList(nodesSldLayout[nodeKey][i], ['p:nvSpPr', 'p:nvPr', 'p:ph']) 333 | if (!ph) { 334 | const ret = await processNodesInSlide(nodeKey, nodesSldLayout[nodeKey][i], nodesSldLayout, warpObj, 'slideLayoutBg') 335 | if (ret) elements.push(ret) 336 | } 337 | } 338 | } 339 | else { 340 | const ph = getTextByPathList(nodesSldLayout[nodeKey], ['p:nvSpPr', 'p:nvPr', 'p:ph']) 341 | if (!ph) { 342 | const ret = await processNodesInSlide(nodeKey, nodesSldLayout[nodeKey], nodesSldLayout, warpObj, 'slideLayoutBg') 343 | if (ret) elements.push(ret) 344 | } 345 | } 346 | } 347 | } 348 | if (nodesSldMaster && showMasterSp !== '0') { 349 | for (const nodeKey in nodesSldMaster) { 350 | if (nodesSldMaster[nodeKey].constructor === Array) { 351 | for (let i = 0; i < nodesSldMaster[nodeKey].length; i++) { 352 | const ph = getTextByPathList(nodesSldMaster[nodeKey][i], ['p:nvSpPr', 'p:nvPr', 'p:ph']) 353 | if (!ph) { 354 | const ret = await processNodesInSlide(nodeKey, nodesSldMaster[nodeKey][i], nodesSldMaster, warpObj, 'slideMasterBg') 355 | if (ret) elements.push(ret) 356 | } 357 | } 358 | } 359 | else { 360 | const ph = getTextByPathList(nodesSldMaster[nodeKey], ['p:nvSpPr', 'p:nvPr', 'p:ph']) 361 | if (!ph) { 362 | const ret = await processNodesInSlide(nodeKey, nodesSldMaster[nodeKey], nodesSldMaster, warpObj, 'slideMasterBg') 363 | if (ret) elements.push(ret) 364 | } 365 | } 366 | } 367 | } 368 | return elements 369 | } 370 | 371 | function indexNodes(content) { 372 | const keys = Object.keys(content) 373 | const spTreeNode = content[keys[0]]['p:cSld']['p:spTree'] 374 | const idTable = {} 375 | const idxTable = {} 376 | const typeTable = {} 377 | 378 | for (const key in spTreeNode) { 379 | if (key === 'p:nvGrpSpPr' || key === 'p:grpSpPr') continue 380 | 381 | const targetNode = spTreeNode[key] 382 | 383 | if (targetNode.constructor === Array) { 384 | for (const targetNodeItem of targetNode) { 385 | const nvSpPrNode = targetNodeItem['p:nvSpPr'] 386 | const id = getTextByPathList(nvSpPrNode, ['p:cNvPr', 'attrs', 'id']) 387 | const idx = getTextByPathList(nvSpPrNode, ['p:nvPr', 'p:ph', 'attrs', 'idx']) 388 | const type = getTextByPathList(nvSpPrNode, ['p:nvPr', 'p:ph', 'attrs', 'type']) 389 | 390 | if (id) idTable[id] = targetNodeItem 391 | if (idx) idxTable[idx] = targetNodeItem 392 | if (type) typeTable[type] = targetNodeItem 393 | } 394 | } 395 | else { 396 | const nvSpPrNode = targetNode['p:nvSpPr'] 397 | const id = getTextByPathList(nvSpPrNode, ['p:cNvPr', 'attrs', 'id']) 398 | const idx = getTextByPathList(nvSpPrNode, ['p:nvPr', 'p:ph', 'attrs', 'idx']) 399 | const type = getTextByPathList(nvSpPrNode, ['p:nvPr', 'p:ph', 'attrs', 'type']) 400 | 401 | if (id) idTable[id] = targetNode 402 | if (idx) idxTable[idx] = targetNode 403 | if (type) typeTable[type] = targetNode 404 | } 405 | } 406 | 407 | return { idTable, idxTable, typeTable } 408 | } 409 | 410 | async function processNodesInSlide(nodeKey, nodeValue, nodes, warpObj, source, groupHierarchy = []) { 411 | let json 412 | 413 | switch (nodeKey) { 414 | case 'p:sp': // Shape, Text 415 | json = await processSpNode(nodeValue, nodes, warpObj, source, groupHierarchy) 416 | break 417 | case 'p:cxnSp': // Shape, Text 418 | json = await processCxnSpNode(nodeValue, nodes, warpObj, source) 419 | break 420 | case 'p:pic': // Image, Video, Audio 421 | json = await processPicNode(nodeValue, warpObj, source) 422 | break 423 | case 'p:graphicFrame': // Chart, Diagram, Table 424 | json = await processGraphicFrameNode(nodeValue, warpObj, source) 425 | break 426 | case 'p:grpSp': 427 | json = await processGroupSpNode(nodeValue, warpObj, source, groupHierarchy) 428 | break 429 | case 'mc:AlternateContent': 430 | if (getTextByPathList(nodeValue, ['mc:Fallback', 'p:grpSpPr', 'a:xfrm'])) { 431 | json = await processGroupSpNode(getTextByPathList(nodeValue, ['mc:Fallback']), warpObj, source, groupHierarchy) 432 | } 433 | else if (getTextByPathList(nodeValue, ['mc:Choice'])) { 434 | json = await processMathNode(nodeValue, warpObj, source) 435 | } 436 | break 437 | default: 438 | } 439 | 440 | return json 441 | } 442 | 443 | async function processMathNode(node, warpObj, source) { 444 | const choice = getTextByPathList(node, ['mc:Choice']) 445 | const fallback = getTextByPathList(node, ['mc:Fallback']) 446 | 447 | const order = node['attrs']['order'] 448 | const xfrmNode = getTextByPathList(choice, ['p:sp', 'p:spPr', 'a:xfrm']) 449 | const { top, left } = getPosition(xfrmNode, undefined, undefined) 450 | const { width, height } = getSize(xfrmNode, undefined, undefined) 451 | 452 | const oMath = findOMath(choice)[0] 453 | const latex = latexFormart(parseOMath(oMath)) 454 | 455 | const blipFill = getTextByPathList(fallback, ['p:sp', 'p:spPr', 'a:blipFill']) 456 | const picBase64 = await getPicFill(source, blipFill, warpObj) 457 | 458 | let text = '' 459 | if (getTextByPathList(choice, ['p:sp', 'p:txBody', 'a:p', 'a:r'])) { 460 | const sp = getTextByPathList(choice, ['p:sp']) 461 | text = genTextBody(sp['p:txBody'], sp, undefined, undefined, warpObj) 462 | } 463 | 464 | return { 465 | type: 'math', 466 | top, 467 | left, 468 | width, 469 | height, 470 | latex, 471 | picBase64, 472 | text, 473 | order, 474 | } 475 | } 476 | 477 | async function processGroupSpNode(node, warpObj, source, parentGroupHierarchy = []) { 478 | const order = node['attrs']['order'] 479 | const xfrmNode = getTextByPathList(node, ['p:grpSpPr', 'a:xfrm']) 480 | if (!xfrmNode) return null 481 | 482 | const x = parseInt(xfrmNode['a:off']['attrs']['x']) * RATIO_EMUs_Points 483 | const y = parseInt(xfrmNode['a:off']['attrs']['y']) * RATIO_EMUs_Points 484 | const chx = parseInt(xfrmNode['a:chOff']['attrs']['x']) * RATIO_EMUs_Points 485 | const chy = parseInt(xfrmNode['a:chOff']['attrs']['y']) * RATIO_EMUs_Points 486 | const cx = parseInt(xfrmNode['a:ext']['attrs']['cx']) * RATIO_EMUs_Points 487 | const cy = parseInt(xfrmNode['a:ext']['attrs']['cy']) * RATIO_EMUs_Points 488 | const chcx = parseInt(xfrmNode['a:chExt']['attrs']['cx']) * RATIO_EMUs_Points 489 | const chcy = parseInt(xfrmNode['a:chExt']['attrs']['cy']) * RATIO_EMUs_Points 490 | 491 | const isFlipV = getTextByPathList(xfrmNode, ['attrs', 'flipV']) === '1' 492 | const isFlipH = getTextByPathList(xfrmNode, ['attrs', 'flipH']) === '1' 493 | 494 | let rotate = getTextByPathList(xfrmNode, ['attrs', 'rot']) || 0 495 | if (rotate) rotate = angleToDegrees(rotate) 496 | 497 | const ws = cx / chcx 498 | const hs = cy / chcy 499 | 500 | // 构建当前组合层级(将当前组合添加到父级层级中) 501 | const currentGroupHierarchy = [...parentGroupHierarchy, node] 502 | 503 | const elements = [] 504 | for (const nodeKey in node) { 505 | if (node[nodeKey].constructor === Array) { 506 | for (const item of node[nodeKey]) { 507 | const ret = await processNodesInSlide(nodeKey, item, node, warpObj, source, currentGroupHierarchy) 508 | if (ret) elements.push(ret) 509 | } 510 | } 511 | else { 512 | const ret = await processNodesInSlide(nodeKey, node[nodeKey], node, warpObj, source, currentGroupHierarchy) 513 | if (ret) elements.push(ret) 514 | } 515 | } 516 | 517 | return { 518 | type: 'group', 519 | top: numberToFixed(y), 520 | left: numberToFixed(x), 521 | width: numberToFixed(cx), 522 | height: numberToFixed(cy), 523 | rotate, 524 | order, 525 | isFlipV, 526 | isFlipH, 527 | elements: elements.map(element => ({ 528 | ...element, 529 | left: numberToFixed((element.left - chx) * ws), 530 | top: numberToFixed((element.top - chy) * hs), 531 | width: numberToFixed(element.width * ws), 532 | height: numberToFixed(element.height * hs), 533 | })) 534 | } 535 | } 536 | 537 | async function processSpNode(node, pNode, warpObj, source, groupHierarchy = []) { 538 | const name = getTextByPathList(node, ['p:nvSpPr', 'p:cNvPr', 'attrs', 'name']) 539 | const idx = getTextByPathList(node, ['p:nvSpPr', 'p:nvPr', 'p:ph', 'attrs', 'idx']) 540 | let type = getTextByPathList(node, ['p:nvSpPr', 'p:nvPr', 'p:ph', 'attrs', 'type']) 541 | const order = getTextByPathList(node, ['attrs', 'order']) 542 | 543 | let slideLayoutSpNode, slideMasterSpNode 544 | 545 | if (type) { 546 | if (idx) { 547 | slideLayoutSpNode = warpObj['slideLayoutTables']['typeTable'][type] 548 | slideMasterSpNode = warpObj['slideMasterTables']['typeTable'][type] 549 | } 550 | else { 551 | slideLayoutSpNode = warpObj['slideLayoutTables']['typeTable'][type] 552 | slideMasterSpNode = warpObj['slideMasterTables']['typeTable'][type] 553 | } 554 | } 555 | else if (idx) { 556 | slideLayoutSpNode = warpObj['slideLayoutTables']['idxTable'][idx] 557 | slideMasterSpNode = warpObj['slideMasterTables']['idxTable'][idx] 558 | } 559 | 560 | if (!type) { 561 | const txBoxVal = getTextByPathList(node, ['p:nvSpPr', 'p:cNvSpPr', 'attrs', 'txBox']) 562 | if (txBoxVal === '1') type = 'text' 563 | } 564 | if (!type) type = getTextByPathList(slideLayoutSpNode, ['p:nvSpPr', 'p:nvPr', 'p:ph', 'attrs', 'type']) 565 | if (!type) type = getTextByPathList(slideMasterSpNode, ['p:nvSpPr', 'p:nvPr', 'p:ph', 'attrs', 'type']) 566 | 567 | if (!type) { 568 | if (source === 'diagramBg') type = 'diagram' 569 | else type = 'obj' 570 | } 571 | 572 | return await genShape(node, pNode, slideLayoutSpNode, slideMasterSpNode, name, type, order, warpObj, source, groupHierarchy) 573 | } 574 | 575 | async function processCxnSpNode(node, pNode, warpObj, source) { 576 | const name = node['p:nvCxnSpPr']['p:cNvPr']['attrs']['name'] 577 | const type = (node['p:nvCxnSpPr']['p:nvPr']['p:ph'] === undefined) ? undefined : node['p:nvSpPr']['p:nvPr']['p:ph']['attrs']['type'] 578 | const order = node['attrs']['order'] 579 | 580 | return await genShape(node, pNode, undefined, undefined, name, type, order, warpObj, source) 581 | } 582 | 583 | async function genShape(node, pNode, slideLayoutSpNode, slideMasterSpNode, name, type, order, warpObj, source, groupHierarchy = []) { 584 | const xfrmList = ['p:spPr', 'a:xfrm'] 585 | const slideXfrmNode = getTextByPathList(node, xfrmList) 586 | const slideLayoutXfrmNode = getTextByPathList(slideLayoutSpNode, xfrmList) 587 | const slideMasterXfrmNode = getTextByPathList(slideMasterSpNode, xfrmList) 588 | 589 | const shapType = getTextByPathList(node, ['p:spPr', 'a:prstGeom', 'attrs', 'prst']) 590 | const custShapType = getTextByPathList(node, ['p:spPr', 'a:custGeom']) 591 | 592 | const { top, left } = getPosition(slideXfrmNode, slideLayoutXfrmNode, slideMasterXfrmNode) 593 | const { width, height } = getSize(slideXfrmNode, slideLayoutXfrmNode, slideMasterXfrmNode) 594 | 595 | const isFlipV = getTextByPathList(slideXfrmNode, ['attrs', 'flipV']) === '1' 596 | const isFlipH = getTextByPathList(slideXfrmNode, ['attrs', 'flipH']) === '1' 597 | 598 | const rotate = angleToDegrees(getTextByPathList(slideXfrmNode, ['attrs', 'rot'])) 599 | 600 | const txtXframeNode = getTextByPathList(node, ['p:txXfrm']) 601 | let txtRotate 602 | if (txtXframeNode) { 603 | const txtXframeRot = getTextByPathList(txtXframeNode, ['attrs', 'rot']) 604 | if (txtXframeRot) txtRotate = angleToDegrees(txtXframeRot) + 90 605 | } 606 | else txtRotate = rotate 607 | 608 | let content = '' 609 | if (node['p:txBody']) content = genTextBody(node['p:txBody'], node, slideLayoutSpNode, type, warpObj) 610 | 611 | const { borderColor, borderWidth, borderType, strokeDasharray } = getBorder(node, type, warpObj) 612 | const fill = await getShapeFill(node, pNode, undefined, warpObj, source, groupHierarchy) || '' 613 | 614 | let shadow 615 | const outerShdwNode = getTextByPathList(node, ['p:spPr', 'a:effectLst', 'a:outerShdw']) 616 | if (outerShdwNode) shadow = getShadow(outerShdwNode, warpObj) 617 | 618 | const vAlign = getVerticalAlign(node, slideLayoutSpNode, slideMasterSpNode, type) 619 | const isVertical = getTextByPathList(node, ['p:txBody', 'a:bodyPr', 'attrs', 'vert']) === 'eaVert' 620 | 621 | const data = { 622 | left, 623 | top, 624 | width, 625 | height, 626 | borderColor, 627 | borderWidth, 628 | borderType, 629 | borderStrokeDasharray: strokeDasharray, 630 | fill, 631 | content, 632 | isFlipV, 633 | isFlipH, 634 | rotate, 635 | vAlign, 636 | name, 637 | order, 638 | } 639 | 640 | if (shadow) data.shadow = shadow 641 | 642 | const isHasValidText = data.content && hasValidText(data.content) 643 | 644 | if (custShapType && type !== 'diagram') { 645 | const ext = getTextByPathList(slideXfrmNode, ['a:ext', 'attrs']) 646 | const w = parseInt(ext['cx']) * RATIO_EMUs_Points 647 | const h = parseInt(ext['cy']) * RATIO_EMUs_Points 648 | const d = getCustomShapePath(custShapType, w, h) 649 | if (!isHasValidText) data.content = '' 650 | 651 | return { 652 | ...data, 653 | type: 'shape', 654 | shapType: 'custom', 655 | path: d, 656 | } 657 | } 658 | 659 | let shapePath = '' 660 | if (shapType) shapePath = getShapePath(shapType, width, height, node) 661 | 662 | if (shapType && (type === 'obj' || !type || shapType !== 'rect')) { 663 | if (!isHasValidText) data.content = '' 664 | return { 665 | ...data, 666 | type: 'shape', 667 | shapType, 668 | path: shapePath, 669 | } 670 | } 671 | if (shapType && !isHasValidText && (fill || borderWidth)) { 672 | return { 673 | ...data, 674 | type: 'shape', 675 | content: '', 676 | shapType, 677 | path: shapePath, 678 | } 679 | } 680 | return { 681 | ...data, 682 | type: 'text', 683 | isVertical, 684 | rotate: txtRotate, 685 | } 686 | } 687 | 688 | async function processPicNode(node, warpObj, source) { 689 | let resObj 690 | if (source === 'slideMasterBg') resObj = warpObj['masterResObj'] 691 | else if (source === 'slideLayoutBg') resObj = warpObj['layoutResObj'] 692 | else resObj = warpObj['slideResObj'] 693 | 694 | const order = node['attrs']['order'] 695 | 696 | const rid = node['p:blipFill']['a:blip']['attrs']['r:embed'] 697 | const imgName = resObj[rid]['target'] 698 | const imgFileExt = extractFileExtension(imgName).toLowerCase() 699 | const zip = warpObj['zip'] 700 | const imgArrayBuffer = await zip.file(imgName).async('arraybuffer') 701 | 702 | let xfrmNode = node['p:spPr']['a:xfrm'] 703 | if (!xfrmNode) { 704 | const idx = getTextByPathList(node, ['p:nvPicPr', 'p:nvPr', 'p:ph', 'attrs', 'idx']) 705 | if (idx) xfrmNode = getTextByPathList(warpObj['slideLayoutTables'], ['idxTable', idx, 'p:spPr', 'a:xfrm']) 706 | } 707 | 708 | const mimeType = getMimeType(imgFileExt) 709 | const { top, left } = getPosition(xfrmNode, undefined, undefined) 710 | const { width, height } = getSize(xfrmNode, undefined, undefined) 711 | const src = `data:${mimeType};base64,${base64ArrayBuffer(imgArrayBuffer)}` 712 | 713 | const isFlipV = getTextByPathList(xfrmNode, ['attrs', 'flipV']) === '1' 714 | const isFlipH = getTextByPathList(xfrmNode, ['attrs', 'flipH']) === '1' 715 | 716 | let rotate = 0 717 | const rotateNode = getTextByPathList(node, ['p:spPr', 'a:xfrm', 'attrs', 'rot']) 718 | if (rotateNode) rotate = angleToDegrees(rotateNode) 719 | 720 | const videoNode = getTextByPathList(node, ['p:nvPicPr', 'p:nvPr', 'a:videoFile']) 721 | let videoRid, videoFile, videoFileExt, videoMimeType, uInt8ArrayVideo, videoBlob 722 | let isVdeoLink = false 723 | 724 | if (videoNode) { 725 | videoRid = videoNode['attrs']['r:link'] 726 | videoFile = resObj[videoRid]['target'] 727 | if (isVideoLink(videoFile)) { 728 | videoFile = escapeHtml(videoFile) 729 | isVdeoLink = true 730 | } 731 | else { 732 | videoFileExt = extractFileExtension(videoFile).toLowerCase() 733 | if (videoFileExt === 'mp4' || videoFileExt === 'webm' || videoFileExt === 'ogg') { 734 | uInt8ArrayVideo = await zip.file(videoFile).async('arraybuffer') 735 | videoMimeType = getMimeType(videoFileExt) 736 | videoBlob = URL.createObjectURL(new Blob([uInt8ArrayVideo], { 737 | type: videoMimeType 738 | })) 739 | } 740 | } 741 | } 742 | 743 | const audioNode = getTextByPathList(node, ['p:nvPicPr', 'p:nvPr', 'a:audioFile']) 744 | let audioRid, audioFile, audioFileExt, uInt8ArrayAudio, audioBlob 745 | if (audioNode) { 746 | audioRid = audioNode['attrs']['r:link'] 747 | audioFile = resObj[audioRid]['target'] 748 | audioFileExt = extractFileExtension(audioFile).toLowerCase() 749 | if (audioFileExt === 'mp3' || audioFileExt === 'wav' || audioFileExt === 'ogg') { 750 | uInt8ArrayAudio = await zip.file(audioFile).async('arraybuffer') 751 | audioBlob = URL.createObjectURL(new Blob([uInt8ArrayAudio])) 752 | } 753 | } 754 | 755 | if (videoNode && !isVdeoLink) { 756 | return { 757 | type: 'video', 758 | top, 759 | left, 760 | width, 761 | height, 762 | rotate, 763 | blob: videoBlob, 764 | order, 765 | } 766 | } 767 | if (videoNode && isVdeoLink) { 768 | return { 769 | type: 'video', 770 | top, 771 | left, 772 | width, 773 | height, 774 | rotate, 775 | src: videoFile, 776 | order, 777 | } 778 | } 779 | if (audioNode) { 780 | return { 781 | type: 'audio', 782 | top, 783 | left, 784 | width, 785 | height, 786 | rotate, 787 | blob: audioBlob, 788 | order, 789 | } 790 | } 791 | 792 | let rect 793 | const srcRectAttrs = getTextByPathList(node, ['p:blipFill', 'a:srcRect', 'attrs']) 794 | if (srcRectAttrs && (srcRectAttrs.t || srcRectAttrs.b || srcRectAttrs.l || srcRectAttrs.r)) { 795 | rect = {} 796 | if (srcRectAttrs.t) rect.t = srcRectAttrs.t / 1000 797 | if (srcRectAttrs.b) rect.b = srcRectAttrs.b / 1000 798 | if (srcRectAttrs.l) rect.l = srcRectAttrs.l / 1000 799 | if (srcRectAttrs.r) rect.r = srcRectAttrs.r / 1000 800 | } 801 | const geom = getTextByPathList(node, ['p:spPr', 'a:prstGeom', 'attrs', 'prst']) || 'rect' 802 | 803 | const { borderColor, borderWidth, borderType, strokeDasharray } = getBorder(node, undefined, warpObj) 804 | 805 | const filters = getPicFilters(node['p:blipFill']) 806 | 807 | const imageData = { 808 | type: 'image', 809 | top, 810 | left, 811 | width, 812 | height, 813 | rotate, 814 | src, 815 | isFlipV, 816 | isFlipH, 817 | order, 818 | rect, 819 | geom, 820 | borderColor, 821 | borderWidth, 822 | borderType, 823 | borderStrokeDasharray: strokeDasharray, 824 | } 825 | 826 | if (filters) imageData.filters = filters 827 | 828 | return imageData 829 | } 830 | 831 | async function processGraphicFrameNode(node, warpObj, source) { 832 | const graphicTypeUri = getTextByPathList(node, ['a:graphic', 'a:graphicData', 'attrs', 'uri']) 833 | 834 | let result 835 | switch (graphicTypeUri) { 836 | case 'http://schemas.openxmlformats.org/drawingml/2006/table': 837 | result = await genTable(node, warpObj) 838 | break 839 | case 'http://schemas.openxmlformats.org/drawingml/2006/chart': 840 | result = await genChart(node, warpObj) 841 | break 842 | case 'http://schemas.openxmlformats.org/drawingml/2006/diagram': 843 | result = await genDiagram(node, warpObj) 844 | break 845 | case 'http://schemas.openxmlformats.org/presentationml/2006/ole': 846 | let oleObjNode = getTextByPathList(node, ['a:graphic', 'a:graphicData', 'mc:AlternateContent', 'mc:Fallback', 'p:oleObj']) 847 | if (!oleObjNode) oleObjNode = getTextByPathList(node, ['a:graphic', 'a:graphicData', 'p:oleObj']) 848 | if (oleObjNode) result = await processGroupSpNode(oleObjNode, warpObj, source) 849 | break 850 | default: 851 | } 852 | return result 853 | } 854 | 855 | async function genTable(node, warpObj) { 856 | const order = node['attrs']['order'] 857 | const tableNode = getTextByPathList(node, ['a:graphic', 'a:graphicData', 'a:tbl']) 858 | const xfrmNode = getTextByPathList(node, ['p:xfrm']) 859 | const { top, left } = getPosition(xfrmNode, undefined, undefined) 860 | const { width, height } = getSize(xfrmNode, undefined, undefined) 861 | 862 | const getTblPr = getTextByPathList(node, ['a:graphic', 'a:graphicData', 'a:tbl', 'a:tblPr']) 863 | let getColsGrid = getTextByPathList(node, ['a:graphic', 'a:graphicData', 'a:tbl', 'a:tblGrid', 'a:gridCol']) 864 | if (getColsGrid.constructor !== Array) getColsGrid = [getColsGrid] 865 | 866 | const colWidths = [] 867 | if (getColsGrid) { 868 | for (const item of getColsGrid) { 869 | const colWidthParam = getTextByPathList(item, ['attrs', 'w']) || 0 870 | const colWidth = parseInt(colWidthParam) * RATIO_EMUs_Points 871 | colWidths.push(colWidth) 872 | } 873 | } 874 | 875 | const firstRowAttr = getTblPr['attrs'] ? getTblPr['attrs']['firstRow'] : undefined 876 | const firstColAttr = getTblPr['attrs'] ? getTblPr['attrs']['firstCol'] : undefined 877 | const lastRowAttr = getTblPr['attrs'] ? getTblPr['attrs']['lastRow'] : undefined 878 | const lastColAttr = getTblPr['attrs'] ? getTblPr['attrs']['lastCol'] : undefined 879 | const bandRowAttr = getTblPr['attrs'] ? getTblPr['attrs']['bandRow'] : undefined 880 | const bandColAttr = getTblPr['attrs'] ? getTblPr['attrs']['bandCol'] : undefined 881 | const tblStylAttrObj = { 882 | isFrstRowAttr: (firstRowAttr && firstRowAttr === '1') ? 1 : 0, 883 | isFrstColAttr: (firstColAttr && firstColAttr === '1') ? 1 : 0, 884 | isLstRowAttr: (lastRowAttr && lastRowAttr === '1') ? 1 : 0, 885 | isLstColAttr: (lastColAttr && lastColAttr === '1') ? 1 : 0, 886 | isBandRowAttr: (bandRowAttr && bandRowAttr === '1') ? 1 : 0, 887 | isBandColAttr: (bandColAttr && bandColAttr === '1') ? 1 : 0, 888 | } 889 | 890 | let thisTblStyle 891 | const tbleStyleId = getTblPr['a:tableStyleId'] 892 | if (tbleStyleId) { 893 | const tbleStylList = warpObj['tableStyles']['a:tblStyleLst']['a:tblStyle'] 894 | if (tbleStylList) { 895 | if (tbleStylList.constructor === Array) { 896 | for (let k = 0; k < tbleStylList.length; k++) { 897 | if (tbleStylList[k]['attrs']['styleId'] === tbleStyleId) { 898 | thisTblStyle = tbleStylList[k] 899 | } 900 | } 901 | } 902 | else { 903 | if (tbleStylList['attrs']['styleId'] === tbleStyleId) { 904 | thisTblStyle = tbleStylList 905 | } 906 | } 907 | } 908 | } 909 | if (thisTblStyle) thisTblStyle['tblStylAttrObj'] = tblStylAttrObj 910 | 911 | let borders = {} 912 | const tblStyl = getTextByPathList(thisTblStyle, ['a:wholeTbl', 'a:tcStyle']) 913 | const tblBorderStyl = getTextByPathList(tblStyl, ['a:tcBdr']) 914 | if (tblBorderStyl) borders = getTableBorders(tblBorderStyl, warpObj) 915 | 916 | let tbl_bgcolor = '' 917 | let tbl_bgFillschemeClr = getTextByPathList(thisTblStyle, ['a:tblBg', 'a:fillRef']) 918 | if (tbl_bgFillschemeClr) { 919 | tbl_bgcolor = getSolidFill(tbl_bgFillschemeClr, undefined, undefined, warpObj) 920 | } 921 | if (tbl_bgFillschemeClr === undefined) { 922 | tbl_bgFillschemeClr = getTextByPathList(thisTblStyle, ['a:wholeTbl', 'a:tcStyle', 'a:fill', 'a:solidFill']) 923 | tbl_bgcolor = getSolidFill(tbl_bgFillschemeClr, undefined, undefined, warpObj) 924 | } 925 | 926 | let trNodes = tableNode['a:tr'] 927 | if (trNodes.constructor !== Array) trNodes = [trNodes] 928 | 929 | const data = [] 930 | const rowHeights = [] 931 | for (let i = 0; i < trNodes.length; i++) { 932 | const trNode = trNodes[i] 933 | 934 | const rowHeightParam = getTextByPathList(trNodes[i], ['attrs', 'h']) || 0 935 | const rowHeight = parseInt(rowHeightParam) * RATIO_EMUs_Points 936 | rowHeights.push(rowHeight) 937 | 938 | const { 939 | fillColor, 940 | fontColor, 941 | fontBold, 942 | } = getTableRowParams(trNodes, i, tblStylAttrObj, thisTblStyle, warpObj) 943 | 944 | const tcNodes = trNode['a:tc'] 945 | const tr = [] 946 | 947 | if (tcNodes.constructor === Array) { 948 | for (let j = 0; j < tcNodes.length; j++) { 949 | const tcNode = tcNodes[j] 950 | let a_sorce 951 | if (j === 0 && tblStylAttrObj['isFrstColAttr'] === 1) { 952 | a_sorce = 'a:firstCol' 953 | if (tblStylAttrObj['isLstRowAttr'] === 1 && i === (trNodes.length - 1) && getTextByPathList(thisTblStyle, ['a:seCell'])) { 954 | a_sorce = 'a:seCell' 955 | } 956 | else if (tblStylAttrObj['isFrstRowAttr'] === 1 && i === 0 && 957 | getTextByPathList(thisTblStyle, ['a:neCell'])) { 958 | a_sorce = 'a:neCell' 959 | } 960 | } 961 | else if ( 962 | (j > 0 && tblStylAttrObj['isBandColAttr'] === 1) && 963 | !(tblStylAttrObj['isFrstColAttr'] === 1 && i === 0) && 964 | !(tblStylAttrObj['isLstRowAttr'] === 1 && i === (trNodes.length - 1)) && 965 | j !== (tcNodes.length - 1) 966 | ) { 967 | if ((j % 2) !== 0) { 968 | let aBandNode = getTextByPathList(thisTblStyle, ['a:band2V']) 969 | if (aBandNode === undefined) { 970 | aBandNode = getTextByPathList(thisTblStyle, ['a:band1V']) 971 | if (aBandNode) a_sorce = 'a:band2V' 972 | } 973 | else a_sorce = 'a:band2V' 974 | } 975 | } 976 | if (j === (tcNodes.length - 1) && tblStylAttrObj['isLstColAttr'] === 1) { 977 | a_sorce = 'a:lastCol' 978 | if (tblStylAttrObj['isLstRowAttr'] === 1 && i === (trNodes.length - 1) && getTextByPathList(thisTblStyle, ['a:swCell'])) { 979 | a_sorce = 'a:swCell' 980 | } 981 | else if (tblStylAttrObj['isFrstRowAttr'] === 1 && i === 0 && getTextByPathList(thisTblStyle, ['a:nwCell'])) { 982 | a_sorce = 'a:nwCell' 983 | } 984 | } 985 | const text = genTextBody(tcNode['a:txBody'], tcNode, undefined, undefined, warpObj) 986 | const cell = await getTableCellParams(tcNode, thisTblStyle, a_sorce, warpObj) 987 | const td = { text } 988 | if (cell.rowSpan) td.rowSpan = cell.rowSpan 989 | if (cell.colSpan) td.colSpan = cell.colSpan 990 | if (cell.vMerge) td.vMerge = cell.vMerge 991 | if (cell.hMerge) td.hMerge = cell.hMerge 992 | if (cell.fontBold || fontBold) td.fontBold = cell.fontBold || fontBold 993 | if (cell.fontColor || fontColor) td.fontColor = cell.fontColor || fontColor 994 | if (cell.fillColor || fillColor || tbl_bgcolor) td.fillColor = cell.fillColor || fillColor || tbl_bgcolor 995 | if (cell.borders) td.borders = cell.borders 996 | 997 | tr.push(td) 998 | } 999 | } 1000 | else { 1001 | let a_sorce 1002 | if (tblStylAttrObj['isFrstColAttr'] === 1 && tblStylAttrObj['isLstRowAttr'] !== 1) { 1003 | a_sorce = 'a:firstCol' 1004 | } 1005 | else if (tblStylAttrObj['isBandColAttr'] === 1 && tblStylAttrObj['isLstRowAttr'] !== 1) { 1006 | let aBandNode = getTextByPathList(thisTblStyle, ['a:band2V']) 1007 | if (!aBandNode) { 1008 | aBandNode = getTextByPathList(thisTblStyle, ['a:band1V']) 1009 | if (aBandNode) a_sorce = 'a:band2V' 1010 | } 1011 | else a_sorce = 'a:band2V' 1012 | } 1013 | if (tblStylAttrObj['isLstColAttr'] === 1 && tblStylAttrObj['isLstRowAttr'] !== 1) { 1014 | a_sorce = 'a:lastCol' 1015 | } 1016 | 1017 | const text = genTextBody(tcNodes['a:txBody'], tcNodes, undefined, undefined, warpObj) 1018 | const cell = await getTableCellParams(tcNodes, thisTblStyle, a_sorce, warpObj) 1019 | const td = { text } 1020 | if (cell.rowSpan) td.rowSpan = cell.rowSpan 1021 | if (cell.colSpan) td.colSpan = cell.colSpan 1022 | if (cell.vMerge) td.vMerge = cell.vMerge 1023 | if (cell.hMerge) td.hMerge = cell.hMerge 1024 | if (cell.fontBold || fontBold) td.fontBold = cell.fontBold || fontBold 1025 | if (cell.fontColor || fontColor) td.fontColor = cell.fontColor || fontColor 1026 | if (cell.fillColor || fillColor || tbl_bgcolor) td.fillColor = cell.fillColor || fillColor || tbl_bgcolor 1027 | if (cell.borders) td.borders = cell.borders 1028 | 1029 | tr.push(td) 1030 | } 1031 | data.push(tr) 1032 | } 1033 | 1034 | return { 1035 | type: 'table', 1036 | top, 1037 | left, 1038 | width, 1039 | height, 1040 | data, 1041 | order, 1042 | borders, 1043 | rowHeights, 1044 | colWidths, 1045 | } 1046 | } 1047 | 1048 | async function genChart(node, warpObj) { 1049 | const order = node['attrs']['order'] 1050 | const xfrmNode = getTextByPathList(node, ['p:xfrm']) 1051 | const { top, left } = getPosition(xfrmNode, undefined, undefined) 1052 | const { width, height } = getSize(xfrmNode, undefined, undefined) 1053 | 1054 | const rid = node['a:graphic']['a:graphicData']['c:chart']['attrs']['r:id'] 1055 | let refName = getTextByPathList(warpObj['slideResObj'], [rid, 'target']) 1056 | if (!refName) refName = getTextByPathList(warpObj['layoutResObj'], [rid, 'target']) 1057 | if (!refName) refName = getTextByPathList(warpObj['masterResObj'], [rid, 'target']) 1058 | if (!refName) return {} 1059 | 1060 | const content = await readXmlFile(warpObj['zip'], refName) 1061 | const plotArea = getTextByPathList(content, ['c:chartSpace', 'c:chart', 'c:plotArea']) 1062 | 1063 | const chart = getChartInfo(plotArea, warpObj) 1064 | 1065 | if (!chart) return {} 1066 | 1067 | const data = { 1068 | type: 'chart', 1069 | top, 1070 | left, 1071 | width, 1072 | height, 1073 | data: chart.data, 1074 | colors: chart.colors, 1075 | chartType: chart.type, 1076 | order, 1077 | } 1078 | if (chart.marker !== undefined) data.marker = chart.marker 1079 | if (chart.barDir !== undefined) data.barDir = chart.barDir 1080 | if (chart.holeSize !== undefined) data.holeSize = chart.holeSize 1081 | if (chart.grouping !== undefined) data.grouping = chart.grouping 1082 | if (chart.style !== undefined) data.style = chart.style 1083 | 1084 | return data 1085 | } 1086 | 1087 | async function genDiagram(node, warpObj) { 1088 | const order = node['attrs']['order'] 1089 | const xfrmNode = getTextByPathList(node, ['p:xfrm']) 1090 | const { left, top } = getPosition(xfrmNode, undefined, undefined) 1091 | const { width, height } = getSize(xfrmNode, undefined, undefined) 1092 | 1093 | const dgmDrwSpArray = getTextByPathList(warpObj['digramFileContent'], ['p:drawing', 'p:spTree', 'p:sp']) 1094 | const elements = [] 1095 | if (dgmDrwSpArray) { 1096 | for (const item of dgmDrwSpArray) { 1097 | const el = await processSpNode(item, node, warpObj, 'diagramBg') 1098 | if (el) elements.push(el) 1099 | } 1100 | } 1101 | 1102 | return { 1103 | type: 'diagram', 1104 | left, 1105 | top, 1106 | width, 1107 | height, 1108 | elements, 1109 | order, 1110 | } 1111 | } --------------------------------------------------------------------------------