@veltra/desktop 1.7.13 → 1.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/components/batch-edit/batch-edit-form.js +9 -3
  2. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  3. package/dist/components/batch-edit/batch-edit-list.js +2 -1
  4. package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
  5. package/dist/components/batch-edit/batch-edit.js +57 -30
  6. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  7. package/dist/components/batch-edit/style.d.ts +1 -0
  8. package/dist/components/batch-edit/style.js +1 -0
  9. package/dist/components/batch-edit/style2.css +14 -0
  10. package/dist/components/batch-edit/use-dialog-effect.js +20 -0
  11. package/dist/components/batch-edit/use-dialog-effect.js.map +1 -0
  12. package/dist/components/batch-edit/use-edit-state.js +22 -3
  13. package/dist/components/batch-edit/use-edit-state.js.map +1 -1
  14. package/dist/components/batch-edit/use-handlers.js +5 -0
  15. package/dist/components/batch-edit/use-handlers.js.map +1 -1
  16. package/dist/components/batch-edit/use-shortcut-key.js +5 -2
  17. package/dist/components/batch-edit/use-shortcut-key.js.map +1 -1
  18. package/dist/components/file-viewer/file-viewer.js +21 -14
  19. package/dist/components/file-viewer/file-viewer.js.map +1 -1
  20. package/dist/components/file-viewer/helper.js +2 -0
  21. package/dist/components/file-viewer/helper.js.map +1 -1
  22. package/dist/components/file-viewer/previewers/ofd-previewer.js +196 -0
  23. package/dist/components/file-viewer/previewers/ofd-previewer.js.map +1 -0
  24. package/dist/components/file-viewer/style2.css +27 -0
  25. package/dist/components/table-editor/style2.css +23 -3
  26. package/dist/components/table-editor/table-editor.js +213 -63
  27. package/dist/components/table-editor/table-editor.js.map +1 -1
  28. package/dist/components/tip/style2.css +63 -0
  29. package/dist/components/tip/tip.js +6 -1
  30. package/dist/components/tip/tip.js.map +1 -1
  31. package/dist/index.d.ts +3 -3
  32. package/dist/packages/ofd-core/dist/color.js +69 -0
  33. package/dist/packages/ofd-core/dist/color.js.map +1 -0
  34. package/dist/packages/ofd-core/dist/ctm.js +55 -0
  35. package/dist/packages/ofd-core/dist/ctm.js.map +1 -0
  36. package/dist/packages/ofd-core/dist/error.js +14 -0
  37. package/dist/packages/ofd-core/dist/error.js.map +1 -0
  38. package/dist/packages/ofd-core/dist/font.js +384 -0
  39. package/dist/packages/ofd-core/dist/font.js.map +1 -0
  40. package/dist/packages/ofd-core/dist/index.js +6 -0
  41. package/dist/packages/ofd-core/dist/number.js +13 -0
  42. package/dist/packages/ofd-core/dist/number.js.map +1 -0
  43. package/dist/packages/ofd-core/dist/page-objects.js +181 -0
  44. package/dist/packages/ofd-core/dist/page-objects.js.map +1 -0
  45. package/dist/packages/ofd-core/dist/parse.js +65 -0
  46. package/dist/packages/ofd-core/dist/parse.js.map +1 -0
  47. package/dist/packages/ofd-core/dist/render.js +361 -0
  48. package/dist/packages/ofd-core/dist/render.js.map +1 -0
  49. package/dist/packages/ofd-core/dist/xml.js +199 -0
  50. package/dist/packages/ofd-core/dist/xml.js.map +1 -0
  51. package/dist/packages/ofd-core/dist/zip.js +93 -0
  52. package/dist/packages/ofd-core/dist/zip.js.map +1 -0
  53. package/dist/style.d.ts +1 -1
  54. package/dist/style.js +1 -1
  55. package/dist/types/batch-edit.d.ts +6 -0
  56. package/dist/types/file-viewer.d.ts +1 -1
  57. package/dist/types/index.d.ts +3 -3
  58. package/dist/types/table-editor.d.ts +28 -7
  59. package/dist/types/tip.d.ts +5 -1
  60. package/package.json +8 -7
  61. package/src/components/batch-edit/__test__/batch-edit.test.ts +191 -4
  62. package/src/components/batch-edit/batch-edit-form.vue +17 -3
  63. package/src/components/batch-edit/batch-edit-list.vue +2 -1
  64. package/src/components/batch-edit/batch-edit.vue +22 -4
  65. package/src/components/batch-edit/style.scss +20 -0
  66. package/src/components/batch-edit/style.ts +1 -0
  67. package/src/components/batch-edit/use-dialog-effect.ts +9 -3
  68. package/src/components/batch-edit/use-edit-state.ts +32 -4
  69. package/src/components/batch-edit/use-handlers.ts +10 -0
  70. package/src/components/batch-edit/use-shortcut-key.ts +6 -2
  71. package/src/components/file-viewer/__test__/helper.test.ts +13 -0
  72. package/src/components/file-viewer/file-viewer.vue +23 -16
  73. package/src/components/file-viewer/helper.ts +2 -0
  74. package/src/components/file-viewer/previewers/ofd-previewer.vue +216 -0
  75. package/src/components/file-viewer/style.scss +33 -0
  76. package/src/components/table-editor/__test__/table-editor.test.ts +471 -0
  77. package/src/components/table-editor/style.scss +42 -4
  78. package/src/components/table-editor/table-editor.vue +265 -38
  79. package/src/components/tip/style.scss +15 -0
  80. package/src/components/tip/tip.vue +1 -1
  81. package/src/types/batch-edit.ts +6 -0
  82. package/src/types/file-viewer.ts +1 -1
  83. package/src/types/table-editor.ts +29 -7
  84. package/src/types/tip.ts +6 -1
@@ -0,0 +1,181 @@
1
+ import { parseBoundary, parseMatrix, parseNumberList } from "./ctm.js";
2
+ import { parseOfdColor } from "./color.js";
3
+ import { childElements, childrenNamed, firstChildNamed, localNameOf, parseLayerType, parseXml, rootElementOf } from "./xml.js";
4
+ //#region ../ofd-core/dist/page-objects.js
5
+ /**
6
+ * Page.xml 内容对象模型:图层内的文本 / 图片 / 路径 / 复合对象。
7
+ *
8
+ * 与 P1 的 OfdPage(图层声明)分离:渲染按页懒执行,这里在渲染前按需解析,
9
+ * 使页与页相互独立、未渲染页不产生解析开销。
10
+ */
11
+ /** 页面内容文件根元素:普通页为 Page,模板页内容也有产出写作 TemplatePage */
12
+ const PAGE_ROOT_NAMES = ["Page", "TemplatePage"];
13
+ /** 解析页 / 模板页内容的图元对象;未知对象类型跳过 */
14
+ function parsePageContent(xml, source) {
15
+ const root = rootElementOf(parseXml(xml, source), PAGE_ROOT_NAMES, source);
16
+ const layerParent = firstChildNamed(root, "Content") ?? root;
17
+ const layers = [];
18
+ const objectsById = /* @__PURE__ */ new Map();
19
+ for (const layer of childrenNamed(layerParent, "Layer")) {
20
+ const objects = [];
21
+ collectLayerObjects(layer, objects, objectsById);
22
+ layers.push({
23
+ type: parseLayerType(layer.getAttribute("Type")),
24
+ drawParamId: layer.getAttribute("DrawParam"),
25
+ objects
26
+ });
27
+ }
28
+ return {
29
+ layers,
30
+ objectsById,
31
+ templateRefs: childrenNamed(root, "Template").map(parseTemplateRef)
32
+ };
33
+ }
34
+ /**
35
+ * 图层内对象收集:PageBlock 是纯分组容器(新版数电票正文层实证,自身无
36
+ * Boundary/CTM),递归扁平化不改变成员对象坐标。
37
+ */
38
+ function collectLayerObjects(container, out, objectsById) {
39
+ for (const element of childElements(container)) {
40
+ if (localNameOf(element) === "PageBlock") {
41
+ collectLayerObjects(element, out, objectsById);
42
+ continue;
43
+ }
44
+ const object = parsePageObject(element);
45
+ if (!object) continue;
46
+ if (object.id) objectsById.set(object.id, object);
47
+ out.push(object);
48
+ }
49
+ }
50
+ /** 解析 Template 引用:TemplateID 取声明 ID,ZOrder 缺省按背景叠加 */
51
+ function parseTemplateRef(element) {
52
+ const stackedForeground = element.getAttribute("ZOrder")?.toLowerCase() === "foreground";
53
+ return {
54
+ templateId: element.getAttribute("TemplateID"),
55
+ zOrder: stackedForeground ? "foreground" : "background"
56
+ };
57
+ }
58
+ function parsePageObject(element) {
59
+ const base = {
60
+ id: element.getAttribute("ID"),
61
+ boundary: parseBoundary(element.getAttribute("Boundary")),
62
+ ctm: parseMatrix(element.getAttribute("CTM")),
63
+ drawParamId: element.getAttribute("DrawParam")
64
+ };
65
+ switch (localNameOf(element)) {
66
+ case "TextObject": return {
67
+ ...base,
68
+ kind: "text",
69
+ fontId: element.getAttribute("FontID") ?? element.getAttribute("Font"),
70
+ fontSize: parseNumber(element.getAttribute("FontSize") ?? element.getAttribute("Size")),
71
+ fillColor: objectColor(element, "FillColor"),
72
+ codes: childrenNamed(element, "TextCode").map(parseTextCode),
73
+ glyphSubstitutions: childrenNamed(element, "CGTransform").map(parseGlyphSubstitution)
74
+ };
75
+ case "ImageObject": return {
76
+ ...base,
77
+ kind: "image",
78
+ resourceId: element.getAttribute("ResourceID")
79
+ };
80
+ case "PathObject": return {
81
+ ...base,
82
+ kind: "path",
83
+ fill: element.getAttribute("Fill") === "true",
84
+ fillColor: objectColor(element, "FillColor"),
85
+ strokeColor: objectColor(element, "StrokeColor"),
86
+ lineWidth: parseNumber(element.getAttribute("LineWidth")),
87
+ dashPattern: parseNumberList(element.getAttribute("DashPattern")),
88
+ commands: parsePathCommands(firstChildNamed(element, "AbbreviatedData")?.textContent ?? null)
89
+ };
90
+ case "CompositeObject": return {
91
+ ...base,
92
+ kind: "composite",
93
+ referenceId: element.getAttribute("ReferenceID")
94
+ };
95
+ default: return null;
96
+ }
97
+ }
98
+ /**
99
+ * 对象颜色声明兼容两种形式:属性 `FillColor="128 0 0"` 与子元素
100
+ * `<FillColor Value="128 0 0"/>`(新版数电票实证为子元素形式)。
101
+ */
102
+ function objectColor(element, name) {
103
+ return parseOfdColor(element.getAttribute(name) ?? firstChildNamed(element, name)?.getAttribute("Value") ?? null);
104
+ }
105
+ /** 解析 CGTransform:Glyphs 支持 ST_Array 的 `g n v` 压缩写法 */
106
+ function parseGlyphSubstitution(element) {
107
+ return {
108
+ codePosition: parseNumber(element.getAttribute("CodePosition")) ?? 0,
109
+ codeCount: parseNumber(element.getAttribute("CodeCount")) ?? 0,
110
+ glyphIds: parseDeltaList(firstChildNamed(element, "Glyphs")?.textContent ?? null)
111
+ };
112
+ }
113
+ function parseTextCode(code) {
114
+ return {
115
+ x: parseNumber(code.getAttribute("X")),
116
+ y: parseNumber(code.getAttribute("Y")),
117
+ deltaX: parseDeltaList(code.getAttribute("DeltaX")),
118
+ deltaY: parseDeltaList(code.getAttribute("DeltaY")),
119
+ text: code.textContent ?? ""
120
+ };
121
+ }
122
+ /**
123
+ * 解析 TextCode 位移数组:数值按序对应相邻字符位移;`g n v` 是 ST_Array 的
124
+ * 压缩写法(数电发票实证),表示 n 个连续位移均为 v。
125
+ */
126
+ function parseDeltaList(value) {
127
+ const tokens = value?.trim().split(/[\s,]+/).filter((token) => token !== "") ?? [];
128
+ const deltas = [];
129
+ for (let index = 0; index < tokens.length;) {
130
+ if (tokens[index] === "g") {
131
+ const count = Number(tokens[index + 1]);
132
+ const repeated = Number(tokens[index + 2]);
133
+ if (Number.isInteger(count) && count > 0 && Number.isFinite(repeated)) {
134
+ for (let repeat = 0; repeat < count; repeat++) deltas.push(repeated);
135
+ index += 3;
136
+ continue;
137
+ }
138
+ index += 1;
139
+ continue;
140
+ }
141
+ const delta = Number(tokens[index]);
142
+ if (Number.isFinite(delta)) deltas.push(delta);
143
+ index += 1;
144
+ }
145
+ return deltas;
146
+ }
147
+ const PATH_ARITY = {
148
+ M: 2,
149
+ L: 2,
150
+ C: 6,
151
+ B: 4,
152
+ Q: 4,
153
+ Z: 0
154
+ };
155
+ function parsePathCommands(data) {
156
+ const tokens = data?.trim().split(/[\s,]+/).filter((token) => token !== "") ?? [];
157
+ const commands = [];
158
+ for (let index = 0; index < tokens.length;) {
159
+ const type = tokens[index].toUpperCase();
160
+ const arity = PATH_ARITY[type];
161
+ if (arity === void 0) {
162
+ index += 1;
163
+ continue;
164
+ }
165
+ const points = tokens.slice(index + 1, index + 1 + arity).map(Number);
166
+ if (points.length === arity && points.every((point) => Number.isFinite(point))) commands.push({
167
+ type,
168
+ points
169
+ });
170
+ index += 1 + arity;
171
+ }
172
+ return commands;
173
+ }
174
+ function parseNumber(value) {
175
+ const numbers = parseNumberList(value);
176
+ return numbers.length > 0 ? numbers[0] : null;
177
+ }
178
+ //#endregion
179
+ export { parsePageContent };
180
+
181
+ //# sourceMappingURL=page-objects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-objects.js","names":[],"sources":["../../../../../ofd-core/dist/page-objects.js"],"sourcesContent":["import { parseBoundary, parseMatrix, parseNumberList } from \"./ctm.js\";\nimport { parseOfdColor } from \"./color.js\";\nimport { childElements, childrenNamed, firstChildNamed, localNameOf, parseLayerType, parseXml, rootElementOf } from \"./xml.js\";\n//#region src/page-objects.ts\n/**\n* Page.xml 内容对象模型:图层内的文本 / 图片 / 路径 / 复合对象。\n*\n* 与 P1 的 OfdPage(图层声明)分离:渲染按页懒执行,这里在渲染前按需解析,\n* 使页与页相互独立、未渲染页不产生解析开销。\n*/\n/** 页面内容文件根元素:普通页为 Page,模板页内容也有产出写作 TemplatePage */\nconst PAGE_ROOT_NAMES = [\"Page\", \"TemplatePage\"];\n/** 解析页 / 模板页内容的图元对象;未知对象类型跳过 */\nfunction parsePageContent(xml, source) {\n\tconst root = rootElementOf(parseXml(xml, source), PAGE_ROOT_NAMES, source);\n\tconst layerParent = firstChildNamed(root, \"Content\") ?? root;\n\tconst layers = [];\n\tconst objectsById = /* @__PURE__ */ new Map();\n\tfor (const layer of childrenNamed(layerParent, \"Layer\")) {\n\t\tconst objects = [];\n\t\tcollectLayerObjects(layer, objects, objectsById);\n\t\tlayers.push({\n\t\t\ttype: parseLayerType(layer.getAttribute(\"Type\")),\n\t\t\tdrawParamId: layer.getAttribute(\"DrawParam\"),\n\t\t\tobjects\n\t\t});\n\t}\n\treturn {\n\t\tlayers,\n\t\tobjectsById,\n\t\ttemplateRefs: childrenNamed(root, \"Template\").map(parseTemplateRef)\n\t};\n}\n/**\n* 图层内对象收集:PageBlock 是纯分组容器(新版数电票正文层实证,自身无\n* Boundary/CTM),递归扁平化不改变成员对象坐标。\n*/\nfunction collectLayerObjects(container, out, objectsById) {\n\tfor (const element of childElements(container)) {\n\t\tif (localNameOf(element) === \"PageBlock\") {\n\t\t\tcollectLayerObjects(element, out, objectsById);\n\t\t\tcontinue;\n\t\t}\n\t\tconst object = parsePageObject(element);\n\t\tif (!object) continue;\n\t\tif (object.id) objectsById.set(object.id, object);\n\t\tout.push(object);\n\t}\n}\n/** 解析 Template 引用:TemplateID 取声明 ID,ZOrder 缺省按背景叠加 */\nfunction parseTemplateRef(element) {\n\tconst stackedForeground = element.getAttribute(\"ZOrder\")?.toLowerCase() === \"foreground\";\n\treturn {\n\t\ttemplateId: element.getAttribute(\"TemplateID\"),\n\t\tzOrder: stackedForeground ? \"foreground\" : \"background\"\n\t};\n}\nfunction parsePageObject(element) {\n\tconst base = {\n\t\tid: element.getAttribute(\"ID\"),\n\t\tboundary: parseBoundary(element.getAttribute(\"Boundary\")),\n\t\tctm: parseMatrix(element.getAttribute(\"CTM\")),\n\t\tdrawParamId: element.getAttribute(\"DrawParam\")\n\t};\n\tswitch (localNameOf(element)) {\n\t\tcase \"TextObject\": return {\n\t\t\t...base,\n\t\t\tkind: \"text\",\n\t\t\tfontId: element.getAttribute(\"FontID\") ?? element.getAttribute(\"Font\"),\n\t\t\tfontSize: parseNumber(element.getAttribute(\"FontSize\") ?? element.getAttribute(\"Size\")),\n\t\t\tfillColor: objectColor(element, \"FillColor\"),\n\t\t\tcodes: childrenNamed(element, \"TextCode\").map(parseTextCode),\n\t\t\tglyphSubstitutions: childrenNamed(element, \"CGTransform\").map(parseGlyphSubstitution)\n\t\t};\n\t\tcase \"ImageObject\": return {\n\t\t\t...base,\n\t\t\tkind: \"image\",\n\t\t\tresourceId: element.getAttribute(\"ResourceID\")\n\t\t};\n\t\tcase \"PathObject\": return {\n\t\t\t...base,\n\t\t\tkind: \"path\",\n\t\t\tfill: element.getAttribute(\"Fill\") === \"true\",\n\t\t\tfillColor: objectColor(element, \"FillColor\"),\n\t\t\tstrokeColor: objectColor(element, \"StrokeColor\"),\n\t\t\tlineWidth: parseNumber(element.getAttribute(\"LineWidth\")),\n\t\t\tdashPattern: parseNumberList(element.getAttribute(\"DashPattern\")),\n\t\t\tcommands: parsePathCommands(firstChildNamed(element, \"AbbreviatedData\")?.textContent ?? null)\n\t\t};\n\t\tcase \"CompositeObject\": return {\n\t\t\t...base,\n\t\t\tkind: \"composite\",\n\t\t\treferenceId: element.getAttribute(\"ReferenceID\")\n\t\t};\n\t\tdefault: return null;\n\t}\n}\n/**\n* 对象颜色声明兼容两种形式:属性 `FillColor=\"128 0 0\"` 与子元素\n* `<FillColor Value=\"128 0 0\"/>`(新版数电票实证为子元素形式)。\n*/\nfunction objectColor(element, name) {\n\treturn parseOfdColor(element.getAttribute(name) ?? firstChildNamed(element, name)?.getAttribute(\"Value\") ?? null);\n}\n/** 解析 CGTransform:Glyphs 支持 ST_Array 的 `g n v` 压缩写法 */\nfunction parseGlyphSubstitution(element) {\n\treturn {\n\t\tcodePosition: parseNumber(element.getAttribute(\"CodePosition\")) ?? 0,\n\t\tcodeCount: parseNumber(element.getAttribute(\"CodeCount\")) ?? 0,\n\t\tglyphIds: parseDeltaList(firstChildNamed(element, \"Glyphs\")?.textContent ?? null)\n\t};\n}\nfunction parseTextCode(code) {\n\treturn {\n\t\tx: parseNumber(code.getAttribute(\"X\")),\n\t\ty: parseNumber(code.getAttribute(\"Y\")),\n\t\tdeltaX: parseDeltaList(code.getAttribute(\"DeltaX\")),\n\t\tdeltaY: parseDeltaList(code.getAttribute(\"DeltaY\")),\n\t\ttext: code.textContent ?? \"\"\n\t};\n}\n/**\n* 解析 TextCode 位移数组:数值按序对应相邻字符位移;`g n v` 是 ST_Array 的\n* 压缩写法(数电发票实证),表示 n 个连续位移均为 v。\n*/\nfunction parseDeltaList(value) {\n\tconst tokens = value?.trim().split(/[\\s,]+/).filter((token) => token !== \"\") ?? [];\n\tconst deltas = [];\n\tfor (let index = 0; index < tokens.length;) {\n\t\tif (tokens[index] === \"g\") {\n\t\t\tconst count = Number(tokens[index + 1]);\n\t\t\tconst repeated = Number(tokens[index + 2]);\n\t\t\tif (Number.isInteger(count) && count > 0 && Number.isFinite(repeated)) {\n\t\t\t\tfor (let repeat = 0; repeat < count; repeat++) deltas.push(repeated);\n\t\t\t\tindex += 3;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst delta = Number(tokens[index]);\n\t\tif (Number.isFinite(delta)) deltas.push(delta);\n\t\tindex += 1;\n\t}\n\treturn deltas;\n}\nconst PATH_ARITY = {\n\tM: 2,\n\tL: 2,\n\tC: 6,\n\tB: 4,\n\tQ: 4,\n\tZ: 0\n};\nfunction parsePathCommands(data) {\n\tconst tokens = data?.trim().split(/[\\s,]+/).filter((token) => token !== \"\") ?? [];\n\tconst commands = [];\n\tfor (let index = 0; index < tokens.length;) {\n\t\tconst type = tokens[index].toUpperCase();\n\t\tconst arity = PATH_ARITY[type];\n\t\tif (arity === void 0) {\n\t\t\tindex += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tconst points = tokens.slice(index + 1, index + 1 + arity).map(Number);\n\t\tif (points.length === arity && points.every((point) => Number.isFinite(point))) commands.push({\n\t\t\ttype,\n\t\t\tpoints\n\t\t});\n\t\tindex += 1 + arity;\n\t}\n\treturn commands;\n}\nfunction parseNumber(value) {\n\tconst numbers = parseNumberList(value);\n\treturn numbers.length > 0 ? numbers[0] : null;\n}\n//#endregion\nexport { parsePageContent };\n\n//# sourceMappingURL=page-objects.js.map"],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ import { OfdParseError } from "./error.js";
2
+ import { openOfdZip } from "./zip.js";
3
+ import { joinZipPath, parseDocumentRes, parseDocumentXml, parseOfdRoot, parsePageXml } from "./xml.js";
4
+ //#region ../ofd-core/dist/parse.js
5
+ /** 解析 OFD 文件字节:解包 ZIP 容器并解析全部文档的页面模型 */
6
+ async function parseOfd(data) {
7
+ return parseOfdContainer(openOfdZip(data));
8
+ }
9
+ /** 在已打开的容器上解析(复用同一个 OfdZip 读多个文档) */
10
+ async function parseOfdContainer(zip) {
11
+ if (!zip.has("OFD.xml")) throw new OfdParseError("missing-entry", "容器中缺少 OFD.xml,不是合法的 OFD 文件");
12
+ const bodies = parseOfdRoot(await zip.text("OFD.xml"));
13
+ if (bodies.length === 0) throw new OfdParseError("invalid-structure", "OFD.xml 中没有 DocBody");
14
+ return { docs: await Promise.all(bodies.map((body) => parseDoc(zip, body.docRoot, body.info))) };
15
+ }
16
+ async function parseDoc(zip, docRoot, info) {
17
+ const docLocation = joinZipPath(docRoot);
18
+ const dir = docLocation.includes("/") ? docLocation.slice(0, docLocation.lastIndexOf("/")) : "";
19
+ const documentModel = parseDocumentXml(await zip.text(docLocation), docLocation);
20
+ const [resources, pages] = await Promise.all([loadResources(zip, dir, documentModel.resLocation, documentModel.publicResLocation), Promise.all(documentModel.pageLocations.map((baseLoc, index) => loadPage(zip, dir, baseLoc, index)))]);
21
+ return {
22
+ dir,
23
+ info,
24
+ pageSize: documentModel.pageSize,
25
+ templates: documentModel.templates,
26
+ resources,
27
+ pages
28
+ };
29
+ }
30
+ /** 合并文档资源与公共资源声明;同 ID 时文档资源声明在前、查找优先 */
31
+ async function loadResources(zip, dir, resLocation, publicResLocation) {
32
+ const locations = [resLocation, publicResLocation].filter((location) => location !== null);
33
+ if (locations.length === 0) return {
34
+ fonts: [],
35
+ medias: [],
36
+ drawParams: []
37
+ };
38
+ const parts = await Promise.all(locations.map(async (location) => parseDocumentRes(await zip.text(joinZipPath(dir, location)), location)));
39
+ return {
40
+ fonts: parts.flatMap((part) => part.fonts),
41
+ medias: parts.flatMap((part) => part.medias),
42
+ drawParams: parts.flatMap((part) => part.drawParams)
43
+ };
44
+ }
45
+ async function loadPage(zip, dir, baseLoc, index) {
46
+ const pageLocation = resolveContentLocation(dir, baseLoc);
47
+ const pageModel = parsePageXml(await zip.text(pageLocation), pageLocation);
48
+ return {
49
+ index,
50
+ location: pageLocation,
51
+ size: pageModel.size,
52
+ layers: pageModel.layers
53
+ };
54
+ }
55
+ /**
56
+ * 页 / 模板页内容定位共用:BaseLoc 以 .xml 结尾时是内容文件(WPS / 数科 /
57
+ * 数电发票的页与模板页均写 Content.xml),否则按页目录处理(内含 Page.xml)。
58
+ */
59
+ function resolveContentLocation(dir, baseLoc) {
60
+ return /\.xml$/i.test(baseLoc) ? joinZipPath(dir, baseLoc) : joinZipPath(dir, baseLoc, "Page.xml");
61
+ }
62
+ //#endregion
63
+ export { joinZipPath, parseOfd, parseOfdContainer, resolveContentLocation };
64
+
65
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","names":[],"sources":["../../../../../ofd-core/dist/parse.js"],"sourcesContent":["import { OfdParseError } from \"./error.js\";\nimport { joinZipPath, parseDocumentRes, parseDocumentXml, parseOfdRoot, parsePageXml } from \"./xml.js\";\nimport { openOfdZip } from \"./zip.js\";\n//#region src/parse.ts\n/** 解析 OFD 文件字节:解包 ZIP 容器并解析全部文档的页面模型 */\nasync function parseOfd(data) {\n\treturn parseOfdContainer(openOfdZip(data));\n}\n/** 在已打开的容器上解析(复用同一个 OfdZip 读多个文档) */\nasync function parseOfdContainer(zip) {\n\tif (!zip.has(\"OFD.xml\")) throw new OfdParseError(\"missing-entry\", \"容器中缺少 OFD.xml,不是合法的 OFD 文件\");\n\tconst bodies = parseOfdRoot(await zip.text(\"OFD.xml\"));\n\tif (bodies.length === 0) throw new OfdParseError(\"invalid-structure\", \"OFD.xml 中没有 DocBody\");\n\treturn { docs: await Promise.all(bodies.map((body) => parseDoc(zip, body.docRoot, body.info))) };\n}\nasync function parseDoc(zip, docRoot, info) {\n\tconst docLocation = joinZipPath(docRoot);\n\tconst dir = docLocation.includes(\"/\") ? docLocation.slice(0, docLocation.lastIndexOf(\"/\")) : \"\";\n\tconst documentModel = parseDocumentXml(await zip.text(docLocation), docLocation);\n\tconst [resources, pages] = await Promise.all([loadResources(zip, dir, documentModel.resLocation, documentModel.publicResLocation), Promise.all(documentModel.pageLocations.map((baseLoc, index) => loadPage(zip, dir, baseLoc, index)))]);\n\treturn {\n\t\tdir,\n\t\tinfo,\n\t\tpageSize: documentModel.pageSize,\n\t\ttemplates: documentModel.templates,\n\t\tresources,\n\t\tpages\n\t};\n}\n/** 合并文档资源与公共资源声明;同 ID 时文档资源声明在前、查找优先 */\nasync function loadResources(zip, dir, resLocation, publicResLocation) {\n\tconst locations = [resLocation, publicResLocation].filter((location) => location !== null);\n\tif (locations.length === 0) return {\n\t\tfonts: [],\n\t\tmedias: [],\n\t\tdrawParams: []\n\t};\n\tconst parts = await Promise.all(locations.map(async (location) => parseDocumentRes(await zip.text(joinZipPath(dir, location)), location)));\n\treturn {\n\t\tfonts: parts.flatMap((part) => part.fonts),\n\t\tmedias: parts.flatMap((part) => part.medias),\n\t\tdrawParams: parts.flatMap((part) => part.drawParams)\n\t};\n}\nasync function loadPage(zip, dir, baseLoc, index) {\n\tconst pageLocation = resolveContentLocation(dir, baseLoc);\n\tconst pageModel = parsePageXml(await zip.text(pageLocation), pageLocation);\n\treturn {\n\t\tindex,\n\t\tlocation: pageLocation,\n\t\tsize: pageModel.size,\n\t\tlayers: pageModel.layers\n\t};\n}\n/**\n* 页 / 模板页内容定位共用:BaseLoc 以 .xml 结尾时是内容文件(WPS / 数科 /\n* 数电发票的页与模板页均写 Content.xml),否则按页目录处理(内含 Page.xml)。\n*/\nfunction resolveContentLocation(dir, baseLoc) {\n\treturn /\\.xml$/i.test(baseLoc) ? joinZipPath(dir, baseLoc) : joinZipPath(dir, baseLoc, \"Page.xml\");\n}\n//#endregion\nexport { joinZipPath, parseOfd, parseOfdContainer, resolveContentLocation };\n\n//# sourceMappingURL=parse.js.map"],"mappings":";;;;;AAKA,eAAe,SAAS,MAAM;CAC7B,OAAO,kBAAkB,WAAW,IAAI,CAAC;AAC1C;;AAEA,eAAe,kBAAkB,KAAK;CACrC,IAAI,CAAC,IAAI,IAAI,SAAS,GAAG,MAAM,IAAI,cAAc,iBAAiB,4BAA4B;CAC9F,MAAM,SAAS,aAAa,MAAM,IAAI,KAAK,SAAS,CAAC;CACrD,IAAI,OAAO,WAAW,GAAG,MAAM,IAAI,cAAc,qBAAqB,qBAAqB;CAC3F,OAAO,EAAE,MAAM,MAAM,QAAQ,IAAI,OAAO,KAAK,SAAS,SAAS,KAAK,KAAK,SAAS,KAAK,IAAI,CAAC,CAAC,EAAE;AAChG;AACA,eAAe,SAAS,KAAK,SAAS,MAAM;CAC3C,MAAM,cAAc,YAAY,OAAO;CACvC,MAAM,MAAM,YAAY,SAAS,GAAG,IAAI,YAAY,MAAM,GAAG,YAAY,YAAY,GAAG,CAAC,IAAI;CAC7F,MAAM,gBAAgB,iBAAiB,MAAM,IAAI,KAAK,WAAW,GAAG,WAAW;CAC/E,MAAM,CAAC,WAAW,SAAS,MAAM,QAAQ,IAAI,CAAC,cAAc,KAAK,KAAK,cAAc,aAAa,cAAc,iBAAiB,GAAG,QAAQ,IAAI,cAAc,cAAc,KAAK,SAAS,UAAU,SAAS,KAAK,KAAK,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC;CACxO,OAAO;EACN;EACA;EACA,UAAU,cAAc;EACxB,WAAW,cAAc;EACzB;EACA;CACD;AACD;;AAEA,eAAe,cAAc,KAAK,KAAK,aAAa,mBAAmB;CACtE,MAAM,YAAY,CAAC,aAAa,iBAAiB,CAAC,CAAC,QAAQ,aAAa,aAAa,IAAI;CACzF,IAAI,UAAU,WAAW,GAAG,OAAO;EAClC,OAAO,CAAC;EACR,QAAQ,CAAC;EACT,YAAY,CAAC;CACd;CACA,MAAM,QAAQ,MAAM,QAAQ,IAAI,UAAU,IAAI,OAAO,aAAa,iBAAiB,MAAM,IAAI,KAAK,YAAY,KAAK,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC;CACzI,OAAO;EACN,OAAO,MAAM,SAAS,SAAS,KAAK,KAAK;EACzC,QAAQ,MAAM,SAAS,SAAS,KAAK,MAAM;EAC3C,YAAY,MAAM,SAAS,SAAS,KAAK,UAAU;CACpD;AACD;AACA,eAAe,SAAS,KAAK,KAAK,SAAS,OAAO;CACjD,MAAM,eAAe,uBAAuB,KAAK,OAAO;CACxD,MAAM,YAAY,aAAa,MAAM,IAAI,KAAK,YAAY,GAAG,YAAY;CACzE,OAAO;EACN;EACA,UAAU;EACV,MAAM,UAAU;EAChB,QAAQ,UAAU;CACnB;AACD;;;;;AAKA,SAAS,uBAAuB,KAAK,SAAS;CAC7C,OAAO,UAAU,KAAK,OAAO,IAAI,YAAY,KAAK,OAAO,IAAI,YAAY,KAAK,SAAS,UAAU;AAClG"}
@@ -0,0 +1,361 @@
1
+ import { OfdParseError } from "./error.js";
2
+ import { IDENTITY_MATRIX, multiplyMatrix } from "./ctm.js";
3
+ import { joinZipPath } from "./xml.js";
4
+ import { resolveContentLocation } from "./parse.js";
5
+ import { formatNumber, roundTo } from "./number.js";
6
+ import { glyphPathD, parseEmbeddedFont } from "./font.js";
7
+ import { parsePageContent } from "./page-objects.js";
8
+ //#region ../ofd-core/dist/render.js
9
+ /**
10
+ * 逐页 SVG 输出(P2 渲染内核出口)。
11
+ *
12
+ * 页面物理尺寸按 OFD 毫米 → CSS 像素换算(96dpi),内容坐标系保持 OFD 原始
13
+ * 毫米单位(通过 viewBox 缩放),坐标与文档声明一一对应,便于断言与调试。
14
+ * 页与页相互独立、可单独调用,供上层做懒渲染 / 虚拟滚动。
15
+ */
16
+ /** CSS 约定 96px = 1in = 25.4mm */
17
+ const PX_PER_MM = 96 / 25.4;
18
+ /** 对象未声明填充色时的默认色(GB/T 33190 未声明时文字按黑色呈现,Fill 路径同) */
19
+ const DEFAULT_FILL = "#000";
20
+ /** 输出指定文档指定页的完整 SVG 字符串 */
21
+ async function pageToSvg(zip, container, documentIndex, pageIndex) {
22
+ const doc = container.docs[documentIndex];
23
+ if (!doc) throw new OfdParseError("out-of-range", `文档序号 ${documentIndex} 超出范围(容器共 ${container.docs.length} 个文档)`);
24
+ const page = doc.pages[pageIndex];
25
+ if (!page) throw new OfdParseError("out-of-range", `页序号 ${pageIndex} 超出范围(文档 ${documentIndex} 共 ${doc.pages.length} 页)`);
26
+ const content = parsePageContent(await zip.text(page.location), page.location);
27
+ const size = page.size ?? doc.pageSize ?? {
28
+ width: 210,
29
+ height: 297
30
+ };
31
+ const context = {
32
+ zip,
33
+ doc,
34
+ fonts: /* @__PURE__ */ new Map(),
35
+ templateContents: /* @__PURE__ */ new Map()
36
+ };
37
+ const renderedTemplates = await Promise.all(content.templateRefs.map((ref) => renderTemplateRef(ref, context, [content.objectsById], /* @__PURE__ */ new Set())));
38
+ const background = [];
39
+ const foreground = [];
40
+ const templateFrames = [];
41
+ for (const rendered of renderedTemplates) {
42
+ if (rendered.background !== "") background.push(rendered.background);
43
+ if (rendered.foreground !== "") foreground.push(rendered.foreground);
44
+ templateFrames.push(rendered.frames);
45
+ }
46
+ const pageFrames = [content.objectsById, ...templateFrames.flat()];
47
+ const layers = await Promise.all(content.layers.map((layer) => renderLayer(layer, context, pageFrames)));
48
+ return `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${formatNumber(roundTo(size.width * PX_PER_MM, 2))}px" height="${formatNumber(roundTo(size.height * PX_PER_MM, 2))}px" viewBox="0 0 ${formatNumber(size.width)} ${formatNumber(size.height)}">${background.join("")}${layers.join("")}${foreground.join("")}</svg>`;
49
+ }
50
+ /** 渲染单个图层:对象按声明顺序叠加,空图层不输出外壳 */
51
+ async function renderLayer(layer, context, frames) {
52
+ const rendered = (await Promise.all(layer.objects.map((object) => renderObject(object, IDENTITY_MATRIX, null, context, /* @__PURE__ */ new Set(), {
53
+ frames,
54
+ drawParamId: layer.drawParamId
55
+ })))).join("");
56
+ if (rendered === "") return "";
57
+ return `<g${layer.type === null ? "" : ` data-ofd-layer="${layer.type}"`}>${rendered}</g>`;
58
+ }
59
+ /**
60
+ * 按引用渲染一个模板页:自身引用的模板先于其内容(背景)或后于其内容(前景)
61
+ * 展开,与页面同规则;以内容文件路径判环,递归引用直接断开。
62
+ */
63
+ async function renderTemplateRef(ref, context, outerFrames, visiting) {
64
+ const baseLocation = (ref.templateId === null ? void 0 : context.doc.templates.find((template) => template.id === ref.templateId))?.location ?? null;
65
+ if (baseLocation === null) return {
66
+ background: "",
67
+ foreground: "",
68
+ frames: []
69
+ };
70
+ const location = resolveContentLocation(context.doc.dir, baseLocation);
71
+ if (visiting.has(location)) return {
72
+ background: "",
73
+ foreground: "",
74
+ frames: []
75
+ };
76
+ const content = await loadTemplateContent(context, location);
77
+ const frames = [content.objectsById, ...outerFrames];
78
+ const nextVisiting = new Set(visiting).add(location);
79
+ const innerRendered = await Promise.all(content.templateRefs.map((innerRef) => renderTemplateRef(innerRef, context, frames, nextVisiting)));
80
+ const innerBackground = [];
81
+ const innerForeground = [];
82
+ for (const rendered of innerRendered) {
83
+ innerBackground.push(rendered.background);
84
+ innerForeground.push(rendered.foreground);
85
+ }
86
+ const ownLayers = (await Promise.all(content.layers.map((layer) => renderLayer(layer, context, frames)))).join("");
87
+ const bundle = innerBackground.join("") + ownLayers + innerForeground.join("");
88
+ return ref.zOrder === "foreground" ? {
89
+ background: "",
90
+ foreground: bundle,
91
+ frames
92
+ } : {
93
+ background: bundle,
94
+ foreground: "",
95
+ frames
96
+ };
97
+ }
98
+ /** 加载并解析模板页内容;同一页内重复或递归引用复用解析结果 */
99
+ async function loadTemplateContent(context, location) {
100
+ const cached = context.templateContents.get(location);
101
+ if (cached) return cached;
102
+ const content = parsePageContent(await context.zip.text(location), location);
103
+ context.templateContents.set(location, content);
104
+ return content;
105
+ }
106
+ /** 渲染单个对象:CTM 链 + Boundary 定位外壳,内容坐标相对 Boundary 原点 */
107
+ async function renderObject(object, parentCtm, boundaryOverride, context, visiting, scope) {
108
+ if (object.id && visiting.has(object.id)) return "";
109
+ const ctm = object.ctm ? multiplyMatrix(parentCtm, object.ctm) : parentCtm;
110
+ if (object.kind === "composite") return renderComposite(object, ctm, context, visiting, scope);
111
+ const boundary = boundaryOverride ?? object.boundary;
112
+ const inner = await renderContent(object, boundary, ctm, context, scope);
113
+ if (inner === "") return "";
114
+ const transform = groupTransform(ctm, boundary);
115
+ return `<g${transform ? ` transform="${transform}"` : ""}>${inner}</g>`;
116
+ }
117
+ async function renderContent(object, boundary, ctm, context, scope) {
118
+ switch (object.kind) {
119
+ case "text": return renderText(object, context, scope);
120
+ case "image": return renderImage(object, boundary, ctm, context);
121
+ case "path": return renderPath(object, context, scope);
122
+ }
123
+ }
124
+ /** 复合对象:递归渲染被引用对象,CTM 链向下继承,循环引用直接断开 */
125
+ async function renderComposite(object, parentCtm, context, visiting, scope) {
126
+ const referenced = findReferencedObject(object, scope);
127
+ if (!referenced) return "";
128
+ const nextVisiting = new Set(visiting);
129
+ if (object.id) nextVisiting.add(object.id);
130
+ return renderObject(referenced, parentCtm, object.boundary, context, nextVisiting, scope);
131
+ }
132
+ /** 复合对象引用查找:当前内容域优先,未命中再沿外层内容域兜底 */
133
+ function findReferencedObject(object, scope) {
134
+ if (object.referenceId === null) return void 0;
135
+ for (const frame of scope.frames) {
136
+ const referenced = frame.get(object.referenceId);
137
+ if (referenced) return referenced;
138
+ }
139
+ }
140
+ /** 文本渲染:声明内嵌字体(Font@FontFile)时按 glyph 轮廓输出 path,否则回退系统字体 text */
141
+ async function renderText(object, context, scope) {
142
+ const draw = resolveDrawAttributes(object.drawParamId ?? scope.drawParamId, context);
143
+ const fill = object.fillColor ?? draw.fillColor ?? DEFAULT_FILL;
144
+ const font = await loadEmbeddedFont(object.fontId, context);
145
+ if (!font || object.fontSize === null) return renderSystemFontText(object, fill, context);
146
+ const fontSize = object.fontSize;
147
+ const parts = [];
148
+ let baseIndex = 0;
149
+ for (const code of object.codes) {
150
+ const part = textCodePathD(font, code, fontSize, object.glyphSubstitutions, baseIndex);
151
+ if (part !== "") parts.push(part);
152
+ baseIndex += code.text.length;
153
+ }
154
+ const d = parts.join(" ");
155
+ if (d === "") return object.codes.some((code) => code.text.length > 0) ? renderSystemFontText(object, fill, context) : "";
156
+ return `<path d="${d}" fill="${fill}"/>`;
157
+ }
158
+ /** 加载 TextObject@FontID 声明的内嵌 TTF;缺失或损坏返回 null(不阻断整页渲染) */
159
+ async function loadEmbeddedFont(fontId, context) {
160
+ if (fontId === null) return null;
161
+ const cached = context.fonts.get(fontId);
162
+ if (cached !== void 0) return cached;
163
+ const decl = context.doc.resources.fonts.find((font) => font.id === fontId);
164
+ let font = null;
165
+ if (decl?.fontFile) try {
166
+ font = parseEmbeddedFont(await context.zip.read(joinZipPath(context.doc.dir, decl.fontFile)), decl.fontFile);
167
+ } catch {
168
+ font = null;
169
+ }
170
+ context.fonts.set(fontId, font);
171
+ return font;
172
+ }
173
+ /**
174
+ * 单个 TextCode 的 glyph 路径:从 X/Y 基线原点起笔逐字排布。
175
+ * DeltaX/DeltaY 按标准是「相对前一个字符的位移」,声明时替代字形步进宽度。
176
+ * 字形来源优先取 CGTransform 替换序号(子集字体无 cmap 时的唯一映射),
177
+ * 其次 cmap 码点映射;两者都未命中时跳过该字符的轮廓、仅按位移推进。
178
+ */
179
+ function textCodePathD(font, code, fontSize, substitutions, baseIndex) {
180
+ const scale = fontSize / font.unitsPerEm;
181
+ const parts = [];
182
+ let penX = code.x ?? 0;
183
+ let penY = code.y ?? 0;
184
+ let dxIndex = 0;
185
+ let dyIndex = 0;
186
+ let charIndex = 0;
187
+ for (const char of code.text) {
188
+ const gid = substitutedGlyphId(substitutions, baseIndex + charIndex) ?? font.glyphIndexOf(char.codePointAt(0));
189
+ if (gid !== null) {
190
+ const d = glyphPathD(font, gid, fontSize, penX, penY);
191
+ if (d !== "") parts.push(d);
192
+ }
193
+ const deltaX = dxIndex < code.deltaX.length ? code.deltaX[dxIndex++] : null;
194
+ penX += deltaX ?? (gid !== null ? font.advanceWidth(gid) * scale : 0);
195
+ const deltaY = dyIndex < code.deltaY.length ? code.deltaY[dyIndex++] : null;
196
+ penY += deltaY ?? 0;
197
+ charIndex++;
198
+ }
199
+ return parts.join(" ");
200
+ }
201
+ /** 字形替换查找:字符落在声明区间时取 Glyphs 中对应序号,合字耗尽或未覆盖返回 null */
202
+ function substitutedGlyphId(substitutions, charIndex) {
203
+ for (const substitution of substitutions) if (charIndex >= substitution.codePosition && charIndex < substitution.codePosition + substitution.codeCount) {
204
+ const offset = charIndex - substitution.codePosition;
205
+ return offset < substitution.glyphIds.length ? substitution.glyphIds[offset] : null;
206
+ }
207
+ return null;
208
+ }
209
+ /** 无内嵌字体可用时的系统字体回退:保留 P2 的 text 输出与文字内容 */
210
+ function renderSystemFontText(object, fill, context) {
211
+ const fontFamily = resolveFontFamily(object.fontId, context);
212
+ return object.codes.map((code) => {
213
+ return `<text ${[
214
+ "xml:space=\"preserve\"",
215
+ codePositionAttr("x", code.x, code.text.length, code.deltaX),
216
+ codePositionAttr("y", code.y, code.text.length, code.deltaY),
217
+ attr("font-size", object.fontSize),
218
+ fontFamily === null ? null : `font-family="${escapeXml(fontFamily)}"`,
219
+ `fill="${fill}"`
220
+ ].filter((value) => value !== null).join(" ")}>${escapeXml(code.text)}</text>`;
221
+ }).join("");
222
+ }
223
+ /**
224
+ * TextCode 位移转 SVG 逐字符坐标:DeltaX/DeltaY 是相邻字符间的绝对步长
225
+ * (替代字形自然步进),而 SVG 的 dx/dy 是在自然步进上的附加量,语义不同,
226
+ * 这里换算成绝对坐标列表;位移未覆盖的字符交由自然排布。
227
+ */
228
+ function codePositionAttr(name, start, charCount, deltas) {
229
+ if (start === null && deltas.length === 0) return null;
230
+ const positions = [start ?? 0];
231
+ const covered = Math.min(charCount - 1, deltas.length);
232
+ for (let index = 0; index < covered; index++) positions.push(positions[index] + deltas[index]);
233
+ return positions.length === 1 ? `${name}="${formatNumber(positions[0])}"` : `${name}="${positions.map(formatNumber).join(" ")}"`;
234
+ }
235
+ function resolveFontFamily(fontId, context) {
236
+ if (!fontId) return null;
237
+ return context.doc.resources.fonts.find((font) => font.id === fontId)?.fontName ?? null;
238
+ }
239
+ /**
240
+ * 图片:解析 MultiMedia 声明并读资源字节,缺资源或不可识别的媒体类型跳过(不阻断整页)。
241
+ * Type 兼容标准单字母 'g' 与主流产出的 'Image' 拼写。
242
+ */
243
+ async function renderImage(object, boundary, ctm, context) {
244
+ const media = object.resourceId === null ? void 0 : context.doc.resources.medias.find((item) => item.id === object.resourceId);
245
+ const location = media?.location;
246
+ const type = media?.type?.toLowerCase();
247
+ if (!location || type !== void 0 && type !== "g" && type !== "image") return "";
248
+ let bytes;
249
+ try {
250
+ bytes = await context.zip.read(joinZipPath(context.doc.dir, location));
251
+ } catch {
252
+ return "";
253
+ }
254
+ const mime = sniffImageMime(bytes);
255
+ if (!mime) return "";
256
+ const content = boundary === null ? null : imageContentSize(boundary, ctm);
257
+ return `<image${content === null ? "" : ` width="${formatNumber(content.width)}" height="${formatNumber(content.height)}"`} href="data:${mime};base64,${toBase64(bytes)}"/>`;
258
+ }
259
+ /**
260
+ * 图片内容盒尺寸:无缩放 CTM 时 Boundary 即内容盒;带缩放 CTM 的产出
261
+ * (WPS / 新版数电发票实证)图片内容为 [0,1] 单位盒,尺寸与定位由 CTM 给出,
262
+ * Boundary 只提供平移原点(WPS 写最终包围盒、数电票二维码写整页盒,均不参与定尺寸)。
263
+ */
264
+ function imageContentSize(boundary, ctm) {
265
+ if (!hasScaleComponent(ctm)) return {
266
+ width: boundary.width,
267
+ height: boundary.height
268
+ };
269
+ return {
270
+ width: 1,
271
+ height: 1
272
+ };
273
+ }
274
+ /** 按魔数识别可渲染的图片媒体类型;多帧格式(GIF/APNG)由展示端取首帧 */
275
+ function sniffImageMime(bytes) {
276
+ if (bytes.length >= 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) return "image/png";
277
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
278
+ if (bytes.length >= 3 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) return "image/gif";
279
+ if (bytes.length >= 2 && bytes[0] === 66 && bytes[1] === 77) return "image/bmp";
280
+ if (bytes.length >= 12 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
281
+ return null;
282
+ }
283
+ function toBase64(bytes) {
284
+ let binary = "";
285
+ const chunkSize = 32768;
286
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
287
+ return btoa(binary);
288
+ }
289
+ /** 对象生效绘制参数:自身 FillColor/StrokeColor/LineWidth 声明优先于 DrawParam 继承链 */
290
+ function resolveDrawAttributes(drawParamId, context) {
291
+ const chain = [];
292
+ const visited = /* @__PURE__ */ new Set();
293
+ let current = drawParamId === null ? void 0 : findDrawParam(drawParamId, context);
294
+ while (current && !visited.has(current.id)) {
295
+ visited.add(current.id);
296
+ chain.unshift(current);
297
+ current = current.relative === null ? void 0 : findDrawParam(current.relative, context);
298
+ }
299
+ let fillColor = null;
300
+ let strokeColor = null;
301
+ let lineWidth = null;
302
+ for (const declaration of chain) {
303
+ fillColor ??= declaration.fillColor;
304
+ strokeColor ??= declaration.strokeColor;
305
+ lineWidth ??= declaration.lineWidth;
306
+ }
307
+ return {
308
+ fillColor,
309
+ strokeColor,
310
+ lineWidth
311
+ };
312
+ }
313
+ function findDrawParam(id, context) {
314
+ return context.doc.resources.drawParams.find((param) => param.id === id);
315
+ }
316
+ function renderPath(object, context, scope) {
317
+ const d = pathD(object.commands);
318
+ if (d === "") return "";
319
+ const draw = resolveDrawAttributes(object.drawParamId ?? scope.drawParamId, context);
320
+ const strokeColor = object.strokeColor ?? draw.strokeColor;
321
+ const fill = object.fillColor ?? draw.fillColor ?? (object.fill ? DEFAULT_FILL : "none");
322
+ return `<path ${[
323
+ `d="${d}"`,
324
+ `fill="${fill}"`,
325
+ strokeColor === null ? null : `stroke="${strokeColor}"`,
326
+ attr("stroke-width", object.lineWidth ?? draw.lineWidth),
327
+ listAttr("stroke-dasharray", object.dashPattern)
328
+ ].filter((value) => value !== null).join(" ")}/>`;
329
+ }
330
+ function pathD(commands) {
331
+ return commands.map((command) => command.type === "Z" ? "Z" : `${command.type === "B" ? "Q" : command.type} ${command.points.map(formatNumber).join(" ")}`).join(" ");
332
+ }
333
+ /** 对象外壳 transform:CTM 矩阵 + 平移到 Boundary 原点,单位矩阵不输出 */
334
+ function groupTransform(ctm, boundary) {
335
+ const matrix = isIdentityMatrix(ctm) ? null : `matrix(${ctm.map(formatNumber).join(" ")})`;
336
+ if (!boundary) return matrix ?? "";
337
+ const translate = `translate(${formatNumber(boundary.x)} ${formatNumber(boundary.y)})`;
338
+ if (hasScaleComponent(ctm)) return matrix ? `${translate} ${matrix}` : translate;
339
+ return matrix ? `${matrix} ${translate}` : translate;
340
+ }
341
+ /** CTM 是否带缩放分量(旋转/切变的轴长为 1,缩放会偏离 1) */
342
+ function hasScaleComponent(ctm) {
343
+ return Math.abs(Math.hypot(ctm[0], ctm[1]) - 1) > 1e-6 || Math.abs(Math.hypot(ctm[2], ctm[3]) - 1) > 1e-6;
344
+ }
345
+ function isIdentityMatrix(matrix) {
346
+ return matrix[0] === 1 && matrix[1] === 0 && matrix[2] === 0 && matrix[3] === 1 && matrix[4] === 0 && matrix[5] === 0;
347
+ }
348
+ function attr(name, value) {
349
+ if (value === null) return null;
350
+ return `${name}="${typeof value === "number" ? formatNumber(value) : escapeXml(value)}"`;
351
+ }
352
+ function listAttr(name, values) {
353
+ return values.length === 0 ? null : `${name}="${values.map(formatNumber).join(" ")}"`;
354
+ }
355
+ function escapeXml(text) {
356
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
357
+ }
358
+ //#endregion
359
+ export { pageToSvg };
360
+
361
+ //# sourceMappingURL=render.js.map