@office-kit/pptx-preview 0.9.1 → 0.9.2

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.
package/README.md CHANGED
@@ -125,8 +125,10 @@ placeholder layout/master cascade), strokes, rotation, effects (shadow, glow,
125
125
  soft edge, reflection), images with adjustments, charts (column, bar, line,
126
126
  area, pie, doughnut, scatter, radar, bubble), tables with per-run cell text,
127
127
  vertical and multi-column text in both text-layout modes, picture bullets, and
128
- template (layout/master) decoration all render. SmartArt, animations, 3D, and
129
- EMF/WMF fall back to labelled placeholders carrying a machine-readable marker
128
+ template (layout/master) decoration all render. EMF pictures containing solid-filled
129
+ line/Bézier paths, including rectangular and path clipping with copy semantics,
130
+ render as transparent SVG images. Other EMF drawing commands, WMF, SmartArt,
131
+ animations, and 3D fall back to labelled placeholders carrying a machine-readable marker
130
132
  (below). Per-slide closeness to a LibreOffice baseline is measured and gated
131
133
  in CI by the fidelity harness in the monorepo (`site/fidelity`) — mean
132
134
  fg-SSIM ≈ 0.78 across the corpus, with the residual gaps documented there.
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as MONO, c as TIMES, d as substituteFamily, l as defaultMeasurer, n as renderSlideSvg, o as SANS, r as ARIAL, s as SERIF, t as auditTextLayout } from "./src-C_bsFwWV.js";
1
+ import { a as MONO, c as TIMES, d as substituteFamily, l as defaultMeasurer, n as renderSlideSvg, o as SANS, r as ARIAL, s as SERIF, t as auditTextLayout } from "./src-CXGhih2x.js";
2
2
  export { ARIAL, MONO, SANS, SERIF, TIMES, auditTextLayout, defaultMeasurer, renderSlideSvg as renderSlideToSvg, substituteFamily };
package/dist/node.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as MONO, c as TIMES, d as substituteFamily, i as AVG_GLYPH_W_RATIO, l as defaultMeasurer, n as renderSlideSvg, o as SANS, r as ARIAL, s as SERIF, t as auditTextLayout, u as isCjk } from "./src-C_bsFwWV.js";
1
+ import { a as MONO, c as TIMES, d as substituteFamily, i as AVG_GLYPH_W_RATIO, l as defaultMeasurer, n as renderSlideSvg, o as SANS, r as ARIAL, s as SERIF, t as auditTextLayout, u as isCjk } from "./src-CXGhih2x.js";
2
2
  import { getSlideSize } from "@office-kit/pptx";
3
3
  import { Resvg } from "@resvg/resvg-js";
4
4
  import * as fontkit from "fontkit";
@@ -1,4 +1,247 @@
1
1
  import { getEffectiveColorMap, getGroupChildren, getGroupTransform, getParagraphAlignment, getParagraphBullet, getParagraphBulletImageBytes, getParagraphBulletStyle, getParagraphIndent, getParagraphLevel, getParagraphPropertiesEffective, getParagraphSpacing, getPresentationFonts, getPresentationTheme, getShapeAdjustValues, getShapeAltTitle, getShapeBodyPrEffective, getShapeBoundsResolved, getShapeChartSpec, getShapeClickAction, getShapeCustomGeometry, getShapeDescription, getShapeEffectsEffective, getShapeFillColorResolved, getShapeFillEffective, getShapeFlip, getShapeGradientFill, getShapeGradientFillEffective, getShapeHyperlink, getShapeHyperlinkTooltip, getShapeImageBiLevelThreshold, getShapeImageBrightness, getShapeImageBytes, getShapeImageContrast, getShapeImageCrop, getShapeImageDuotone, getShapeImageFillBytes, getShapeImageFormat, getShapeImageLinkUrl, getShapeImageOpacity, getShapeImagePartName, getShapeKind, getShapeName, getShapeParagraphCount, getShapeParagraphElements, getShapePatternFill, getShapePlaceholderType, getShapePreset, getShapeRotation, getShapeRunClickAction, getShapeRunFormat, getShapeRunFormatEffective, getShapeRunHyperlink, getShapeRunHyperlinkTooltip, getShapeStrokeArrow, getShapeStrokeCap, getShapeStrokeColorResolved, getShapeStrokeCompound, getShapeStrokeDash, getShapeStrokeEffective, getShapeStrokeJoin, getShapeTextAnchor, getShapeTextAutoFitParams, getShapeTextBodyRotationDeg, getShapeTextColumns, getShapeTextDirection, getShapeTextMargins, getShapeXmlString, getSlideBackground, getSlideBackgroundGradientFill, getSlideBackgroundImageBytes, getSlideBackgroundPatternFill, getSlideIndex, getSlideLayout, getSlideLayoutBackground, getSlideLayoutBackgroundGradientFill, getSlideLayoutBackgroundImageBytes, getSlideLayoutBackgroundPatternFill, getSlideLayoutShapes, getSlideMasterBackground, getSlideMasterBackgroundGradientFill, getSlideMasterBackgroundImageBytes, getSlideMasterBackgroundPatternFill, getSlideMasterShapes, getSlideShapes, getSlideSize, getSlides, getTableCellAnchor, getTableCellBorders, getTableCellFill, getTableCellMargins, getTableCellParagraphs, getTableCellSpan, getTableCells, getTableColumnWidths, getTableDimensions, getTableRowHeights, getTableStyleFlags, isChartShape, isParagraphBulletPicture, isShapeImageGrayscale, isShapePlaceholder, isShapeTextBox, isTableShape, resolveDeckBodyTextColor } from "@office-kit/pptx";
2
+ //#region src/emf.ts
3
+ const WORD_BYTES = 4;
4
+ const SHORT_BYTES = 2;
5
+ const RECORD_HEADER_BYTES = 8;
6
+ const HEADER_MIN_BYTES = 88;
7
+ const VALUE_RECORD_BYTES = 12;
8
+ const PAIR_RECORD_BYTES = 16;
9
+ const BOUNDS_RECORD_BYTES = 24;
10
+ const BRUSH_RECORD_BYTES = 24;
11
+ const EOF_MIN_BYTES = 20;
12
+ const HEADER_BOUNDS_OFFSET = 8;
13
+ const HEADER_FILE_SIZE_OFFSET = 48;
14
+ const HEADER_RECORD_COUNT_OFFSET = 52;
15
+ const VALUE_OFFSET = RECORD_HEADER_BYTES;
16
+ const SECOND_VALUE_OFFSET = 12;
17
+ const BRUSH_COLOR_OFFSET = 16;
18
+ const RGB_CHANNEL_COUNT = 3;
19
+ const HEX_RADIX = 16;
20
+ const EMFPLUS_COMMENT = 726027589;
21
+ const SIGNATURE_OFFSET = 40;
22
+ const SIGNATURE = 1179469088;
23
+ const POINT_COUNT_OFFSET = 24;
24
+ const POINTS_OFFSET = 28;
25
+ const BEZIER_POINT_COUNT = 3;
26
+ const STOCK_OBJECT_FLAG = 2147483648;
27
+ const REGION_COPY = 5;
28
+ const MAP_TEXT = 1;
29
+ const MAP_ANISOTROPIC = 8;
30
+ const BRUSH_SOLID = 0;
31
+ const BRUSH_NULL = 1;
32
+ const FILL_ALTERNATE = 1;
33
+ const FILL_WINDING = 2;
34
+ const STOCK_BRUSHES = [
35
+ "#ffffff",
36
+ "#c0c0c0",
37
+ "#808080",
38
+ "#404040",
39
+ "#000000",
40
+ "none"
41
+ ];
42
+ const RECORD = {
43
+ HEADER: 1,
44
+ POLYBEZIERTO: 5,
45
+ POLYLINETO: 6,
46
+ SETWINDOWEXTEX: 9,
47
+ SETWINDOWORGEX: 10,
48
+ SETVIEWPORTEXTEX: 11,
49
+ SETVIEWPORTORGEX: 12,
50
+ SETBRUSHORGEX: 13,
51
+ EOF: 14,
52
+ SETMAPMODE: 17,
53
+ SETBKMODE: 18,
54
+ SETPOLYFILLMODE: 19,
55
+ SETSTRETCHBLTMODE: 21,
56
+ SETTEXTALIGN: 22,
57
+ MOVETOEX: 27,
58
+ SELECTOBJECT: 37,
59
+ CREATEBRUSHINDIRECT: 39,
60
+ DELETEOBJECT: 40,
61
+ BEGINPATH: 59,
62
+ ENDPATH: 60,
63
+ CLOSEFIGURE: 61,
64
+ FILLPATH: 62,
65
+ SELECTCLIPPATH: 67,
66
+ COMMENT: 70,
67
+ EXTSELECTCLIPRGN: 75,
68
+ POLYBEZIERTO16: 88,
69
+ POLYLINETO16: 89
70
+ };
71
+ const renderEmfToSvg = (bytes) => {
72
+ if (bytes.byteLength < HEADER_MIN_BYTES) return null;
73
+ const file = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
74
+ if (file.getUint32(0, true) !== RECORD.HEADER || file.getUint32(SIGNATURE_OFFSET, true) !== SIGNATURE) return null;
75
+ const left = file.getInt32(HEADER_BOUNDS_OFFSET, true);
76
+ const top = file.getInt32(12, true);
77
+ const width = file.getInt32(16, true) - left;
78
+ const height = file.getInt32(20, true) - top;
79
+ if (width <= 0 || height <= 0 || file.getUint32(HEADER_FILE_SIZE_OFFSET, true) !== bytes.byteLength) return null;
80
+ let windowX = 0;
81
+ let windowY = 0;
82
+ let windowWidth = 1;
83
+ let windowHeight = 1;
84
+ let viewportX = 0;
85
+ let viewportY = 0;
86
+ let viewportWidth = 1;
87
+ let viewportHeight = 1;
88
+ let mapMode = MAP_TEXT;
89
+ let fillRule = "evenodd";
90
+ let brush = "#ffffff";
91
+ const brushes = /* @__PURE__ */ new Map();
92
+ let path = [];
93
+ let recordingPath = false;
94
+ let clip = "";
95
+ let clipCount = 0;
96
+ const definitions = [];
97
+ const drawings = [];
98
+ let recordCount = 0;
99
+ const point = (x, y) => {
100
+ if (mapMode === MAP_TEXT) return `${x} ${y}`;
101
+ return `${(x - windowX) * viewportWidth / windowWidth + viewportX} ${(y - windowY) * viewportHeight / windowHeight + viewportY}`;
102
+ };
103
+ for (let offset = 0; offset < bytes.byteLength;) {
104
+ if (bytes.byteLength - offset < RECORD_HEADER_BYTES) return null;
105
+ const type = file.getUint32(offset, true);
106
+ const size = file.getUint32(offset + WORD_BYTES, true);
107
+ if (size < RECORD_HEADER_BYTES || size % WORD_BYTES !== 0 || size > bytes.byteLength - offset) return null;
108
+ const record = new DataView(bytes.buffer, bytes.byteOffset + offset, size);
109
+ const unsigned = (at) => record.getUint32(at, true);
110
+ const signed = (at) => record.getInt32(at, true);
111
+ recordCount++;
112
+ switch (type) {
113
+ case RECORD.HEADER:
114
+ if (offset !== 0 || size < HEADER_MIN_BYTES) return null;
115
+ break;
116
+ case RECORD.EOF:
117
+ if (size < EOF_MIN_BYTES || offset + size !== bytes.byteLength || recordingPath || recordCount !== file.getUint32(HEADER_RECORD_COUNT_OFFSET, true)) return null;
118
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${left} ${top} ${width} ${height}" width="${width}" height="${height}"><defs>${definitions.join("")}</defs>${drawings.join("")}</svg>`;
119
+ case RECORD.SETMAPMODE:
120
+ if (size < VALUE_RECORD_BYTES) return null;
121
+ mapMode = unsigned(VALUE_OFFSET);
122
+ if (mapMode !== MAP_TEXT && mapMode !== MAP_ANISOTROPIC) return null;
123
+ break;
124
+ case RECORD.SETWINDOWORGEX:
125
+ case RECORD.SETVIEWPORTORGEX:
126
+ case RECORD.SETWINDOWEXTEX:
127
+ case RECORD.SETVIEWPORTEXTEX: {
128
+ if (size < PAIR_RECORD_BYTES) return null;
129
+ const x = signed(VALUE_OFFSET);
130
+ const y = signed(SECOND_VALUE_OFFSET);
131
+ if (type === RECORD.SETWINDOWORGEX) {
132
+ windowX = x;
133
+ windowY = y;
134
+ } else if (type === RECORD.SETVIEWPORTORGEX) {
135
+ viewportX = x;
136
+ viewportY = y;
137
+ } else {
138
+ if (x === 0 || y === 0) return null;
139
+ if (type === RECORD.SETWINDOWEXTEX) {
140
+ windowWidth = x;
141
+ windowHeight = y;
142
+ } else {
143
+ viewportWidth = x;
144
+ viewportHeight = y;
145
+ }
146
+ }
147
+ break;
148
+ }
149
+ case RECORD.CREATEBRUSHINDIRECT: {
150
+ if (size < BRUSH_RECORD_BYTES) return null;
151
+ const style = unsigned(SECOND_VALUE_OFFSET);
152
+ if (style !== BRUSH_SOLID && style !== BRUSH_NULL) return null;
153
+ const color = [...bytes.subarray(offset + BRUSH_COLOR_OFFSET, offset + BRUSH_COLOR_OFFSET + RGB_CHANNEL_COUNT)].map((channel) => channel.toString(HEX_RADIX).padStart(2, "0")).join("");
154
+ brushes.set(unsigned(VALUE_OFFSET), style === BRUSH_NULL ? "none" : `#${color}`);
155
+ break;
156
+ }
157
+ case RECORD.SELECTOBJECT: {
158
+ if (size < VALUE_RECORD_BYTES) return null;
159
+ const handle = unsigned(VALUE_OFFSET);
160
+ const selected = handle >= STOCK_OBJECT_FLAG ? STOCK_BRUSHES[handle - STOCK_OBJECT_FLAG] : brushes.get(handle);
161
+ if (selected === void 0) return null;
162
+ brush = selected;
163
+ break;
164
+ }
165
+ case RECORD.DELETEOBJECT:
166
+ if (size < VALUE_RECORD_BYTES) return null;
167
+ brushes.delete(unsigned(VALUE_OFFSET));
168
+ break;
169
+ case RECORD.SETPOLYFILLMODE:
170
+ if (size < VALUE_RECORD_BYTES || unsigned(VALUE_OFFSET) !== FILL_ALTERNATE && unsigned(VALUE_OFFSET) !== FILL_WINDING) return null;
171
+ fillRule = unsigned(VALUE_OFFSET) === FILL_WINDING ? "nonzero" : "evenodd";
172
+ break;
173
+ case RECORD.BEGINPATH:
174
+ path = [];
175
+ recordingPath = true;
176
+ break;
177
+ case RECORD.MOVETOEX:
178
+ if (size < PAIR_RECORD_BYTES || !recordingPath) return null;
179
+ path.push(`M${point(signed(VALUE_OFFSET), signed(SECOND_VALUE_OFFSET))}`);
180
+ break;
181
+ case RECORD.POLYLINETO:
182
+ case RECORD.POLYLINETO16:
183
+ case RECORD.POLYBEZIERTO:
184
+ case RECORD.POLYBEZIERTO16: {
185
+ if (size < POINTS_OFFSET || !recordingPath || path.length === 0) return null;
186
+ const short = type === RECORD.POLYLINETO16 || type === RECORD.POLYBEZIERTO16;
187
+ const bezier = type === RECORD.POLYBEZIERTO || type === RECORD.POLYBEZIERTO16;
188
+ const coordinateBytes = short ? SHORT_BYTES : WORD_BYTES;
189
+ const pointBytes = coordinateBytes * 2;
190
+ const count = unsigned(POINT_COUNT_OFFSET);
191
+ if (count > Math.floor((size - POINTS_OFFSET) / pointBytes) || bezier && count % BEZIER_POINT_COUNT !== 0) return null;
192
+ for (let index = 0; index < count; index++) {
193
+ const at = POINTS_OFFSET + index * pointBytes;
194
+ const x = short ? record.getInt16(at, true) : signed(at);
195
+ const y = short ? record.getInt16(at + coordinateBytes, true) : signed(at + coordinateBytes);
196
+ const command = bezier ? index % BEZIER_POINT_COUNT === 0 ? "C" : " " : "L";
197
+ path.push(`${command}${point(x, y)}`);
198
+ }
199
+ break;
200
+ }
201
+ case RECORD.CLOSEFIGURE:
202
+ if (!recordingPath || path.length === 0) return null;
203
+ path.push("Z");
204
+ break;
205
+ case RECORD.ENDPATH:
206
+ if (!recordingPath) return null;
207
+ recordingPath = false;
208
+ break;
209
+ case RECORD.FILLPATH:
210
+ if (size < BOUNDS_RECORD_BYTES || recordingPath || path.length === 0) return null;
211
+ drawings.push(`<path d="${path.join("")}" fill="${brush}" fill-rule="${fillRule}"${clip}/>`);
212
+ path = [];
213
+ break;
214
+ case RECORD.SELECTCLIPPATH: {
215
+ if (size < VALUE_RECORD_BYTES || unsigned(VALUE_OFFSET) !== REGION_COPY || recordingPath || path.length === 0) return null;
216
+ const id = `emf-clip-${clipCount++}`;
217
+ definitions.push(`<clipPath id="${id}"><path d="${path.join("")}" clip-rule="${fillRule}"/></clipPath>`);
218
+ clip = ` clip-path="url(#${id})"`;
219
+ path = [];
220
+ break;
221
+ }
222
+ case RECORD.EXTSELECTCLIPRGN:
223
+ if (size < PAIR_RECORD_BYTES || unsigned(VALUE_OFFSET) !== 0 || unsigned(SECOND_VALUE_OFFSET) !== REGION_COPY) return null;
224
+ clip = "";
225
+ break;
226
+ case RECORD.COMMENT:
227
+ if (size < VALUE_RECORD_BYTES || unsigned(VALUE_OFFSET) > size - VALUE_RECORD_BYTES) return null;
228
+ if (unsigned(VALUE_OFFSET) >= WORD_BYTES && unsigned(SECOND_VALUE_OFFSET) === EMFPLUS_COMMENT) return null;
229
+ break;
230
+ case RECORD.SETBKMODE:
231
+ case RECORD.SETSTRETCHBLTMODE:
232
+ case RECORD.SETTEXTALIGN:
233
+ if (size < VALUE_RECORD_BYTES) return null;
234
+ break;
235
+ case RECORD.SETBRUSHORGEX:
236
+ if (size < PAIR_RECORD_BYTES) return null;
237
+ break;
238
+ default: return null;
239
+ }
240
+ offset += size;
241
+ }
242
+ return null;
243
+ };
244
+ //#endregion
2
245
  //#region src/text-layout.ts
3
246
  const SANS = "Carlito";
4
247
  const SERIF = "Caladea";
@@ -530,6 +773,13 @@ const mimeFromPartName = (name) => {
530
773
  const renderPicture = (shape, pres, x, y, w, h, transform, textOverlay, bytes, format) => {
531
774
  let mime = null;
532
775
  if (bytes && format) mime = imageMime[format] ?? null;
776
+ if (bytes && !mime) {
777
+ const svg = renderEmfToSvg(bytes);
778
+ if (svg !== null) {
779
+ bytes = new TextEncoder().encode(svg);
780
+ mime = "image/svg+xml";
781
+ }
782
+ }
533
783
  if (bytes && !mime) mime = mimeFromPartName(getShapeImagePartName(shape));
534
784
  if (bytes && mime) {
535
785
  const dataUrl = `data:${mime};base64,${u8ToBase64(bytes)}`;
@@ -669,7 +919,8 @@ let activeColorMap = null;
669
919
  let activeDeckTextColor = "#000000";
670
920
  const gradientDef = (grad, theme) => {
671
921
  const id = mintId();
672
- const stops = grad.stops.map((s) => `<stop offset="${s.offset.toFixed(4)}" stop-color="${resolveColor(s.color, theme, "#E5E7EB")}"/>`).join("");
922
+ const orderedStops = [...grad.stops].sort((a, b) => a.offset - b.offset);
923
+ const stops = orderedStops.map((s) => `<stop offset="${s.offset.toFixed(4)}" stop-color="${resolveColor(s.color, theme, "#E5E7EB")}"/>`).join("");
673
924
  if (grad.path === "circle" || grad.path === "rect" || grad.path === "shape") {
674
925
  const focus = grad.focus ?? {
675
926
  left: .5,
@@ -679,7 +930,7 @@ const gradientDef = (grad, theme) => {
679
930
  };
680
931
  const cx = (focus.left + focus.right) / 2;
681
932
  const cy = (focus.top + focus.bottom) / 2;
682
- const reversed = grad.stops.slice().reverse().map((s) => `<stop offset="${(1 - s.offset).toFixed(4)}" stop-color="${resolveColor(s.color, theme, "#E5E7EB")}"/>`).join("");
933
+ const reversed = orderedStops.slice().reverse().map((s) => `<stop offset="${(1 - s.offset).toFixed(4)}" stop-color="${resolveColor(s.color, theme, "#E5E7EB")}"/>`).join("");
683
934
  return {
684
935
  defs: `<defs><radialGradient id="${id}" gradientUnits="objectBoundingBox" cx="${cx.toFixed(4)}" cy="${cy.toFixed(4)}" r="${Math.max(.5, Math.max(cx, cy, 1 - cx, 1 - cy)).toFixed(4)}">${reversed}</radialGradient></defs>`,
685
936
  fillAttr: `url(#${id})`
@@ -4685,4 +4936,4 @@ const auditTextLayout = (pres, options = {}) => {
4685
4936
  //#endregion
4686
4937
  export { MONO as a, TIMES as c, substituteFamily as d, AVG_GLYPH_W_RATIO as i, defaultMeasurer as l, renderSlideSvg as n, SANS as o, ARIAL as r, SERIF as s, auditTextLayout as t, isCjk as u };
4687
4938
 
4688
- //# sourceMappingURL=src-C_bsFwWV.js.map
4939
+ //# sourceMappingURL=src-CXGhih2x.js.map