@stll/folio-core 0.29.0 → 0.31.0

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 (103) hide show
  1. package/dist/controller/folioEditor.d.ts +28 -1
  2. package/dist/controller/folioEditor.js +37 -26
  3. package/dist/controller/fontReadiness.js +28 -24
  4. package/dist/controller/hiddenEditorManager.d.ts +2 -7
  5. package/dist/controller/layoutPipeline.js +33 -3
  6. package/dist/controller/noteEditorManager.d.ts +3 -6
  7. package/dist/docx/metafileSvg.d.ts +12 -0
  8. package/dist/docx/metafileSvg.js +462 -0
  9. package/dist/docx/paragraphTextBoxEnrichment.js +55 -4
  10. package/dist/docx/parser.d.ts +2 -2
  11. package/dist/docx/parser.js +26 -0
  12. package/dist/docx/runParser.js +2 -1
  13. package/dist/docx/serializer/runSerializer.js +14 -2
  14. package/dist/docx/server/createBilingualDocument.d.ts +20 -3
  15. package/dist/docx/server/createBilingualDocument.js +153 -22
  16. package/dist/docx/styleParser.js +2 -1
  17. package/dist/docx/textBoxParser.js +14 -0
  18. package/dist/docx/vmlImageParser.js +72 -152
  19. package/dist/docx/vmlPreview.d.ts +31 -0
  20. package/dist/docx/vmlPreview.js +535 -0
  21. package/dist/docx/wrapTypes.d.ts +2 -36
  22. package/dist/docx/wrapTypes.js +1 -39
  23. package/dist/fonts/fontAlternates.d.ts +8 -0
  24. package/dist/fonts/fontAlternates.js +16 -0
  25. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  26. package/dist/layout-bridge/convert/footnoteLayout.js +34 -3
  27. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  28. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -0
  29. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -0
  30. package/dist/layout-bridge/convert/toFlowBlocks.js +78 -28
  31. package/dist/layout-bridge/dom/noteStoryDom.d.ts +1 -1
  32. package/dist/layout-bridge/engine/selectionRects.js +3 -1
  33. package/dist/layout-engine/index.js +65 -32
  34. package/dist/layout-engine/justifiedLineFit.d.ts +7 -0
  35. package/dist/layout-engine/justifiedLineFit.js +6 -0
  36. package/dist/layout-engine/keep-together.js +15 -10
  37. package/dist/layout-engine/measure/complexScriptFormatting.d.ts +6 -1
  38. package/dist/layout-engine/measure/complexScriptFormatting.js +11 -2
  39. package/dist/layout-engine/measure/floatingTablePosition.d.ts +16 -1
  40. package/dist/layout-engine/measure/floatingTablePosition.js +29 -6
  41. package/dist/layout-engine/measure/lineBreakProvider.js +67 -8
  42. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  43. package/dist/layout-engine/measure/listMarkerWidth.js +23 -10
  44. package/dist/layout-engine/measure/measureBlocks.d.ts +15 -0
  45. package/dist/layout-engine/measure/measureBlocks.js +197 -30
  46. package/dist/layout-engine/measure/measureContainer.js +20 -24
  47. package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
  48. package/dist/layout-engine/measure/measureHelpers.js +14 -8
  49. package/dist/layout-engine/measure/measureParagraph.js +111 -37
  50. package/dist/layout-engine/measure/measureTypes.d.ts +3 -0
  51. package/dist/layout-engine/measure/tabCalculator.d.ts +123 -0
  52. package/dist/layout-engine/measure/tabCalculator.js +144 -0
  53. package/dist/layout-engine/measure/tableCellFlow.d.ts +21 -1
  54. package/dist/layout-engine/measure/tableCellFlow.js +45 -8
  55. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +10 -2
  56. package/dist/layout-engine/measure/tableInlinePlacement.js +17 -15
  57. package/dist/layout-engine/paginator.d.ts +26 -2
  58. package/dist/layout-engine/paginator.js +76 -14
  59. package/dist/layout-engine/paragraphSpacing.d.ts +3 -1
  60. package/dist/layout-engine/paragraphSpacing.js +4 -3
  61. package/dist/layout-engine/renderedBreakReconciliation.d.ts +14 -3
  62. package/dist/layout-engine/renderedBreakReconciliation.js +45 -14
  63. package/dist/layout-engine/section-breaks.d.ts +3 -1
  64. package/dist/layout-engine/section-breaks.js +6 -2
  65. package/dist/layout-engine/types.d.ts +18 -1
  66. package/dist/layout-engine/types.js +1 -1
  67. package/dist/layout-painter/imageLayout.d.ts +1 -1
  68. package/dist/layout-painter/renderPage.js +6 -1
  69. package/dist/layout-painter/renderParagraph.js +169 -82
  70. package/dist/layout-painter/renderTable.d.ts +1 -1
  71. package/dist/layout-painter/renderTable.js +18 -7
  72. package/dist/layout-painter/renderTextBox.d.ts +1 -1
  73. package/dist/layout-painter/renderTextBox.js +6 -1
  74. package/dist/model.d.ts +3 -3
  75. package/dist/model.js +2 -2
  76. package/dist/paged-layout/sectionBlockWidths.d.ts +14 -0
  77. package/dist/paged-layout/sectionBlockWidths.js +103 -14
  78. package/dist/prosemirror/attrs/index.js +12 -1
  79. package/dist/prosemirror/conversion/fromProseDoc.js +6 -2
  80. package/dist/prosemirror/conversion/toProseDoc.js +10 -3
  81. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +9 -5
  82. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -0
  83. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +6 -2
  84. package/dist/prosemirror/schema/nodes.d.ts +4 -0
  85. package/dist/prosemirror/utils/tabCalculator.d.ts +2 -123
  86. package/dist/prosemirror/utils/tabCalculator.js +1 -140
  87. package/dist/render-dom/RemoteSelectionOverlay.d.ts +4 -4
  88. package/dist/server.d.ts +2 -2
  89. package/dist/server.js +2 -2
  90. package/dist/types/documentEnumValues.d.ts +4 -1
  91. package/dist/types/documentEnumValues.js +12 -1
  92. package/dist/types/editor-story.d.ts +8 -0
  93. package/dist/types/editor-story.js +0 -0
  94. package/dist/types/remote-selection.d.ts +11 -0
  95. package/dist/types/remote-selection.js +0 -0
  96. package/dist/types/wrap.d.ts +36 -0
  97. package/dist/types/wrap.js +40 -0
  98. package/dist/utils/fontResolver.d.ts +1 -1
  99. package/dist/utils/fontResolver.js +42 -2
  100. package/dist/utils/formatToStyle.js +4 -2
  101. package/dist/utils/horizontalScale.d.ts +28 -0
  102. package/dist/utils/horizontalScale.js +42 -0
  103. package/package.json +2 -2
@@ -0,0 +1,462 @@
1
+ //#region src/docx/metafileSvg.ts
2
+ /**
3
+ * Render a deliberately small, browser-safe EMF subset to generated SVG.
4
+ *
5
+ * This covers vector logos built from filled paths. Unsupported records fail
6
+ * closed: a partial metafile is more misleading than the existing placeholder.
7
+ * Original EMF bytes remain the source of truth for DOCX round-trip.
8
+ */
9
+ const EMF_SIGNATURE = 1179469088;
10
+ const MAX_EMF_RECORDS = 1e4;
11
+ const MAX_EMF_POINTS = 2e4;
12
+ const MAX_EMF_DIMENSION = 1e5;
13
+ const MAX_EMF_SVG_CHARACTERS = 1e6;
14
+ const MAX_EMF_STATE_DEPTH = 256;
15
+ const EMR = {
16
+ header: 1,
17
+ setWindowExtEx: 9,
18
+ setWindowOrgEx: 10,
19
+ setViewportExtEx: 11,
20
+ setViewportOrgEx: 12,
21
+ eof: 14,
22
+ setMapMode: 17,
23
+ setBackgroundMode: 18,
24
+ setPolyFillMode: 19,
25
+ moveToEx: 27,
26
+ saveDc: 33,
27
+ restoreDc: 34,
28
+ selectObject: 37,
29
+ createBrushIndirect: 39,
30
+ deleteObject: 40,
31
+ beginPath: 59,
32
+ endPath: 60,
33
+ closeFigure: 61,
34
+ fillPath: 62,
35
+ polyBezierTo16: 88,
36
+ polyPolygon16: 91,
37
+ extCreatePen: 95
38
+ };
39
+ const MAP_MODE_TEXT = 1;
40
+ const MAP_MODE_ANISOTROPIC = 8;
41
+ const POLY_FILL_ALTERNATE = 1;
42
+ const POLY_FILL_WINDING = 2;
43
+ const STOCK_OBJECT_FLAG = 2147483648;
44
+ const initialState = () => ({
45
+ mapMode: MAP_MODE_TEXT,
46
+ windowOrigin: {
47
+ x: 0,
48
+ y: 0
49
+ },
50
+ windowExtent: {
51
+ x: 1,
52
+ y: 1
53
+ },
54
+ viewportOrigin: {
55
+ x: 0,
56
+ y: 0
57
+ },
58
+ viewportExtent: {
59
+ x: 1,
60
+ y: 1
61
+ },
62
+ fillRule: "nonzero",
63
+ brush: null,
64
+ pen: null,
65
+ currentPoint: {
66
+ x: 0,
67
+ y: 0
68
+ }
69
+ });
70
+ const cloneState = (state) => ({
71
+ ...state,
72
+ windowOrigin: { ...state.windowOrigin },
73
+ windowExtent: { ...state.windowExtent },
74
+ viewportOrigin: { ...state.viewportOrigin },
75
+ viewportExtent: { ...state.viewportExtent },
76
+ currentPoint: { ...state.currentPoint }
77
+ });
78
+ const int16 = (view, offset, end) => offset >= 0 && offset + 2 <= end ? view.getInt16(offset, true) : void 0;
79
+ const uint32 = (view, offset, end) => offset >= 0 && offset + 4 <= end ? view.getUint32(offset, true) : void 0;
80
+ const int32 = (view, offset, end) => offset >= 0 && offset + 4 <= end ? view.getInt32(offset, true) : void 0;
81
+ const numberText = (value) => {
82
+ const rounded = Math.round(value * 1e4) / 1e4;
83
+ return Object.is(rounded, -0) ? "0" : String(rounded);
84
+ };
85
+ const colorRefToCss = (color) => {
86
+ const red = color & 255;
87
+ const green = color >>> 8 & 255;
88
+ const blue = color >>> 16 & 255;
89
+ return `#${red.toString(16).padStart(2, "0")}${green.toString(16).padStart(2, "0")}${blue.toString(16).padStart(2, "0")}`;
90
+ };
91
+ const stockObject = (handle) => {
92
+ switch (handle) {
93
+ case 2147483648: return {
94
+ type: "brush",
95
+ fill: "#ffffff"
96
+ };
97
+ case 2147483649: return {
98
+ type: "brush",
99
+ fill: "#c0c0c0"
100
+ };
101
+ case 2147483650: return {
102
+ type: "brush",
103
+ fill: "#808080"
104
+ };
105
+ case 2147483651: return {
106
+ type: "brush",
107
+ fill: "#404040"
108
+ };
109
+ case 2147483652: return {
110
+ type: "brush",
111
+ fill: "#000000"
112
+ };
113
+ case 2147483653: return {
114
+ type: "brush",
115
+ fill: null
116
+ };
117
+ case 2147483656: return {
118
+ type: "pen",
119
+ stroke: null
120
+ };
121
+ default: return;
122
+ }
123
+ };
124
+ const mappedPoint = (state, bounds, x, y) => {
125
+ let mappedX;
126
+ let mappedY;
127
+ if (state.mapMode === MAP_MODE_TEXT) {
128
+ mappedX = x - state.windowOrigin.x + state.viewportOrigin.x - bounds.left;
129
+ mappedY = y - state.windowOrigin.y + state.viewportOrigin.y - bounds.top;
130
+ } else {
131
+ const { windowExtent, windowOrigin, viewportExtent, viewportOrigin } = state;
132
+ if (windowExtent.x === 0 || windowExtent.y === 0) return;
133
+ mappedX = (x - windowOrigin.x) * viewportExtent.x / windowExtent.x + viewportOrigin.x - bounds.left;
134
+ mappedY = (y - windowOrigin.y) * viewportExtent.y / windowExtent.y + viewportOrigin.y - bounds.top;
135
+ }
136
+ if (!Number.isFinite(mappedX) || !Number.isFinite(mappedY)) return;
137
+ if (Math.abs(mappedX) > MAX_EMF_DIMENSION * 100 || Math.abs(mappedY) > MAX_EMF_DIMENSION * 100) return;
138
+ return {
139
+ x: mappedX,
140
+ y: mappedY
141
+ };
142
+ };
143
+ const pathPoint = (command, point) => `${command}${numberText(point.x)} ${numberText(point.y)}`;
144
+ const selectGraphicsObject = (state, objects, handle) => {
145
+ const object = handle >= STOCK_OBJECT_FLAG ? stockObject(handle) : objects.get(handle);
146
+ if (!object) return false;
147
+ if (object.type === "brush") state.brush = object;
148
+ else state.pen = object;
149
+ return true;
150
+ };
151
+ /** Convert the supported filled-path EMF subset to inert generated SVG. */
152
+ function renderEmfSvg(data) {
153
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
154
+ if (bytes.byteLength < 88) return null;
155
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
156
+ const headerType = uint32(view, 0, bytes.byteLength);
157
+ const headerSize = uint32(view, 4, bytes.byteLength);
158
+ const signature = uint32(view, 40, bytes.byteLength);
159
+ const declaredBytes = uint32(view, 48, bytes.byteLength);
160
+ const declaredRecords = uint32(view, 52, bytes.byteLength);
161
+ const left = int32(view, 8, bytes.byteLength);
162
+ const top = int32(view, 12, bytes.byteLength);
163
+ const right = int32(view, 16, bytes.byteLength);
164
+ const bottom = int32(view, 20, bytes.byteLength);
165
+ if (headerType !== EMR.header || headerSize === void 0 || headerSize < 88 || headerSize % 4 !== 0 || headerSize > bytes.byteLength || signature !== EMF_SIGNATURE || declaredBytes !== bytes.byteLength || declaredRecords === void 0 || declaredRecords < 2 || declaredRecords > MAX_EMF_RECORDS || left === void 0 || top === void 0 || right === void 0 || bottom === void 0) return null;
166
+ const width = right - left;
167
+ const height = bottom - top;
168
+ if (width <= 0 || height <= 0 || width > MAX_EMF_DIMENSION || height > MAX_EMF_DIMENSION) return null;
169
+ const bounds = {
170
+ left,
171
+ top,
172
+ width,
173
+ height
174
+ };
175
+ const objects = /* @__PURE__ */ new Map();
176
+ const stack = [];
177
+ let state = initialState();
178
+ let offset = headerSize;
179
+ let recordCount = 1;
180
+ let pointCount = 0;
181
+ let pathState = { type: "idle" };
182
+ const renderedPaths = [];
183
+ let sawEof = false;
184
+ while (offset + 8 <= bytes.byteLength && recordCount < MAX_EMF_RECORDS) {
185
+ const type = uint32(view, offset, bytes.byteLength);
186
+ const size = uint32(view, offset + 4, bytes.byteLength);
187
+ if (type === void 0 || size === void 0 || size < 8 || size % 4 !== 0 || size > bytes.byteLength - offset) return null;
188
+ const end = offset + size;
189
+ recordCount += 1;
190
+ switch (type) {
191
+ case EMR.eof:
192
+ if (size !== 20 || end !== bytes.byteLength || recordCount !== declaredRecords) return null;
193
+ sawEof = true;
194
+ break;
195
+ case EMR.setMapMode: {
196
+ const mode = int32(view, offset + 8, end);
197
+ if (size !== 12 || mode !== MAP_MODE_TEXT && mode !== MAP_MODE_ANISOTROPIC) return null;
198
+ state.mapMode = mode;
199
+ break;
200
+ }
201
+ case EMR.setBackgroundMode:
202
+ if (size !== 12 || uint32(view, offset + 8, end) === void 0) return null;
203
+ break;
204
+ case EMR.setPolyFillMode: {
205
+ const mode = uint32(view, offset + 8, end);
206
+ if (size !== 12 || mode !== POLY_FILL_ALTERNATE && mode !== POLY_FILL_WINDING) return null;
207
+ state.fillRule = mode === POLY_FILL_ALTERNATE ? "evenodd" : "nonzero";
208
+ break;
209
+ }
210
+ case EMR.setWindowOrgEx:
211
+ case EMR.setWindowExtEx:
212
+ case EMR.setViewportOrgEx:
213
+ case EMR.setViewportExtEx: {
214
+ const x = int32(view, offset + 8, end);
215
+ const y = int32(view, offset + 12, end);
216
+ if (size !== 16 || x === void 0 || y === void 0) return null;
217
+ if (type === EMR.setWindowOrgEx) state.windowOrigin = {
218
+ x,
219
+ y
220
+ };
221
+ else if (type === EMR.setWindowExtEx) {
222
+ if (state.mapMode === MAP_MODE_ANISOTROPIC) {
223
+ if (x === 0 || y === 0) return null;
224
+ state.windowExtent = {
225
+ x,
226
+ y
227
+ };
228
+ }
229
+ } else if (type === EMR.setViewportOrgEx) state.viewportOrigin = {
230
+ x,
231
+ y
232
+ };
233
+ else if (state.mapMode === MAP_MODE_ANISOTROPIC) {
234
+ if (x === 0 || y === 0) return null;
235
+ state.viewportExtent = {
236
+ x,
237
+ y
238
+ };
239
+ }
240
+ break;
241
+ }
242
+ case EMR.saveDc:
243
+ if (size !== 8 || stack.length >= MAX_EMF_STATE_DEPTH) return null;
244
+ stack.push(cloneState(state));
245
+ break;
246
+ case EMR.restoreDc: {
247
+ const relative = int32(view, offset + 8, end);
248
+ if (size !== 12 || relative === void 0 || relative >= 0 || -relative > stack.length) return null;
249
+ let restored;
250
+ for (let index = 0; index < -relative; index += 1) restored = stack.pop();
251
+ if (!restored) return null;
252
+ state = restored;
253
+ break;
254
+ }
255
+ case EMR.createBrushIndirect: {
256
+ const handle = uint32(view, offset + 8, end);
257
+ const style = uint32(view, offset + 12, end);
258
+ const color = uint32(view, offset + 16, end);
259
+ if (size !== 24 || handle === void 0 || handle >= STOCK_OBJECT_FLAG || objects.has(handle) || style === void 0 || color === void 0) return null;
260
+ if (style !== 0 && style !== 1) return null;
261
+ objects.set(handle, {
262
+ type: "brush",
263
+ fill: style === 1 ? null : colorRefToCss(color)
264
+ });
265
+ break;
266
+ }
267
+ case EMR.extCreatePen: {
268
+ const handle = uint32(view, offset + 8, end);
269
+ const style = uint32(view, offset + 28, end);
270
+ const bmiBytes = uint32(view, offset + 16, end);
271
+ const bitmapBytes = uint32(view, offset + 24, end);
272
+ const styleEntries = uint32(view, offset + 48, end);
273
+ if (size !== 52 || handle === void 0 || handle >= STOCK_OBJECT_FLAG || objects.has(handle) || style === void 0 || bmiBytes !== 0 || bitmapBytes !== 0 || styleEntries !== 0 || (style & 15) !== 5) return null;
274
+ objects.set(handle, {
275
+ type: "pen",
276
+ stroke: null
277
+ });
278
+ break;
279
+ }
280
+ case EMR.selectObject: {
281
+ const handle = uint32(view, offset + 8, end);
282
+ if (size !== 12 || handle === void 0 || !selectGraphicsObject(state, objects, handle)) return null;
283
+ break;
284
+ }
285
+ case EMR.deleteObject: {
286
+ const handle = uint32(view, offset + 8, end);
287
+ if (size !== 12 || handle === void 0) return null;
288
+ const object = objects.get(handle);
289
+ if (object === state.brush) state.brush = null;
290
+ else if (object === state.pen) state.pen = null;
291
+ for (const savedState of stack) if (object === savedState.brush) savedState.brush = null;
292
+ else if (object === savedState.pen) savedState.pen = null;
293
+ objects.delete(handle);
294
+ break;
295
+ }
296
+ case EMR.beginPath:
297
+ if (size !== 8) return null;
298
+ switch (pathState.type) {
299
+ case "idle":
300
+ pathState = {
301
+ type: "building",
302
+ commands: []
303
+ };
304
+ break;
305
+ case "building":
306
+ case "completed": return null;
307
+ default:
308
+ }
309
+ break;
310
+ case EMR.moveToEx: {
311
+ const x = int32(view, offset + 8, end);
312
+ const y = int32(view, offset + 12, end);
313
+ if (size !== 16 || x === void 0 || y === void 0) return null;
314
+ state.currentPoint = {
315
+ x,
316
+ y
317
+ };
318
+ switch (pathState.type) {
319
+ case "idle":
320
+ case "completed": break;
321
+ case "building": {
322
+ const mapped = mappedPoint(state, bounds, x, y);
323
+ if (!mapped) return null;
324
+ pathState.commands.push(pathPoint("M", mapped));
325
+ break;
326
+ }
327
+ default:
328
+ }
329
+ break;
330
+ }
331
+ case EMR.polyBezierTo16: {
332
+ let commands;
333
+ switch (pathState.type) {
334
+ case "building":
335
+ commands = pathState.commands;
336
+ break;
337
+ case "idle":
338
+ case "completed": return null;
339
+ default: return null;
340
+ }
341
+ const count = uint32(view, offset + 24, end);
342
+ if (count === void 0 || count === 0 || count % 3 !== 0 || count > MAX_EMF_POINTS - pointCount || size !== 28 + count * 4) return null;
343
+ pointCount += count;
344
+ if (commands.length === 0) {
345
+ const mapped = mappedPoint(state, bounds, state.currentPoint.x, state.currentPoint.y);
346
+ if (!mapped) return null;
347
+ commands.push(pathPoint("M", mapped));
348
+ }
349
+ for (let index = 0; index < count; index += 3) {
350
+ const points = [];
351
+ for (let controlIndex = 0; controlIndex < 3; controlIndex += 1) {
352
+ const pointOffset = offset + 28 + (index + controlIndex) * 4;
353
+ const x = int16(view, pointOffset, end);
354
+ const y = int16(view, pointOffset + 2, end);
355
+ if (x === void 0 || y === void 0) return null;
356
+ const mapped = mappedPoint(state, bounds, x, y);
357
+ if (!mapped) return null;
358
+ points.push(mapped);
359
+ state.currentPoint = {
360
+ x,
361
+ y
362
+ };
363
+ }
364
+ const [first, second, third] = points;
365
+ if (!first || !second || !third) return null;
366
+ commands.push(`C${numberText(first.x)} ${numberText(first.y)} ${numberText(second.x)} ${numberText(second.y)} ${numberText(third.x)} ${numberText(third.y)}`);
367
+ }
368
+ break;
369
+ }
370
+ case EMR.polyPolygon16: {
371
+ const polygonCount = uint32(view, offset + 24, end);
372
+ const totalPoints = uint32(view, offset + 28, end);
373
+ if (polygonCount === void 0 || totalPoints === void 0 || polygonCount === 0 || totalPoints === 0 || polygonCount > totalPoints || totalPoints > MAX_EMF_POINTS - pointCount || polygonCount > Math.floor((end - (offset + 32)) / 4)) return null;
374
+ const pointsOffset = offset + 32 + polygonCount * 4;
375
+ if (size !== 32 + polygonCount * 4 + totalPoints * 4) return null;
376
+ pointCount += totalPoints;
377
+ let consumedPoints = 0;
378
+ const polygonPath = [];
379
+ for (let polygonIndex = 0; polygonIndex < polygonCount; polygonIndex += 1) {
380
+ const count = uint32(view, offset + 32 + polygonIndex * 4, end);
381
+ if (count === void 0 || count < 2 || count > totalPoints - consumedPoints) return null;
382
+ for (let index = 0; index < count; index += 1) {
383
+ const pointOffset = pointsOffset + (consumedPoints + index) * 4;
384
+ const x = int16(view, pointOffset, end);
385
+ const y = int16(view, pointOffset + 2, end);
386
+ if (x === void 0 || y === void 0) return null;
387
+ const mapped = mappedPoint(state, bounds, x, y);
388
+ if (!mapped) return null;
389
+ polygonPath.push(pathPoint(index === 0 ? "M" : "L", mapped));
390
+ }
391
+ polygonPath.push("Z");
392
+ consumedPoints += count;
393
+ }
394
+ if (consumedPoints !== totalPoints) return null;
395
+ switch (pathState.type) {
396
+ case "building":
397
+ pathState.commands.push(...polygonPath);
398
+ break;
399
+ case "idle":
400
+ case "completed":
401
+ if (!state.brush?.fill) return null;
402
+ renderedPaths.push(`<path d="${polygonPath.join(" ")}" fill="${state.brush.fill}" fill-rule="${state.fillRule}"/>`);
403
+ break;
404
+ default:
405
+ }
406
+ break;
407
+ }
408
+ case EMR.closeFigure:
409
+ if (size !== 8) return null;
410
+ switch (pathState.type) {
411
+ case "building":
412
+ pathState.commands.push("Z");
413
+ break;
414
+ case "idle":
415
+ case "completed": return null;
416
+ default:
417
+ }
418
+ break;
419
+ case EMR.endPath:
420
+ if (size !== 8) return null;
421
+ switch (pathState.type) {
422
+ case "building":
423
+ if (pathState.commands.length === 0) return null;
424
+ pathState = {
425
+ type: "completed",
426
+ commands: pathState.commands
427
+ };
428
+ break;
429
+ case "idle":
430
+ case "completed": return null;
431
+ default:
432
+ }
433
+ break;
434
+ case EMR.fillPath:
435
+ if (size !== 24 || !state.brush?.fill) return null;
436
+ switch (pathState.type) {
437
+ case "completed":
438
+ renderedPaths.push(`<path d="${pathState.commands.join(" ")}" fill="${state.brush.fill}" fill-rule="${state.fillRule}"/>`);
439
+ pathState = { type: "idle" };
440
+ break;
441
+ case "idle":
442
+ case "building": return null;
443
+ default:
444
+ }
445
+ break;
446
+ default: return null;
447
+ }
448
+ offset = end;
449
+ if (sawEof) break;
450
+ }
451
+ if (!sawEof || offset !== bytes.byteLength || recordCount !== declaredRecords || renderedPaths.length === 0) return null;
452
+ switch (pathState.type) {
453
+ case "idle": break;
454
+ case "building":
455
+ case "completed": return null;
456
+ default:
457
+ }
458
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${numberText(width)} ${numberText(height)}">${renderedPaths.join("")}</svg>`;
459
+ return svg.length <= MAX_EMF_SVG_CHARACTERS ? svg : null;
460
+ }
461
+ //#endregion
462
+ export { renderEmfSvg };
@@ -23,9 +23,18 @@ const VML_VERTICAL_RELATIVES = /* @__PURE__ */ new Set([
23
23
  "topMargin",
24
24
  "bottomMargin"
25
25
  ]);
26
+ const VML_TEXTBOX_INSET_DEFAULTS = [
27
+ "0.1in",
28
+ "0.05in",
29
+ "0.1in",
30
+ "0.05in"
31
+ ];
32
+ const MAX_VML_TEXTBOX_INSET_TOKEN_CHARACTERS = 64;
33
+ const MAX_VML_STYLE_CHARACTERS = 16384;
26
34
  const parseVmlStyle = (value) => {
27
35
  const declarations = {};
28
- for (const declaration of value?.split(";") ?? []) {
36
+ if (!value || value.length > MAX_VML_STYLE_CHARACTERS) return declarations;
37
+ for (const declaration of value.split(";")) {
29
38
  const separator = declaration.indexOf(":");
30
39
  if (separator < 0) continue;
31
40
  const key = declaration.slice(0, separator).trim().toLowerCase();
@@ -56,9 +65,39 @@ const verticalRelativeTo = (value) => {
56
65
  for (const relative of VML_VERTICAL_RELATIVES) if (relative.toLowerCase() === value?.toLowerCase()) return relative;
57
66
  return "paragraph";
58
67
  };
68
+ const parseVmlInsetFields = (rawInset) => {
69
+ const fields = [];
70
+ let offset = 0;
71
+ let segmentHasField = false;
72
+ while (offset <= rawInset.length) {
73
+ while (/\s/u.test(rawInset.charAt(offset))) offset += 1;
74
+ if (offset >= rawInset.length) {
75
+ if (!segmentHasField) fields.push(void 0);
76
+ return fields.length <= VML_TEXTBOX_INSET_DEFAULTS.length ? fields : void 0;
77
+ }
78
+ if (rawInset.charAt(offset) === ",") {
79
+ if (!segmentHasField) fields.push(void 0);
80
+ if (fields.length > VML_TEXTBOX_INSET_DEFAULTS.length) return;
81
+ segmentHasField = false;
82
+ offset += 1;
83
+ continue;
84
+ }
85
+ const tokenStart = offset;
86
+ while (offset < rawInset.length && !/[,\s]/u.test(rawInset.charAt(offset))) {
87
+ offset += 1;
88
+ if (offset - tokenStart > MAX_VML_TEXTBOX_INSET_TOKEN_CHARACTERS) return;
89
+ }
90
+ fields.push(rawInset.slice(tokenStart, offset));
91
+ if (fields.length > VML_TEXTBOX_INSET_DEFAULTS.length) return;
92
+ segmentHasField = true;
93
+ }
94
+ };
59
95
  const parseVmlInsets = (textBoxEl) => {
60
- const [left, top, right, bottom, extra] = getAttribute(textBoxEl, null, "inset")?.split(",").map((value) => vmlLengthToPixels(value)) ?? [];
61
- if (extra !== void 0 || left === void 0 || top === void 0 || right === void 0 || bottom === void 0) return;
96
+ const rawInset = getAttribute(textBoxEl, null, "inset") ?? "";
97
+ const fields = parseVmlInsetFields(rawInset);
98
+ if (!fields) return;
99
+ const [left, top, right, bottom] = VML_TEXTBOX_INSET_DEFAULTS.map((fallback, index) => vmlLengthToPixels(fields.at(index) ?? fallback));
100
+ if (left === void 0 || top === void 0 || right === void 0 || bottom === void 0) return;
62
101
  return {
63
102
  left: pixelsToEmu(left),
64
103
  top: pixelsToEmu(top),
@@ -67,6 +106,14 @@ const parseVmlInsets = (textBoxEl) => {
67
106
  };
68
107
  };
69
108
  const parseVmlFill = (shapeEl) => getAttribute(shapeEl, null, "filled")?.toLowerCase() === "f" ? { type: "none" } : void 0;
109
+ const parseVmlTextAnchor = (value) => {
110
+ switch (value?.toLowerCase()) {
111
+ case "top": return "top";
112
+ case "middle": return "middle";
113
+ case "bottom": return "bottom";
114
+ default: return;
115
+ }
116
+ };
70
117
  const vmlWrapType = (positioned, zIndex) => {
71
118
  if (!positioned) return "inline";
72
119
  if (Number.isFinite(zIndex) && zIndex < 0) return "behind";
@@ -159,6 +206,7 @@ const enrichTextBoxRuns = ({ content, xmlChildren, styles, theme, numbering, rel
159
206
  content: textBox.content,
160
207
  ...textBox.autoFit !== void 0 ? { autoFit: textBox.autoFit } : {},
161
208
  ...textBox.textWrap !== void 0 ? { textWrap: textBox.textWrap } : {},
209
+ ...textBox.verticalAlign !== void 0 ? { anchor: textBox.verticalAlign } : {},
162
210
  ...textBox.margins !== void 0 ? { margins: textBox.margins } : {}
163
211
  }
164
212
  };
@@ -258,6 +306,7 @@ const parseVmlTextBoxShape = (pictEl, styles, theme, numbering, rels, media, par
258
306
  const contentEl = textBoxEl ? findDeep(textBoxEl, "w", "txbxContent") : null;
259
307
  if (!shapeEl || !textBoxEl || !contentEl) return null;
260
308
  const style = parseVmlStyle(getAttribute(shapeEl, null, "style"));
309
+ const textBoxStyle = parseVmlStyle(getAttribute(textBoxEl, null, "style"));
261
310
  const width = vmlLengthToPixels(style["width"]);
262
311
  const height = vmlLengthToPixels(style["height"]);
263
312
  if (width === void 0 || height === void 0 || width <= 0 || height <= 0) return null;
@@ -266,6 +315,7 @@ const parseVmlTextBoxShape = (pictEl, styles, theme, numbering, rels, media, par
266
315
  const positioned = style["position"]?.toLowerCase() === "absolute";
267
316
  const zIndex = Number.parseInt(style["z-index"] ?? "", 10);
268
317
  const margins = parseVmlInsets(textBoxEl);
318
+ const anchor = parseVmlTextAnchor(textBoxStyle["v-text-anchor"]);
269
319
  const fill = parseVmlFill(shapeEl);
270
320
  const shape = {
271
321
  type: "shape",
@@ -278,7 +328,8 @@ const parseVmlTextBoxShape = (pictEl, styles, theme, numbering, rels, media, par
278
328
  wrap: { type: vmlWrapType(positioned, zIndex) },
279
329
  textBody: {
280
330
  content: parseTextBoxContent(contentEl, parseParagraph, parseTable, styles, theme, numbering, rels, media),
281
- ...margins === void 0 ? {} : { margins }
331
+ ...margins === void 0 ? {} : { margins },
332
+ ...anchor === void 0 ? {} : { anchor }
282
333
  }
283
334
  };
284
335
  const id = getAttribute(shapeEl, null, "id");
@@ -11,8 +11,8 @@ type ProgressCallback = (stage: string, percent: number) => void;
11
11
  * (EMF/WMF/TIFF) into a displayable `data:` or `blob:` URL. Receives the
12
12
  * parsed {@link MediaFile} (original bytes on `.data`); return the replacement
13
13
  * URL, or `null`/`undefined` to keep the built-in handling. Built-in handling
14
- * already extracts an embedded PNG/JPEG from EMF/WMF when one exists; this
15
- * hook is for vector-only metafiles where the host rasterizes server-side.
14
+ * extracts embedded PNG/JPEG previews and renders a bounded filled-path EMF
15
+ * subset; this hook covers remaining formats and unsupported vector records.
16
16
  */
17
17
  type MediaResolver = (file: document_d_exports.MediaFile) => Promise<string | null | undefined>;
18
18
  /**
@@ -14,6 +14,7 @@ import { parseFooter, parseHeader } from "./headerFooterParser.js";
14
14
  import { normalizeHeaderFooterReferences } from "./headerFooterReferenceNormalization.js";
15
15
  import { assignHeaderFooterVerbatimXml, refreshHeaderFooterVerbatimFingerprint } from "./headerFooterVerbatim.js";
16
16
  import { extractMetafileRaster, isMetafileMimeType } from "./metafileRaster.js";
17
+ import { renderEmfSvg } from "./metafileSvg.js";
17
18
  import { DocxModelValidationError, formatDocumentModelIssues, validateFolioDocumentModel } from "./modelValidation.js";
18
19
  import { parseNumbering } from "./numberingParser.js";
19
20
  import { normalizeNumberingReferences } from "./numberingReferenceNormalization.js";
@@ -24,6 +25,7 @@ import { parseStylesPackage } from "./styleParser.js";
24
25
  import { applyThemeFontLang, parseTheme } from "./themeParser.js";
25
26
  import { normalizeTrackedMoveRanges } from "./trackedMoveRangeNormalization.js";
26
27
  import { getMediaMimeType, mediaToDataUrl, unzipDocx } from "./unzip.js";
28
+ import { enforcePackageVmlPreviewBudget } from "./vmlPreview.js";
27
29
  import { TaggedError } from "better-result";
28
30
  //#region src/docx/parser.ts
29
31
  /**
@@ -45,6 +47,9 @@ import { TaggedError } from "better-result";
45
47
  * 10. Build media file map
46
48
  * 11. Assemble final Document
47
49
  */
50
+ const EMF_MIME_TYPE = "image/x-emf";
51
+ const SVG_MIME_TYPE = "image/svg+xml";
52
+ const MAX_PACKAGE_EMF_PREVIEW_BYTES = 8 * 1024 * 1024;
48
53
  /**
49
54
  * Parse a DOCX file into a complete Document model
50
55
  *
@@ -211,6 +216,7 @@ async function parseDocx(input, options = {}) {
211
216
  ...templateVariables !== void 0 ? { templateVariables } : {},
212
217
  ...requiredFonts.length > 0 ? { requiredFonts } : {}
213
218
  };
219
+ enforcePackageVmlPreviewBudget(document.package);
214
220
  const validation = validateFolioDocumentModel(document);
215
221
  const parsedCompleteModel = parseHeadersFooters && parseNotes;
216
222
  if (!validation.valid && parsedCompleteModel) throw new DocxModelValidationError("Parsed DOCX produced an invalid document model", validation.issues);
@@ -260,6 +266,7 @@ async function buildMediaMap(raw, rels) {
260
266
  const media = /* @__PURE__ */ new Map();
261
267
  const referenced = collectReferencedMediaPaths(raw, rels);
262
268
  let remainingTiffPixels = MAX_PACKAGE_TIFF_PIXELS;
269
+ let remainingEmfPreviewBytes = MAX_PACKAGE_EMF_PREVIEW_BYTES;
263
270
  for (const [path, data] of raw.media.entries()) {
264
271
  const filename = path.split("/").pop() || path;
265
272
  const mimeType = getMediaMimeType(path);
@@ -295,6 +302,25 @@ async function buildMediaMap(raw, rels) {
295
302
  if (normalizedPath !== path) media.set(normalizedPath, mediaFile);
296
303
  continue;
297
304
  }
305
+ const emfSvg = isReferenced && mimeType === EMF_MIME_TYPE && remainingEmfPreviewBytes > 0 ? renderEmfSvg(data) : null;
306
+ if (emfSvg) {
307
+ const svgBytes = new TextEncoder().encode(emfSvg);
308
+ if (svgBytes.byteLength <= remainingEmfPreviewBytes) {
309
+ remainingEmfPreviewBytes -= svgBytes.byteLength;
310
+ const mediaFile = {
311
+ path,
312
+ filename,
313
+ mimeType,
314
+ data,
315
+ dataUrl: mediaToDataUrl(copyBytesToArrayBuffer(svgBytes), SVG_MIME_TYPE)
316
+ };
317
+ media.set(path, mediaFile);
318
+ const normalizedPath = path.replace(/^word\//u, "");
319
+ if (normalizedPath !== path) media.set(normalizedPath, mediaFile);
320
+ continue;
321
+ }
322
+ remainingEmfPreviewBytes = 0;
323
+ }
298
324
  const mediaFile = {
299
325
  path,
300
326
  filename,
@@ -1,4 +1,5 @@
1
1
  import { isValidHexColor } from "../utils/colorResolver.js";
2
+ import { parseHorizontalScalePercent } from "../utils/horizontalScale.js";
2
3
  import { parseGroupDrawing } from "./groupDrawingParser.js";
3
4
  import { parseImage } from "./imageParser.js";
4
5
  import { EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, PositionalTabAlignmentSchema, PositionalTabLeaderSchema, PositionalTabRelativeToSchema, ShadingPatternSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
@@ -302,7 +303,7 @@ function parseRunProperties(rPr, theme, _styles) {
302
303
  }
303
304
  const w = propertyChildren.w;
304
305
  if (w) {
305
- const val = parseNumericAttribute(w, "w", "val");
306
+ const val = parseHorizontalScalePercent(getAttribute(w, "w", "val"));
306
307
  if (val !== void 0) formatting.scale = val;
307
308
  }
308
309
  const kern = propertyChildren.kern;