feat: 导出PPTX文件

This commit is contained in:
pipipi-pikachu 2021-07-24 23:06:58 +08:00
parent 2391f56f20
commit 475fb495e6
19 changed files with 847 additions and 124 deletions

View File

@ -1,11 +1,9 @@
# 🎨 PPTist # 🎨 PPTist
> 一个基于 Vue3.x + TypeScript 的在线演示文稿应用还原了大部分PPT常用功能,支持 文字、图片、形状、线条、图表、表格 6种最常用的元素类型每一种元素都拥有高度可编辑能力同时支持丰富的快捷键和右键菜单尽可能还原本地桌面应用的使用体验。 > 一个基于 Vue3.x + TypeScript 的在线演示文稿(幻灯片)应用,还原了大部分 Office PowerPoint 常用功能,支持 文字、图片、形状、线条、图表、表格 6种最常用的元素类型每一种元素都拥有高度可编辑能力同时支持丰富的快捷键和右键菜单尽可能还原本地桌面应用的使用体验。
在线体验地址:[https://pipipi-pikachu.github.io/PPTist/](https://pipipi-pikachu.github.io/PPTist/) 在线体验地址:[https://pipipi-pikachu.github.io/PPTist/](https://pipipi-pikachu.github.io/PPTist/)
如果网络状态不佳,可以访问国内镜像:[https://pptist.gitee.io/](https://pptist.gitee.io/) 如果网络状态不佳,可以访问国内镜像(非实时更新):[https://pptist.gitee.io/](https://pptist.gitee.io/)
Github仓库[https://github.com/pipipi-pikachu/PPTist](https://github.com/pipipi-pikachu/PPTist)
# 🚀 项目运行 # 🚀 项目运行
@ -30,6 +28,7 @@ npm run serve
- 画布缩放 - 画布缩放
- 主题设置 - 主题设置
- 幻灯片备注 - 幻灯片备注
- 幻灯片模板
### 幻灯片元素编辑 ### 幻灯片元素编辑
- 元素添加、删除 - 元素添加、删除
- 元素复制粘贴 - 元素复制粘贴
@ -93,7 +92,8 @@ npm run serve
- 翻页动画 - 翻页动画
- 元素入场动画 - 元素入场动画
- 全部幻灯片预览 - 全部幻灯片预览
- 画笔工具 - 画笔、黑板工具
- 自动放映
# 💡 常见问题 # 💡 常见问题
@ -125,6 +125,16 @@ A. 设置预置主题的作用是使新添加的元素和页面应用主题样
A. 设置在线字体时会下载对应的字体文件,该文件较大,需要等待下载完成后才会应用新的字体。 A. 设置在线字体时会下载对应的字体文件,该文件较大,需要等待下载完成后才会应用新的字体。
**Q. 关于导入导出PPTX文件**
A. 作为一个在线幻灯片应用导出、导入PPTX文件是非常重要的功能但是经过调研发现该功能实现起来的复杂度远超过了预期。由于个人能力和时间有限这部分功能只能借助第三方的轮子来完成。
目前导出功能主要基于 [PptxGenJS](https://github.com/gitbrent/PptxGenJS/) 完成能够实现大多数基本元素的导出但还有非常多的缺陷需要一点点完善。同时需要知晓的是1、该功能依赖 PptxGenJS对于该库本身无法实现的部分我也无能为力2、导出功能的目标只是【导出样式尽可能一致的元素】而不是一比一将网页还原到PPT一些样式差异是必然存在的。
导入功能目前暂时没有合适的解决方案,还在调研和观望中。
如果有感兴趣或做过相关内容的朋友,欢迎在 [issues](https://github.com/pipipi-pikachu/PPTist/issues/57) 中讨论。
# 📁 项目目录结构 # 📁 项目目录结构
``` ```
@ -162,15 +172,6 @@ A. 设置在线字体时会下载对应的字体文件,该文件较大,需
具体类型的定义可见:[https://github.com/pipipi-pikachu/PPTist/blob/master/src/types/slides.ts](https://github.com/pipipi-pikachu/PPTist/blob/master/src/types/slides.ts) 具体类型的定义可见:[https://github.com/pipipi-pikachu/PPTist/blob/master/src/types/slides.ts](https://github.com/pipipi-pikachu/PPTist/blob/master/src/types/slides.ts)
# 📃 TODO
- [ ] 幻灯片模板
- [ ] 图表缩略图优化
- [ ] 公式元素
- [ ] 导出、导入
> 作为一个在线幻灯片导出、导入PPTX文件是非常重要的功能。但是经过本人一段时间的调研发现该功能实现起来的复杂度远超过了预期。由于个人时间有限暂时可能无法集中精力来做该功能短时间内还是会更多优先去提升其他方面的使用体验。如果有做过相关内容的朋友也欢迎给我提建议。
# 💻 贡献代码 # 💻 贡献代码
首先感谢每一位关注本项目的朋友,由于本人时间精力有限,且目前项目规模不大,暂时没有团队化开发维护本项目的打算。但非常欢迎每一位对本项目感兴趣的朋友贡献代码。 首先感谢每一位关注本项目的朋友,由于本人时间精力有限,且目前项目规模不大,暂时没有团队化开发维护本项目的打算。但非常欢迎每一位对本项目感兴趣的朋友贡献代码。
### 具体参考如下: ### 具体参考如下:

130
package-lock.json generated
View File

@ -2479,6 +2479,11 @@
"integrity": "sha1-mqMMBNshKpoGSdaub9UKzMQHSKE=", "integrity": "sha1-mqMMBNshKpoGSdaub9UKzMQHSKE=",
"dev": true "dev": true
}, },
"@types/svg-arc-to-cubic-bezier": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@types/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
"integrity": "sha512-3h04sJhF2rjOq8zUhyomORyKdr0RUts7FAz/JajBKGpTF0JSXjaj9fjWtAqj+pU1fwsGsHzcm3Neew3t/McUXA=="
},
"@types/tapable": { "@types/tapable": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npm.taobao.org/@types/tapable/download/@types/tapable-1.0.7.tgz", "resolved": "https://registry.npm.taobao.org/@types/tapable/download/@types/tapable-1.0.7.tgz",
@ -6341,8 +6346,7 @@
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz", "resolved": "https://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
"dev": true
}, },
"cosmiconfig": { "cosmiconfig": {
"version": "7.0.0", "version": "7.0.0",
@ -9567,6 +9571,11 @@
"sshpk": "^1.7.0" "sshpk": "^1.7.0"
} }
}, },
"https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha1-PDfHrhqO65ZpBKKtHpdaGUt+06Q="
},
"https-browserify": { "https-browserify": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npm.taobao.org/https-browserify/download/https-browserify-1.0.0.tgz", "resolved": "https://registry.npm.taobao.org/https-browserify/download/https-browserify-1.0.0.tgz",
@ -9706,6 +9715,11 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"import-cwd": { "import-cwd": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npm.taobao.org/import-cwd/download/import-cwd-2.1.0.tgz", "resolved": "https://registry.npm.taobao.org/import-cwd/download/import-cwd-2.1.0.tgz",
@ -9848,8 +9862,7 @@
"inherits": { "inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz", "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz",
"integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=", "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w="
"dev": true
}, },
"ini": { "ini": {
"version": "1.3.8", "version": "1.3.8",
@ -10363,8 +10376,7 @@
"isarray": { "isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz", "resolved": "https://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
"dev": true
}, },
"isexe": { "isexe": {
"version": "2.0.0", "version": "2.0.0",
@ -12381,6 +12393,46 @@
"verror": "1.10.0" "verror": "1.10.0"
} }
}, },
"jszip": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz",
"integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==",
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"set-immediate-shim": "~1.0.1"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"killable": { "killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz", "resolved": "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz",
@ -12583,6 +12635,14 @@
"type-check": "~0.3.2" "type-check": "~0.3.2"
} }
}, },
"lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"requires": {
"immediate": "~3.0.5"
}
},
"lines-and-columns": { "lines-and-columns": {
"version": "1.1.6", "version": "1.1.6",
"resolved": "https://registry.npm.taobao.org/lines-and-columns/download/lines-and-columns-1.1.6.tgz", "resolved": "https://registry.npm.taobao.org/lines-and-columns/download/lines-and-columns-1.1.6.tgz",
@ -14155,8 +14215,7 @@
"pako": { "pako": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npm.taobao.org/pako/download/pako-1.0.11.tgz?cache=0&sync_timestamp=1610208924901&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpako%2Fdownload%2Fpako-1.0.11.tgz", "resolved": "https://registry.npm.taobao.org/pako/download/pako-1.0.11.tgz?cache=0&sync_timestamp=1610208924901&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpako%2Fdownload%2Fpako-1.0.11.tgz",
"integrity": "sha1-bJWZ00DVTf05RjgCUqNXBaa5kr8=", "integrity": "sha1-bJWZ00DVTf05RjgCUqNXBaa5kr8="
"dev": true
}, },
"parallel-transform": { "parallel-transform": {
"version": "1.2.0", "version": "1.2.0",
@ -15257,6 +15316,32 @@
"integrity": "sha1-RD9qIM7WSBor2k+oUypuVdeJoss=", "integrity": "sha1-RD9qIM7WSBor2k+oUypuVdeJoss=",
"dev": true "dev": true
}, },
"pptxgenjs": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-3.7.0.tgz",
"integrity": "sha512-ZtKT11DA3arRhyQ0AJoaa4iizHEghfreobQhbDq9EVtJhZ0JN3it/lREnvervuHinHi6sGBcSlHC6scfubEm7Q==",
"requires": {
"@types/node": "^16.0.0",
"https": "^1.0.0",
"image-size": "^1.0.0",
"jszip": "^3.6.0"
},
"dependencies": {
"@types/node": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.0.tgz",
"integrity": "sha512-HrJuE7Mlqcjj+00JqMWpZ3tY8w7EUd+S0U3L1+PQSWiXZbOgyQDvi+ogoUxaHApPJq5diKxYBQwA3iIlNcPqOg=="
},
"image-size": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz",
"integrity": "sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw==",
"requires": {
"queue": "6.0.2"
}
}
}
},
"prelude-ls": { "prelude-ls": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npm.taobao.org/prelude-ls/download/prelude-ls-1.1.2.tgz", "resolved": "https://registry.npm.taobao.org/prelude-ls/download/prelude-ls-1.1.2.tgz",
@ -15361,8 +15446,7 @@
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npm.taobao.org/process-nextick-args/download/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npm.taobao.org/process-nextick-args/download/process-nextick-args-2.0.1.tgz",
"integrity": "sha1-eCDZsWEgzFXKmud5JoCufbptf+I=", "integrity": "sha1-eCDZsWEgzFXKmud5JoCufbptf+I="
"dev": true
}, },
"progress": { "progress": {
"version": "2.0.3", "version": "2.0.3",
@ -15632,6 +15716,14 @@
"integrity": "sha1-M0WUG0FTy50ILY7uTNogFqmu9/Y=", "integrity": "sha1-M0WUG0FTy50ILY7uTNogFqmu9/Y=",
"dev": true "dev": true
}, },
"queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"requires": {
"inherits": "~2.0.3"
}
},
"queue-microtask": { "queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npm.taobao.org/queue-microtask/download/queue-microtask-1.2.3.tgz?cache=0&sync_timestamp=1616391548624&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fqueue-microtask%2Fdownload%2Fqueue-microtask-1.2.3.tgz", "resolved": "https://registry.npm.taobao.org/queue-microtask/download/queue-microtask-1.2.3.tgz?cache=0&sync_timestamp=1616391548624&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fqueue-microtask%2Fdownload%2Fqueue-microtask-1.2.3.tgz",
@ -16483,6 +16575,11 @@
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true "dev": true
}, },
"set-immediate-shim": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
},
"set-value": { "set-value": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npm.taobao.org/set-value/download/set-value-2.0.1.tgz", "resolved": "https://registry.npm.taobao.org/set-value/download/set-value-2.0.1.tgz",
@ -17714,6 +17811,16 @@
"has-flag": "^4.0.0" "has-flag": "^4.0.0"
} }
}, },
"svg-arc-to-cubic-bezier": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz",
"integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g=="
},
"svg-pathdata": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.0.tgz",
"integrity": "sha512-8XoCZjKbP0fvJbDsm6KFxVau2N3SkWkMj8OniADm87q4OiFXk/gSgri5Uyr8hE1fQ9npI+9XzRlTUObgWmBBNw=="
},
"svg-tags": { "svg-tags": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npm.taobao.org/svg-tags/download/svg-tags-1.0.0.tgz", "resolved": "https://registry.npm.taobao.org/svg-tags/download/svg-tags-1.0.0.tgz",
@ -18949,8 +19056,7 @@
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
"dev": true
}, },
"util.promisify": { "util.promisify": {
"version": "1.1.1", "version": "1.1.1",

View File

@ -20,6 +20,7 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"pptxgenjs": "^3.7.0",
"prosemirror-commands": "^1.1.7", "prosemirror-commands": "^1.1.7",
"prosemirror-dropcursor": "^1.3.2", "prosemirror-dropcursor": "^1.3.2",
"prosemirror-gapcursor": "^1.1.5", "prosemirror-gapcursor": "^1.1.5",
@ -30,6 +31,8 @@
"prosemirror-schema-list": "^1.1.4", "prosemirror-schema-list": "^1.1.4",
"prosemirror-state": "^1.3.3", "prosemirror-state": "^1.3.3",
"prosemirror-view": "^1.18.1", "prosemirror-view": "^1.18.1",
"svg-arc-to-cubic-bezier": "^3.2.0",
"svg-pathdata": "^6.0.0",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
"vue": "^3.1.4", "vue": "^3.1.4",
"vuedraggable": "^4.0.1", "vuedraggable": "^4.0.1",
@ -52,6 +55,7 @@
"@types/prosemirror-schema-basic": "^1.0.1", "@types/prosemirror-schema-basic": "^1.0.1",
"@types/prosemirror-schema-list": "^1.0.1", "@types/prosemirror-schema-list": "^1.0.1",
"@types/resize-observer-browser": "^0.1.4", "@types/resize-observer-browser": "^0.1.4",
"@types/svg-arc-to-cubic-bezier": "^3.2.0",
"@types/tinycolor2": "^1.4.2", "@types/tinycolor2": "^1.4.2",
"@typescript-eslint/eslint-plugin": "^4.23.0", "@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0", "@typescript-eslint/parser": "^4.23.0",

View File

@ -0,0 +1,34 @@
<template>
<div class="fullscreen-spin" v-if="loading"><Spin :tip="tip" size="large" /></div>
</template>
<script lang="ts">
export default {
name: 'fullscreen-spin',
props: {
loading: {
type: Boolean,
default: false,
},
tip: {
type: String,
default: '',
},
},
}
</script>
<style lang="scss" scoped>
.fullscreen-spin {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba($color: #f1f1f1, $alpha: .7);
}
</style>

View File

@ -1,6 +1,7 @@
export interface ShapePoolItem { export interface ShapePoolItem {
viewBox: number; viewBox: number;
path: string; path: string;
special?: boolean;
} }
export const SHAPE_LIST = [ export const SHAPE_LIST = [
@ -282,10 +283,12 @@ export const SHAPE_LIST = [
{ {
viewBox: 1024, viewBox: 1024,
path: 'M398.208 302.912V64L0 482.112l398.208 418.176V655.36c284.48 0 483.584 95.552 625.792 304.64-56.896-298.688-227.584-597.312-625.792-657.088z', path: 'M398.208 302.912V64L0 482.112l398.208 418.176V655.36c284.48 0 483.584 95.552 625.792 304.64-56.896-298.688-227.584-597.312-625.792-657.088z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M625.792 302.912V64L1024 482.112l-398.208 418.176V655.36C341.312 655.36 142.208 750.912 0 960c56.896-298.688 227.584-597.312 625.792-657.088z', path: 'M625.792 302.912V64L1024 482.112l-398.208 418.176V655.36C341.312 655.36 142.208 750.912 0 960c56.896-298.688 227.584-597.312 625.792-657.088z',
special: true,
}, },
], ],
}, },
@ -296,66 +299,82 @@ export const SHAPE_LIST = [
{ {
viewBox: 1024, viewBox: 1024,
path: 'M995.336 243.4016c-15.7584-36.5736-38.3376-69.26639999-66.91440001-97.37280001-28.5768-27.98879999-61.73999999-49.8624-98.78399999-65.26799998-38.22-15.876-78.6744-23.8728-120.4224-23.87280001-57.97680001 0-114.5424 15.876-163.69919999 45.864-11.76 7.17360001-22.932 15.05279999-33.51600001 23.63760001-10.584-8.5848-21.75600001-16.46400001-33.51600001-23.63760001-49.1568-29.98799999-105.7224-45.86399999-163.69919999-45.864-41.74799999 0-82.2024 7.9968-120.4224 23.87280001-36.9264 15.28799999-70.2072 37.27919999-98.78399999 65.26799998-28.6944 28.10640001-51.156 60.79919999-66.91440001 97.37280001-16.34639999 37.9848-24.696 78.3216-24.696 119.83439999 0 39.1608 7.9968 79.96800001 23.8728 121.48080001 13.28880001 34.692 32.34000001 70.67760001 56.6832 107.016 38.57279999 57.5064 91.61040001 117.4824 157.4664 178.28160001 109.1328 100.78319999 217.2072 170.4024 221.79359999 173.22479998l27.87120001 17.8752c12.348 7.8792 28.224 7.8792 40.572 0l27.87119999-17.8752c4.58639999-2.94 112.54319999-72.44159999 221.79360001-173.22479998 65.85599999-60.79919999 118.89359999-120.7752 157.4664-178.28160001 24.3432-36.33839999 43.512-72.324 56.68319999-107.016 15.876-41.5128 23.8728-82.32 23.87280001-121.48080001 0.1176-41.5128-8.232-81.8496-24.5784-119.83439999z', path: 'M995.336 243.4016c-15.7584-36.5736-38.3376-69.26639999-66.91440001-97.37280001-28.5768-27.98879999-61.73999999-49.8624-98.78399999-65.26799998-38.22-15.876-78.6744-23.8728-120.4224-23.87280001-57.97680001 0-114.5424 15.876-163.69919999 45.864-11.76 7.17360001-22.932 15.05279999-33.51600001 23.63760001-10.584-8.5848-21.75600001-16.46400001-33.51600001-23.63760001-49.1568-29.98799999-105.7224-45.86399999-163.69919999-45.864-41.74799999 0-82.2024 7.9968-120.4224 23.87280001-36.9264 15.28799999-70.2072 37.27919999-98.78399999 65.26799998-28.6944 28.10640001-51.156 60.79919999-66.91440001 97.37280001-16.34639999 37.9848-24.696 78.3216-24.696 119.83439999 0 39.1608 7.9968 79.96800001 23.8728 121.48080001 13.28880001 34.692 32.34000001 70.67760001 56.6832 107.016 38.57279999 57.5064 91.61040001 117.4824 157.4664 178.28160001 109.1328 100.78319999 217.2072 170.4024 221.79359999 173.22479998l27.87120001 17.8752c12.348 7.8792 28.224 7.8792 40.572 0l27.87119999-17.8752c4.58639999-2.94 112.54319999-72.44159999 221.79360001-173.22479998 65.85599999-60.79919999 118.89359999-120.7752 157.4664-178.28160001 24.3432-36.33839999 43.512-72.324 56.68319999-107.016 15.876-41.5128 23.8728-82.32 23.87280001-121.48080001 0.1176-41.5128-8.232-81.8496-24.5784-119.83439999z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M985.20746667 343.50079998l-303.32586667-44.08319999L546.28693333 24.5248c-3.70346666-7.5264-9.79626667-13.6192-17.32266665-17.32266668-18.87573334-9.3184-41.81333333-1.55306667-51.25120001 17.32266668L342.1184 299.41759999l-303.32586667 44.08319999c-8.36266667 1.19466667-16.00853333 5.13706667-21.8624 11.11040001-14.69440001 15.17226667-14.45546667 39.30453334 0.71679999 54.1184l219.46026668 213.9648-51.84853333 302.1312c-1.43359999 8.24320001-0.11946667 16.8448 3.82293333 24.25173333 9.79626667 18.6368 32.9728 25.92426667 51.6096 16.00853334L512 822.44266665l271.3088 142.64320001c7.40693333 3.9424 16.00853333 5.25653333 24.25173333 3.82293333 20.78719999-3.584 34.7648-23.296 31.1808-44.0832l-51.84853333-302.1312 219.46026668-213.9648c5.97333334-5.85386666 9.91573333-13.49973334 11.11039999-21.8624 3.2256-20.90666667-11.34933333-40.26026667-32.256-43.36640001z', path: 'M985.20746667 343.50079998l-303.32586667-44.08319999L546.28693333 24.5248c-3.70346666-7.5264-9.79626667-13.6192-17.32266665-17.32266668-18.87573334-9.3184-41.81333333-1.55306667-51.25120001 17.32266668L342.1184 299.41759999l-303.32586667 44.08319999c-8.36266667 1.19466667-16.00853333 5.13706667-21.8624 11.11040001-14.69440001 15.17226667-14.45546667 39.30453334 0.71679999 54.1184l219.46026668 213.9648-51.84853333 302.1312c-1.43359999 8.24320001-0.11946667 16.8448 3.82293333 24.25173333 9.79626667 18.6368 32.9728 25.92426667 51.6096 16.00853334L512 822.44266665l271.3088 142.64320001c7.40693333 3.9424 16.00853333 5.25653333 24.25173333 3.82293333 20.78719999-3.584 34.7648-23.296 31.1808-44.0832l-51.84853333-302.1312 219.46026668-213.9648c5.97333334-5.85386666 9.91573333-13.49973334 11.11039999-21.8624 3.2256-20.90666667-11.34933333-40.26026667-32.256-43.36640001z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M852.65066667 405.84533333C800.54044445 268.40177778 667.76177778 170.66666667 512.22755555 170.66666667S223.91466667 268.288 171.80444445 405.73155555C74.29688889 431.33155555 2.27555555 520.07822222 2.27555555 625.77777778c0 125.72444445 101.83111111 227.55555555 227.44177778 227.55555555h564.56533334C919.89333333 853.33333333 1021.72444445 751.50222222 1021.72444445 625.77777778c0-105.472-71.79377778-194.21866667-169.07377778-219.93244445z', path: 'M852.65066667 405.84533333C800.54044445 268.40177778 667.76177778 170.66666667 512.22755555 170.66666667S223.91466667 268.288 171.80444445 405.73155555C74.29688889 431.33155555 2.27555555 520.07822222 2.27555555 625.77777778c0 125.72444445 101.83111111 227.55555555 227.44177778 227.55555555h564.56533334C919.89333333 853.33333333 1021.72444445 751.50222222 1021.72444445 625.77777778c0-105.472-71.79377778-194.21866667-169.07377778-219.93244445z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M926.25224691 323.7371485H654.6457886L898.88200917 15.14388241c5.05486373-6.53433603 0.49315743-16.02761669-7.76722963-16.02761668H418.30008701c-3.45210206 0-6.78091476 1.84934039-8.50696579 4.93157436L90.35039154 555.76772251c-3.82197013 6.53433603 0.86302552 14.7947231 8.50696578 14.79472311h215.01664245l-110.22068713 440.88274851c-2.34249783 9.61657002 9.24670194 16.39748478 16.39748477 9.49328065L933.03316167 340.62779071c6.41104668-6.0411786 2.09591911-16.8906422-6.78091476-16.89064221z', path: 'M926.25224691 323.7371485H654.6457886L898.88200917 15.14388241c5.05486373-6.53433603 0.49315743-16.02761669-7.76722963-16.02761668H418.30008701c-3.45210206 0-6.78091476 1.84934039-8.50696579 4.93157436L90.35039154 555.76772251c-3.82197013 6.53433603 0.86302552 14.7947231 8.50696578 14.79472311h215.01664245l-110.22068713 440.88274851c-2.34249783 9.61657002 9.24670194 16.39748478 16.39748477 9.49328065L933.03316167 340.62779071c6.41104668-6.0411786 2.09591911-16.8906422-6.78091476-16.89064221z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M878.47822222 463.30311111c-22.18666667-49.83466667-53.93066667-93.98044445-94.32177777-131.072l-33.10933334-30.37866666c-4.89244445-4.32355555-12.62933333-2.38933333-14.79111111 3.75466666l-14.79111111 42.43911111c-9.216 26.624-26.16888889 53.81688889-50.176 80.55466667-1.59288889 1.70666667-3.41333333 2.16177778-4.66488889 2.27555556-1.25155555 0.11377778-3.18577778-0.11377778-4.89244445-1.70666667-1.59288889-1.36533333-2.38933333-3.41333333-2.27555555-5.46133333 4.20977778-68.49422222-16.27022222-145.74933333-61.09866667-229.83111112C561.26577778 124.01777778 509.72444445 69.51822222 445.32622222 31.51644445l-46.99022222-27.648c-6.144-3.64088889-13.99466667 1.13777778-13.65333333 8.30577777l2.50311111 54.61333333c1.70666667 37.31911111-2.61688889 70.31466667-12.85688889 97.73511112-12.51555555 33.56444445-30.49244445 64.73955555-53.47555556 92.72888888-16.15644445 19.56977778-34.24711111 37.20533333-54.04444444 52.45155556-47.90044445 36.75022222-87.38133333 84.65066667-114.11911111 138.24C125.72444445 502.10133333 111.50222222 562.74488889 111.50222222 623.50222222c0 53.70311111 10.58133333 105.69955555 31.51644445 154.73777778 20.25244445 47.21777778 49.152 89.77066667 85.90222222 126.17955555 36.864 36.40888889 79.64444445 65.08088889 127.31733333 84.992C405.61777778 1010.11911111 457.95555555 1020.58666667 512 1020.58666667s106.38222222-10.46755555 155.76177778-31.06133334c47.67288889-19.91111111 90.56711111-48.46933333 127.31733333-84.992 36.864-36.40888889 65.76355555-78.96177778 85.90222222-126.17955555 20.93511111-49.03822222 31.51644445-101.03466667 31.51644445-154.73777778 0-55.52355555-11.37777778-109.45422222-34.01955556-160.31288889z', path: 'M878.47822222 463.30311111c-22.18666667-49.83466667-53.93066667-93.98044445-94.32177777-131.072l-33.10933334-30.37866666c-4.89244445-4.32355555-12.62933333-2.38933333-14.79111111 3.75466666l-14.79111111 42.43911111c-9.216 26.624-26.16888889 53.81688889-50.176 80.55466667-1.59288889 1.70666667-3.41333333 2.16177778-4.66488889 2.27555556-1.25155555 0.11377778-3.18577778-0.11377778-4.89244445-1.70666667-1.59288889-1.36533333-2.38933333-3.41333333-2.27555555-5.46133333 4.20977778-68.49422222-16.27022222-145.74933333-61.09866667-229.83111112C561.26577778 124.01777778 509.72444445 69.51822222 445.32622222 31.51644445l-46.99022222-27.648c-6.144-3.64088889-13.99466667 1.13777778-13.65333333 8.30577777l2.50311111 54.61333333c1.70666667 37.31911111-2.61688889 70.31466667-12.85688889 97.73511112-12.51555555 33.56444445-30.49244445 64.73955555-53.47555556 92.72888888-16.15644445 19.56977778-34.24711111 37.20533333-54.04444444 52.45155556-47.90044445 36.75022222-87.38133333 84.65066667-114.11911111 138.24C125.72444445 502.10133333 111.50222222 562.74488889 111.50222222 623.50222222c0 53.70311111 10.58133333 105.69955555 31.51644445 154.73777778 20.25244445 47.21777778 49.152 89.77066667 85.90222222 126.17955555 36.864 36.40888889 79.64444445 65.08088889 127.31733333 84.992C405.61777778 1010.11911111 457.95555555 1020.58666667 512 1020.58666667s106.38222222-10.46755555 155.76177778-31.06133334c47.67288889-19.91111111 90.56711111-48.46933333 127.31733333-84.992 36.864-36.40888889 65.76355555-78.96177778 85.90222222-126.17955555 20.93511111-49.03822222 31.51644445-101.03466667 31.51644445-154.73777778 0-55.52355555-11.37777778-109.45422222-34.01955556-160.31288889z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M968.20337778 20.11591112H705.44042667c-22.17301333 0-41.92483556 15.16430222-47.14951111 37.33731555C642.36202666 124.73685332 582.08711111 173.03324444 512 173.03324444s-130.36202666-48.29639112-146.29091556-115.58001777c-5.22467555-22.17301333-24.84906667-37.33731556-47.14951111-37.33731555H55.79662222c-30.96576 0-56.06968889 25.10392889-56.06968888 56.06968888v321.12639999c0 30.96576 25.10392889 56.06968889 56.06968888 56.06968889h95.57333334v494.43271112c0 30.96576 25.10392889 56.06968889 56.06968889 56.06968888h609.1207111c30.96576 0 56.06968889-25.10392889 56.06968889-56.06968888V453.38168888h95.57333334c30.96576 0 56.06968889-25.10392889 56.06968888-56.06968889V76.1856c0-30.96576-25.10392889-56.06968889-56.06968888-56.06968888z', path: 'M968.20337778 20.11591112H705.44042667c-22.17301333 0-41.92483556 15.16430222-47.14951111 37.33731555C642.36202666 124.73685332 582.08711111 173.03324444 512 173.03324444s-130.36202666-48.29639112-146.29091556-115.58001777c-5.22467555-22.17301333-24.84906667-37.33731556-47.14951111-37.33731555H55.79662222c-30.96576 0-56.06968889 25.10392889-56.06968888 56.06968888v321.12639999c0 30.96576 25.10392889 56.06968889 56.06968888 56.06968889h95.57333334v494.43271112c0 30.96576 25.10392889 56.06968889 56.06968889 56.06968888h609.1207111c30.96576 0 56.06968889-25.10392889 56.06968889-56.06968888V453.38168888h95.57333334c30.96576 0 56.06968889-25.10392889 56.06968888-56.06968889V76.1856c0-30.96576-25.10392889-56.06968889-56.06968888-56.06968888z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M980.94648889 239.80714666H523.46880001L373.99210666 96.82944c-1.91146667-1.78403556-4.46008889-2.80348444-7.00871111-2.80348445H43.05351111c-22.55530667 0-40.77795555 18.22264888-40.77795555 40.77795557v754.39217776c0 22.55530667 18.22264888 40.77795555 40.77795555 40.77795557h937.89297778c22.55530667 0 40.77795555-18.22264888 40.77795555-40.77795557V280.58510222c0-22.55530667-18.22264888-40.77795555-40.77795555-40.77795556z', path: 'M980.94648889 239.80714666H523.46880001L373.99210666 96.82944c-1.91146667-1.78403556-4.46008889-2.80348444-7.00871111-2.80348445H43.05351111c-22.55530667 0-40.77795555 18.22264888-40.77795555 40.77795557v754.39217776c0 22.55530667 18.22264888 40.77795555 40.77795555 40.77795557h937.89297778c22.55530667 0 40.77795555-18.22264888 40.77795555-40.77795557V280.58510222c0-22.55530667-18.22264888-40.77795555-40.77795555-40.77795556z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M972.60904597 164.57058577L841.30587843 33.39070759c-18.86327195-18.86327195-44.1375906-29.34286748-70.64480282-29.3428675-26.75379095 0-51.90482023 10.47959553-70.76809219 29.3428675L558.60337778 174.68031322c-18.86327195 18.86327195-29.34286748 44.1375906-29.34286749 70.64480283 0 26.75379095 10.47959553 51.90482023 29.34286749 70.76809218l103.31648301 103.31648302c-24.28800376 53.50758189-57.69942011 101.59043198-99.24793416 143.13894603-41.42522469 41.67180341-89.63136414 75.08321976-143.13894603 99.61780223L316.21649759 558.84995649c-18.86327195-18.86327195-44.1375906-29.34286748-70.64480283-29.34286747-26.75379095 0-51.90482023 10.47959553-70.76809217 29.34286747L33.39070759 700.01627278c-18.86327195 18.86327195-29.34286748 44.1375906-29.3428675 70.76809217 0 26.75379095 10.47959553 51.90482023 29.3428675 70.76809219l131.05658883 131.05658883c30.08260365 30.205893 71.63111769 47.34311394 114.28923598 47.34311394 9.00012323 0 17.63037836-0.73973616 26.13734414-2.21920846 166.19405621-27.37023774 331.03192945-115.76870829 464.06114804-248.67463751C901.84095379 636.27567408 990.11613498 471.56109018 1017.85624079 304.87387654c8.38367642-50.91850535-8.50696579-103.31648302-45.24719482-140.30329077z', path: 'M972.60904597 164.57058577L841.30587843 33.39070759c-18.86327195-18.86327195-44.1375906-29.34286748-70.64480282-29.3428675-26.75379095 0-51.90482023 10.47959553-70.76809219 29.3428675L558.60337778 174.68031322c-18.86327195 18.86327195-29.34286748 44.1375906-29.34286749 70.64480283 0 26.75379095 10.47959553 51.90482023 29.34286749 70.76809218l103.31648301 103.31648302c-24.28800376 53.50758189-57.69942011 101.59043198-99.24793416 143.13894603-41.42522469 41.67180341-89.63136414 75.08321976-143.13894603 99.61780223L316.21649759 558.84995649c-18.86327195-18.86327195-44.1375906-29.34286748-70.64480283-29.34286747-26.75379095 0-51.90482023 10.47959553-70.76809217 29.34286747L33.39070759 700.01627278c-18.86327195 18.86327195-29.34286748 44.1375906-29.3428675 70.76809217 0 26.75379095 10.47959553 51.90482023 29.3428675 70.76809219l131.05658883 131.05658883c30.08260365 30.205893 71.63111769 47.34311394 114.28923598 47.34311394 9.00012323 0 17.63037836-0.73973616 26.13734414-2.21920846 166.19405621-27.37023774 331.03192945-115.76870829 464.06114804-248.67463751C901.84095379 636.27567408 990.11613498 471.56109018 1017.85624079 304.87387654c8.38367642-50.91850535-8.50696579-103.31648302-45.24719482-140.30329077z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M910.60451556 640.96028445c-20.38897778-65.49959112-43.83630221-120.54983112-79.89930667-210.64362666C836.31217778 193.67708444 737.93535999 2.27555556 511.36284444 2.27555556 282.24170667 2.27555556 186.03121778 197.50001778 192.14791111 430.31665779c-36.19043555 90.22122667-59.51032888 144.88917333-79.89930667 210.64362666-43.32657778 139.53706668-29.30915556 197.26336001-18.60494222 198.53767111 22.9376 2.80348444 89.32920888-105.00323556 89.32920889-105.00323556 0 62.44124445 32.11264001 143.86972444 101.69002667 202.61546667-33.64181333 10.32192-109.20846222 38.10190221-91.24067556 68.55793777 14.52714667 24.59420444 250.01984 15.67402668 317.94062222 8.02816 67.92078222 7.64586667 303.41347556 16.56604444 317.94062223-8.02816 17.96778667-30.32860444-57.72629333-58.23601779-91.24067555-68.55793777 69.57738667-58.87317334 101.69002667-140.30165333 101.69002667-202.61546667 0 0 66.39160889 107.80672 89.32920888 105.00323556 10.83164445-1.40174222 24.84906667-59.12803556-18.47751111-198.53767111z', path: 'M910.60451556 640.96028445c-20.38897778-65.49959112-43.83630221-120.54983112-79.89930667-210.64362666C836.31217778 193.67708444 737.93535999 2.27555556 511.36284444 2.27555556 282.24170667 2.27555556 186.03121778 197.50001778 192.14791111 430.31665779c-36.19043555 90.22122667-59.51032888 144.88917333-79.89930667 210.64362666-43.32657778 139.53706668-29.30915556 197.26336001-18.60494222 198.53767111 22.9376 2.80348444 89.32920888-105.00323556 89.32920889-105.00323556 0 62.44124445 32.11264001 143.86972444 101.69002667 202.61546667-33.64181333 10.32192-109.20846222 38.10190221-91.24067556 68.55793777 14.52714667 24.59420444 250.01984 15.67402668 317.94062222 8.02816 67.92078222 7.64586667 303.41347556 16.56604444 317.94062223-8.02816 17.96778667-30.32860444-57.72629333-58.23601779-91.24067555-68.55793777 69.57738667-58.87317334 101.69002667-140.30165333 101.69002667-202.61546667 0 0 66.39160889 107.80672 89.32920888 105.00323556 10.83164445-1.40174222 24.84906667-59.12803556-18.47751111-198.53767111z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M1016.86992592 199.24764445c-37.13706667 16.01991111-77.55093333 27.54939259-119.17842962 32.03982222 42.96248889-25.60758518 75.60912592-66.02145185 91.02222222-114.08118519-39.68568889 23.66577778-84.58998518 41.02068148-131.31472593 50.00154074C819.53374815 126.79395555 765.76995555 101.79318518 706.18074075 101.79318518c-114.688 0-206.92385185 92.96402963-206.92385186 207.04521482 0 16.01991111 1.94180741 32.03982222 5.09724444 47.45291852-171.72859259-8.98085925-324.88865185-91.02222222-426.71217778-216.63288889-17.96171852 30.82619259-28.15620741 66.02145185-28.1562074 104.49351112 0 71.84687408 36.53025185 135.19834075 92.23585185 172.45677036-33.98162963-1.33499259-66.02145185-10.92266667-93.57084445-26.33576296v2.54862222c0 100.6098963 71.1186963 183.98625185 165.90317037 203.1616-17.3549037 4.49042963-35.92343703 7.03905185-54.49197037 7.03905185-13.47128889 0-26.2144-1.33499259-39.07887407-3.15543704C146.69748148 681.90814815 223.03478518 741.49736297 313.93564445 743.43917037c-71.1186963 55.7056-160.19911111 88.4736-256.9253926 88.4736-17.3549037 0-33.37481482-0.60681482-50.00154074-2.54862222C98.75911111 888.22518518 207.62168889 922.20681482 324.85831111 922.20681482 705.45256297 922.20681482 913.71140741 606.90583703 913.71140741 333.23235555c0-8.98085925 0-17.96171852-0.60681482-26.94257777 40.2925037-29.4912 75.60912592-66.02145185 103.76533333-107.04213333z', path: 'M1016.86992592 199.24764445c-37.13706667 16.01991111-77.55093333 27.54939259-119.17842962 32.03982222 42.96248889-25.60758518 75.60912592-66.02145185 91.02222222-114.08118519-39.68568889 23.66577778-84.58998518 41.02068148-131.31472593 50.00154074C819.53374815 126.79395555 765.76995555 101.79318518 706.18074075 101.79318518c-114.688 0-206.92385185 92.96402963-206.92385186 207.04521482 0 16.01991111 1.94180741 32.03982222 5.09724444 47.45291852-171.72859259-8.98085925-324.88865185-91.02222222-426.71217778-216.63288889-17.96171852 30.82619259-28.15620741 66.02145185-28.1562074 104.49351112 0 71.84687408 36.53025185 135.19834075 92.23585185 172.45677036-33.98162963-1.33499259-66.02145185-10.92266667-93.57084445-26.33576296v2.54862222c0 100.6098963 71.1186963 183.98625185 165.90317037 203.1616-17.3549037 4.49042963-35.92343703 7.03905185-54.49197037 7.03905185-13.47128889 0-26.2144-1.33499259-39.07887407-3.15543704C146.69748148 681.90814815 223.03478518 741.49736297 313.93564445 743.43917037c-71.1186963 55.7056-160.19911111 88.4736-256.9253926 88.4736-17.3549037 0-33.37481482-0.60681482-50.00154074-2.54862222C98.75911111 888.22518518 207.62168889 922.20681482 324.85831111 922.20681482 705.45256297 922.20681482 913.71140741 606.90583703 913.71140741 333.23235555c0-8.98085925 0-17.96171852-0.60681482-26.94257777 40.2925037-29.4912 75.60912592-66.02145185 103.76533333-107.04213333z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M917.96720197 1.08889505H106.03279803C53.56084718 1.08889505 9.37393998 45.27580225 9.37393998 97.74775309v5.52336372c0 19.33177108 8.28504494 41.42522469 22.0934536 55.23363205l331.40179753 392.15879462v325.87843379c0 16.57008987 8.28504494 30.37849854 22.09345359 35.90186098l209.88780469 104.94390299 2.76168121 2.76168121c27.61681602 11.04672615 55.23363335-8.28504494 55.23363335-38.66354218V550.66354348l331.40179753-392.15879462c35.90186097-41.42522469 30.37849854-102.18222047-11.04672616-135.32240022-11.04672615-13.80840865-33.14017975-22.0934536-55.23363335-22.09345359z', path: 'M917.96720197 1.08889505H106.03279803C53.56084718 1.08889505 9.37393998 45.27580225 9.37393998 97.74775309v5.52336372c0 19.33177108 8.28504494 41.42522469 22.0934536 55.23363205l331.40179753 392.15879462v325.87843379c0 16.57008987 8.28504494 30.37849854 22.09345359 35.90186098l209.88780469 104.94390299 2.76168121 2.76168121c27.61681602 11.04672615 55.23363335-8.28504494 55.23363335-38.66354218V550.66354348l331.40179753-392.15879462c35.90186097-41.42522469 30.37849854-102.18222047-11.04672616-135.32240022-11.04672615-13.80840865-33.14017975-22.0934536-55.23363335-22.09345359z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M491.70164031 97.48884502a25.89076502 25.89076502 0 0 1 40.59671938 0L745.66415762 367.01171317a25.89076502 25.89076502 0 0 0 30.49932208 7.72839349l208.00640948-89.14190458a25.89076502 25.89076502 0 0 1 35.56096592 29.06238339l-115.18801541 554.96855704A103.56306132 103.56306132 0 0 1 803.14165689 952.14301275H220.85834311a103.56306132 103.56306132 0 0 1-101.4011828-82.51387024l-115.18801541-554.96855704a25.89076502 25.89076502 0 0 1 35.54802012-29.06238339l208.01935528 89.14190458a25.89076502 25.89076502 0 0 0 30.49932208-7.72839349l213.36579793-269.52286815z', path: 'M491.70164031 97.48884502a25.89076502 25.89076502 0 0 1 40.59671938 0L745.66415762 367.01171317a25.89076502 25.89076502 0 0 0 30.49932208 7.72839349l208.00640948-89.14190458a25.89076502 25.89076502 0 0 1 35.56096592 29.06238339l-115.18801541 554.96855704A103.56306132 103.56306132 0 0 1 803.14165689 952.14301275H220.85834311a103.56306132 103.56306132 0 0 1-101.4011828-82.51387024l-115.18801541-554.96855704a25.89076502 25.89076502 0 0 1 35.54802012-29.06238339l208.01935528 89.14190458a25.89076502 25.89076502 0 0 0 30.49932208-7.72839349l213.36579793-269.52286815z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M643.02466884 387.7801525c19.85376751-88.69205333 33.718272-152.84087467 41.61900049-192.57389433C704.52292267 95.17283515 652.90057916 2.27555515 550.58614084 2.27555515c-92.26012484 0-138.59407685 45.84971417-165.91530666 137.49816969l-0.70087152 2.67605334c-16.40038399 74.13942085-41.47882668 131.61085116-74.6746315 172.73287031a189.06953915 189.06953915 0 0 1-143.04142182 70.44391902l-26.17434983 0.5606965C77.66380049 387.52529067 27.76177817 438.90551468 27.76177817 501.84374084V881.55022182c0 77.4144 62.25009818 140.17422182 139.05282766 140.17422303h492.82707951c101.23127467 0 191.59267516-63.995904 225.93535999-159.98976l102.37815468-286.22301868c26.04691951-72.82688-11.39234134-153.15945284-83.63303784-179.42300483a138.04612267 138.04612267 0 0 0-47.17499733-8.30850884H643.02466884z', path: 'M643.02466884 387.7801525c19.85376751-88.69205333 33.718272-152.84087467 41.61900049-192.57389433C704.52292267 95.17283515 652.90057916 2.27555515 550.58614084 2.27555515c-92.26012484 0-138.59407685 45.84971417-165.91530666 137.49816969l-0.70087152 2.67605334c-16.40038399 74.13942085-41.47882668 131.61085116-74.6746315 172.73287031a189.06953915 189.06953915 0 0 1-143.04142182 70.44391902l-26.17434983 0.5606965C77.66380049 387.52529067 27.76177817 438.90551468 27.76177817 501.84374084V881.55022182c0 77.4144 62.25009818 140.17422182 139.05282766 140.17422303h492.82707951c101.23127467 0 191.59267516-63.995904 225.93535999-159.98976l102.37815468-286.22301868c26.04691951-72.82688-11.39234134-153.15945284-83.63303784-179.42300483a138.04612267 138.04612267 0 0 0-47.17499733-8.30850884H643.02466884z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M512 512c140.82958222 0 254.86222222-114.03264 254.86222222-254.86222222S652.82958222 2.27555555 512 2.27555555a254.78940445 254.78940445 0 0 0-254.86222222 254.86222223C257.13777778 397.96736 371.17041778 512 512 512z m0 72.81777778c-170.10232889 0-509.72444445 97.57582222-509.72444445 291.27111111v145.63555556h1019.4488889v-145.63555556c0-193.69528889-339.62211555-291.27111111-509.72444445-291.27111111z', path: 'M512 512c140.82958222 0 254.86222222-114.03264 254.86222222-254.86222222S652.82958222 2.27555555 512 2.27555555a254.78940445 254.78940445 0 0 0-254.86222222 254.86222223C257.13777778 397.96736 371.17041778 512 512 512z m0 72.81777778c-170.10232889 0-509.72444445 97.57582222-509.72444445 291.27111111v145.63555556h1019.4488889v-145.63555556c0-193.69528889-339.62211555-291.27111111-509.72444445-291.27111111z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M1019.81297778 564.50161779l-138.89991111-472.51456c-8.66531556-25.99594668-29.43658667-43.45400889-57.21656889-43.45400891s-50.33528889 15.67402668-59.00060446 41.66997334l-92.00526221 274.48661334H351.69166222L259.6864 90.33045333c-8.66531556-25.99594668-31.22062222-41.66997333-59.00060444-41.66997332s-50.33528889 17.33063112-57.2165689 43.45400887L4.69674667 564.50161779c-5.22467555 17.33063112 1.78403556 36.44529778 15.67402667 46.89464887l491.11950221 368.27591113 492.77610666-368.27591113c13.76256-10.32192 20.77127111-29.43658667 15.54659557-46.89464887z', path: 'M1019.81297778 564.50161779l-138.89991111-472.51456c-8.66531556-25.99594668-29.43658667-43.45400889-57.21656889-43.45400891s-50.33528889 15.67402668-59.00060446 41.66997334l-92.00526221 274.48661334H351.69166222L259.6864 90.33045333c-8.66531556-25.99594668-31.22062222-41.66997333-59.00060444-41.66997332s-50.33528889 17.33063112-57.2165689 43.45400887L4.69674667 564.50161779c-5.22467555 17.33063112 1.78403556 36.44529778 15.67402667 46.89464887l491.11950221 368.27591113 492.77610666-368.27591113c13.76256-10.32192 20.77127111-29.43658667 15.54659557-46.89464887z',
special: true,
}, },
{ {
viewBox: 1024, viewBox: 1024,
path: 'M927.78951111 340.39277037c-12.01493333-47.81700741 12.01493333-124.03294815 89.08041481-150.97552592l-82.40545184-4.36906667s-31.19028148-109.22666667-174.27721483-118.9357037c-143.08693333-9.8304-236.65777778-3.64088889-236.65777777-3.6408889s106.07122963 67.47780741 63.5941926 187.74850371c-31.06891852 63.71555555-79.85682963 116.02299259-132.04290371 175.61220741-1.57771852 1.57771852-3.03407408 3.15543703-4.2477037 4.49042962C278.25493333 624.86755555 7.13007408 934.34311111 7.13007408 934.34311111c298.43152592 78.15774815 498.43768889-7.64586667 616.76657777-110.56165926 24.87940741-0.24272592 43.5693037-0.36408889 56.19105185-0.36408888 164.8109037 0 304.13558518-142.72284445 298.43152593-301.4656-3.88361482-109.1053037-38.71478518-133.74198518-50.72971852-181.5589926z', path: 'M927.78951111 340.39277037c-12.01493333-47.81700741 12.01493333-124.03294815 89.08041481-150.97552592l-82.40545184-4.36906667s-31.19028148-109.22666667-174.27721483-118.9357037c-143.08693333-9.8304-236.65777778-3.64088889-236.65777777-3.6408889s106.07122963 67.47780741 63.5941926 187.74850371c-31.06891852 63.71555555-79.85682963 116.02299259-132.04290371 175.61220741-1.57771852 1.57771852-3.03407408 3.15543703-4.2477037 4.49042962C278.25493333 624.86755555 7.13007408 934.34311111 7.13007408 934.34311111c298.43152592 78.15774815 498.43768889-7.64586667 616.76657777-110.56165926 24.87940741-0.24272592 43.5693037-0.36408889 56.19105185-0.36408888 164.8109037 0 304.13558518-142.72284445 298.43152593-301.4656-3.88361482-109.1053037-38.71478518-133.74198518-50.72971852-181.5589926z',
special: true,
}, },
], ],
}, },

View File

@ -3,7 +3,7 @@ import { MutationTypes, useStore } from '@/store'
import { createRandomCode } from '@/utils/common' import { createRandomCode } from '@/utils/common'
import { getImageSize } from '@/utils/image' import { getImageSize } from '@/utils/image'
import { VIEWPORT_SIZE } from '@/configs/canvas' import { VIEWPORT_SIZE } from '@/configs/canvas'
import { PPTLineElement, ChartType, PPTElement, TableCell, TableCellStyle } from '@/types/slides' import { PPTLineElement, ChartType, PPTElement, TableCell, TableCellStyle, PPTShapeElement } from '@/types/slides'
import { ShapePoolItem } from '@/configs/shapes' import { ShapePoolItem } from '@/configs/shapes'
import { LinePoolItem } from '@/configs/lines' import { LinePoolItem } from '@/configs/lines'
import useHistorySnapshot from '@/hooks/useHistorySnapshot' import useHistorySnapshot from '@/hooks/useHistorySnapshot'
@ -176,7 +176,7 @@ export default () => {
*/ */
const createShapeElement = (position: CommonElementPosition, data: ShapePoolItem) => { const createShapeElement = (position: CommonElementPosition, data: ShapePoolItem) => {
const { left, top, width, height } = position const { left, top, width, height } = position
createElement({ const newElement: PPTShapeElement = {
type: 'shape', type: 'shape',
id: createRandomCode(), id: createRandomCode(),
left, left,
@ -188,7 +188,9 @@ export default () => {
fill: themeColor.value, fill: themeColor.value,
fixedRatio: false, fixedRatio: false,
rotate: 0, rotate: 0,
}) }
if (data.special) newElement.special = true
createElement(newElement)
} }
/** /**

453
src/hooks/useExport.ts Normal file
View File

@ -0,0 +1,453 @@
import { computed, ref } from 'vue'
import { trim } from 'lodash'
import { saveAs } from 'file-saver'
import pptxgen from 'pptxgenjs'
import tinycolor from 'tinycolor2'
import { getElementRange } from '@/utils/element'
import { AST, toAST } from '@/utils/htmlParser'
import { SvgPoints, toPoints } from '@/utils/svgPathParser'
import { svg2Base64 } from '@/utils/svg2Base64'
import { useStore } from '@/store'
import { message } from 'ant-design-vue'
export default () => {
const store = useStore()
const slides = computed(() => store.state.slides)
const exporting = ref(false)
const exportJSON = () => {
const blob = new Blob([JSON.stringify(slides.value)], { type: '' })
saveAs(blob, 'pptist_slides.json')
}
const formatColor = (_color: string) => {
const c = tinycolor(_color)
const alpha = c.getAlpha()
const color = alpha === 0 ? '#ffffff' : c.setAlpha(1).toHexString()
return {
alpha,
color,
}
}
const formatHTML = (html: string) => {
const ast = toAST(html)
const slices: pptxgen.TextProps[] = []
const parse = (obj: AST[], baseStyleObj = {}) => {
for (const item of obj) {
if ('tagName' in item && ['div', 'ul', 'li', 'p'].includes(item.tagName) && slices.length) {
const lastSlice = slices[slices.length - 1]
if (!lastSlice.options) lastSlice.options = {}
lastSlice.options.breakLine = true
}
const styleObj = { ...baseStyleObj }
const styleAttr = 'attributes' in item ? item.attributes.find(attr => attr.key === 'style') : null
if (styleAttr && styleAttr.value) {
const styleArr = styleAttr.value.split(';')
for (const styleItem of styleArr) {
const [_key, _value] = styleItem.split(': ')
const [key, value] = [trim(_key), trim(_value)]
if (key && value) styleObj[key] = value
}
}
if ('tagName' in item) {
if (item.tagName === 'em') {
styleObj['font-style'] = 'italic'
}
if (item.tagName === 'strong') {
styleObj['font-weight'] = 'bold'
}
if (item.tagName === 'sup') {
styleObj['vertical-align'] = 'super'
}
if (item.tagName === 'sub') {
styleObj['vertical-align'] = 'sub'
}
}
if ('tagName' in item && item.tagName === 'br') {
slices.push({ text: '', options: { breakLine: true } })
}
else if ('content' in item) {
const text = item.content.replace(/\n/g, '').replace(/&nbsp;/g, ' ')
const options: pptxgen.TextPropsOptions = {}
if (styleObj['font-size']) {
options.fontSize = parseInt(styleObj['font-size']) * 0.75
}
if (styleObj['color']) {
options.color = formatColor(styleObj['color']).color
}
if (styleObj['background-color']) {
options.highlight = formatColor(styleObj['background-color']).color
}
if (styleObj['text-decoration-line']) {
if (styleObj['text-decoration-line'].indexOf('underline') !== -1) {
options.underline = {
color: options.color || '#000000',
style: 'sng',
}
}
if (styleObj['text-decoration-line'].indexOf('line-through') !== -1) {
options.strike = 'sngStrike'
}
}
if (styleObj['text-decoration']) {
if (styleObj['text-decoration'].indexOf('underline') !== -1) {
options.underline = {
color: options.color || '#000000',
style: 'sng',
}
}
if (styleObj['text-decoration'].indexOf('line-through') !== -1) {
options.strike = 'sngStrike'
}
}
if (styleObj['vertical-align']) {
if (styleObj['vertical-align'] === 'super') options.superscript = true
if (styleObj['vertical-align'] === 'sub') options.subscript = true
}
if (styleObj['text-align']) options.align = styleObj['text-align']
if (styleObj['font-weight']) options.bold = styleObj['font-weight'] === 'bold'
if (styleObj['font-style']) options.italic = styleObj['font-style'] === 'italic'
if (styleObj['font-family']) options.fontFace = styleObj['font-family']
slices.push({ text, options })
}
else if ('children' in item) parse(item.children, styleObj)
}
}
parse(ast)
return slices
}
type Points = Array<
| { x: number; y: number; moveTo?: boolean }
| { x: number; y: number; curve: { type: 'arc'; hR: number; wR: number; stAng: number; swAng: number } }
| { x: number; y: number; curve: { type: 'quadratic'; x1: number; y1: number } }
| { x: number; y: number; curve: { type: 'cubic'; x1: number; y1: number; x2: number; y2: number } }
| { close: true }
>
const formatPoints = (points: SvgPoints, scale = { x: 1, y: 1 }): Points => {
return points.map(point => {
if (point.close !== undefined) {
return { close: true }
}
else if (point.type === 'M') {
return {
x: point.x / 100 * scale.x,
y: point.y / 100 * scale.y,
moveTo: true,
}
}
else if (point.curve) {
if (point.curve.type === 'cubic') {
return {
x: point.x / 100 * scale.x,
y: point.y / 100 * scale.y,
curve: {
type: 'cubic',
x1: (point.curve.x1 as number) / 100 * scale.x,
y1: (point.curve.y1 as number) / 100 * scale.y,
x2: (point.curve.x2 as number) / 100 * scale.x,
y2: (point.curve.y2 as number) / 100 * scale.y,
},
}
}
else if (point.curve.type === 'quadratic') {
return {
x: point.x / 100 * scale.x,
y: point.y / 100 * scale.y,
curve: {
type: 'quadratic',
x1: (point.curve.x1 as number) / 100 * scale.x,
y1: (point.curve.y1 as number) / 100 * scale.y,
},
}
}
}
return {
x: point.x / 100 * scale.x,
y: point.y / 100 * scale.y,
}
})
}
const exportPPTX = () => {
exporting.value = true
const pptx = new pptxgen()
for (const slide of slides.value) {
const pptxSlide = pptx.addSlide()
if (slide.background) {
const background = slide.background
if (background.type === 'image' && background.image) {
pptxSlide.background = { data: background.image }
}
else if (background.type === 'solid' && background.color) {
const c = formatColor(background.color)
pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 }
}
else if (background.type === 'gradient' && background.gradientColor) {
const [color1, color2] = background.gradientColor
const color = tinycolor.mix(color1, color2).toHexString()
const c = formatColor(color)
pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 }
}
}
if (!slide.elements) continue
for (const el of slide.elements) {
if (el.type === 'text') {
const textProps = formatHTML(el.content)
const options: pptxgen.TextPropsOptions = {
x: el.left / 100,
y: el.top / 100,
w: el.width / 100,
h: el.height / 100,
fontSize: 20 * 0.75,
fontFace: '微软雅黑',
color: '#000000',
valign: 'middle',
}
if (el.rotate) options.rotate = el.rotate
if (el.wordSpace) options.charSpacing = el.wordSpace * 0.75
if (el.lineHeight) options.lineSpacingMultiple = el.lineHeight * 0.75
if (el.fill) {
const c = formatColor(el.fill)
const opacity = el.opacity === undefined ? 1 : el.opacity
options.fill = { color: c.color, transparency: (1 - c.alpha * opacity) * 100 }
}
if (el.defaultColor) options.color = formatColor(el.defaultColor).color
if (el.defaultFontName) options.fontFace = el.defaultFontName
pptxSlide.addText(textProps, options)
}
else if (el.type === 'image') {
const options: pptxgen.ImageProps = {
path: el.src,
x: el.left / 100,
y: el.top / 100,
w: el.width / 100,
h: el.height / 100,
}
if (el.flipH) options.flipH = el.flipH
if (el.flipV) options.flipV = el.flipV
if (el.rotate) options.rotate = el.rotate
if (el.clip && el.clip.shape === 'ellipse') options.rounding = true
pptxSlide.addImage(options)
}
else if (el.type === 'shape') {
if (el.special) {
const svgRef = document.querySelector(`#base-element-${el.id} svg`) as HTMLElement
const base64SVG = svg2Base64(svgRef)
const options: pptxgen.ImageProps = {
data: base64SVG,
x: el.left / 100,
y: el.top / 100,
w: el.width / 100,
h: el.height / 100,
}
if (el.rotate) options.rotate = el.rotate
pptxSlide.addImage(options)
}
else {
const scale = {
x: el.width / el.viewBox,
y: el.height / el.viewBox,
}
const points = formatPoints(toPoints(el.path), scale)
const fillColor = formatColor(el.fill)
const opacity = el.opacity === undefined ? 1 : el.opacity
const options: pptxgen.ShapeProps = {
x: el.left / 100,
y: el.top / 100,
w: el.width / 100,
h: el.height / 100,
fill: { color: fillColor.color, transparency: (1 - fillColor.alpha * opacity) * 100 },
points,
}
if (el.flipH) options.flipH = el.flipH
if (el.flipV) options.flipV = el.flipV
if (el.outline?.width) {
options.line = {
color: formatColor(el.outline?.color || '#000000').color,
width: el.outline.width * 0.75,
dashType: el.outline.style === 'solid' ? 'solid' : 'dash',
}
}
pptxSlide.addShape('custGeom' as pptxgen.ShapeType, options)
}
}
else if (el.type === 'line') {
let path = ''
const start = el.start.join(',')
const end = el.end.join(',')
if (el.broken) {
const mid = el.broken.join(',')
path = `M${start} L${mid} L${end}`
}
else if (el.curve) {
const mid = el.curve.join(',')
path = `M${start} Q${mid} ${end}`
}
else path = `M${start} L${end}`
const points = formatPoints(toPoints(path))
const { minX, maxX, minY, maxY } = getElementRange(el)
const options: pptxgen.ShapeProps = {
x: el.left / 100,
y: el.top / 100,
w: (maxX - minX) / 100,
h: (maxY - minY) / 100,
line: {
color: formatColor(el.color).color,
width: el.width * 0.75,
dashType: el.style === 'solid' ? 'solid' : 'dash',
beginArrowType: el.points[0] ? 'arrow' : 'none',
endArrowType: el.points[1] ? 'arrow' : 'none',
},
points,
}
pptxSlide.addShape('custGeom' as pptxgen.ShapeType, options)
}
else if (el.type === 'chart') {
const chartData = []
for (let i = 0; i < el.data.series.length; i++) {
const item = el.data.series[i]
chartData.push({
name: `系列${i + 1}`,
labels: el.data.labels,
values: item,
})
}
const chartColors = tinycolor(el.themeColor).analogous(10).map(item => item.toHexString())
const options: pptxgen.IChartOpts = {
x: el.left / 100,
y: el.top / 100,
w: el.width / 100,
h: el.height / 100,
chartColors: el.chartType === 'pie' ? chartColors : chartColors.slice(0, el.data.series.length),
}
let type = pptx.ChartType.bar
if (el.chartType === 'bar') {
type = pptx.ChartType.bar
options.barDir = el.options?.horizontalBars ? 'bar' : 'col'
}
else if (el.chartType === 'line') {
if (el.options?.showArea) type = pptx.ChartType.area
else if (el.options?.showLine === false) {
type = pptx.ChartType.scatter
chartData.unshift({ name: 'X-Axis', values: Array(el.data.series[0].length).fill(0).map((v, i) => i) })
options.lineSize = 0
}
else type = pptx.ChartType.line
if (el.options?.lineSmooth) options.lineSmooth = true
}
else if (el.chartType === 'pie') {
if (el.options?.donut) {
type = pptx.ChartType.doughnut
options.holeSize = 75
}
else type = pptx.ChartType.pie
}
pptxSlide.addChart(type, chartData, options)
}
else if (el.type === 'table') {
const hiddenCells = []
for (let i = 0; i < el.data.length; i++) {
const rowData = el.data[i]
for (let j = 0; j < rowData.length; j++) {
const cell = rowData[j]
if (cell.colspan > 1 || cell.rowspan > 1) {
for (let row = i; row < i + cell.rowspan; row++) {
for (let col = row === i ? j + 1 : j; col < j + cell.colspan; col++) hiddenCells.push(`${row}_${col}`)
}
}
}
}
const tableData = []
for (let i = 0; i < el.data.length; i++) {
const row = el.data[i]
const _row = []
for (let j = 0; j < row.length; j++) {
const cell = row[j]
const cellOptions: pptxgen.TableCellProps = {
colspan: cell.colspan,
rowspan: cell.rowspan,
bold: cell.style?.bold || false,
italic: cell.style?.em || false,
underline: { style: cell.style?.underline ? 'sng' : 'none' },
align: cell.style?.align || 'left',
valign: 'middle',
fontFace: cell.style?.fontname || '微软雅黑',
fontSize: (cell.style?.fontsize ? parseInt(cell.style?.fontsize) : 14) * 0.75,
}
if (cell.style?.backcolor) {
const c = formatColor(cell.style.backcolor)
cellOptions.fill = { color: c.color, transparency: (1 - c.alpha) * 100 }
}
if (cell.style?.color) cellOptions.color = formatColor(cell.style.color).color
if (!hiddenCells.includes(`${i}_${j}`)) {
_row.push({
text: cell.text,
options: cellOptions,
})
}
}
if (_row.length) tableData.push(_row)
}
const options: pptxgen.TableProps = {
x: el.left / 100,
y: el.top / 100,
w: el.width / 100,
h: el.height / 100,
colW: el.colWidths.map(item => el.width * item / 100),
}
if (el.outline.width && el.outline.color) {
options.border = {
type: el.outline.style === 'solid' ? 'solid' : 'dash',
pt: el.outline.width * 0.75,
color: formatColor(el.outline.color).color,
}
}
pptxSlide.addTable(tableData, options)
}
}
}
pptx.writeFile({ fileName: `pptist.pptx` }).then(() => exporting.value = false).catch(() => {
exporting.value = false
message.error('导出失败')
})
}
return {
exporting,
exportJSON,
exportPPTX,
}
}

View File

@ -21,6 +21,7 @@ import SvgWrapper from '@/components/SvgWrapper.vue'
import CheckboxButton from '@/components/CheckboxButton.vue' import CheckboxButton from '@/components/CheckboxButton.vue'
import CheckboxButtonGroup from '@/components/CheckboxButtonGroup.vue' import CheckboxButtonGroup from '@/components/CheckboxButtonGroup.vue'
import ColorPicker from '@/components/ColorPicker/index.vue' import ColorPicker from '@/components/ColorPicker/index.vue'
import FullscreenSpin from '@/components/FullscreenSpin.vue'
// antd 组件 // antd 组件
import { import {
@ -53,6 +54,7 @@ app.component('SvgWrapper', SvgWrapper)
app.component('CheckboxButton', CheckboxButton) app.component('CheckboxButton', CheckboxButton)
app.component('CheckboxButtonGroup', CheckboxButtonGroup) app.component('CheckboxButtonGroup', CheckboxButtonGroup)
app.component('ColorPicker', ColorPicker) app.component('ColorPicker', ColorPicker)
app.component('FullscreenSpin', FullscreenSpin)
app.component('InputNumber', InputNumber) app.component('InputNumber', InputNumber)
app.component('Divider', Divider) app.component('Divider', Divider)

View File

@ -154,7 +154,7 @@ export const layouts: Slide[] = [
path: 'M 0 0 L 0 200 L 200 200 Z', path: 'M 0 0 L 0 200 L 200 200 Z',
fill: '#5b9bd5', fill: '#5b9bd5',
fixedRatio: false, fixedRatio: false,
flipH: true, flipV: true,
rotate: 0 rotate: 0
}, },
{ {

View File

@ -29,7 +29,7 @@ export const slides: Slide[] = [
path: 'M 0 0 L 0 200 L 200 200 Z', path: 'M 0 0 L 0 200 L 200 200 Z',
fill: '#5b9bd5', fill: '#5b9bd5',
fixedRatio: false, fixedRatio: false,
flipH: true, flipV: true,
rotate: 0 rotate: 0
}, },
{ {

View File

@ -80,6 +80,7 @@ import {
Logout, Logout,
Erase, Erase,
Clear, Clear,
FolderClose,
} from '@icon-park/vue-next' } from '@icon-park/vue-next'
export default { export default {
@ -187,5 +188,6 @@ export default {
app.component('IconArrowCircleLeft', ArrowCircleLeft) app.component('IconArrowCircleLeft', ArrowCircleLeft)
app.component('IconLogout', Logout) app.component('IconLogout', Logout)
app.component('IconClear', Clear) app.component('IconClear', Clear)
app.component('IconFolderClose', FolderClose)
} }
} }

View File

@ -94,6 +94,7 @@ export interface PPTShapeElement extends PPTBaseElement {
flipH?: boolean; flipH?: boolean;
flipV?: boolean; flipV?: boolean;
shadow?: PPTElementShadow; shadow?: PPTElementShadow;
special?: boolean;
} }
export interface PPTLineElement extends Omit<PPTBaseElement, 'height'> { export interface PPTLineElement extends Omit<PPTBaseElement, 'height'> {
@ -133,7 +134,7 @@ export interface TableCellStyle {
backcolor?: string; backcolor?: string;
fontsize?: string; fontsize?: string;
fontname?: string; fontname?: string;
align?: string; align?: 'left' | 'center' | 'right';
} }
export interface TableCell { export interface TableCell {
id: string; id: string;

55
src/utils/svg2Base64.ts Normal file
View File

@ -0,0 +1,55 @@
// https://github.com/scriptex/svg64
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
const PREFIX = 'data:image/svg+xml;base64,'
const utf8Encode = (string: string) => {
string = string.replace(/\r\n/g, '\n')
let utftext = ''
for (let n = 0; n < string.length; n++) {
const c = string.charCodeAt(n)
if (c < 128) {
utftext += String.fromCharCode(c)
}
else if (c > 127 && c < 2048) {
utftext += String.fromCharCode((c >> 6) | 192)
utftext += String.fromCharCode((c & 63) | 128)
}
else {
utftext += String.fromCharCode((c >> 12) | 224)
utftext += String.fromCharCode(((c >> 6) & 63) | 128)
utftext += String.fromCharCode((c & 63) | 128)
}
}
return utftext
}
const encode = (input: string) => {
let output = ''
let chr1, chr2, chr3, enc1, enc2, enc3, enc4
let i = 0
input = utf8Encode(input)
while (i < input.length) {
chr1 = input.charCodeAt(i++)
chr2 = input.charCodeAt(i++)
chr3 = input.charCodeAt(i++)
enc1 = chr1 >> 2
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4)
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6)
enc4 = chr3 & 63
if (isNaN(chr2)) enc3 = enc4 = 64
else if (isNaN(chr3)) enc4 = 64
output = output + characters.charAt(enc1) + characters.charAt(enc2) + characters.charAt(enc3) + characters.charAt(enc4)
}
return output
}
export const svg2Base64 = (element: Element) => {
const XMLS = new XMLSerializer()
const svg = XMLS.serializeToString(element)
return PREFIX + encode(svg)
}

110
src/utils/svgPathParser.ts Normal file
View File

@ -0,0 +1,110 @@
import { SVGPathData } from 'svg-pathdata'
import arcToBezier from 'svg-arc-to-cubic-bezier'
const typeMap = {
1: 'Z',
2: 'M',
4: 'H',
8: 'V',
16: 'L',
32: 'C',
64: 'S',
128: 'Q',
256: 'T',
512: 'A',
}
export const parseSvgPath = (d: string) => {
const pathData = new SVGPathData(d)
const ret = pathData.commands.map(item => {
return { ...item, type: typeMap[item.type] }
})
return ret
}
export type SvgPath = ReturnType<typeof parseSvgPath>
export const toPoints = (d: string) => {
const pathData = new SVGPathData(d)
const points = []
for (const item of pathData.commands) {
const type = typeMap[item.type]
if (item.type === 2 || item.type === 16) {
points.push({
x: item.x,
y: item.y,
relative: item.relative,
type,
})
}
if (item.type === 32) {
points.push({
x: item.x,
y: item.y,
curve: {
type: 'cubic',
x1: item.x1,
y1: item.y1,
x2: item.x2,
y2: item.y2,
},
relative: item.relative,
type,
})
}
else if (item.type === 128) {
points.push({
x: item.x,
y: item.y,
curve: {
type: 'quadratic',
x1: item.x1,
y1: item.y1,
},
relative: item.relative,
type,
})
}
else if (item.type === 512) {
const lastPoint = points[points.length - 1]
if (!['M', 'L', 'Q', 'C'].includes(lastPoint.type)) continue
const cubicBezierPoints = arcToBezier({
px: lastPoint.x as number,
py: lastPoint.y as number,
cx: item.x,
cy: item.y,
rx: item.rX,
ry: item.rY,
xAxisRotation: item.xRot,
largeArcFlag: item.lArcFlag,
sweepFlag: item.sweepFlag,
})
for (const cbPoint of cubicBezierPoints) {
points.push({
x: cbPoint.x,
y: cbPoint.y,
curve: {
type: 'cubic',
x1: cbPoint.x1,
y1: cbPoint.y1,
x2: cbPoint.x2,
y2: cbPoint.y2,
},
relative: false,
type: 'C',
})
}
}
else if (item.type === 1) {
points.push({ close: true, type })
}
else continue
}
return points
}
export type SvgPoints = ReturnType<typeof toPoints>

View File

@ -1,67 +0,0 @@
<template>
<div class="export-dialog">
<div class="preview">
<pre>{{slides}}</pre>
</div>
<div class="handle">
<Button class="btn" type="primary" @click="exportJSON()">导出</Button>
<Button class="btn" @click="emit('close')">关闭</Button>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { useStore } from '@/store'
import { saveAs } from 'file-saver'
export default defineComponent({
name: 'export-dialog',
setup(props, { emit }) {
const store = useStore()
const slides = computed(() => store.state.slides)
const exportJSON = () => {
const blob = new Blob([JSON.stringify(slides.value)], { type: '' })
saveAs(blob, 'pptist_slides.json')
}
return {
slides,
exportJSON,
emit,
}
},
})
</script>
<style lang="scss" scoped>
.export-dialog {
height: 500px;
display: flex;
justify-content: center;
position: relative;
}
.preview {
width: 460px;
height: 100%;
overflow: auto;
margin-right: 20px;
background-color: #2d2d30;
color: #fff;
pre {
width: 100%;
height: 100%;
}
}
.handle {
flex: 1;
.btn {
width: 100%;
margin-bottom: 10px;
}
}
</style>

View File

@ -1,6 +1,15 @@
<template> <template>
<div class="editor-header"> <div class="editor-header">
<div class="left"> <div class="left">
<Dropdown :trigger="['click']">
<div class="menu-item"><IconFolderClose /> <span class="text">文件</span></div>
<template #overlay>
<Menu>
<MenuItem @click="exportJSON()">导出 JSON</MenuItem>
<MenuItem @click="exportPPTX()">导出 PPTX</MenuItem>
</Menu>
</template>
</Dropdown>
<Dropdown :trigger="['click']"> <Dropdown :trigger="['click']">
<div class="menu-item"><IconEdit /> <span class="text">编辑</span></div> <div class="menu-item"><IconEdit /> <span class="text">编辑</span></div>
<template #overlay> <template #overlay>
@ -11,7 +20,6 @@
<MenuItem @click="deleteSlide()">删除页面</MenuItem> <MenuItem @click="deleteSlide()">删除页面</MenuItem>
<MenuItem @click="toggleGridLines()">{{ showGridLines ? '关闭网格线' : '打开网格线' }}</MenuItem> <MenuItem @click="toggleGridLines()">{{ showGridLines ? '关闭网格线' : '打开网格线' }}</MenuItem>
<MenuItem @click="resetSlides()">重置幻灯片</MenuItem> <MenuItem @click="resetSlides()">重置幻灯片</MenuItem>
<MenuItem @click="exportDialogVisible = true">导出 JSON</MenuItem>
</Menu> </Menu>
</template> </template>
</Dropdown> </Dropdown>
@ -55,16 +63,7 @@
<HotkeyDoc /> <HotkeyDoc />
</Drawer> </Drawer>
<Modal <FullscreenSpin :loading="exporting" tip="正在导出..." />
v-model:visible="exportDialogVisible"
:footer="null"
centered
:closable="false"
:width="680"
destroyOnClose
>
<ExportDialog @close="exportDialogVisible = false"/>
</Modal>
</div> </div>
</template> </template>
@ -74,15 +73,14 @@ import { MutationTypes, useStore } from '@/store'
import useScreening from '@/hooks/useScreening' import useScreening from '@/hooks/useScreening'
import useSlideHandler from '@/hooks/useSlideHandler' import useSlideHandler from '@/hooks/useSlideHandler'
import useHistorySnapshot from '@/hooks/useHistorySnapshot' import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import useExport from '@/hooks/useExport'
import HotkeyDoc from './HotkeyDoc.vue' import HotkeyDoc from './HotkeyDoc.vue'
import ExportDialog from './ExportDialog.vue'
export default defineComponent({ export default defineComponent({
name: 'editor-header', name: 'editor-header',
components: { components: {
HotkeyDoc, HotkeyDoc,
ExportDialog,
}, },
setup() { setup() {
const store = useStore() const store = useStore()
@ -90,6 +88,7 @@ export default defineComponent({
const { enterScreening, enterScreeningFromStart } = useScreening() const { enterScreening, enterScreeningFromStart } = useScreening()
const { createSlide, deleteSlide, resetSlides } = useSlideHandler() const { createSlide, deleteSlide, resetSlides } = useSlideHandler()
const { redo, undo } = useHistorySnapshot() const { redo, undo } = useHistorySnapshot()
const { exporting, exportJSON, exportPPTX } = useExport()
const showGridLines = computed(() => store.state.showGridLines) const showGridLines = computed(() => store.state.showGridLines)
const toggleGridLines = () => { const toggleGridLines = () => {
@ -97,24 +96,25 @@ export default defineComponent({
} }
const hotkeyDrawerVisible = ref(false) const hotkeyDrawerVisible = ref(false)
const exportDialogVisible = ref(false)
const goIssues = () => { const goIssues = () => {
window.open('https://github.com/pipipi-pikachu/PPTist/issues') window.open('https://github.com/pipipi-pikachu/PPTist/issues')
} }
return { return {
redo,
undo,
showGridLines,
hotkeyDrawerVisible,
exporting,
enterScreening, enterScreening,
enterScreeningFromStart, enterScreeningFromStart,
createSlide, createSlide,
deleteSlide, deleteSlide,
redo,
undo,
toggleGridLines, toggleGridLines,
showGridLines,
resetSlides, resetSlides,
hotkeyDrawerVisible, exportJSON,
exportDialogVisible, exportPPTX,
goIssues, goIssues,
} }
}, },

View File

@ -3,13 +3,13 @@
<CheckboxButtonGroup class="row"> <CheckboxButtonGroup class="row">
<CheckboxButton <CheckboxButton
style="flex: 1;" style="flex: 1;"
:checked="flipH" :checked="flipV"
@click="updateFlip({ flipH: !flipH })" @click="updateFlip({ flipV: !flipV })"
><IconFlipVertically /> 垂直翻转</CheckboxButton> ><IconFlipVertically /> 垂直翻转</CheckboxButton>
<CheckboxButton <CheckboxButton
style="flex: 1;" style="flex: 1;"
:checked="flipV" :checked="flipH"
@click="updateFlip({ flipV: !flipV })" @click="updateFlip({ flipH: !flipH })"
><IconFlipHorizontally /> 水平翻转</CheckboxButton> ><IconFlipHorizontally /> 水平翻转</CheckboxButton>
</CheckboxButtonGroup> </CheckboxButtonGroup>
</div> </div>

View File

@ -1,6 +1,7 @@
<template> <template>
<div <div
class="base-element" class="base-element"
:id="`base-element-${elementInfo.id}`"
:style="{ :style="{
zIndex: elementIndex, zIndex: elementIndex,
}" }"

View File

@ -6,8 +6,8 @@ export default (flipH: Ref<boolean | undefined>, flipV: Ref<boolean | undefined>
let style = '' let style = ''
if (flipH.value && flipV.value) style = 'rotateX(180deg) rotateY(180deg)' if (flipH.value && flipV.value) style = 'rotateX(180deg) rotateY(180deg)'
else if (flipH.value) style = 'rotateX(180deg)' else if (flipV.value) style = 'rotateX(180deg)'
else if (flipV.value) style = 'rotateY(180deg)' else if (flipH.value) style = 'rotateY(180deg)'
return style return style
}) })