├── .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 += `${isList}>`
63 | text += `<${listType}>`
64 | isList = listType
65 | }
66 | text += ``
67 | }
68 | else {
69 | if (isList) {
70 | text += `${isList}>`
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 |
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 |
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 | }
--------------------------------------------------------------------------------