reamkit 1.31.0 → 1.32.1

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 (98) hide show
  1. package/README.md +10 -0
  2. package/dist/esm/core/bidi/index.d.ts +10 -0
  3. package/dist/esm/core/bidi/index.js +18 -2
  4. package/dist/esm/core/converter/facade.js +10 -5
  5. package/dist/esm/core/converter/project.js +1 -0
  6. package/dist/esm/core/converter/ream.js +17 -6
  7. package/dist/esm/core/converter/sheet-images.d.ts +17 -0
  8. package/dist/esm/core/converter/sheet-images.js +33 -0
  9. package/dist/esm/core/crypto/offcrypto.js +56 -12
  10. package/dist/esm/core/document-model/index.d.ts +1 -1
  11. package/dist/esm/core/document-model/types.d.ts +112 -0
  12. package/dist/esm/core/drawingml/chart-geometry.d.ts +36 -13
  13. package/dist/esm/core/drawingml/chart-geometry.js +372 -146
  14. package/dist/esm/core/drawingml/chart-parser.d.ts +19 -0
  15. package/dist/esm/core/drawingml/chart-parser.js +126 -18
  16. package/dist/esm/core/drawingml/colors.d.ts +6 -0
  17. package/dist/esm/core/drawingml/colors.js +23 -3
  18. package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
  19. package/dist/esm/core/drawingml/shape-render.js +4 -2
  20. package/dist/esm/core/font/measure.d.ts +9 -0
  21. package/dist/esm/core/font/measure.js +9 -1
  22. package/dist/esm/core/fonts/families.js +4 -2
  23. package/dist/esm/core/fonts/scripts.js +4 -2
  24. package/dist/esm/core/ir/flow.d.ts +6 -0
  25. package/dist/esm/core/ir/sheet.d.ts +43 -0
  26. package/dist/esm/core/number-format.d.ts +40 -15
  27. package/dist/esm/core/number-format.js +295 -83
  28. package/dist/esm/core/opc/tag-scan.d.ts +15 -0
  29. package/dist/esm/core/opc/tag-scan.js +41 -0
  30. package/dist/esm/core/opc/xml-entities.js +39 -2
  31. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  32. package/dist/esm/core/spreadsheet-model/types.d.ts +96 -0
  33. package/dist/esm/core/style-cascade/resolver.js +4 -2
  34. package/dist/esm/excel/column-bands.d.ts +39 -6
  35. package/dist/esm/excel/column-bands.js +41 -17
  36. package/dist/esm/excel/comments-parser.js +19 -0
  37. package/dist/esm/excel/conditional-format.d.ts +7 -3
  38. package/dist/esm/excel/conditional-format.js +77 -39
  39. package/dist/esm/excel/formula/context.d.ts +18 -0
  40. package/dist/esm/excel/formula/eval.js +51 -0
  41. package/dist/esm/excel/formula/functions.js +37 -14
  42. package/dist/esm/excel/formula/index.d.ts +2 -2
  43. package/dist/esm/excel/formula/lexer.d.ts +1 -1
  44. package/dist/esm/excel/formula/lexer.js +41 -0
  45. package/dist/esm/excel/formula/parser.d.ts +25 -0
  46. package/dist/esm/excel/formula/parser.js +95 -0
  47. package/dist/esm/excel/print-model.d.ts +38 -1
  48. package/dist/esm/excel/print-model.js +536 -83
  49. package/dist/esm/excel/shared-strings-parser.d.ts +9 -0
  50. package/dist/esm/excel/shared-strings-parser.js +11 -1
  51. package/dist/esm/excel/sheet-drawing.d.ts +38 -2
  52. package/dist/esm/excel/sheet-drawing.js +47 -13
  53. package/dist/esm/excel/sheet-shape-parser.d.ts +6 -2
  54. package/dist/esm/excel/sheet-shape-parser.js +18 -6
  55. package/dist/esm/excel/sheet-to-flow.d.ts +17 -0
  56. package/dist/esm/excel/sheet-to-flow.js +434 -47
  57. package/dist/esm/excel/styles-parser.d.ts +9 -0
  58. package/dist/esm/excel/styles-parser.js +80 -26
  59. package/dist/esm/excel/table-parser.js +5 -0
  60. package/dist/esm/excel/table-style-presets.d.ts +26 -0
  61. package/dist/esm/excel/table-style-presets.js +1555 -0
  62. package/dist/esm/excel/table-style.d.ts +13 -0
  63. package/dist/esm/excel/table-style.js +75 -0
  64. package/dist/esm/excel/tint.d.ts +10 -0
  65. package/dist/esm/excel/tint.js +75 -0
  66. package/dist/esm/excel/vml-drawing.d.ts +41 -3
  67. package/dist/esm/excel/vml-drawing.js +144 -11
  68. package/dist/esm/excel/worksheet-parser.js +89 -15
  69. package/dist/esm/excel/xls/biff-reader.js +10 -2
  70. package/dist/esm/excel/xlsx-reader.js +234 -19
  71. package/dist/esm/excel/xlsx-writer.js +8 -7
  72. package/dist/esm/html/html-writer.js +202 -20
  73. package/dist/esm/layout/fit-pages.d.ts +11 -0
  74. package/dist/esm/layout/fit-pages.js +86 -0
  75. package/dist/esm/layout/line-paint.d.ts +58 -0
  76. package/dist/esm/layout/line-paint.js +107 -0
  77. package/dist/esm/layout/page-doc.d.ts +51 -0
  78. package/dist/esm/layout/page-doc.js +141 -1
  79. package/dist/esm/layout/styled-layout.d.ts +7 -0
  80. package/dist/esm/layout/styled-layout.js +275 -52
  81. package/dist/esm/layout/turned-section.js +14 -2
  82. package/dist/esm/markdown/markdown-writer.js +75 -13
  83. package/dist/esm/pdf/styled-page-emitter.js +49 -71
  84. package/dist/esm/pdf/styled-page-renderer.js +1 -1
  85. package/dist/esm/pdf-reader/annot-draw.js +2 -2
  86. package/dist/esm/pdf-reader/annots.js +3 -1
  87. package/dist/esm/pdf-reader/font.js +4 -4
  88. package/dist/esm/pdf-reader/layout.js +1 -1
  89. package/dist/esm/svg/svg-text.d.ts +58 -0
  90. package/dist/esm/svg/svg-text.js +322 -0
  91. package/dist/esm/svg/svg-writer.js +135 -71
  92. package/dist/esm/word/document-parser.js +1 -1
  93. package/dist/esm/word/docx-reader.js +13 -8
  94. package/dist/esm/word/docx-to-pdf.js +3 -3
  95. package/dist/esm/word/docx-writer.js +11 -2
  96. package/dist/esm/word/font-table.js +89 -10
  97. package/dist/esm/word/numbering-parser.js +1 -1
  98. package/package.json +7 -3
@@ -0,0 +1,322 @@
1
+ import { svgPathData } from "../core/vector.js";
2
+ import { reverseByCodePoint } from "../core/bidi/index.js";
3
+ import { FAUX_ITALIC_SHEAR, computeJustifyExtra, lineVisualOrder } from "../layout/line-paint.js";
4
+ import { warpGlyphMatrix } from "../core/drawingml/text-warp.js";
5
+ import { sanitizeHref } from "../core/links.js";
6
+ import { cffOutlineSource, openTypeCff } from "../pdf-reader/cff-outline.js";
7
+ import { outlineSource } from "../pdf-reader/glyf-outline.js";
8
+ //#region src/svg/svg-text.ts
9
+ /** The colour of a comment range's highlight — the PDF emitter's. */
10
+ var HIGHLIGHT_HEX = "fff3a3";
11
+ /** The glyph outlines of a page's faces, each defined once and used by id. */
12
+ var SvgGlyphs = class {
13
+ sources = /* @__PURE__ */ new Map();
14
+ ids = /* @__PURE__ */ new Map();
15
+ defs = [];
16
+ faceIds = /* @__PURE__ */ new Map();
17
+ /**
18
+ * The id of the path that draws glyph `gid` of `font`, defining it on first
19
+ * use: `null` for a glyph with no ink (a space), `undefined` where the face
20
+ * carries no outlines this reads — the caller then sets the token as text.
21
+ *
22
+ * @param font The face.
23
+ * @param gid The glyph.
24
+ * @returns The `<path>` id, `null`, or `undefined`.
25
+ */
26
+ use(font, gid) {
27
+ const source = this.sourceOf(font);
28
+ if (!source) return void 0;
29
+ let face = this.faceIds.get(font);
30
+ if (face === void 0) {
31
+ face = this.faceIds.size;
32
+ this.faceIds.set(font, face);
33
+ }
34
+ const key = `g${String(face)}-${String(gid)}`;
35
+ const known = this.ids.get(key);
36
+ if (known !== void 0) return known;
37
+ const path = source.path(gid);
38
+ if (!path || path.length === 0) {
39
+ this.ids.set(key, null);
40
+ return null;
41
+ }
42
+ this.defs.push(`<path id="${key}" d="${svgPathData(path, fmtEm)}"/>`);
43
+ this.ids.set(key, key);
44
+ return key;
45
+ }
46
+ /** The `<defs>` holding every glyph used, or nothing when none was. */
47
+ defsXml() {
48
+ return this.defs.length > 0 ? `<defs>${this.defs.join("")}</defs>` : "";
49
+ }
50
+ sourceOf(font) {
51
+ let source = this.sources.get(font);
52
+ if (source === void 0) {
53
+ const raw = font.parsed.raw;
54
+ const cff = font.parsed.tables.has("CFF ") ? openTypeCff(raw) : void 0;
55
+ source = (cff ? cffOutlineSource(cff) : outlineSource(raw)) ?? null;
56
+ this.sources.set(font, source);
57
+ }
58
+ return source ?? void 0;
59
+ }
60
+ };
61
+ /**
62
+ * Draw one laid-out line: what stands behind its glyphs, then the glyphs, a
63
+ * picture or formula wherever one stands in it.
64
+ *
65
+ * @param out The SVG fragment sink.
66
+ * @param item The line, as the layout placed it.
67
+ * @param ctx The glyph definitions and the writer's own drawing.
68
+ */
69
+ function emitSvgLine(out, item, ctx) {
70
+ const clip = item.clip;
71
+ if (clip) {
72
+ const id = `clip${String(ctx.ids.n++)}`;
73
+ out.push(`<clipPath id="${id}"><rect x="${fmt(clip.x)}" y="${fmt(clip.y)}" width="${fmt(clip.width)}" height="${fmt(clip.height)}"/></clipPath>`, `<g clip-path="url(#${id})">`);
74
+ }
75
+ if (item.warp) emitWarped(out, item, ctx);
76
+ else if (item.rotationDeg) {
77
+ out.push(`<g transform="rotate(${fmt(-item.rotationDeg)} ${fmt(item.originX)} ${fmt(item.baselineY)})">`);
78
+ emitFlat(out, item, ctx);
79
+ out.push("</g>");
80
+ } else emitFlat(out, item, ctx);
81
+ if (clip) out.push("</g>");
82
+ }
83
+ /** A line set along its baseline, token by token. */
84
+ function emitFlat(out, item, ctx) {
85
+ const line = item.line;
86
+ const originX = item.originX;
87
+ const baseline = item.baselineY;
88
+ const extraPerSpace = computeJustifyExtra(line);
89
+ const advance = (tok) => tok.widthPt + (tok.kind === "text" && tok.isSpace ? extraPerSpace : 0);
90
+ const lineFs = line.maxFontSizePt || 12;
91
+ const boxAscent = Math.max(lineFs, line.mathAscentPt ?? 0);
92
+ const boxDescent = Math.max(lineFs * .2, line.mathDescentPt ?? 0);
93
+ let x = originX;
94
+ for (const tok of line.tokens) {
95
+ const w = advance(tok);
96
+ if (tok.kind === "text") {
97
+ const hex = tok.resolvedRun.shadingColorHex ?? (tok.highlight === true ? HIGHLIGHT_HEX : void 0);
98
+ if (hex !== void 0) out.push(`<rect x="${fmt(x)}" y="${fmt(baseline - boxAscent)}" width="${fmt(w)}" height="${fmt(boxAscent + boxDescent)}" fill="#${hex}"/>`);
99
+ }
100
+ x += w;
101
+ }
102
+ x = originX;
103
+ for (const tok of line.tokens) {
104
+ const w = advance(tok);
105
+ if (tok.kind === "text" && (tok.resolvedRun.underline !== "none" || tok.resolvedRun.strike)) emitDecoration(out, tok, x, w, baseline);
106
+ x += w;
107
+ }
108
+ const order = line.tokens.some((t) => t.bidiLevel % 2 === 1) ? lineVisualOrder(line) : line.tokens.map((_, i) => i);
109
+ x = originX;
110
+ for (const i of order) {
111
+ const tok = line.tokens[i];
112
+ if (tok.kind === "image") emitImageToken(out, tok, x, baseline, ctx);
113
+ else if (tok.kind === "math") emitMathToken(out, tok, x, baseline, ctx);
114
+ else emitTextToken(out, tok, x, baseline, ctx);
115
+ x += advance(tok);
116
+ }
117
+ const words = line.tokens.map((t) => t.kind === "text" ? t.text : "").join("");
118
+ if (words.trim().length > 0) out.push(`<text x="${fmt(originX)}" y="${fmt(baseline)}" font-size="${fmt(lineFs)}" textLength="${fmt(x - originX)}" lengthAdjust="spacingAndGlyphs" fill-opacity="0" xml:space="preserve">${escapeXml(words)}</text>`);
119
+ }
120
+ /**
121
+ * One run's glyphs from `x` along the baseline: the shaped glyphs of its text
122
+ * (reversed for a right-to-left run, as the PDF shows it), each at the advance
123
+ * the shaper gave it plus the run's letter spacing.
124
+ */
125
+ function emitTextToken(out, tok, x, baseline, ctx) {
126
+ if (tok.text.length === 0) return;
127
+ const size = tok.fontSizePt;
128
+ const scale = (tok.synthetic?.widthScale ?? 1) * (tok.resolvedRun.widthScale ?? 1);
129
+ const slant = tok.synthetic?.italic === true ? FAUX_ITALIC_SHEAR : 0;
130
+ const rise = tok.risePt ?? 0;
131
+ const text = tok.bidiLevel % 2 === 1 ? reverseByCodePoint(tok.text) : tok.text;
132
+ const run = tok.font.measure.glyphRun(text);
133
+ const upem = tok.font.parsed.unitsPerEm;
134
+ const spacingEm = (tok.resolvedRun.letterSpacingPt ?? 0) / (size * scale);
135
+ const uses = [];
136
+ let pen = 0;
137
+ for (let g = 0; g < run.gids.length; g++) {
138
+ const id = ctx.glyphs.use(tok.font, run.gids[g]);
139
+ if (id === void 0) {
140
+ emitAsText(out, tok, x, baseline);
141
+ return;
142
+ }
143
+ if (id !== null) uses.push(`<use href="#${id}"${pen !== 0 ? ` x="${fmtEm(pen)}"` : ""}/>`);
144
+ pen += (run.advances[g] ?? 0) / upem + spacingEm;
145
+ }
146
+ if (uses.length === 0) return;
147
+ const outline = tok.resolvedRun.textOutline;
148
+ const strokeWidth = outline !== void 0 ? outline.widthPt : tok.synthetic?.bold === true ? size * .03 : 0;
149
+ const stroke = strokeWidth > 0 ? ` stroke="#${outline?.colorHex ?? tok.resolvedRun.colorHex}" stroke-width="${fmtEm(strokeWidth / size)}"` : "";
150
+ const group = `<g transform="matrix(${fmt(size * scale)} 0 ${fmt(slant * size)} ${fmt(-size)} ${fmt(x)} ${fmt(baseline - rise)})" fill="#${tok.resolvedRun.colorHex}"${stroke}>${uses.join("")}</g>`;
151
+ const href = tok.href !== void 0 ? sanitizeHref(tok.href) : void 0;
152
+ out.push(href !== void 0 ? `<a href="${escapeXml(href)}">${group}</a>` : group);
153
+ }
154
+ /** A run of a face whose outlines cannot be read: its characters, for the viewer's fonts to set. */
155
+ function emitAsText(out, tok, x, baseline) {
156
+ if (tok.text.trim().length === 0) return;
157
+ out.push(`<text x="${fmt(x)}" y="${fmt(baseline - (tok.risePt ?? 0))}" font-family="sans-serif" font-size="${fmt(tok.fontSizePt)}" fill="#${tok.resolvedRun.colorHex}">${escapeXml(tok.text)}</text>`);
158
+ }
159
+ /** A run's underline and strikethrough, where the PDF emitter draws them. */
160
+ function emitDecoration(out, tok, x, w, baseline) {
161
+ const size = tok.fontSizePt;
162
+ const thickness = Math.max(.4, size * .06);
163
+ const style = tok.resolvedRun.underline;
164
+ if (style !== "none") {
165
+ const hex = tok.resolvedRun.underlineColorHex ?? tok.resolvedRun.colorHex;
166
+ const t = style === "thick" || style === "dottedHeavy" || style === "dashHeavy" ? thickness * 2 : thickness;
167
+ const top = baseline + size * .12;
168
+ const dashed = style === "dotted" || style === "dottedHeavy" ? [t, t * 2] : style === "dash" || style === "dashHeavy" ? [t * 4, t * 3] : void 0;
169
+ if (dashed) out.push(`<line x1="${fmt(x)}" y1="${fmt(top + t / 2)}" x2="${fmt(x + w)}" y2="${fmt(top + t / 2)}" stroke="#${hex}" stroke-width="${fmt(t)}" stroke-dasharray="${dashed.map(fmt).join(" ")}"/>`);
170
+ else {
171
+ out.push(`<rect x="${fmt(x)}" y="${fmt(top)}" width="${fmt(w)}" height="${fmt(t)}" fill="#${hex}"/>`);
172
+ if (style === "double") out.push(`<rect x="${fmt(x)}" y="${fmt(top + t * 2)}" width="${fmt(w)}" height="${fmt(t)}" fill="#${hex}"/>`);
173
+ }
174
+ }
175
+ if (tok.resolvedRun.strike) out.push(`<rect x="${fmt(x)}" y="${fmt(baseline - size * .26 - thickness)}" width="${fmt(w)}" height="${fmt(thickness)}" fill="#${tok.resolvedRun.colorHex}"/>`);
176
+ }
177
+ /**
178
+ * An inline picture standing on the baseline, its bottom edge on it — or a
179
+ * metafile, which draws itself from that corner in a frame of its own (y up).
180
+ */
181
+ function emitImageToken(out, tok, x, baseline, ctx) {
182
+ const b = tok.drawBox;
183
+ const left = x + (b?.dxPt ?? 0);
184
+ const bottom = baseline - (b?.dyPt ?? 0);
185
+ const width = b?.widthPt ?? tok.widthPt;
186
+ const height = b?.heightPt ?? tok.heightPt;
187
+ const meta = tok.metafile;
188
+ if (meta) {
189
+ for (const img of meta.images ?? []) {
190
+ if (img.resourceName === "") continue;
191
+ ctx.image(out, {
192
+ resourceName: img.resourceName,
193
+ x: left + img.x,
194
+ y: bottom - img.y - img.height,
195
+ width: img.width,
196
+ height: img.height,
197
+ ...img.rotationDeg !== void 0 ? { rotationDeg: img.rotationDeg } : {}
198
+ });
199
+ }
200
+ for (const sh of meta.shapes) ctx.shape(out, {
201
+ ...sh,
202
+ transform: [
203
+ 1,
204
+ 0,
205
+ 0,
206
+ -1,
207
+ left,
208
+ bottom
209
+ ]
210
+ });
211
+ for (const t of meta.texts) {
212
+ const first = t.line.tokens.find((k) => k.kind === "text");
213
+ if (first?.kind !== "text") continue;
214
+ emitTextToken(out, first, left + t.x, bottom - t.y, ctx);
215
+ }
216
+ return;
217
+ }
218
+ if (!tok.imageResourceName) return;
219
+ const shadow = tok.shadow;
220
+ if (shadow) {
221
+ const id = `shadow${String(ctx.ids.n++)}`;
222
+ const blur = shadow.blurPt > 0;
223
+ if (blur) out.push(`<filter id="${id}" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${fmt(shadow.blurPt / 2)}"/></filter>`);
224
+ out.push(`<rect x="${fmt(left + shadow.dxPt)}" y="${fmt(bottom - height + shadow.dyPt)}" width="${fmt(width)}" height="${fmt(height)}" fill="#${shadow.colorHex}" opacity="${fmt(shadow.alpha)}"${blur ? ` filter="url(#${id})"` : ""}/>`);
225
+ }
226
+ ctx.image(out, {
227
+ resourceName: tok.imageResourceName,
228
+ x: left,
229
+ y: bottom - height,
230
+ width,
231
+ height,
232
+ ...tok.crop ? { crop: tok.crop } : {},
233
+ ...tok.rotationDeg !== void 0 ? { rotationDeg: tok.rotationDeg } : {},
234
+ ...tok.flipH === true ? { flipH: true } : {},
235
+ ...tok.flipV === true ? { flipV: true } : {}
236
+ });
237
+ const frame = tok.outline;
238
+ if (frame) out.push(`<rect x="${fmt(left)}" y="${fmt(bottom - height)}" width="${fmt(width)}" height="${fmt(height)}" fill="none" stroke="#${frame.colorHex}" stroke-width="${fmt(frame.widthPt)}"/>`);
239
+ }
240
+ /** An inline formula: its glyphs, rules and paths, placed from its origin on the baseline (y up). */
241
+ function emitMathToken(out, tok, x, baseline, ctx) {
242
+ for (const it of tok.items) if (it.kind === "glyph") {
243
+ const run = it.font.measure.glyphRun(it.text);
244
+ const upem = it.font.parsed.unitsPerEm;
245
+ const uses = [];
246
+ let pen = 0;
247
+ for (let g = 0; g < run.gids.length; g++) {
248
+ const id = ctx.glyphs.use(it.font, run.gids[g]);
249
+ if (id) uses.push(`<use href="#${id}"${pen !== 0 ? ` x="${fmtEm(pen)}"` : ""}/>`);
250
+ pen += (run.advances[g] ?? 0) / upem;
251
+ }
252
+ if (uses.length > 0) out.push(`<g transform="matrix(${fmt(it.sizePt)} 0 0 ${fmt(-it.sizePt)} ${fmt(x + it.x)} ${fmt(baseline - it.y)})" fill="#000000">${uses.join("")}</g>`);
253
+ } else if (it.kind === "rule") out.push(`<rect x="${fmt(x + it.x)}" y="${fmt(baseline - it.y - it.h)}" width="${fmt(it.w)}" height="${fmt(it.h)}" fill="#000000"/>`);
254
+ else ctx.shape(out, {
255
+ paths: [{ segments: it.segments }],
256
+ ...it.fill === true ? { fillColorHex: "000000" } : {},
257
+ ...it.strokeWidthPt !== void 0 ? { stroke: {
258
+ colorHex: "000000",
259
+ widthPt: it.strokeWidthPt
260
+ } } : {},
261
+ transform: [
262
+ 1,
263
+ 0,
264
+ 0,
265
+ -1,
266
+ x,
267
+ baseline
268
+ ]
269
+ });
270
+ }
271
+ /**
272
+ * §20.1.9.10 — WordArt: every glyph at its own point on the warp's curve, each
273
+ * with its own matrix. The token's glyphs are rescaled to the width it was
274
+ * measured at, as the PDF does, so the block still ends where the layout said.
275
+ */
276
+ function emitWarped(out, item, ctx) {
277
+ const warp = item.warp;
278
+ let cursor = item.originX;
279
+ for (const tok of item.line.tokens) {
280
+ if (tok.kind !== "text" || tok.isSpace) {
281
+ cursor += tok.widthPt;
282
+ continue;
283
+ }
284
+ const size = tok.fontSizePt;
285
+ const chars = [...tok.text];
286
+ const raw = chars.map((c) => tok.font.measure.textWidthPt(c, size));
287
+ const total = raw.reduce((a, b) => a + b, 0);
288
+ const k = total > 0 ? tok.widthPt / total : 1;
289
+ for (let i = 0; i < chars.length; i++) {
290
+ const adv = raw[i] * k;
291
+ const m = warpGlyphMatrix(warp, cursor, adv, item.baselineY);
292
+ cursor += adv;
293
+ if (!m) continue;
294
+ const run = tok.font.measure.glyphRun(chars[i]);
295
+ const upem = tok.font.parsed.unitsPerEm;
296
+ const uses = [];
297
+ let pen = 0;
298
+ for (let g = 0; g < run.gids.length; g++) {
299
+ const id = ctx.glyphs.use(tok.font, run.gids[g]);
300
+ if (id) uses.push(`<use href="#${id}"${pen !== 0 ? ` x="${fmtEm(pen)}"` : ""}/>`);
301
+ pen += (run.advances[g] ?? 0) / upem;
302
+ }
303
+ if (uses.length === 0) continue;
304
+ out.push(`<g transform="matrix(${fmt(m[0] * size)} ${fmt(m[1] * size)} ${fmt(m[2] * size)} ${fmt(m[3] * size)} ${fmt(m[4])} ${fmt(m[5])})" fill="#${tok.resolvedRun.colorHex}">${uses.join("")}</g>`);
305
+ }
306
+ }
307
+ }
308
+ function escapeXml(s) {
309
+ return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
310
+ }
311
+ /** A page coordinate, to the hundredth of a point. */
312
+ function fmt(n) {
313
+ const r = Math.round(n * 100) / 100;
314
+ return Object.is(r, -0) ? "0" : String(r);
315
+ }
316
+ /** A coordinate in a one-unit em, to a ten-thousandth of it. */
317
+ function fmtEm(n) {
318
+ const r = Math.round(n * 1e4) / 1e4;
319
+ return Object.is(r, -0) ? "0" : String(r);
320
+ }
321
+ //#endregion
322
+ export { SvgGlyphs, emitSvgLine };
@@ -1,8 +1,10 @@
1
+ import { pageLayers, paintPlan } from "../layout/page-doc.js";
1
2
  import { FEATURES } from "../core/ir/features.js";
2
3
  import { svgPathData } from "../core/vector.js";
3
4
  import { gradientSvgDef } from "../core/drawingml/shape-render.js";
4
- import { paintPlan } from "../layout/page-doc.js";
5
+ import { BORDER_DASHES, washVeil } from "../layout/line-paint.js";
5
6
  import { toBase64 } from "../core/bytes.js";
7
+ import { SvgGlyphs, emitSvgLine } from "./svg-text.js";
6
8
  //#region src/svg/svg-writer.ts
7
9
  var PAGE_GAP = 12;
8
10
  /**
@@ -23,17 +25,25 @@ function writeSvg(laid, opts = {}) {
23
25
  const width = Math.max(1, ...laid.pages.map((p) => p.width));
24
26
  const height = laid.pages.reduce((s, p) => s + p.height, 0) + gap * Math.max(0, laid.pages.length - 1);
25
27
  const parts = [];
26
- parts.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${fmt(width)}" height="${fmt(height)}" viewBox="0 0 ${fmt(width)} ${fmt(height)}">`);
28
+ const ids = { n: 0 };
29
+ const glyphs = new SvgGlyphs();
30
+ const ctx = {
31
+ glyphs,
32
+ ids,
33
+ laid,
34
+ image: (out, placed) => emitPlaced(out, placed, laid, ids),
35
+ shape: (out, shape) => emitShape(out, shape, ids)
36
+ };
27
37
  let yOffset = 0;
28
- const idc = { n: 0 };
29
38
  laid.pages.forEach((page, i) => {
30
39
  parts.push(`<g transform="translate(0 ${fmt(yOffset)})" data-page="${i + 1}">`);
31
40
  parts.push(`<rect x="0" y="0" width="${fmt(page.width)}" height="${fmt(page.height)}" fill="#ffffff" stroke="#cccccc"/>`);
32
- emitPage(parts, page, laid, losses, idc);
41
+ emitPage(parts, page, ctx);
33
42
  parts.push("</g>");
34
43
  yOffset += page.height + gap;
35
44
  });
36
45
  parts.push("</svg>");
46
+ parts.unshift(`<svg xmlns="http://www.w3.org/2000/svg" width="${fmt(width)}" height="${fmt(height)}" viewBox="0 0 ${fmt(width)} ${fmt(height)}">`, glyphs.defsXml());
37
47
  return {
38
48
  bytes: new TextEncoder().encode(parts.join("\n")),
39
49
  losses
@@ -54,11 +64,24 @@ var svgWriter = {
54
64
  ]),
55
65
  write: (doc, opts) => writeSvg(doc, opts ?? {})
56
66
  };
57
- function emitPage(out, page, laid, losses, idc) {
58
- const plan = paintPlan(page.commands);
59
- for (const item of plan.behind) emitPageItem(out, item, laid, losses, idc);
67
+ function emitPage(out, page, ctx) {
68
+ for (const layer of pageLayers(page.commands)) {
69
+ const w = layer.window;
70
+ if (!w) {
71
+ emitLayer(out, layer.items, ctx);
72
+ continue;
73
+ }
74
+ const id = `win${String(ctx.ids.n++)}`;
75
+ out.push(`<clipPath id="${id}"><rect x="${fmt(w.x)}" y="${fmt(w.y)}" width="${fmt(w.width)}" height="${fmt(w.height)}"/></clipPath>`, `<g clip-path="url(#${id})">`);
76
+ emitLayer(out, layer.items, ctx);
77
+ out.push("</g>");
78
+ }
79
+ }
80
+ function emitLayer(out, commands, ctx) {
81
+ const plan = paintPlan(commands);
82
+ for (const item of plan.behind) emitPageItem(out, item, ctx);
60
83
  for (const f of plan.fills) out.push(`<rect x="${fmt(f.x)}" y="${fmt(f.y)}" width="${fmt(f.width)}" height="${fmt(f.height)}" fill="#${f.fillColorHex}"/>`);
61
- for (const img of plan.images) emitImage(out, img, laid, idc);
84
+ for (const img of plan.images) emitImage(out, img, ctx);
62
85
  for (const b of plan.borders) {
63
86
  const x2 = b.x + b.width;
64
87
  const yTop = b.y;
@@ -84,104 +107,148 @@ function emitPage(out, page, laid, losses, idc) {
84
107
  x2,
85
108
  yBottom
86
109
  ];
87
- out.push(`<line x1="${fmt(ax)}" y1="${fmt(ay)}" x2="${fmt(bx)}" y2="${fmt(by)}" stroke="#${b.borderColorHex}" stroke-width="${fmt(b.borderSizePt)}"${b.borderStyle === "dashed" ? " stroke-dasharray=\"8 2.5\"" : b.borderStyle === "dashSmallGap" ? " stroke-dasharray=\"3 1\"" : b.borderStyle === "dotted" ? " stroke-dasharray=\"0.5 1\"" : ""}/>`);
110
+ const dash = b.borderStyle !== void 0 ? BORDER_DASHES.get(b.borderStyle) : void 0;
111
+ out.push(`<line x1="${fmt(ax)}" y1="${fmt(ay)}" x2="${fmt(bx)}" y2="${fmt(by)}" stroke="#${b.borderColorHex}" stroke-width="${fmt(b.borderSizePt)}"${dash ? ` stroke-dasharray="${dash.map(fmt).join(" ")}"` : ""}/>`);
88
112
  }
89
- for (const sh of plan.shapes) emitShape(out, sh.shape, idc);
90
- for (const picture of plan.pictures) for (const item of picture) emitPageItem(out, item, laid, losses, idc);
91
- for (const run of plan.ordered) for (const item of run) emitPageItem(out, item, laid, losses, idc);
92
- for (const t of plan.lines) emitTextLine(out, t, losses, idc);
113
+ for (const sh of plan.shapes) emitShape(out, sh.shape, ctx.ids);
114
+ for (const picture of plan.pictures) for (const item of picture) emitPageItem(out, item, ctx);
115
+ for (const run of plan.ordered) for (const item of run) emitPageItem(out, item, ctx);
116
+ for (const t of plan.lines) emitSvgLine(out, t, ctx);
93
117
  }
94
118
  /**
95
- * One picture. §20.1.8.23 `a:duotone` recolours it between two colours — its
96
- * dark end and its light end — which is a luminance-to-two-tone map, and SVG
97
- * states exactly that: luminance into every channel, then a two-entry transfer
98
- * table per channel.
119
+ * One picture item, its placement and its effects: §20.1.8.23 `a:duotone`
120
+ * recolours it between its dark end and its light end — luminance into every
121
+ * channel, then a two-entry transfer table per channel — and a picture FILL
122
+ * (§20.1.8.14) is clipped to the outline of the shape it fills.
99
123
  *
100
124
  * @param out The SVG fragment sink.
101
125
  * @param img The image item.
102
- * @param laid The laid-out document, for the image's data URI.
103
- * @param idc The shared id counter, for the filter's id.
126
+ * @param ctx The document, its id counter, its drawing.
104
127
  */
105
- function emitImage(out, img, laid, idc) {
106
- const href = imageHref(img.imageResourceName, laid);
107
- if (!href) return;
108
- let filter = "";
128
+ function emitImage(out, img, ctx) {
129
+ let filter;
109
130
  if (img.duotone) {
110
- const id = `duo${String(idc.n++)}`;
131
+ const id = `duo${String(ctx.ids.n++)}`;
111
132
  const chan = (hex, at) => parseInt(hex.slice(at, at + 2), 16) / 255;
112
133
  const table = (at) => `${fmt(chan(img.duotone.shadowHex, at))} ${fmt(chan(img.duotone.highlightHex, at))}`;
113
134
  out.push(`<filter id="${id}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/><feComponentTransfer><feFuncR type="table" tableValues="${table(0)}"/><feFuncG type="table" tableValues="${table(2)}"/><feFuncB type="table" tableValues="${table(4)}"/></feComponentTransfer></filter>`);
114
- filter = ` filter="url(#${id})"`;
135
+ filter = id;
136
+ }
137
+ let clipped = false;
138
+ if (img.clip) {
139
+ const id = `pic${String(ctx.ids.n++)}`;
140
+ const [a, b, c, d, e, f] = img.clip.transform;
141
+ out.push(`<clipPath id="${id}">` + img.clip.paths.map((p) => `<path d="${pathData(p.segments)}" transform="matrix(${fmt(a)} ${fmt(b)} ${fmt(c)} ${fmt(d)} ${fmt(e)} ${fmt(f)})"/>`).join("") + `</clipPath>`, `<g clip-path="url(#${id})">`);
142
+ clipped = true;
143
+ }
144
+ emitPlaced(out, {
145
+ resourceName: img.imageResourceName,
146
+ x: img.x,
147
+ y: img.y,
148
+ width: img.width,
149
+ height: img.height,
150
+ ...img.crop ? { crop: img.crop } : {},
151
+ ...img.rotationDeg !== void 0 ? { rotationDeg: img.rotationDeg } : {},
152
+ ...img.flipH === true ? { flipH: true } : {},
153
+ ...img.flipV === true ? { flipV: true } : {},
154
+ ...img.alpha !== void 0 ? { alpha: img.alpha } : {}
155
+ }, ctx.laid, ctx.ids, filter, img.wash);
156
+ if (clipped) out.push("</g>");
157
+ }
158
+ /**
159
+ * A picture in its box, as the PDF places one: turned about the box's centre
160
+ * (clockwise, as DrawingML measures), mirrored about it, and — cropped by
161
+ * `a:srcRect` (§20.1.8.55) — drawn larger than the box with the box as its clip,
162
+ * so the part that was kept fills it. A wash (§14.1.2.10) is the veil laid over
163
+ * it.
164
+ */
165
+ function emitPlaced(out, placed, laid, ids, filter, wash) {
166
+ const href = imageHref(placed.resourceName, laid);
167
+ if (!href) return;
168
+ const { x, y, width, height } = placed;
169
+ const cx = x + width / 2;
170
+ const cy = y + height / 2;
171
+ const turns = [];
172
+ if (placed.rotationDeg) turns.push(`rotate(${fmt(placed.rotationDeg)} ${fmt(cx)} ${fmt(cy)})`);
173
+ if (placed.flipH === true || placed.flipV === true) {
174
+ const sx = placed.flipH === true ? -1 : 1;
175
+ const sy = placed.flipV === true ? -1 : 1;
176
+ turns.push(`matrix(${String(sx)} 0 0 ${String(sy)} ${fmt(sx === -1 ? 2 * cx : 0)} ${fmt(sy === -1 ? 2 * cy : 0)})`);
115
177
  }
116
- out.push(`<image x="${fmt(img.x)}" y="${fmt(img.y)}" width="${fmt(img.width)}" height="${fmt(img.height)}" href="${href}" preserveAspectRatio="none"${filter}/>`);
178
+ const open = turns.length > 0 ? `<g transform="${turns.join(" ")}">` : "";
179
+ if (open) out.push(open);
180
+ const crop = placed.crop;
181
+ let clipId;
182
+ let drawn = {
183
+ x,
184
+ y,
185
+ width,
186
+ height
187
+ };
188
+ if (crop) {
189
+ const keptW = 1 - crop.left - crop.right;
190
+ const keptH = 1 - crop.top - crop.bottom;
191
+ if (keptW > 0 && keptH > 0) {
192
+ const fullW = width / keptW;
193
+ const fullH = height / keptH;
194
+ drawn = {
195
+ x: x - crop.left * fullW,
196
+ y: y - crop.top * fullH,
197
+ width: fullW,
198
+ height: fullH
199
+ };
200
+ clipId = `crop${String(ids.n++)}`;
201
+ out.push(`<clipPath id="${clipId}"><rect x="${fmt(x)}" y="${fmt(y)}" width="${fmt(width)}" height="${fmt(height)}"/></clipPath>`);
202
+ }
203
+ }
204
+ const alpha = placed.alpha !== void 0 && placed.alpha < 1 ? ` opacity="${fmt(placed.alpha)}"` : "";
205
+ out.push(`<image x="${fmt(drawn.x)}" y="${fmt(drawn.y)}" width="${fmt(drawn.width)}" height="${fmt(drawn.height)}" href="${href}" preserveAspectRatio="none"${clipId ? ` clip-path="url(#${clipId})"` : ""}${filter ? ` filter="url(#${filter})"` : ""}${alpha}/>`);
206
+ const veil = washVeil(wash);
207
+ if (veil) {
208
+ const grey = Math.round(veil.grey * 255).toString(16).padStart(2, "0");
209
+ out.push(`<rect x="${fmt(x)}" y="${fmt(y)}" width="${fmt(width)}" height="${fmt(height)}" fill="#${grey}${grey}${grey}" opacity="${fmt(veil.alpha)}"/>`);
210
+ }
211
+ if (open) out.push("</g>");
117
212
  }
118
213
  /**
119
214
  * One page item, whatever its kind — for the runs that paint in their own order
120
215
  * (what stands behind the content, and a picture's own primitives) rather than
121
216
  * in the by-kind passes.
122
217
  *
123
- * @param out The SVG fragment sink.
124
- * @param item The item to draw.
125
- * @param laid The laid-out document, for image resources.
126
- * @param losses Where an unrenderable item records itself.
127
- * @param idc The shared id counter for generated defs.
218
+ * @param out The SVG fragment sink.
219
+ * @param item The item to draw.
220
+ * @param ctx The document, its glyphs, its id counter and its drawing.
128
221
  */
129
- function emitPageItem(out, item, laid, losses, idc) {
222
+ function emitPageItem(out, item, ctx) {
130
223
  if (item.type === "shape") {
131
- emitShape(out, item.shape, idc);
224
+ emitShape(out, item.shape, ctx.ids);
132
225
  return;
133
226
  }
134
227
  if (item.type === "line") {
135
- emitTextLine(out, item, losses, idc);
228
+ emitSvgLine(out, item, ctx);
136
229
  return;
137
230
  }
138
231
  if (item.type === "image") {
139
- emitImage(out, item, laid, idc);
232
+ emitImage(out, item, ctx);
140
233
  return;
141
234
  }
142
235
  if (item.type === "fill") out.push(`<rect x="${fmt(item.x)}" y="${fmt(item.y)}" width="${fmt(item.width)}" height="${fmt(item.height)}" fill="#${item.fillColorHex}"/>`);
143
236
  }
144
- function emitTextLine(out, item, losses, idc) {
145
- const clip = item.clip;
146
- if (clip) {
147
- const id = `clip${idc.n++}`;
148
- out.push(`<clipPath id="${id}"><rect x="${fmt(clip.x)}" y="${fmt(clip.y)}" width="${fmt(clip.width)}" height="${fmt(clip.height)}"/></clipPath>`, `<g clip-path="url(#${id})">`);
149
- }
150
- const y = item.baselineY;
151
- let x = item.originX;
152
- const rot = item.rotationDeg ? ` transform="rotate(${fmt(-item.rotationDeg)} ${fmt(item.originX)} ${fmt(item.baselineY)})"` : "";
153
- for (const tok of item.line.tokens) {
154
- if (tok.kind === "image") {
155
- x += tok.widthPt;
156
- continue;
157
- }
158
- if (tok.kind === "math") {
159
- losses.push({
160
- severity: "dropped",
161
- feature: FEATURES.math,
162
- detail: "inline math box not rendered by the SVG writer (v0)"
163
- });
164
- x += tok.widthPt;
165
- continue;
166
- }
167
- if (tok.text.trim().length > 0) out.push(`<text x="${fmt(x)}" y="${fmt(y)}"${rot} font-family="sans-serif" font-size="${fmt(tok.fontSizePt)}" fill="#${tok.resolvedRun.colorHex}">${escapeXml(tok.text)}</text>`);
168
- x += tok.widthPt;
169
- }
170
- if (clip) out.push("</g>");
171
- }
172
- function emitShape(out, shape, idc) {
237
+ function emitShape(out, shape, ids) {
173
238
  const [a, b, c, d, e, f] = shape.transform;
174
239
  const transform = `matrix(${fmt(a)} ${fmt(b)} ${fmt(c)} ${fmt(d)} ${fmt(e)} ${fmt(f)})`;
175
240
  let fill;
176
241
  if (shape.fillGradient) {
177
- const id = `grad${idc.n++}`;
242
+ const id = `grad${String(ids.n++)}`;
178
243
  out.push(gradientSvgDef(id, shape.fillGradient));
179
244
  fill = `url(#${id})`;
180
245
  } else fill = shape.fillColorHex ? `#${shape.fillColorHex}` : "none";
181
- const stroke = shape.stroke ? ` stroke="#${shape.stroke.colorHex}" stroke-width="${fmt(shape.stroke.widthPt)}"` : "";
246
+ const fillAlpha = shape.fillAlpha !== void 0 && shape.fillAlpha < 1 && fill !== "none" ? ` fill-opacity="${fmt(shape.fillAlpha)}"` : "";
247
+ const s = shape.stroke;
248
+ const stroke = s ? ` stroke="#${s.colorHex}" stroke-width="${fmt(s.widthPt)}"` + (s.cap !== void 0 ? ` stroke-linecap="${s.cap}"` : "") + (s.join !== void 0 ? ` stroke-linejoin="${s.join}"` : "") + (s.dash && s.dash.length > 0 ? ` stroke-dasharray="${s.dash.map(fmt).join(" ")}"` : "") : "";
182
249
  const shadow = shape.shadow;
183
250
  if (shadow) {
184
- const id = `shadow${idc.n++}`;
251
+ const id = `shadow${String(ids.n++)}`;
185
252
  if (shadow.blurPt > 0) out.push(`<filter id="${id}" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${fmt(shadow.blurPt / 2)}"/></filter>`);
186
253
  const shTransform = `matrix(${fmt(a)} ${fmt(b)} ${fmt(c)} ${fmt(d)} ${fmt(e + shadow.dxPt)} ${fmt(f + shadow.dyPt)})`;
187
254
  out.push(`<g opacity="${fmt(shadow.alpha)}"${shadow.blurPt > 0 ? ` filter="url(#${id})"` : ""}>`);
@@ -194,7 +261,7 @@ function emitShape(out, shape, idc) {
194
261
  for (const path of shape.paths) {
195
262
  const d2 = pathData(path.segments);
196
263
  const rule = path.fillRule === "evenodd" ? " fill-rule=\"evenodd\"" : "";
197
- out.push(`<path d="${d2}" fill="${fill}"${rule}${stroke} transform="${transform}"/>`);
264
+ out.push(`<path d="${d2}" fill="${fill}"${fillAlpha}${rule}${stroke} transform="${transform}"/>`);
198
265
  }
199
266
  }
200
267
  function pathData(segments) {
@@ -209,9 +276,6 @@ function imageHref(resourceName, laid) {
209
276
  return `data:${res.prepared.mimeType};base64,${toBase64(bytes)}`;
210
277
  }
211
278
  }
212
- function escapeXml(s) {
213
- return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
214
- }
215
279
  function fmt(n) {
216
280
  const r = Math.round(n * 100) / 100;
217
281
  return Object.is(r, -0) ? "0" : String(r);
@@ -619,7 +619,7 @@ function collectPositionTabs(p) {
619
619
  * @returns The display text, or `undefined` when this is not a MACROBUTTON.
620
620
  */
621
621
  function macroButtonText(instr) {
622
- const shown = /^\s*MACROBUTTON\s+\S+\s(.*)$/su.exec(instr)?.[1]?.replace(/\s+$/u, "");
622
+ const shown = /^\s*MACROBUTTON\s+\S+\s(.*)$/su.exec(instr)?.[1]?.trimEnd();
623
623
  return shown !== void 0 && shown !== "" ? shown : void 0;
624
624
  }
625
625
  function parseFieldInstr(instr) {