reamkit 1.23.0 → 1.25.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 (102) hide show
  1. package/README.md +22 -11
  2. package/dist/esm/core/bmp.d.ts +33 -0
  3. package/dist/esm/core/bmp.js +276 -0
  4. package/dist/esm/core/converter/facade.d.ts +3 -3
  5. package/dist/esm/core/converter/facade.js +12 -0
  6. package/dist/esm/core/converter/ream.d.ts +48 -4
  7. package/dist/esm/core/converter/ream.js +25 -4
  8. package/dist/esm/core/document-model/index.d.ts +1 -1
  9. package/dist/esm/core/document-model/types.d.ts +54 -0
  10. package/dist/esm/core/drawingml/chart-geometry.js +3 -2
  11. package/dist/esm/core/drawingml/diagram/colors.d.ts +52 -0
  12. package/dist/esm/core/drawingml/diagram/colors.js +125 -0
  13. package/dist/esm/core/drawingml/diagram/data-model.d.ts +47 -0
  14. package/dist/esm/core/drawingml/diagram/data-model.js +119 -0
  15. package/dist/esm/core/drawingml/diagram/layout-engine.d.ts +46 -0
  16. package/dist/esm/core/drawingml/diagram/layout-engine.js +948 -0
  17. package/dist/esm/core/drawingml/diagram/run.d.ts +37 -0
  18. package/dist/esm/core/drawingml/diagram/run.js +50 -0
  19. package/dist/esm/core/drawingml/diagram/to-drawing.d.ts +16 -0
  20. package/dist/esm/core/drawingml/diagram/to-drawing.js +86 -0
  21. package/dist/esm/core/drawingml/preset-geometry.js +116 -6
  22. package/dist/esm/core/drawingml/text-warp.d.ts +58 -0
  23. package/dist/esm/core/drawingml/text-warp.js +355 -0
  24. package/dist/esm/core/drawingml/theme-parser.js +9 -3
  25. package/dist/esm/core/font/measure.d.ts +12 -0
  26. package/dist/esm/core/font/measure.js +36 -0
  27. package/dist/esm/core/images.d.ts +30 -2
  28. package/dist/esm/core/images.js +121 -5
  29. package/dist/esm/core/metafile/emf.js +144 -16
  30. package/dist/esm/core/metafile/picture.d.ts +49 -0
  31. package/dist/esm/core/metafile/picture.js +70 -1
  32. package/dist/esm/core/metafile/wmf.js +53 -4
  33. package/dist/esm/core/ole/escher-blip.js +11 -1
  34. package/dist/esm/core/outline.d.ts +17 -0
  35. package/dist/esm/core/outline.js +30 -0
  36. package/dist/esm/core/style-cascade/resolver.js +1 -0
  37. package/dist/esm/core/style-cascade/types.d.ts +3 -1
  38. package/dist/esm/excel/print-model.js +2 -2
  39. package/dist/esm/excel/sheet-drawing.js +1 -1
  40. package/dist/esm/excel/sheet-to-flow.d.ts +10 -0
  41. package/dist/esm/excel/sheet-to-flow.js +30 -3
  42. package/dist/esm/html/html-writer.js +5 -3
  43. package/dist/esm/index.d.ts +3 -0
  44. package/dist/esm/index.js +2 -1
  45. package/dist/esm/layout/page-doc.d.ts +35 -0
  46. package/dist/esm/layout/page-doc.js +14 -1
  47. package/dist/esm/layout/styled-layout.js +217 -30
  48. package/dist/esm/markdown/markdown-writer.d.ts +41 -0
  49. package/dist/esm/markdown/markdown-writer.js +733 -0
  50. package/dist/esm/pdf/shading.js +8 -6
  51. package/dist/esm/pdf/styled-page-emitter.js +62 -4
  52. package/dist/esm/pdf/vector-graphics.js +1 -1
  53. package/dist/esm/pdf-reader/annots.d.ts +24 -0
  54. package/dist/esm/pdf-reader/annots.js +126 -0
  55. package/dist/esm/pdf-reader/content.d.ts +131 -5
  56. package/dist/esm/pdf-reader/content.js +169 -12
  57. package/dist/esm/pdf-reader/display.d.ts +56 -0
  58. package/dist/esm/pdf-reader/display.js +162 -0
  59. package/dist/esm/pdf-reader/document.d.ts +36 -1
  60. package/dist/esm/pdf-reader/document.js +92 -25
  61. package/dist/esm/pdf-reader/embedded-fonts.d.ts +31 -0
  62. package/dist/esm/pdf-reader/embedded-fonts.js +94 -0
  63. package/dist/esm/pdf-reader/flow-build.d.ts +61 -6
  64. package/dist/esm/pdf-reader/flow-build.js +128 -22
  65. package/dist/esm/pdf-reader/font.js +185 -4
  66. package/dist/esm/pdf-reader/image-decode.js +56 -5
  67. package/dist/esm/pdf-reader/images.d.ts +6 -0
  68. package/dist/esm/pdf-reader/images.js +25 -5
  69. package/dist/esm/pdf-reader/jpeg.d.ts +18 -0
  70. package/dist/esm/pdf-reader/jpeg.js +419 -0
  71. package/dist/esm/pdf-reader/layout.d.ts +1 -1
  72. package/dist/esm/pdf-reader/layout.js +221 -32
  73. package/dist/esm/pdf-reader/pattern-tint.d.ts +17 -0
  74. package/dist/esm/pdf-reader/pattern-tint.js +181 -0
  75. package/dist/esm/pdf-reader/reader.d.ts +9 -1
  76. package/dist/esm/pdf-reader/reader.js +22 -6
  77. package/dist/esm/pdf-reader/shading.d.ts +14 -0
  78. package/dist/esm/pdf-reader/shading.js +27 -1
  79. package/dist/esm/pdf-reader/tagged.js +156 -17
  80. package/dist/esm/pdf-reader/text.d.ts +13 -1
  81. package/dist/esm/pdf-reader/text.js +70 -3
  82. package/dist/esm/pdf-reader/vector.d.ts +25 -1
  83. package/dist/esm/pdf-reader/vector.js +168 -12
  84. package/dist/esm/pptx/ppt/ppt-reader.js +95 -27
  85. package/dist/esm/pptx/ppt/ppt-text.d.ts +41 -0
  86. package/dist/esm/pptx/ppt/ppt-text.js +327 -37
  87. package/dist/esm/pptx/pptx-reader.js +90 -10
  88. package/dist/esm/pptx/slide-parser.d.ts +4 -1
  89. package/dist/esm/pptx/slide-parser.js +104 -13
  90. package/dist/esm/pptx/sp-helpers.d.ts +7 -3
  91. package/dist/esm/pptx/sp-helpers.js +45 -7
  92. package/dist/esm/pptx/table-style.d.ts +7 -0
  93. package/dist/esm/pptx/table-style.js +59 -7
  94. package/dist/esm/svg/svg-writer.js +1 -0
  95. package/dist/esm/word/document-parser.d.ts +4 -1
  96. package/dist/esm/word/document-parser.js +4 -1
  97. package/dist/esm/word/docx-reader.js +26 -13
  98. package/dist/esm/word/docx-writer.js +15 -1
  99. package/dist/esm/word/drawing-parser.d.ts +20 -0
  100. package/dist/esm/word/drawing-parser.js +44 -5
  101. package/dist/esm/word/table-parser.js +3 -0
  102. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { PathBuilder } from "../vector.js";
2
- import { applyTransform, cloneDc, colorRef, newDeviceContext } from "./picture.js";
2
+ import { applyTransform, clippedAway, cloneDc, colorRef, intersectClip, newDeviceContext, primBounds } from "./picture.js";
3
3
  import { cropDib, fadedDib, readDib } from "./dib.js";
4
4
  import { makeBlitter } from "./blit.js";
5
5
  import { fromSymbolFont } from "./symbol-fonts.js";
@@ -71,7 +71,14 @@ function readEmf(bytes) {
71
71
  };
72
72
  let building;
73
73
  const open = () => building ?? [];
74
- const blitter = makeBlitter((prim) => prims.push(prim));
74
+ const inFigure = () => {
75
+ const tail = building?.[building.length - 1];
76
+ return tail !== void 0 && tail.op !== "close";
77
+ };
78
+ const emit = (prim) => {
79
+ if (!clippedAway(dc, primBounds(prim))) prims.push(prim);
80
+ };
81
+ const blitter = makeBlitter(emit);
75
82
  /**
76
83
  * §2.3.1 — one blit record: the source bitmap into the destination
77
84
  * rectangle, both stated in the record's own fields. The rectangle is mapped
@@ -101,7 +108,10 @@ function readEmf(bytes) {
101
108
  };
102
109
  const strokeOf = () => {
103
110
  if (dc.pen.style === "none") return void 0;
104
- const w = dc.pen.widthLu <= 1 ? 1 : dc.pen.widthLu * Math.abs(sx());
111
+ const m = dc.transform;
112
+ const world = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2])) || 1;
113
+ const lu = dc.pen.widthLu * world;
114
+ const w = lu <= 1 ? 1 : lu * Math.abs(sx());
105
115
  return {
106
116
  colorHex: dc.pen.colorHex,
107
117
  widthPt: w,
@@ -117,13 +127,22 @@ function readEmf(bytes) {
117
127
  const st = stroke ? strokeOf() : void 0;
118
128
  const fillHex = fill && !dc.brush.hollow ? dc.brush.colorHex : void 0;
119
129
  if (fillHex === void 0 && !st) return;
120
- prims.push({
130
+ emit({
121
131
  kind: "path",
122
132
  paths: [path],
123
133
  ...fillHex !== void 0 ? { fillColorHex: fillHex } : {},
124
134
  ...st ? { stroke: st } : {}
125
135
  });
126
136
  };
137
+ /** Two mapped corners → the rectangle they bound, in either order. */
138
+ function rectOf(a, b) {
139
+ return {
140
+ left: Math.min(a.x, b.x),
141
+ top: Math.min(a.y, b.y),
142
+ right: Math.max(a.x, b.x),
143
+ bottom: Math.max(a.y, b.y)
144
+ };
145
+ }
127
146
  /** A closed rectangle, as the geometry records draw one. */
128
147
  const rectSegments = (l, t, r, b) => {
129
148
  const p0 = px(l, t);
@@ -176,6 +195,32 @@ function readEmf(bytes) {
176
195
  };
177
196
  haveView = true;
178
197
  break;
198
+ case 30: {
199
+ const a = px(at(8), at(12));
200
+ const b = px(at(16), at(20));
201
+ dc = {
202
+ ...dc,
203
+ clip: intersectClip(dc, rectOf(a, b))
204
+ };
205
+ break;
206
+ }
207
+ case 75: {
208
+ const cbRgnData = u(8);
209
+ const mode = u(12);
210
+ if (cbRgnData === 0 && mode === 5) {
211
+ const { clip: _drop, ...rest } = dc;
212
+ dc = rest;
213
+ } else if (mode === 5 && cbRgnData >= 32 && u(20) === 1) {
214
+ const base = off + 16 + 32;
215
+ const a = px(v.getInt32(base, true), v.getInt32(base + 4, true));
216
+ const b = px(v.getInt32(base + 8, true), v.getInt32(base + 12, true));
217
+ dc = {
218
+ ...dc,
219
+ clip: rectOf(a, b)
220
+ };
221
+ }
222
+ break;
223
+ }
179
224
  case 33:
180
225
  stack.push(cloneDc(dc));
181
226
  break;
@@ -281,7 +326,7 @@ function readEmf(bytes) {
281
326
  dc.y = at(12);
282
327
  const to = px(dc.x, dc.y);
283
328
  if (building) {
284
- if (building.length === 0) building.push({
329
+ if (!inFigure()) building.push({
285
330
  op: "move",
286
331
  x: from.x,
287
332
  y: from.y
@@ -327,6 +372,11 @@ function readEmf(bytes) {
327
372
  case 44:
328
373
  paint(rectSegments(at(8), at(12), at(16), at(20)), true, true);
329
374
  break;
375
+ case 47:
376
+ case 46:
377
+ case 45:
378
+ paint(arcSegments(px, at(8), at(12), at(16), at(20), at(24), at(28), at(32), at(36), type), type !== 45, true);
379
+ break;
330
380
  case 3:
331
381
  case 4:
332
382
  case 2:
@@ -345,10 +395,13 @@ function readEmf(bytes) {
345
395
  const segs = polySegments(px, pts, {
346
396
  closed,
347
397
  bezier: type === 2 || type === 5 || type === 85 || type === 88,
348
- ...type === 5 || type === 6 || type === 88 || type === 89 ? { from: {
349
- x: dc.x,
350
- y: dc.y
351
- } } : {}
398
+ ...type === 5 || type === 6 || type === 88 || type === 89 ? {
399
+ from: {
400
+ x: dc.x,
401
+ y: dc.y
402
+ },
403
+ joined: inFigure()
404
+ } : {}
352
405
  });
353
406
  if (pts.length > 0) {
354
407
  dc.x = pts[pts.length - 1].x;
@@ -395,7 +448,7 @@ function readEmf(bytes) {
395
448
  if (text.trim() !== "") {
396
449
  const p = px(refX, refY);
397
450
  const font = dc.font;
398
- prims.push({
451
+ emit({
399
452
  kind: "text",
400
453
  text: fromSymbolFont(text, font?.family),
401
454
  x: p.x,
@@ -422,6 +475,7 @@ function readEmf(bytes) {
422
475
  const h = at(36);
423
476
  if (cbBmiSrc === 0) {
424
477
  const hex = rop === 66 ? "000000" : rop === 16711778 ? "FFFFFF" : void 0;
478
+ if (hex === void 0 && rop !== 15728673) break;
425
479
  const brush = dc.brush;
426
480
  if (hex !== void 0) dc.brush = {
427
481
  kind: "brush",
@@ -671,12 +725,14 @@ function polySegments(map, pts, o) {
671
725
  if (pts.length === 0) return segs;
672
726
  let i = 0;
673
727
  if (o.from) {
674
- const s = map(o.from.x, o.from.y);
675
- segs.push({
676
- op: "move",
677
- x: s.x,
678
- y: s.y
679
- });
728
+ if (o.joined !== true) {
729
+ const s = map(o.from.x, o.from.y);
730
+ segs.push({
731
+ op: "move",
732
+ x: s.x,
733
+ y: s.y
734
+ });
735
+ }
680
736
  } else {
681
737
  const s = map(pts[0].x, pts[0].y);
682
738
  segs.push({
@@ -712,6 +768,78 @@ function polySegments(map, pts, o) {
712
768
  return segs;
713
769
  }
714
770
  var KAPPA = .5522847498307936;
771
+ /**
772
+ * §2.3.5.2/.4/.13 — the wedge, chord or bare curve an `EMR_ARC`, `EMR_CHORD` or
773
+ * `EMR_PIE` draws.
774
+ *
775
+ * All three state the same thing: an ellipse inscribed in a box, and two points
776
+ * naming RAYS from its centre. The arc runs from where the first ray crosses
777
+ * the ellipse to where the second does, anticlockwise on the page — GDI's
778
+ * default direction — and the two records differ only in how they close: a pie
779
+ * back through the centre, a chord straight across, an arc not at all.
780
+ *
781
+ * Built in the metafile's own coordinates and mapped point by point, exactly as
782
+ * the ellipse and the polygons are: the mapping is affine, so a Bézier's
783
+ * control points survive it.
784
+ *
785
+ * @param map The metafile's logical → page mapping.
786
+ * @param l,t,r,b The box the ellipse is inscribed in.
787
+ * @param sx,sy The point naming the ray the arc starts at.
788
+ * @param ex,ey The point naming the ray it ends at.
789
+ * @param type The record type, which decides how the figure closes.
790
+ * @returns The path segments, in page coordinates.
791
+ */
792
+ function arcSegments(map, l, t, r, b, sx, sy, ex, ey, type) {
793
+ const cx = (l + r) / 2;
794
+ const cy = (t + b) / 2;
795
+ const rx = (r - l) / 2;
796
+ const ry = (b - t) / 2;
797
+ if (!(rx > 0) || !(ry > 0)) return [];
798
+ const angleOf = (x, y) => Math.atan2((y - cy) / ry, (x - cx) / rx);
799
+ const start = angleOf(sx, sy);
800
+ let sweep = angleOf(ex, ey) - start;
801
+ if (sweep >= 0) sweep -= 2 * Math.PI;
802
+ const point = (a) => map(cx + rx * Math.cos(a), cy + ry * Math.sin(a));
803
+ const segs = [];
804
+ const from = point(start);
805
+ if (type === 47) {
806
+ const c = map(cx, cy);
807
+ segs.push({
808
+ op: "move",
809
+ x: c.x,
810
+ y: c.y
811
+ }, {
812
+ op: "line",
813
+ x: from.x,
814
+ y: from.y
815
+ });
816
+ } else segs.push({
817
+ op: "move",
818
+ x: from.x,
819
+ y: from.y
820
+ });
821
+ const steps = Math.max(1, Math.ceil(Math.abs(sweep) / (Math.PI / 2)));
822
+ const delta = sweep / steps;
823
+ const alpha = 4 / 3 * Math.tan(delta / 4);
824
+ for (let i = 0; i < steps; i++) {
825
+ const a0 = start + delta * i;
826
+ const a1 = a0 + delta;
827
+ const t0 = map(cx + rx * Math.cos(a0) - alpha * rx * Math.sin(a0), cy + ry * Math.sin(a0) + alpha * ry * Math.cos(a0));
828
+ const t1 = map(cx + rx * Math.cos(a1) + alpha * rx * Math.sin(a1), cy + ry * Math.sin(a1) - alpha * ry * Math.cos(a1));
829
+ const to = point(a1);
830
+ segs.push({
831
+ op: "cubic",
832
+ x1: t0.x,
833
+ y1: t0.y,
834
+ x2: t1.x,
835
+ y2: t1.y,
836
+ x: to.x,
837
+ y: to.y
838
+ });
839
+ }
840
+ if (type !== 45) segs.push({ op: "close" });
841
+ return segs;
842
+ }
715
843
  function ellipseSegments(map, l, t, r, b) {
716
844
  const cx = (l + r) / 2;
717
845
  const cy = (t + b) / 2;
@@ -102,12 +102,61 @@ export interface DeviceContext {
102
102
  y: number;
103
103
  /** The world→page transform, as `[a, b, c, d, e, f]`. */
104
104
  transform: readonly [number, number, number, number, number, number];
105
+ /**
106
+ * MS-EMF §2.3.2 / MS-WMF §2.3.2 — the region drawing is limited to, in
107
+ * logical units, when the metafile states a RECTANGULAR one. A primitive
108
+ * that falls wholly outside it is not drawn: an embedded workbook's preview
109
+ * clips its sheet to the used range, and unclipped we painted the whole
110
+ * empty grid around it.
111
+ */
112
+ clip?: {
113
+ readonly left: number;
114
+ readonly top: number;
115
+ readonly right: number;
116
+ readonly bottom: number;
117
+ };
105
118
  }
106
119
  export declare const DEFAULT_PEN: MetaPen;
107
120
  export declare const DEFAULT_BRUSH: MetaBrush;
108
121
  /** A device context in its documented initial state (MS-WMF §3.1.5). */
109
122
  export declare function newDeviceContext(): DeviceContext;
110
123
  export declare function cloneDc(dc: DeviceContext): DeviceContext;
124
+ /**
125
+ * Whether a primitive bounded by `box` is wholly outside the device context's
126
+ * clip. Partial overlaps are kept whole — the reader discards what the clip
127
+ * excludes rather than cutting geometry, which is a missing cut, never wrong
128
+ * content.
129
+ *
130
+ * @param dc The device context, for its clip.
131
+ * @param box The primitive's bounds in logical units.
132
+ * @returns `true` when nothing of it would be drawn.
133
+ */
134
+ export declare function clippedAway(dc: DeviceContext, box: {
135
+ left: number;
136
+ top: number;
137
+ right: number;
138
+ bottom: number;
139
+ }): boolean;
140
+ /** The intersection of a device context's clip with another rectangle. */
141
+ export declare function intersectClip(dc: DeviceContext, rect: {
142
+ left: number;
143
+ top: number;
144
+ right: number;
145
+ bottom: number;
146
+ }): NonNullable<DeviceContext['clip']>;
147
+ /**
148
+ * A primitive's bounds in the units it is stored in, for the clip test.
149
+ *
150
+ * @param prim The primitive.
151
+ * @returns Its bounding rectangle; a text run's is its reference point, since
152
+ * the box it fills is not known until it is measured.
153
+ */
154
+ export declare function primBounds(prim: PicturePrim): {
155
+ left: number;
156
+ top: number;
157
+ right: number;
158
+ bottom: number;
159
+ };
111
160
  /** A COLORREF (`0x00bbggrr`) as the 6-hex this codebase's colours use. */
112
161
  export declare function colorRef(v: number): string;
113
162
  /** Apply the world transform to a point. */
@@ -36,6 +36,75 @@ function newDeviceContext() {
36
36
  function cloneDc(dc) {
37
37
  return { ...dc };
38
38
  }
39
+ /**
40
+ * Whether a primitive bounded by `box` is wholly outside the device context's
41
+ * clip. Partial overlaps are kept whole — the reader discards what the clip
42
+ * excludes rather than cutting geometry, which is a missing cut, never wrong
43
+ * content.
44
+ *
45
+ * @param dc The device context, for its clip.
46
+ * @param box The primitive's bounds in logical units.
47
+ * @returns `true` when nothing of it would be drawn.
48
+ */
49
+ function clippedAway(dc, box) {
50
+ const c = dc.clip;
51
+ if (!c) return false;
52
+ return box.right < c.left || box.left > c.right || box.bottom < c.top || box.top > c.bottom;
53
+ }
54
+ /** The intersection of a device context's clip with another rectangle. */
55
+ function intersectClip(dc, rect) {
56
+ const c = dc.clip;
57
+ if (!c) return rect;
58
+ return {
59
+ left: Math.max(c.left, rect.left),
60
+ top: Math.max(c.top, rect.top),
61
+ right: Math.min(c.right, rect.right),
62
+ bottom: Math.min(c.bottom, rect.bottom)
63
+ };
64
+ }
65
+ /**
66
+ * A primitive's bounds in the units it is stored in, for the clip test.
67
+ *
68
+ * @param prim The primitive.
69
+ * @returns Its bounding rectangle; a text run's is its reference point, since
70
+ * the box it fills is not known until it is measured.
71
+ */
72
+ function primBounds(prim) {
73
+ if (prim.kind === "text") return {
74
+ left: prim.x,
75
+ top: prim.y,
76
+ right: prim.x,
77
+ bottom: prim.y
78
+ };
79
+ if (prim.kind === "image") return {
80
+ left: prim.x,
81
+ top: prim.y,
82
+ right: prim.x + prim.width,
83
+ bottom: prim.y + prim.height
84
+ };
85
+ let left = Infinity;
86
+ let top = Infinity;
87
+ let right = -Infinity;
88
+ let bottom = -Infinity;
89
+ for (const path of prim.paths) for (const sg of path.segments) {
90
+ if (!("x" in sg)) continue;
91
+ left = Math.min(left, sg.x);
92
+ right = Math.max(right, sg.x);
93
+ top = Math.min(top, sg.y);
94
+ bottom = Math.max(bottom, sg.y);
95
+ }
96
+ return Number.isFinite(left) ? {
97
+ left,
98
+ top,
99
+ right,
100
+ bottom
101
+ } : {
102
+ left: 0,
103
+ top: 0,
104
+ right: 0,
105
+ bottom: 0
106
+ };
107
+ }
39
108
  /** A COLORREF (`0x00bbggrr`) as the 6-hex this codebase's colours use. */
40
109
  function colorRef(v) {
41
110
  return [
@@ -52,4 +121,4 @@ function applyTransform(t, x, y) {
52
121
  };
53
122
  }
54
123
  //#endregion
55
- export { applyTransform, cloneDc, colorRef, newDeviceContext };
124
+ export { applyTransform, clippedAway, cloneDc, colorRef, intersectClip, newDeviceContext, primBounds };
@@ -1,5 +1,5 @@
1
1
  import { PathBuilder } from "../vector.js";
2
- import { cloneDc, colorRef, newDeviceContext } from "./picture.js";
2
+ import { clippedAway, cloneDc, colorRef, intersectClip, newDeviceContext, primBounds } from "./picture.js";
3
3
  import { cropDib, readDib } from "./dib.js";
4
4
  import { makeBlitter } from "./blit.js";
5
5
  import { fromSymbolFont, symbolGeometryOf, symbolOutline } from "./symbol-fonts.js";
@@ -62,6 +62,7 @@ function readWmf(bytes) {
62
62
  };
63
63
  let origin;
64
64
  const emit = (prim) => {
65
+ if (clippedAway(dc, primBounds(prim))) return;
65
66
  origin ??= {
66
67
  x: win.x,
67
68
  y: win.y
@@ -145,6 +146,17 @@ function readWmf(bytes) {
145
146
  cy: p(0)
146
147
  };
147
148
  break;
149
+ case 1046:
150
+ dc = {
151
+ ...dc,
152
+ clip: intersectClip(dc, {
153
+ bottom: p(0),
154
+ right: p(1),
155
+ top: p(2),
156
+ left: p(3)
157
+ })
158
+ };
159
+ break;
148
160
  case 30:
149
161
  stack.push(cloneDc(dc));
150
162
  break;
@@ -388,12 +400,13 @@ function readWmf(bytes) {
388
400
  right: win.x + (win.cx || 1e3),
389
401
  bottom: win.y + (win.cy || 1e3)
390
402
  };
403
+ const flipped = win.cy < 0 ? prims.map((p) => mirrorPrimY(p, box.top + box.bottom)) : prims;
391
404
  return {
392
- left: box.left,
393
- top: box.top,
405
+ left: Math.min(box.left, box.right),
406
+ top: Math.min(box.top, box.bottom),
394
407
  width: Math.max(1, Math.abs(box.right - box.left)),
395
408
  height: Math.max(1, Math.abs(box.bottom - box.top)),
396
- prims,
409
+ prims: flipped,
397
410
  skipped: [...skipped]
398
411
  };
399
412
  }
@@ -451,6 +464,42 @@ function shiftPrim(prim, dx, dy) {
451
464
  }))
452
465
  };
453
466
  }
467
+ /**
468
+ * A primitive mirrored about a horizontal axis, for a window whose y grows UP.
469
+ *
470
+ * Only the geometry turns over. A text run's reference point names a corner of
471
+ * the cell IN DEVICE SPACE — GDI never draws the glyphs themselves upside down —
472
+ * so the point moves and the alignment flags stay as they are. An image states
473
+ * the TOP of its destination rectangle, which after the mirror is where its
474
+ * bottom used to be.
475
+ *
476
+ * @param prim The primitive, in the metafile's own logical units.
477
+ * @param twiceAxis Twice the y the mirror runs along, so `y' = twiceAxis - y`.
478
+ * @returns The primitive with its y coordinates turned over.
479
+ */
480
+ function mirrorPrimY(prim, twiceAxis) {
481
+ const at = (y) => twiceAxis - y;
482
+ if (prim.kind === "text") return {
483
+ ...prim,
484
+ y: at(prim.y)
485
+ };
486
+ if (prim.kind === "image") return {
487
+ ...prim,
488
+ y: at(prim.y + prim.height)
489
+ };
490
+ return {
491
+ ...prim,
492
+ paths: prim.paths.map((path) => ({
493
+ ...path,
494
+ segments: path.segments.map((sg) => "x" in sg ? {
495
+ ...sg,
496
+ y: at(sg.y),
497
+ ..."y1" in sg ? { y1: at(sg.y1) } : {},
498
+ ..."y2" in sg ? { y2: at(sg.y2) } : {}
499
+ } : sg)
500
+ }))
501
+ };
502
+ }
454
503
  function penStyle(style) {
455
504
  switch (style & 15) {
456
505
  case 1: return "dash";
@@ -1,3 +1,4 @@
1
+ import { dibToBmp } from "../bmp.js";
1
2
  import { unzlibSync } from "fflate";
2
3
  //#region src/core/ole/escher-blip.ts
3
4
  var BLIP_EMF = 61466;
@@ -54,9 +55,18 @@ var RASTER_MAGICS = [
54
55
  */
55
56
  function readEscherBlip(type, instance, data) {
56
57
  if (type === BLIP_EMF || type === BLIP_WMF || type === BLIP_PICT) return readMetafileBlip(instance, data);
57
- if (type === BLIP_JPEG || type === BLIP_PNG || type === BLIP_DIB || type === BLIP_TIFF || type === BLIP_JPEG_CMYK) return scanRaster(data);
58
+ if (type === BLIP_DIB) return readDibBlip(instance, data) ?? scanRaster(data);
59
+ if (type === BLIP_JPEG || type === BLIP_PNG || type === BLIP_TIFF || type === BLIP_JPEG_CMYK) return scanRaster(data);
58
60
  return scanRaster(data);
59
61
  }
62
+ function readDibBlip(instance, data) {
63
+ const uidBytes = TWO_UID_INSTANCES.has(instance) ? 32 : 16;
64
+ for (const at of [uidBytes + 1, uidBytes]) {
65
+ if (data.length <= at) continue;
66
+ const bmp = dibToBmp(data.subarray(at));
67
+ if (bmp) return bmp;
68
+ }
69
+ }
60
70
  function readMetafileBlip(instance, data) {
61
71
  const uidBytes = TWO_UID_INSTANCES.has(instance) ? 32 : 16;
62
72
  const head = uidBytes + METAFILE_HEADER_BYTES;
@@ -0,0 +1,17 @@
1
+ import { ResolvedParagraphProperties } from './style-cascade/index.js';
2
+ /**
3
+ * The heading level (1–6) a paragraph's resolved properties describe, or
4
+ * `undefined` when it is body text.
5
+ *
6
+ * @param resolved The paragraph's fully-resolved properties.
7
+ * @returns The heading level 1–6, or `undefined` for body text.
8
+ */
9
+ export declare function headingLevelOf(resolved: ResolvedParagraphProperties): number | undefined;
10
+ /**
11
+ * The heading level a `"Heading N"` / `"Title"` / `"Subtitle"` style id names,
12
+ * for a heading style that declares no outline level of its own.
13
+ *
14
+ * @param styleId The paragraph's style id, if it has one.
15
+ * @returns The heading level 1–6, or `undefined` when the id names no heading.
16
+ */
17
+ export declare function headingLevelFromStyleId(styleId: string | undefined): number | undefined;
@@ -0,0 +1,30 @@
1
+ //#region src/core/outline.ts
2
+ /** The deepest heading level the consuming media can express. */
3
+ var MAX_HEADING_LEVEL = 6;
4
+ /**
5
+ * The heading level (1–6) a paragraph's resolved properties describe, or
6
+ * `undefined` when it is body text.
7
+ *
8
+ * @param resolved The paragraph's fully-resolved properties.
9
+ * @returns The heading level 1–6, or `undefined` for body text.
10
+ */
11
+ function headingLevelOf(resolved) {
12
+ const lvl = resolved.outlineLevel;
13
+ if (lvl !== void 0 && lvl >= 0 && lvl <= 8) return Math.min(lvl + 1, MAX_HEADING_LEVEL);
14
+ return headingLevelFromStyleId(resolved.styleId);
15
+ }
16
+ /**
17
+ * The heading level a `"Heading N"` / `"Title"` / `"Subtitle"` style id names,
18
+ * for a heading style that declares no outline level of its own.
19
+ *
20
+ * @param styleId The paragraph's style id, if it has one.
21
+ * @returns The heading level 1–6, or `undefined` when the id names no heading.
22
+ */
23
+ function headingLevelFromStyleId(styleId) {
24
+ if (styleId === void 0) return void 0;
25
+ const m = /^Heading\s*([1-9])$/i.exec(styleId);
26
+ if (m) return Math.min(Number(m[1]), MAX_HEADING_LEVEL);
27
+ if (/^(Title|Subtitle)$/i.test(styleId)) return 1;
28
+ }
29
+ //#endregion
30
+ export { headingLevelOf };
@@ -136,6 +136,7 @@ function mergeRun(base, override) {
136
136
  rtl: override.rtl ?? base.rtl,
137
137
  ...lang !== void 0 ? { lang } : {},
138
138
  ...(override.shadingColorHex ?? base.shadingColorHex) !== void 0 ? { shadingColorHex: override.shadingColorHex ?? base.shadingColorHex } : {},
139
+ ...(override.textOutline ?? base.textOutline) !== void 0 ? { textOutline: override.textOutline ?? base.textOutline } : {},
139
140
  ...(override.letterSpacingPt ?? base.letterSpacingPt) !== void 0 ? { letterSpacingPt: override.letterSpacingPt ?? base.letterSpacingPt } : {}
140
141
  };
141
142
  }
@@ -1,4 +1,4 @@
1
- import { Alignment, CellBorders, CellShading, FontFamilyMap, FrameProperties, NumberingReference, RunProperties, TabStop, UnderlineStyle, VerticalAlign } from '../document-model/index.js';
1
+ import { Alignment, CellBorders, CellShading, FontFamilyMap, FrameProperties, NumberingReference, RunProperties, TabStop, TextOutline, UnderlineStyle, VerticalAlign } from '../document-model/index.js';
2
2
  import { Pt } from '../ir/index.js';
3
3
  /**
4
4
  * A run's fully-resolved properties: every field required because the cascade
@@ -30,6 +30,8 @@ export interface ResolvedRunProperties {
30
30
  readonly shadingColorHex?: string;
31
31
  /** §17.3.2.35 — extra space between the run's characters, in points. */
32
32
  readonly letterSpacingPt?: Pt;
33
+ /** §21.1.2.3.9 — a line drawn round the glyphs themselves. */
34
+ readonly textOutline?: TextOutline;
33
35
  }
34
36
  /**
35
37
  * A paragraph's fully-resolved properties: every field required, the cascade
@@ -1122,8 +1122,8 @@ function shadingFromXf(xf, styles) {
1122
1122
  if (!pattern || pattern === "none") return void 0;
1123
1123
  if (pattern === "solid") return fill.fgColorHex ? { colorHex: fill.fgColorHex } : void 0;
1124
1124
  const density = PATTERN_DENSITY[pattern];
1125
- if (density === void 0 || !fill.fgColorHex) return void 0;
1126
- const blended = blendHex(fill.fgColorHex, fill.bgColorHex ?? "FFFFFF", density);
1125
+ if (density === void 0) return void 0;
1126
+ const blended = blendHex(fill.fgColorHex ?? "000000", fill.bgColorHex ?? "FFFFFF", density);
1127
1127
  return blended ? { colorHex: blended } : void 0;
1128
1128
  }
1129
1129
  function blendHex(fg, bg, fraction) {
@@ -162,7 +162,7 @@ function makeColWidthPt(ws) {
162
162
  return (col) => {
163
163
  for (const c of ws.columns) if (col >= c.min - 1 && col <= c.max - 1) return widthPt(c.widthChars);
164
164
  if (ws.defaultColWidthChars !== void 0) return widthPt(ws.defaultColWidthChars);
165
- if (ws.baseColWidthChars !== void 0) return (columnTwips(ws.baseColWidthChars, 105) + 75) / TWIPS_PER_PT;
165
+ if (ws.baseColWidthChars !== void 0) return (columnTwips(ws.baseColWidthChars, 105) + 150) / TWIPS_PER_PT;
166
166
  return 960 / TWIPS_PER_PT;
167
167
  };
168
168
  }
@@ -42,6 +42,16 @@ export interface ProjectSheetOptions {
42
42
  * supplies it; absent, the code is dropped exactly as before.
43
43
  */
44
44
  readonly fileName?: string;
45
+ /**
46
+ * Open each printed sheet with a heading carrying its NAME.
47
+ *
48
+ * Off by default, and deliberately: a printed page shows no tab name — Excel
49
+ * and Calc both emit it nowhere — so the paginated targets must not see one.
50
+ * A flowed target has no pages to tell one sheet from the next by, and a
51
+ * workbook rendered without them is a pile of tables with nothing to say
52
+ * which is which; markdown asks for this, and gets `# Sheet1`.
53
+ */
54
+ readonly sheetHeadings?: boolean;
45
55
  }
46
56
  /**
47
57
  * Project a {@link SheetDoc} into a {@link FlowDoc} (E-SHEET SA2): each grid sheet
@@ -67,7 +67,20 @@ function projectSheetDoc(sheet, options = {}) {
67
67
  const sheetSection = withHeaderFooter(sectionFromWorksheet(ws.grid), ws, headersFooters, scaleSink.value, sheet.styles.fonts[0]?.sizePt, printed, options.fileName, sheet.themePalette, options.now);
68
68
  if (printed === 0) firstSheetSection = sheetSection;
69
69
  sheetSections.push(sheetSection);
70
- if (printed > 0) body.push({
70
+ if (options.sheetHeadings === true) body.push({
71
+ kind: "paragraph",
72
+ paragraph: {
73
+ properties: {
74
+ outlineLevel: 0,
75
+ ...printed > 0 ? { pageBreakBefore: true } : {}
76
+ },
77
+ runs: [{
78
+ text: ws.name,
79
+ properties: {}
80
+ }]
81
+ }
82
+ });
83
+ else if (printed > 0) body.push({
71
84
  kind: "paragraph",
72
85
  paragraph: PAGE_BREAK_PARAGRAPH
73
86
  });
@@ -401,8 +414,16 @@ function withDrawingsByBand(drawings, gridBody, lefts) {
401
414
  const byBand = lefts.map(() => []);
402
415
  for (const el of drawings) {
403
416
  const x = floatLeftPt(el);
404
- const band = x === void 0 ? 0 : bandOf(x);
405
- byBand[band].push(x === void 0 ? el : shiftFloatLeft(el, lefts[band]));
417
+ if (x === void 0) {
418
+ byBand[0].push(el);
419
+ continue;
420
+ }
421
+ const start = bandOf(x);
422
+ const right = x + (floatWidthPt(el) ?? 0);
423
+ for (let band = start; band < lefts.length; band++) {
424
+ if (band > start && right <= lefts[band]) break;
425
+ byBand[band].push(shiftFloatLeft(el, lefts[band]));
426
+ }
406
427
  }
407
428
  const out = [];
408
429
  let seen = 0;
@@ -416,6 +437,12 @@ function withDrawingsByBand(drawings, gridBody, lefts) {
416
437
  function floatLeftPt(el) {
417
438
  return (el.kind === "chart" ? el.chart.float : el.kind === "image" ? el.image.float : el.kind === "shape" ? el.shape.float : void 0)?.posH?.offsetPt;
418
439
  }
440
+ /** A float's own width, for the block kinds a sheet anchors. */
441
+ function floatWidthPt(el) {
442
+ if (el.kind === "chart") return el.chart.width;
443
+ if (el.kind === "image") return el.image.width;
444
+ if (el.kind === "shape") return el.shape.width;
445
+ }
419
446
  /** The same block with its horizontal anchor measured from `by` points later. */
420
447
  function shiftFloatLeft(el, by) {
421
448
  if (by === 0) return el;