feat: office viewer 支持显示形状 (#6585)

* shape 支持初步

* 完善 shape 的支持

* 支持自定义 shape

* arcTo 换算法

* 修正 arc 圆形的计算

* curvedRightArrow 暂时不支持

* 修复编译报错

* 更新 snapshot

* 增加动作配置

* 新增 api 的时候也触发变化
This commit is contained in:
吴多益 2023-04-12 19:08:22 +08:00 committed by GitHub
parent d13faa6933
commit 48a67a5673
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 64315 additions and 71 deletions

View File

@ -19,9 +19,10 @@ order: 23
- 注音
- 链接
- 文本框
- 形状
- 数学公式(依赖 MathML需要比较新的浏览器或者试试 [polyfill](https://github.com/w3c/mathml-polyfills)
不支持的功能:分页符、形状、艺术字、域、对象、目录
不支持的功能:分页符、艺术字、域、对象、目录
## 基本用法

View File

@ -674,6 +674,11 @@ export function isApiOutdated(
return false;
}
// 通常是编辑器里加了属性,一开始没值,后来有了
if (prevApi === undefined && !nextApi !== undefined) {
return true;
}
nextApi = normalizeApi(nextApi);
if (nextApi.autoRefresh === false) {

View File

@ -1,4 +1,4 @@
import {registerEditorPlugin} from 'amis-editor-core';
import {RendererPluginAction, registerEditorPlugin} from 'amis-editor-core';
import {BaseEventContext, BasePlugin} from 'amis-editor-core';
import {defaultValue, getSchemaTpl} from 'amis-editor-core';
@ -27,6 +27,19 @@ export class OfficeViewerPlugin extends BasePlugin {
panelJustify = true;
actions: RendererPluginAction[] = [
{
actionType: 'print',
actionLabel: '打印',
description: '打印文档'
},
{
actionType: 'saveAs',
actionLabel: '下载',
description: '下载文档'
}
];
panelBodyCreator = (context: BaseEventContext) => {
return [
getSchemaTpl('tabs', [
@ -41,9 +54,18 @@ export class OfficeViewerPlugin extends BasePlugin {
name: 'src',
type: 'input-text',
label: '文档地址'
}),
getSchemaTpl('switch', {
type: 'switch',
label: '是否渲染',
name: 'display',
pipeIn: defaultValue(true),
inline: true
})
]
},
{
title: 'Word 渲染配置',
collapsed: true,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -78,7 +78,7 @@
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh wp14">
<w:body>
<w:p w14:paraId="17EBEE4B" w14:textId="5840C45A" w:rsidR="003456C4"
<w:p w14:paraId="17EBEE4B" w14:textId="5713407F" w:rsidR="003456C4"
w:rsidRDefault="003456C4">
<w:r>
<w:rPr>
@ -89,16 +89,16 @@
<w:drawing>
<wp:anchor distT="0" distB="0" distL="114300" distR="114300" simplePos="0"
relativeHeight="251659264" behindDoc="0" locked="0" layoutInCell="1"
allowOverlap="1" wp14:anchorId="47B983CA" wp14:editId="3301C715">
allowOverlap="1" wp14:anchorId="47B983CA" wp14:editId="189E243A">
<wp:simplePos x="0" y="0" />
<wp:positionH relativeFrom="column">
<wp:posOffset>0</wp:posOffset>
<wp:posOffset>-1108</wp:posOffset>
</wp:positionH>
<wp:positionV relativeFrom="paragraph">
<wp:posOffset>5715</wp:posOffset>
<wp:posOffset>9526</wp:posOffset>
</wp:positionV>
<wp:extent cx="832485" cy="832485" />
<wp:effectExtent l="9525" t="9525" r="15240" b="15240" />
<wp:extent cx="832485" cy="649694" />
<wp:effectExtent l="12700" t="12700" r="18415" b="10795" />
<wp:wrapNone />
<wp:docPr id="12" name="椭圆 12" />
<wp:cNvGraphicFramePr />
@ -109,8 +109,8 @@
<wps:cNvSpPr />
<wps:spPr>
<a:xfrm>
<a:off x="4650105" y="6876415" />
<a:ext cx="832485" cy="832485" />
<a:off x="0" y="0" />
<a:ext cx="832485" cy="649694" />
</a:xfrm>
<a:prstGeom prst="ellipse">
<a:avLst />
@ -148,14 +148,17 @@
</wps:wsp>
</a:graphicData>
</a:graphic>
<wp14:sizeRelV relativeFrom="margin">
<wp14:pctHeight>0</wp14:pctHeight>
</wp14:sizeRelV>
</wp:anchor>
</w:drawing>
</mc:Choice>
<mc:Fallback>
<w:pict>
<v:oval w14:anchorId="099597AD" id="椭圆 12" o:spid="_x0000_s1026"
style="position:absolute;left:0;text-align:left;margin-left:0;margin-top:.45pt;width:65.55pt;height:65.55pt;z-index:251659264;visibility:visible;mso-wrap-style:square;mso-wrap-distance-left:9pt;mso-wrap-distance-top:0;mso-wrap-distance-right:9pt;mso-wrap-distance-bottom:0;mso-position-horizontal:absolute;mso-position-horizontal-relative:text;mso-position-vertical:absolute;mso-position-vertical-relative:text;v-text-anchor:middle"
o:gfxdata="UEsDBBQABgAIAAAAIQC2gziS/gAAAOEBAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbJSRQU7DMBBF&#xD;&#xA;90jcwfIWJU67QAgl6YK0S0CoHGBkTxKLZGx5TGhvj5O2G0SRWNoz/78nu9wcxkFMGNg6quQqL6RA&#xD;&#xA;0s5Y6ir5vt9lD1JwBDIwOMJKHpHlpr69KfdHjyxSmriSfYz+USnWPY7AufNIadK6MEJMx9ApD/oD&#xD;&#xA;OlTrorhX2lFEilmcO2RdNtjC5xDF9pCuTyYBB5bi6bQ4syoJ3g9WQ0ymaiLzg5KdCXlKLjvcW893&#xD;&#xA;SUOqXwnz5DrgnHtJTxOsQfEKIT7DmDSUCaxw7Rqn8787ZsmRM9e2VmPeBN4uqYvTtW7jvijg9N/y&#xD;&#xA;JsXecLq0q+WD6m8AAAD//wMAUEsDBBQABgAIAAAAIQA4/SH/1gAAAJQBAAALAAAAX3JlbHMvLnJl&#xD;&#xA;bHOkkMFqwzAMhu+DvYPRfXGawxijTi+j0GvpHsDYimMaW0Yy2fr2M4PBMnrbUb/Q94l/f/hMi1qR&#xD;&#xA;JVI2sOt6UJgd+ZiDgffL8ekFlFSbvV0oo4EbChzGx4f9GRdb25HMsYhqlCwG5lrLq9biZkxWOiqY&#xD;&#xA;22YiTra2kYMu1l1tQD30/bPm3wwYN0x18gb45AdQl1tp5j/sFB2T0FQ7R0nTNEV3j6o9feQzro1i&#xD;&#xA;OWA14Fm+Q8a1a8+Bvu/d/dMb2JY5uiPbhG/ktn4cqGU/er3pcvwCAAD//wMAUEsDBBQABgAIAAAA&#xD;&#xA;IQC5kQqweAIAAD0FAAAOAAAAZHJzL2Uyb0RvYy54bWysVEtvGjEQvlfqf7B8b3ahQAhiiRARVaWo&#xD;&#xA;iZJWPRuvzVryelzbsNBf37H3AWqjHqruwTv2fPPNw55Z3p9qTY7CeQWmoKObnBJhOJTK7Av67ev2&#xD;&#xA;w5wSH5gpmQYjCnoWnt6v3r9bNnYhxlCBLoUjSGL8orEFrUKwiyzzvBI18zdghUGlBFezgFu3z0rH&#xD;&#xA;GmSvdTbO81nWgCutAy68x9OHVklXiV9KwcOTlF4EoguKsYW0urTu4pqtlmyxd8xWindhsH+IombK&#xD;&#xA;oNOB6oEFRg5O/UFVK+7Agww3HOoMpFRcpBwwm1H+WzavFbMi5YLF8XYok/9/tPzL8dU+OyxDY/3C&#xD;&#xA;oxizOElXxz/GR04FncymGNyUknNBZ/Pb2WQ0bQsnToFwBMw/jidz1HMEdDIyZhci63z4JKAmUSio&#xD;&#xA;0FpZH1NjC3Z89KFF96h4bGCrtE7Xow1p8G3d5dM8WXjQqozaiPNuv9toR44Mb3iTxy/Ghr6vYLjT&#xD;&#xA;Bg8vOSYpnLWIHNq8CElUiZmMWw/x+YmBlnEuTBi1qoqVovU2vXbWWyTXiTAyS4xy4O4IemRL0nO3&#xD;&#xA;MXf4aCrS6x2Mu9T/ZjxYJM9gwmBcKwPurcw0ZtV5bvF9kdrSxCrtoDw/O+Kg7R1v+VbhJT4yH56Z&#xD;&#xA;w2bBtsIBEJ5wkRrwpqCTKKnA/XzrPOLxDaOWkgabr6D+x4E5QYn+bPB1340mk9itaTOZ3o5x4641&#xD;&#xA;u2uNOdQbwNsf4aixPIkRH3QvSgf1d5wT6+gVVcxw9F1QHly/2YR2KOCk4WK9TjDsUMvCo3m1PJLH&#xD;&#xA;qhpYHwJIlR7spTpd1bBH0/V38yQOget9Ql2m3uoXAAAA//8DAFBLAwQUAAYACAAAACEA6hN6id4A&#xD;&#xA;AAAKAQAADwAAAGRycy9kb3ducmV2LnhtbEyPwU7DMBBE70j8g7VIXBC1U6S2pHEqBKIHbrR8gBsv&#xD;&#xA;iUW8Drbbhn49mxNcVrsazey8ajP6XpwwJhdIQzFTIJCaYB21Gj72r/crECkbsqYPhBp+MMGmvr6q&#xD;&#xA;TGnDmd7xtMut4BBKpdHQ5TyUUqamQ2/SLAxIrH2G6E3mM7bSRnPmcN/LuVIL6Y0j/tCZAZ87bL52&#xD;&#xA;R6/BL9/U3l0i3i1Vc1m5b9outqT17c34subxtAaRccx/DpgYuD/UXOwQjmST6DUwTdbwCGLSHooC&#xD;&#xA;xGFa5gpkXcn/CPUvAAAA//8DAFBLAQItABQABgAIAAAAIQC2gziS/gAAAOEBAAATAAAAAAAAAAAA&#xD;&#xA;AAAAAAAAAABbQ29udGVudF9UeXBlc10ueG1sUEsBAi0AFAAGAAgAAAAhADj9If/WAAAAlAEAAAsA&#xD;&#xA;AAAAAAAAAAAAAAAALwEAAF9yZWxzLy5yZWxzUEsBAi0AFAAGAAgAAAAhALmRCrB4AgAAPQUAAA4A&#xD;&#xA;AAAAAAAAAAAAAAAALgIAAGRycy9lMm9Eb2MueG1sUEsBAi0AFAAGAAgAAAAhAOoTeoneAAAACgEA&#xD;&#xA;AA8AAAAAAAAAAAAAAAAA0gQAAGRycy9kb3ducmV2LnhtbFBLBQYAAAAABAAEAPMAAADdBQAAAAA=&#xD;&#xA;"
<v:oval w14:anchorId="407A8DBD" id="椭圆 12" o:spid="_x0000_s1026"
style="position:absolute;left:0;text-align:left;margin-left:-.1pt;margin-top:.75pt;width:65.55pt;height:51.15pt;z-index:251659264;visibility:visible;mso-wrap-style:square;mso-height-percent:0;mso-wrap-distance-left:9pt;mso-wrap-distance-top:0;mso-wrap-distance-right:9pt;mso-wrap-distance-bottom:0;mso-position-horizontal:absolute;mso-position-horizontal-relative:text;mso-position-vertical:absolute;mso-position-vertical-relative:text;mso-height-percent:0;mso-height-relative:margin;v-text-anchor:middle"
o:gfxdata="UEsDBBQABgAIAAAAIQC2gziS/gAAAOEBAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbJSRQU7DMBBF&#xD;&#xA;90jcwfIWJU67QAgl6YK0S0CoHGBkTxKLZGx5TGhvj5O2G0SRWNoz/78nu9wcxkFMGNg6quQqL6RA&#xD;&#xA;0s5Y6ir5vt9lD1JwBDIwOMJKHpHlpr69KfdHjyxSmriSfYz+USnWPY7AufNIadK6MEJMx9ApD/oD&#xD;&#xA;OlTrorhX2lFEilmcO2RdNtjC5xDF9pCuTyYBB5bi6bQ4syoJ3g9WQ0ymaiLzg5KdCXlKLjvcW893&#xD;&#xA;SUOqXwnz5DrgnHtJTxOsQfEKIT7DmDSUCaxw7Rqn8787ZsmRM9e2VmPeBN4uqYvTtW7jvijg9N/y&#xD;&#xA;JsXecLq0q+WD6m8AAAD//wMAUEsDBBQABgAIAAAAIQA4/SH/1gAAAJQBAAALAAAAX3JlbHMvLnJl&#xD;&#xA;bHOkkMFqwzAMhu+DvYPRfXGawxijTi+j0GvpHsDYimMaW0Yy2fr2M4PBMnrbUb/Q94l/f/hMi1qR&#xD;&#xA;JVI2sOt6UJgd+ZiDgffL8ekFlFSbvV0oo4EbChzGx4f9GRdb25HMsYhqlCwG5lrLq9biZkxWOiqY&#xD;&#xA;22YiTra2kYMu1l1tQD30/bPm3wwYN0x18gb45AdQl1tp5j/sFB2T0FQ7R0nTNEV3j6o9feQzro1i&#xD;&#xA;OWA14Fm+Q8a1a8+Bvu/d/dMb2JY5uiPbhG/ktn4cqGU/er3pcvwCAAD//wMAUEsDBBQABgAIAAAA&#xD;&#xA;IQBJzeembgIAADEFAAAOAAAAZHJzL2Uyb0RvYy54bWysVE2PGjEMvVfqf4hy785AgQLaYYVYbVUJ&#xD;&#xA;dVG3Vc8hkzCRMnGaBAb66+tkPkDtqoeqc8g4sf1sP8e5fzjXmpyE8wpMQUd3OSXCcCiVORT029en&#xD;&#xA;d3NKfGCmZBqMKOhFePqwevvmvrFLMYYKdCkcQRDjl40taBWCXWaZ55Womb8DKwwqJbiaBdy6Q1Y6&#xD;&#xA;1iB6rbNxns+yBlxpHXDhPZ4+tkq6SvhSCh6epfQiEF1QzC2k1aV1H9dsdc+WB8dspXiXBvuHLGqm&#xD;&#xA;DAYdoB5ZYOTo1B9QteIOPMhwx6HOQErFRaoBqxnlv1XzUjErUi1IjrcDTf7/wfLPpxe7c0hDY/3S&#xD;&#xA;oxirOEtXxz/mR86JrMtAljgHwvFw/n48mU8p4aiaTRazxSSSmV2drfPho4CaRKGgQmtlfSyHLdlp&#xD;&#xA;60Nr3VvFYwNPSuvUEm1Ig/dpkU/z5OFBqzJqo513h/1GO3Ji2NVNHr8u9o0ZZqINJnStK0nhokXE&#xD;&#xA;0OaLkESVWMm4jRCvnBhgGefChFGrqlgp2mjT22C9Ryo7AUZkiVkO2B1Ab9mC9NgtA519dBXpxg7O&#xD;&#xA;Xel/cx48UmQwYXCulQH3WmUaq+oit/Y9SS01kaU9lJedIw7aefGWPyls4pb5sGMOBwRHCYc+POMi&#xD;&#xA;NWCnoJMoqcD9fO082uO9RS0lDQ5cQf2PI3OCEv3J4I1ejCaTOKFpM5l+GOPG3Wr2txpzrDeA3R/h&#xD;&#xA;82J5EqN90L0oHdTf8W1Yx6ioYoZj7ILy4PrNJrQPAb4uXKzXyQyn0rKwNS+WR/DIqoH1MYBU6cJe&#xD;&#xA;2elYw7lM7e/ekDj4t/tkdX3pVr8AAAD//wMAUEsDBBQABgAIAAAAIQBnIN5n3gAAAAwBAAAPAAAA&#xD;&#xA;ZHJzL2Rvd25yZXYueG1sTE/NTsMwDL4j8Q6RkbigLWETW+maTgjEDtzYeICsMW1E45Qk28qeHu8E&#xD;&#xA;F8v2Z38/1Xr0vThiTC6QhvupAoHUBOuo1fCxe50UIFI2ZE0fCDX8YIJ1fX1VmdKGE73jcZtbwSSU&#xD;&#xA;SqOhy3kopUxNh96kaRiQGPsM0ZvMY2yljebE5L6XM6UW0htHrNCZAZ87bL62B6/BL9/Uzp0j3i1V&#xD;&#xA;cy7cN20WG9L69mZ8WXF5WoHIOOa/D7hkYP9Qs7F9OJBNotcwmfEhrx9AXNC5egSx50bNC5B1Jf+H&#xD;&#xA;qH8BAAD//wMAUEsBAi0AFAAGAAgAAAAhALaDOJL+AAAA4QEAABMAAAAAAAAAAAAAAAAAAAAAAFtD&#xD;&#xA;b250ZW50X1R5cGVzXS54bWxQSwECLQAUAAYACAAAACEAOP0h/9YAAACUAQAACwAAAAAAAAAAAAAA&#xD;&#xA;AAAvAQAAX3JlbHMvLnJlbHNQSwECLQAUAAYACAAAACEASc3npm4CAAAxBQAADgAAAAAAAAAAAAAA&#xD;&#xA;AAAuAgAAZHJzL2Uyb0RvYy54bWxQSwECLQAUAAYACAAAACEAZyDeZ94AAAAMAQAADwAAAAAAAAAA&#xD;&#xA;AAAAAADIBAAAZHJzL2Rvd25yZXYueG1sUEsFBgAAAAAEAAQA8wAAANMFAAAAAA==&#xD;&#xA;"
filled="f" strokecolor="#c00000" strokeweight="1.5pt">
<v:stroke joinstyle="miter" />
</v:oval>
@ -494,9 +497,9 @@
xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh">
<w:zoom w:percent="120" />
<w:doNotDisplayPageBoundaries />
<w:bordersDoNotSurroundHeader />
<w:bordersDoNotSurroundFooter />
<w:proofState w:spelling="clean" w:grammar="clean" />
<w:defaultTabStop w:val="420" />
<w:drawingGridVerticalSpacing w:val="156" />
<w:displayHorizontalDrawingGridEvery w:val="0" />
@ -526,7 +529,11 @@
<w:rsids>
<w:rsidRoot w:val="003456C4" />
<w:rsid w:val="003456C4" />
<w:rsid w:val="008B0168" />
<w:rsid w:val="0097768D" />
<w:rsid w:val="00C406BE" />
<w:rsid w:val="00D127B0" />
<w:rsid w:val="00E010D9" />
</w:rsids>
<m:mathPr>
<m:mathFont m:val="Cambria Math" />
@ -546,6 +553,12 @@
w:accent1="accent1" w:accent2="accent2" w:accent3="accent3" w:accent4="accent4"
w:accent5="accent5" w:accent6="accent6" w:hyperlink="hyperlink"
w:followedHyperlink="followedHyperlink" />
<w:shapeDefaults>
<o:shapedefaults v:ext="edit" spidmax="1026" />
<o:shapelayout v:ext="edit">
<o:idmap v:ext="edit" data="1" />
</o:shapelayout>
</w:shapeDefaults>
<w:decimalSymbol w:val="." />
<w:listSeparator w:val="," />
<w14:docId w14:val="0C40186C" />
@ -568,9 +581,9 @@
<cp:keywords />
<dc:description />
<cp:lastModifiedBy>administrator</cp:lastModifiedBy>
<cp:revision>2</cp:revision>
<cp:revision>6</cp:revision>
<dcterms:created xsi:type="dcterms:W3CDTF">2023-03-28T09:22:00Z</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2023-03-28T09:22:00Z</dcterms:modified>
<dcterms:modified xsi:type="dcterms:W3CDTF">2023-04-10T10:44:00Z</dcterms:modified>
</cp:coreProperties>
</pkg:xmlData>
</pkg:part>
@ -1115,7 +1128,7 @@
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
<Template>Normal.dotm</Template>
<TotalTime>0</TotalTime>
<TotalTime>2</TotalTime>
<Pages>1</Pages>
<Words>0</Words>
<Characters>1</Characters>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -676,8 +676,19 @@ border-right: 0.67px solid black;
class="docx-viewer-0-r"
>
<div
style="z-index: 251664384; position: absolute; left: 482.39px; top: -145.60px; width: 341.99px; height: 1205.97px; background-color: rgb(245, 122, 21); column-count: 1;"
/>
style="z-index: 251664384; position: absolute; left: 482.39px; top: -145.60px; width: 341.99px; height: 1205.97px; column-count: 1;"
>
<svg
height="1205.97px"
style="overflow: visible"
width="341.99px"
>
<path
d="M 0 0 L 341.99 6.184461538461538 L 341.99 1191.5395897435897 L 87.9974269005848 1205.97 L 0 0 Z"
fill="#F57A15"
/>
</svg>
</div>
</span>
</p>
</section>

View File

@ -97,8 +97,20 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251659264; position: absolute; left: 144.00px; top: 77.80px; width: 122.79px; height: 53.58px; column-count: 1; background-color: rgb(255, 255, 255); border-width: 0.67px; border-style: solid;"
style="z-index: 251659264; position: absolute; left: 144.00px; top: 77.80px; width: 122.79px; height: 53.58px; column-count: 1;"
>
<svg
height="53.58px"
style="overflow: visible"
width="122.79px"
>
<path
d="M 0 0 L 122.79 0 L 122.79 53.58 L 0 53.58 Z"
fill="#FFFFFF"
stroke="black"
stroke-width="0.67px"
/>
</svg>
<p
class="docx-viewer-0-p"
>
@ -119,8 +131,18 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251658239; position: absolute; left: 122.79px; top: 64.41px; width: 164.09px; height: 84.84px; background-color: rgb(112, 173, 71); column-count: 1; border-style: solid;"
style="z-index: 251658239; position: absolute; left: 122.79px; top: 64.41px; width: 164.09px; height: 84.84px; column-count: 1;"
>
<svg
height="84.84px"
style="overflow: visible"
width="164.09px"
>
<path
d="M 0 0 L 164.09 0 L 164.09 84.84 L 0 84.84 Z"
fill="#70AD47"
/>
</svg>
<p
class="docx-viewer-0-p"
>

View File

@ -97,8 +97,18 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251661312; position: absolute; left: 214.63px; top: 36.07px; width: 155.16px; height: 82.60px; column-count: 1; background-color: rgb(255, 255, 255); transform: rotate(90deg);"
style="z-index: 251661312; position: absolute; left: 214.63px; top: 36.07px; width: 155.16px; height: 82.60px; column-count: 1; transform: rotate(90deg);"
>
<svg
height="82.6px"
style="overflow: visible"
width="155.16px"
>
<path
d="M 0 0 L 155.16 0 L 155.16 82.6 L 0 82.6 Z"
fill="#FFFFFF"
/>
</svg>
<p
class="docx-viewer-0-p"
>

View File

@ -101,8 +101,18 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251671552; position: absolute; left: 10.05px; top: 439.47px; width: 233.30px; height: 158.46px; background-color: rgb(23, 41, 72); column-count: 1;"
style="z-index: 251671552; position: absolute; left: 10.05px; top: 439.47px; width: 233.30px; height: 158.46px; column-count: 1;"
>
<svg
height="158.46px"
style="overflow: visible"
width="233.3px"
>
<path
d="M 0 0 L 233.3 0 L 233.3 158.46 L 0 158.46 Z"
fill="#172948"
/>
</svg>
<p
class="docx-viewer-0-p"
style="text-align: center;"
@ -132,8 +142,18 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251669504; position: absolute; left: 279.06px; top: 249.71px; width: 195.33px; height: 189.76px; background-color: rgb(23, 41, 72); writing-mode: vertical-rl; text-orientation: mixed; column-count: 1;"
style="z-index: 251669504; position: absolute; left: 279.06px; top: 249.71px; width: 195.33px; height: 189.76px; writing-mode: vertical-rl; text-orientation: mixed; column-count: 1;"
>
<svg
height="189.76px"
style="overflow: visible"
width="195.33px"
>
<path
d="M 0 0 L 195.33 0 L 195.33 189.76 L 0 189.76 Z"
fill="#172948"
/>
</svg>
<p
class="docx-viewer-0-p"
style="text-align: center;"
@ -163,8 +183,18 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251667456; position: absolute; left: 16.00px; top: 174.55px; width: 195.33px; height: 158.51px; background-color: rgb(23, 41, 72); writing-mode: vertical-rl; text-orientation: sideways; column-count: 1;"
style="z-index: 251667456; position: absolute; left: 16.00px; top: 174.55px; width: 195.33px; height: 158.51px; writing-mode: vertical-rl; text-orientation: sideways; column-count: 1;"
>
<svg
height="158.51px"
style="overflow: visible"
width="195.33px"
>
<path
d="M 0 0 L 195.33 0 L 195.33 158.51 L 0 158.51 Z"
fill="#172948"
/>
</svg>
<p
class="docx-viewer-0-p"
style="text-align: center;"
@ -194,8 +224,18 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251659264; position: absolute; left: 279.06px; top: 28.68px; width: 195.33px; height: 158.51px; background-color: rgb(23, 41, 72); writing-mode: vertical-rl; text-orientation: mixed; column-count: 1;"
style="z-index: 251659264; position: absolute; left: 279.06px; top: 28.68px; width: 195.33px; height: 158.51px; writing-mode: vertical-rl; text-orientation: mixed; column-count: 1;"
>
<svg
height="158.51px"
style="overflow: visible"
width="195.33px"
>
<path
d="M 0 0 L 195.33 0 L 195.33 158.51 L 0 158.51 Z"
fill="#172948"
/>
</svg>
<p
class="docx-viewer-0-p"
style="text-align: center;"
@ -213,8 +253,18 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251665408; position: absolute; left: 16.00px; top: 20.46px; width: 195.34px; height: 70.32px; background-color: rgb(23, 41, 72); column-count: 1;"
style="z-index: 251665408; position: absolute; left: 16.00px; top: 20.46px; width: 195.34px; height: 70.32px; column-count: 1;"
>
<svg
height="70.32px"
style="overflow: visible"
width="195.34px"
>
<path
d="M 0 0 L 195.34 0 L 195.34 70.32 L 0 70.32 Z"
fill="#172948"
/>
</svg>
<p
class="docx-viewer-0-p"
style="text-align: center;"
@ -232,8 +282,18 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251663360; position: absolute; left: 16.00px; top: -75.53px; width: 195.34px; height: 70.32px; background-color: rgb(23, 41, 72); column-count: 1;"
style="z-index: 251663360; position: absolute; left: 16.00px; top: -75.53px; width: 195.34px; height: 70.32px; column-count: 1;"
>
<svg
height="70.32px"
style="overflow: visible"
width="195.34px"
>
<path
d="M 0 0 L 195.34 0 L 195.34 70.32 L 0 70.32 Z"
fill="#172948"
/>
</svg>
<p
class="docx-viewer-0-p"
style="text-align: center;"
@ -251,8 +311,18 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251661312; position: absolute; left: 16.00px; top: -75.53px; width: 195.34px; height: 70.32px; background-color: rgb(23, 41, 72); column-count: 1;"
style="z-index: 251661312; position: absolute; left: 16.00px; top: -75.53px; width: 195.34px; height: 70.32px; column-count: 1;"
>
<svg
height="70.32px"
style="overflow: visible"
width="195.34px"
>
<path
d="M 0 0 L 195.34 0 L 195.34 70.32 L 0 70.32 Z"
fill="#172948"
/>
</svg>
<p
class="docx-viewer-0-p"
style="text-align: center;"

View File

@ -97,8 +97,19 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251663360; position: absolute; left: 329.67px; top: 137.67px; width: 155.16px; height: 82.60px; background-color: rgb(255, 255, 255); column-count: 1; border-style: solid; border-color: #ed7d31;"
style="z-index: 251663360; position: absolute; left: 329.67px; top: 137.67px; width: 155.16px; height: 82.60px; column-count: 1;"
>
<svg
height="82.6px"
style="overflow: visible"
width="155.16px"
>
<path
d="M 0 0 L 155.16 0 L 155.16 82.6 L 0 82.6 Z"
fill="#FFFFFF"
stroke="#ED7D31"
/>
</svg>
<p
class="docx-viewer-0-p"
>
@ -119,8 +130,20 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251659264; position: absolute; left: 47.94px; top: 139.17px; width: 155.16px; height: 82.60px; column-count: 1; background-color: rgb(255, 255, 255); border-width: 0.67px; border-style: solid;"
style="z-index: 251659264; position: absolute; left: 47.94px; top: 139.17px; width: 155.16px; height: 82.60px; column-count: 1;"
>
<svg
height="82.6px"
style="overflow: visible"
width="155.16px"
>
<path
d="M 0 0 L 155.16 0 L 155.16 82.6 L 0 82.6 Z"
fill="#FFFFFF"
stroke="black"
stroke-width="0.67px"
/>
</svg>
<p
class="docx-viewer-0-p"
>
@ -136,8 +159,18 @@ padding-right: 7.20px;
class="docx-viewer-0-r"
>
<div
style="z-index: 251661312; position: absolute; left: 214.63px; top: 36.07px; width: 155.16px; height: 82.60px; column-count: 1; background-color: rgb(255, 255, 255);"
style="z-index: 251661312; position: absolute; left: 214.63px; top: 36.07px; width: 155.16px; height: 82.60px; column-count: 1;"
>
<svg
height="82.6px"
style="overflow: visible"
width="155.16px"
>
<path
d="M 0 0 L 155.16 0 L 155.16 82.6 L 0 82.6 Z"
fill="#FFFFFF"
/>
</svg>
<p
class="docx-viewer-0-p"
>

View File

@ -28,6 +28,11 @@ const fileLists = {
'svg.xml',
'sym.xml',
'shadow.xml',
'shape-ellipse.xml',
'shape-custom.xml',
'shape-custom.xml',
'all-shape-1.xml',
'all-shape-2.xml',
'tableborder.xml',
'tablestyle.xml',
'textbox.xml',

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preset Shape</title>
<link rel="stylesheet" href="./static/css/app.css" />
</head>
<body>
<div id="shapes"></div>
<script type="module" src="./presetShape.ts"></script>
</body>
</html>

View File

@ -0,0 +1,32 @@
import {presetShape} from '../src/openxml/word/drawing/presetShape';
import {shapeToSVG} from '../src/openxml/word/drawing/svg/shapeToSVG';
const container = document.getElementById('shapes')! as HTMLElement;
for (const shapeName in presetShape) {
// 临时测试用
if (shapeName !== 'smileyFace') {
// continue;
}
const shape = presetShape[shapeName];
console.log('render shape', shapeName);
const svg = shapeToSVG(shape, [], {}, 100, 100, {
fillColor: '#4472C4'
});
const div = document.createElement('div');
div.style.display = 'inline-block';
div.style.margin = '12px 40px';
div.style.width = '100px';
div.style.height = '120px';
div.style.boxSizing = 'border-box';
div.style.verticalAlign = 'top';
div.style.textAlign = 'center';
div.appendChild(svg);
const p = document.createElement('p');
p.innerText = shapeName;
div.appendChild(p);
container.appendChild(div);
}

View File

@ -14,7 +14,8 @@
"coverage": "jest --coverage",
"declaration": "tsc --project tsconfig-for-declaration.json --allowJs --declaration --emitDeclarationOnly --declarationDir ./lib --rootDir ./src",
"clean-dist": "rimraf lib/** esm/**",
"xsd2ts": "cd tools && ts-node --transpileOnly xsd2ts.ts"
"xsd2ts": "cd tools && ts-node --transpileOnly xsd2ts.ts",
"conver-preset": "cd tools && ts-node --transpileOnly converDrawingML.ts"
},
"exports": {
".": {

View File

@ -6,7 +6,12 @@
* w:val
*/
export function getVal(element: Element) {
return element.getAttribute('w:val') || element.getAttribute('w14:val') || '';
return (
element.getAttribute('w:val') ||
element.getAttribute('w14:val') ||
element.getAttribute('val') ||
''
);
}
/**

View File

@ -0,0 +1,17 @@
/**
*
*/
import Word from '../../../Word';
import {parseShape} from '../../../parse/parseShape';
import {Shape} from './Shape';
export class CustomGeom {
shape: Shape;
static fromXML(word: Word, element: Element): CustomGeom {
const geom = new CustomGeom();
geom.shape = parseShape(element);
return geom;
}
}

View File

@ -1,17 +1,28 @@
/**
*
*
*/
import {ST_ShapeType} from '../../Types';
import Word from '../../../Word';
import {CSSStyle} from './../../Style';
import {parseShapeGuide} from '../../../parse/parseShape';
import {ShapeGuide} from './Shape';
export class Geom {
type: ST_ShapeType;
prst: ST_ShapeType;
avLst?: ShapeGuide[];
static fromXML(word: Word, element: Element, style: CSSStyle): Geom {
static fromXML(word: Word, element: Element): Geom {
const geom = new Geom();
geom.type = element.getAttribute('prst') as ST_ShapeType;
// 后面得改成用 SVG 实现
geom.prst = element.getAttribute('prst') as ST_ShapeType;
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'a:avLst': {
geom.avLst = parseShapeGuide(child);
}
}
}
return geom;
}
}

View File

@ -0,0 +1,52 @@
import {ST_PathFillMode} from '../../Types';
export interface IPath {
type: 'moveTo' | 'lnTo' | 'arcTo' | 'cubicBezTo' | 'quadBezTo' | 'close';
}
export interface PathPoint {
x: string;
y: string;
}
export interface LnTo extends IPath {
pt: PathPoint;
}
export interface MoveTo extends IPath {
pt: PathPoint;
}
export interface ArcTo extends IPath {
wR: string;
hR: string;
stAng: string;
swAng: string;
}
export interface QuadBezTo extends IPath {
pts: PathPoint[];
}
export interface CubicBezTo extends IPath {
pts: PathPoint[];
}
export interface Close extends IPath {}
export type ShapeDefine =
| MoveTo
| LnTo
| ArcTo
| QuadBezTo
| CubicBezTo
| Close;
export interface Path {
h?: number;
w?: number;
fill?: ST_PathFillMode;
extrusionOk?: boolean;
stroke?: boolean;
defines: ShapeDefine[];
}

View File

@ -0,0 +1,29 @@
/**
* Shape
*/
import {Path} from './Path';
export interface ShapeGuide {
// 名称
n: string;
// 公式
f: string;
}
// http://webapp.docx4java.org/OnlineDemo/ecma376/DrawingML/rect.html
export interface Rect {
b: string;
l: string;
r: string;
t: string;
}
export interface Shape {
avLst?: ShapeGuide[];
gdLst?: ShapeGuide[];
rect?: Rect;
pathLst?: Path[];
}

View File

@ -9,6 +9,7 @@ import {CSSStyle} from './../../Style';
import {parseSize, LengthUsage} from '../../../parse/parseSize';
import {Geom} from './Geom';
import {parseChildColor} from '../../../parse/parseChildColor';
import {CustomGeom} from './CustomGeom';
function prstDashToCSSBorderType(prstDash: ST_PresetLineDashVal) {
let borderType = 'solid';
@ -32,29 +33,39 @@ function prstDashToCSSBorderType(prstDash: ST_PresetLineDashVal) {
return borderType;
}
function parseOutline(word: Word, element: Element, style: CSSStyle) {
export type OutLine = {
style?: 'solid' | 'dashed' | 'dotted' | string;
width?: string;
color?: string;
radius?: string;
};
function parseOutline(word: Word, element: Element) {
const borderWidth = parseSize(element, 'w', LengthUsage.Emu);
style['border-width'] = borderWidth;
style['border-style'] = 'solid';
const outline: OutLine = {
width: borderWidth
};
outline.style = 'solid';
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'a:solidFill':
style['border-color'] = parseChildColor(word, child);
outline.color = parseChildColor(word, child);
break;
case 'a:noFill':
style['border'] = 'none';
break;
return {};
case 'a:round':
// 瞎写的,规范里也没写是多少
style['border-radius'] = '8%';
outline.radius = '8%';
break;
case 'a:prstDash':
style['border-style'] = prstDashToCSSBorderType(
outline.style = prstDashToCSSBorderType(
child.getAttribute('val') as ST_PresetLineDashVal
);
break;
@ -63,21 +74,28 @@ function parseOutline(word: Word, element: Element, style: CSSStyle) {
console.warn('parseOutline: Unknown tag ', tagName, child);
}
}
return outline;
}
export class ShapePr {
xfrm?: Transform;
// 内置图形
prstGeom?: Geom;
geom?: Geom;
// 主要是边框样式
style?: CSSStyle;
// 自定义
custGeom?: CustomGeom;
// 边框样式
outline?: OutLine;
// 填充颜色
fillColor?: string;
static fromXML(word: Word, element?: Element | null): ShapePr {
const shapePr = new ShapePr();
const style: CSSStyle = {};
shapePr.style = style;
if (element) {
for (const child of element.children) {
const tagName = child.tagName;
@ -87,12 +105,16 @@ export class ShapePr {
break;
case 'a:prstGeom':
shapePr.prstGeom = Geom.fromXML(word, child, style);
shapePr.geom = Geom.fromXML(word, child);
break;
case 'a:custGeom':
shapePr.custGeom = CustomGeom.fromXML(word, child);
break;
case 'a:ln':
// http://officeopenxml.com/drwSp-outline.php
parseOutline(word, child, style);
shapePr.outline = parseOutline(word, child);
break;
case 'a:noFill':
@ -100,7 +122,7 @@ export class ShapePr {
break;
case 'a:solidFill':
style['background-color'] = parseChildColor(word, child);
shapePr.fillColor = parseChildColor(word, child);
break;
default:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,122 @@
/**
* arc SVG PATH A
*/
function floatEqual(a: number, b: number) {
if (a === b) {
return true;
}
const diff = Math.abs(a - b);
if (diff < Number.EPSILON) {
return true;
}
return diff <= Number.EPSILON * Math.min(Math.abs(a), Math.abs(b));
}
/**
*
*/
const radians = (deg: number) => Math.PI * (deg / 60000 / 180);
/**
* arc A curvedRightArrow
*/
export default function arcToPathA(
wR: number,
hR: number,
stAng: number,
swAng: number,
preX: number,
preY: number
) {
let startR = radians(stAng);
let swAngR = radians(swAng);
let endR = radians(stAng + swAng);
if (floatEqual(swAng, 60000 * 360)) {
// 如果是圆会变成点,所以减少一下避免这种情况
endR = endR - 0.0001;
}
const end = getEndPoint(wR, hR, startR, endR, 0, preX, preY);
// 是否是大弧
const largeArcFlag = Math.abs(swAngR) > Math.PI ? 1 : 0;
// 是否是顺时针
const sweepFlag = swAng > 0 ? 1 : 0;
const path = `A ${wR} ${hR} 0 ${largeArcFlag} ${sweepFlag} ${end.x},${end.y}`;
return {
path,
end
};
}
/**
*
*/
function matrixMul(first: number[][], second: number[]) {
return [
first[0][0] * second[0] + first[0][1] * second[1],
first[1][0] * second[0] + first[1][1] * second[1]
];
}
/**
*
* https://www.cnblogs.com/ryzen/p/15191386.html
* https://wiki.documentfoundation.org/Development/Improve_handles_of_DrawingML_shapes
*
* @param rx
* @param ry
* @param stAng
* @param swAng
* @param rotate
* @param x x
* @param y y
* @returns
*/
function getEndPoint(
rx: number,
ry: number,
stAng: number,
swAng: number,
rotate: number,
x: number,
y: number
) {
let startR = stAng;
let endR = swAng;
// 起始节点坐标
const matrixX1Y1 = [x, y];
const matrix1 = [
[Math.cos(rotate), -Math.sin(rotate)],
[Math.sin(rotate), Math.cos(rotate)]
];
const matrix2 = [rx * Math.cos(startR), ry * Math.sin(startR)];
// 公式第二部分
const secondPart = matrixMul(matrix1, matrix2);
const matrixCxCy = [
matrixX1Y1[0] - secondPart[0],
matrixX1Y1[1] - secondPart[1]
];
const matrix3 = [rx * Math.cos(endR), ry * Math.sin(endR)];
const firstPart = matrixMul(matrix1, matrix3);
const result = [matrixCxCy[0] + firstPart[0], matrixCxCy[1] + firstPart[1]];
return {
x: result[0],
y: result[1]
};
}

View File

@ -0,0 +1,109 @@
/**
* 20.1.9.11
*/
const angleFactor = (1 / 60000 / 180) * Math.PI;
export const formulas = {
'*/': function (x: number, y: number, z: number) {
return (x * y) / z;
},
'+-': function (x: number, y: number, z: number) {
return x + y - z;
},
'+/': function (x: number, y: number, z: number) {
return (x + y) / z;
},
'?:': function (x: number, y: number, z: number) {
return x > 0 ? y : z;
},
'abs': function (x: number) {
return Math.abs(x);
},
'at2': function (x: number, y: number) {
// 转回角度,因为后续的计算是基于角度的
return (Math.atan2(y, x) * 180 * 60000) / Math.PI;
},
'cat2': function (x: number, y: number, z: number) {
return x * Math.cos(Math.atan2(z, y));
},
'cos': function (x: number, y: number) {
return x * Math.cos(y * angleFactor);
},
'max': function (x: number, y: number) {
return Math.max(x, y);
},
'min': function (x: number, y: number) {
return Math.min(x, y);
},
'mod': function (x: number, y: number, z: number) {
return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2));
},
'pin': function (x: number, y: number, z: number) {
return y < x ? x : y > z ? z : y;
},
'sat2': function (x: number, y: number, z: number) {
return x * Math.sin(Math.atan2(z, y));
},
'sin': function (x: number, y: number) {
return x * Math.sin(y * angleFactor);
},
'sqrt': function (x: number) {
return Math.sqrt(x);
},
'tan': function (x: number, y: number) {
return x * Math.tan(y * angleFactor);
},
'val': function (a: string) {
const parse = parseInt(a, 10);
if (isNaN(parse)) {
return parse;
}
return parse;
}
};
/**
*
* @param fmla
* @param vars
* @returns
*/
export function evalFmla(
name: string,
fmla: string,
vars: Record<string, number>
): number {
const fmlaArr = fmla.split(/[ ]+/);
if (fmlaArr.length <= 1) {
console.warn('fmla format error', fmla);
}
const fmlaName = fmlaArr[0];
const fmlaArgs = fmlaArr.slice(1);
// 这里要求 gd 定义必须顺序,不然就找不到之前的值了
const fmlaArgsNum = fmlaArgs.map(arg => {
if (arg in vars) {
return vars[arg];
}
const parse = parseInt(arg, 10);
if (isNaN(parse)) {
console.warn('fmla arg error', arg, fmla);
return 0;
} else {
return parse;
}
});
if (fmlaName in formulas) {
const val = formulas[fmlaName as keyof typeof formulas].apply(
null,
fmlaArgsNum
);
if (isNaN(val)) {
console.warn('fmla eval error', fmla, name);
return 0;
} else {
vars[name] = val;
}
}
return 0;
}

View File

@ -0,0 +1,143 @@
import {ArcTo, LnTo, MoveTo, Path, QuadBezTo, ShapeDefine} from '../Path';
import arcToPathA from './arcToA';
export type Var = Record<string, number>;
function getVal(name: string, vars: Var, scale?: number): number {
let result = 0;
if (name in vars) {
result = vars[name];
} else {
result = parseInt(name, 10);
if (isNaN(result)) {
console.warn('var not found', name);
return 0;
}
}
if (scale) {
return result * scale;
} else {
return result;
}
}
export type Point = {
x: number;
y: number;
};
/**
*
* svg path
* https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
*/
export function generateDefines(path: Path, vars: Var, prevPoint: Point[]) {
const defines = path.defines;
const paths: string[] = [];
const w = path.w;
const h = path.h;
let wScale = 1;
let hScale = 1;
if (w) {
wScale = vars['w'] / w;
}
if (h) {
hScale = vars['h'] / h;
}
for (const def of defines) {
switch (def.type) {
case 'moveTo': {
const pt = (def as MoveTo).pt;
const x = getVal(pt.x, vars, wScale);
const y = getVal(pt.y, vars, hScale);
paths.push(`M ${x} ${y}`);
prevPoint.push({x, y});
break;
}
case 'lnTo': {
const pt = (def as LnTo).pt;
const x = getVal(pt.x, vars, wScale);
const y = getVal(pt.y, vars, hScale);
paths.push(`L ${x} ${y}`);
prevPoint.push({x, y});
break;
}
case 'arcTo': {
const arc = def as ArcTo;
const wR = getVal(arc.wR, vars, wScale);
const hR = getVal(arc.hR, vars, hScale);
const stAng = getVal(arc.stAng, vars);
const swAng = getVal(arc.swAng, vars);
let prev = {
x: 0,
y: 0
};
if (prevPoint.length > 0) {
prev = prevPoint[prevPoint.length - 1];
}
const aPath = arcToPathA(wR, hR, stAng, swAng, prev.x, prev.y);
paths.push(aPath.path);
prevPoint.push({x: aPath.end.x, y: aPath.end.y});
break;
}
case 'quadBezTo': {
const quadBezTo = def as QuadBezTo;
if (quadBezTo.pts.length >= 2) {
const pt1 = quadBezTo.pts[0];
const pt2 = quadBezTo.pts[1];
const x1 = getVal(pt1.x, vars, wScale);
const y1 = getVal(pt1.y, vars, hScale);
const x2 = getVal(pt2.x, vars, wScale);
const y2 = getVal(pt2.y, vars, hScale);
paths.push(`Q ${x1},${y1} ${x2},${y2}`);
if (quadBezTo.pts.length > 2) {
const pt3 = quadBezTo.pts[2];
const x3 = getVal(pt3.x, vars, wScale);
const y3 = getVal(pt3.y, vars, hScale);
paths.push(`T ${x3},${y3}`);
prevPoint.push({x: x3, y: y3});
} else {
prevPoint.push({x: x2, y: y2});
}
} else {
console.warn('quadBezTo pts length must large than 2', def);
}
break;
}
case 'cubicBezTo': {
const cubicBezTo = def as QuadBezTo;
if (cubicBezTo.pts.length === 3) {
const pt1 = cubicBezTo.pts[0];
const pt2 = cubicBezTo.pts[1];
const pt3 = cubicBezTo.pts[2];
const x1 = getVal(pt1.x, vars, wScale);
const y1 = getVal(pt1.y, vars, hScale);
const x2 = getVal(pt2.x, vars, wScale);
const y2 = getVal(pt2.y, vars, hScale);
const x3 = getVal(pt3.x, vars, wScale);
const y3 = getVal(pt3.y, vars, hScale);
paths.push(`C ${x1},${y1} ${x2},${y2} ${x3},${y3}`);
prevPoint.push({x: x3, y: y3});
} else {
console.warn('cubicBezTo pts length must be 3', def);
}
break;
}
case 'close':
paths.push('Z');
break;
default:
break;
}
}
return paths.join(' ');
}

View File

@ -0,0 +1,55 @@
/**
* val20.1.10.56
*
* @param width
* @param height
*/
export function presetVal(w: number, h: number) {
const ss = Math.min(w, h);
const ssd2 = ss / 6;
const ssd6 = ss / 6;
const ssd8 = ss / 8;
const ssd32 = ss / 32;
const ssd16 = ss / 16;
return {
't': 0,
'3cd4': 16200000,
'3cd8': 8100000,
'5cd8': 13500000,
'7cd8': 18900000,
'b': h,
'cd2': 10800000,
'cd4': 5400000,
'cd8': 2700000,
h,
'hd2': h / 2,
'hd3': h / 3,
'hd4': h / 4,
'hd6': h / 6,
'hd8': h / 8,
'l': 0,
'ls': Math.max(w, h),
'r': w,
ss,
ssd2,
ssd6,
ssd8,
ssd16,
ssd32,
'hc': w / 2,
'vc': h / 2,
w,
'wd2': w / 2,
'wd3': w / 3,
'wd4': w / 4,
'wd6': w / 6,
'wd8': w / 8,
'wd10': w / 10,
'wd16': w / 16,
'wd32': w / 32
};
}

View File

@ -0,0 +1,123 @@
/**
* shape svg
*
* https://wiki.documentfoundation.org/Development/Improve_handles_of_DrawingML_shapes
*/
import {Color} from '../../../../util/color';
import {createSVGElement} from '../../../../util/dom';
import {WPSStyle} from '../../wps/WPSStyle';
import {Shape, ShapeGuide} from '../Shape';
import {ShapePr} from '../ShapeProperties';
import {evalFmla} from './formulas';
import {Point, Var, generateDefines} from './generateDefines';
import {presetVal} from './presetVal';
export function shapeToSVG(
shape: Shape,
avLst: ShapeGuide[],
shapePr: ShapePr,
width: number,
height: number,
wpsStyle?: WPSStyle
): SVGElement {
const svg = createSVGElement('svg');
// 边框有时候会超过
svg.setAttribute('style', 'overflow: visible');
svg.setAttribute('width', width.toString() + 'px');
svg.setAttribute('height', height.toString() + 'px');
// 变量值
const vars: Var = presetVal(width, height);
// 先执行 avLst 定义初始变量
for (const gd of shape.avLst || []) {
evalFmla(gd.n, gd.f, vars);
}
// 自定义 avLst
for (const gd of avLst) {
evalFmla(gd.n, gd.f, vars);
}
// 执行 gdLst
for (const gd of shape.gdLst || []) {
evalFmla(gd.n, gd.f, vars);
}
const outline = shapePr.outline;
const prevPoint: Point[] = [];
for (const path of shape.pathLst || []) {
const pathEl = createSVGElement('path');
const d = generateDefines(path, vars, prevPoint);
pathEl.setAttribute('d', d);
if (shapePr.fillColor) {
pathEl.setAttribute('fill', shapePr.fillColor);
} else if (wpsStyle && wpsStyle.fillColor) {
pathEl.setAttribute('fill', wpsStyle.fillColor);
} else {
pathEl.setAttribute('fill', 'none');
}
if (outline) {
if (outline.color) {
pathEl.setAttribute('stroke', outline.color);
}
if (outline.width) {
pathEl.setAttribute('stroke-width', outline.width);
}
} else if (wpsStyle && wpsStyle.lineColor) {
pathEl.setAttribute('stroke', wpsStyle.lineColor);
}
const fillColor = pathEl.getAttribute('fill');
if (fillColor && fillColor !== 'none') {
const color = new Color(fillColor);
const fillMode = path.fill;
let changeColor = '';
switch (fillMode) {
// 这些值目前是瞎编的,官方规范里没说
// http://webapp.docx4java.org/OnlineDemo/ecma376/DrawingML/ST_PathFillMode.html
case 'darken':
changeColor = color.lumOff(-0.5).toHex();
break;
case 'darkenLess':
changeColor = color.lumOff(-0.2).toHex();
break;
case 'lighten':
changeColor = color.lumOff(0.5).toHex();
break;
case 'lightenLess':
changeColor = color.lumOff(0.2).toHex();
break;
}
if (changeColor) {
pathEl.setAttribute('fill', changeColor);
}
}
if (path.fill === 'none') {
pathEl.setAttribute('fill', 'none');
}
// 如果没有 fill 也没有 stroke 就没法看了,所以设置个默认颜色
const strokeColor = pathEl.getAttribute('stroke');
if (!strokeColor && pathEl.getAttribute('fill') === 'none') {
pathEl.setAttribute('stroke', 'black');
}
if (path.stroke === false) {
pathEl.setAttribute('stroke', 'none');
if (!path.fill) {
pathEl.setAttribute('fill', 'none');
}
}
svg.appendChild(pathEl);
}
return svg;
}

View File

@ -11,7 +11,7 @@ import {parseTable} from '../../../parse/parseTable';
import {CSSStyle} from '../../../openxml/Style';
import {ST_TextVerticalType} from '../../../openxml/Types';
import {convertAngle} from '../../../parse/parseSize';
import {parseChildColor} from '../../../parse/parseChildColor';
import {WPSStyle} from './WPSStyle';
export type TxbxContentChild = Paragraph | Table;
@ -58,20 +58,9 @@ function parseBodyPr(element: Element, style: CSSStyle) {
}
}
function parseWpsStyle(word: Word, element: Element, style: CSSStyle) {
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
// 目前只支持这个
case 'a:fillRef':
style['background-color'] = parseChildColor(word, child);
break;
}
}
}
export class WPS {
spPr?: ShapePr;
wpsStyle?: WPSStyle;
txbxContent: TxbxContentChild[];
// 外层容器样式
style: CSSStyle = {};
@ -117,7 +106,7 @@ export class WPS {
case 'wps:style':
// http://webapp.docx4java.org/OnlineDemo/ecma376/DrawingML/style_1.html
parseWpsStyle(word, child, wps.style);
wps.wpsStyle = WPSStyle.fromXML(word, child);
break;
case 'wps:bodyPr':

View File

@ -0,0 +1,31 @@
/**
*
*/
import Word from '../../../Word';
import {parseChildColor} from '../../../parse/parseChildColor';
export class WPSStyle {
lineColor?: string;
fillColor?: string;
static fromXML(word: Word, element: Element) {
const wpsStyle = new WPSStyle();
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'a:fillRef':
wpsStyle.fillColor = parseChildColor(word, child);
break;
case 'a:lnRef':
wpsStyle.lineColor = parseChildColor(word, child);
break;
}
}
return wpsStyle;
}
}

View File

@ -0,0 +1,220 @@
/**
* shape
*/
import {getAttrBoolean} from '../OpenXML';
import {ST_PathFillMode} from '../openxml/Types';
import {
LnTo,
QuadBezTo,
CubicBezTo,
ArcTo,
MoveTo,
Path,
IPath,
PathPoint
} from '../openxml/word/drawing/Path';
import {Rect, Shape, ShapeGuide} from '../openxml/word/drawing/Shape';
export function parsePts(element: Element) {
const pts: PathPoint[] = [];
for (const child of element.children) {
const tagName = child.tagName;
if (tagName === 'a:pt' || tagName === 'pt') {
const x = child.getAttribute('x');
const y = child.getAttribute('y');
if (x && y) {
pts.push({x, y});
}
} else {
console.warn('unknown pt', tagName, child);
}
}
return pts;
}
// http://webapp.docx4java.org/OnlineDemo/ecma376/DrawingML/path_2.html
export function parsePath(element: Element) {
const pathChild: IPath[] = [];
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'a:moveTo':
case 'moveTo':
const moveToPt = parsePts(child);
if (moveToPt.length) {
const moveTo: MoveTo = {
type: 'moveTo',
pt: moveToPt[0]
};
pathChild.push(moveTo);
}
break;
case 'a:lnTo':
case 'lnTo':
const lnToPt = parsePts(child);
if (lnToPt.length) {
const lnTo: LnTo = {
type: 'lnTo',
pt: lnToPt[0]
};
pathChild.push(lnTo);
}
break;
case 'a:quadBezTo':
case 'quadBezTo':
const quadBezToPt = parsePts(child);
if (quadBezToPt.length) {
const quadBezTo: QuadBezTo = {
type: 'quadBezTo',
pts: quadBezToPt
};
pathChild.push(quadBezTo);
}
break;
case 'a:cubicBezTo':
case 'cubicBezTo':
const cubicBezToPt = parsePts(child);
if (cubicBezToPt.length) {
const cubicBezTo: CubicBezTo = {
type: 'cubicBezTo',
pts: cubicBezToPt
};
pathChild.push(cubicBezTo);
}
break;
case 'a:arcTo':
case 'arcTo':
const wR = child.getAttribute('wR');
const hR = child.getAttribute('hR');
const stAng = child.getAttribute('stAng');
const swAng = child.getAttribute('swAng');
if (wR && hR && stAng && swAng) {
const arcTo: ArcTo = {
type: 'arcTo',
wR,
hR,
stAng,
swAng
};
pathChild.push(arcTo);
}
break;
case 'a:close':
case 'close':
pathChild.push({
type: 'close'
});
break;
default:
console.warn('parsePath: unknown tag', tagName, child);
}
}
const path: Path = {defines: pathChild};
const fill = element.getAttribute('fill') as ST_PathFillMode;
if (fill) {
path.fill = fill;
}
path.extrusionOk = getAttrBoolean(element, 'extrusionOk', false);
path.stroke = getAttrBoolean(element, 'stroke', true);
const w = element.getAttribute('w');
if (w) {
path.w = parseInt(w, 10);
}
const h = element.getAttribute('h');
if (h) {
path.h = parseInt(h, 10);
}
return path;
}
export function parsePathLst(element: Element) {
const pathLst: Path[] = [];
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'a:path':
case 'path':
pathLst.push(parsePath(child));
break;
}
}
return pathLst;
}
export function parseShapeGuide(element: Element) {
const gds: ShapeGuide[] = [];
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'a:gd':
case 'gd':
const name = child.getAttribute('name');
const fmla = child.getAttribute('fmla');
if (name && fmla) {
const gd: ShapeGuide = {
n: name,
f: fmla
};
gds.push(gd);
}
break;
}
}
return gds;
}
export function parseShape(element: Element) {
const shape: Shape = {};
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'a:avLst':
case 'avLst':
shape.avLst = parseShapeGuide(child);
break;
case 'a:gdLst':
case 'gdLst':
shape.gdLst = parseShapeGuide(child);
break;
case 'a:rect':
case 'react':
const rect: Rect = {
b: child.getAttribute('b') || '',
l: child.getAttribute('l') || '',
r: child.getAttribute('r') || '',
t: child.getAttribute('t') || ''
};
shape.rect = rect;
break;
case 'a:pathLst':
case 'pathLst':
shape.pathLst = parsePathLst(child);
break;
}
}
return shape;
}

View File

@ -0,0 +1,18 @@
import {ShapePr} from '../openxml/word/drawing/ShapeProperties';
import {shapeToSVG} from '../openxml/word/drawing/svg/shapeToSVG';
import {WPSStyle} from '../openxml/word/wps/WPSStyle';
import {CustomGeom} from '../openxml/word/drawing/CustomGeom';
export function renderCustGeom(
geom: CustomGeom,
shapePr: ShapePr,
width: number,
height: number,
wpsStyle?: WPSStyle
) {
if (geom.shape) {
return shapeToSVG(geom.shape, [], shapePr, width, height, wpsStyle);
}
return null;
}

View File

@ -6,6 +6,8 @@ import {appendChild, applyStyle} from '../util/dom';
import renderParagraph from './renderParagraph';
import renderTable from './renderTable';
import {Table} from '../openxml/word/Table';
import {renderGeom} from './renderGeom';
import {renderCustGeom} from './renderCustGeom';
/**
*
@ -65,13 +67,29 @@ export function renderDrawing(word: Word, drawing: Drawing): HTMLElement {
const wps = drawing.wps;
const spPr = wps.spPr;
applyStyle(container, wps.style);
applyStyle(container, spPr?.style);
if (spPr?.xfrm) {
const ext = spPr.xfrm.ext;
if (ext) {
container.style.width = ext.cx;
container.style.height = ext.cy;
if (spPr.geom) {
const width = parseFloat(ext.cx.replace('px', ''));
const height = parseFloat(ext.cy.replace('px', ''));
appendChild(
container,
renderGeom(spPr.geom, spPr, width, height, wps.wpsStyle)
);
}
if (spPr.custGeom) {
const width = parseFloat(ext.cx.replace('px', ''));
const height = parseFloat(ext.cy.replace('px', ''));
appendChild(
container,
renderCustGeom(spPr.custGeom, spPr, width, height, wps.wpsStyle)
);
}
}
if (spPr.xfrm.rot) {
container.style.transform = `rotate(${spPr.xfrm.rot}deg)`;

View File

@ -0,0 +1,29 @@
import {presetShape} from '../openxml/word/drawing/presetShape';
import {Geom} from '../openxml/word/drawing/Geom';
import {ShapePr} from '../openxml/word/drawing/ShapeProperties';
import {shapeToSVG} from '../openxml/word/drawing/svg/shapeToSVG';
import {WPSStyle} from '../openxml/word/wps/WPSStyle';
export function renderGeom(
geom: Geom,
shapePr: ShapePr,
width: number,
height: number,
wpsStyle?: WPSStyle
) {
if (geom.prst) {
const shape = presetShape[geom.prst];
if (shape) {
return shapeToSVG(
shape,
geom.avLst || [],
shapePr,
width,
height,
wpsStyle
);
}
}
return null;
}

View File

@ -120,6 +120,7 @@ export class Color {
this.r = rgb.r;
this.g = rgb.g;
this.b = rgb.b;
return this;
}
/**
@ -134,6 +135,7 @@ export class Color {
this.r = rgb.r;
this.g = rgb.g;
this.b = rgb.b;
return this;
}
toHex() {

View File

@ -40,6 +40,13 @@ export function createElement(tagName: string): HTMLElement {
return document.createElement(tagName);
}
/**
* SVG
*/
export function createSVGElement(tagName: string): SVGElement {
return document.createElementNS('http://www.w3.org/2000/svg', tagName);
}
/**
*
*/

View File

@ -1 +1,5 @@
schema 文件来自 https://www.ecma-international.org/publications-and-standards/standards/ecma-376/
presetShapeDefinitions.xml 来自 https://github.com/LibreOffice/core/blob/master/oox/source/drawingml/customshapes/presetShapeDefinitions.xml
因为官方的有错误,漏了 upArrow

View File

@ -0,0 +1,51 @@
/**
* drawingml json
*/
import {readFileSync, writeFileSync} from 'fs';
import {parseXML} from '../src/util/xml';
import {Shape} from '../src/openxml/word/drawing/Shape';
import {parseShape} from '../src/parse/parseShape';
import jsdom from 'jsdom';
import prettier from 'prettier';
const {JSDOM} = jsdom;
const {DOMParser} = new JSDOM(``).window;
global.DOMParser = DOMParser;
const drawingML = parseXML(
readFileSync(
'./OfficeOpenXML-DrawingMLGeometries/presetShapeDefinitions.xml',
'utf-8'
)
);
let outputFile: string[] = [
'/** generated by tools/converDarwingML.ts, do not edit */',
`import {Shape} from './Shape';`
];
const shapeMap: {[key: string]: Shape} = {};
for (const shape of drawingML
.getElementsByTagName('presetShapeDefinitons')
.item(0)!.children) {
const shapeName = shape.tagName;
shapeMap[shapeName] = parseShape(shape);
}
outputFile.push(
'export const presetShape: Record<string, Shape> = ' +
JSON.stringify(shapeMap, null, 2) +
';'
);
prettier.resolveConfig('../../../.prettierrc').then(options => {
const formatted = prettier.format(outputFile.join('\n'), {
...options,
parser: 'typescript'
});
writeFileSync('../src/openxml/word/drawing/presetShape.ts', formatted);
});