@opengeni/artifact-tool 0.2.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/LICENSE +190 -0
- package/README.md +104 -0
- package/dist/bounded-zip.d.ts +28 -0
- package/dist/chunk-4BSENMAE.js +39 -0
- package/dist/chunk-4BSENMAE.js.map +1 -0
- package/dist/chunk-4FRFNNBM.js +276 -0
- package/dist/chunk-4FRFNNBM.js.map +1 -0
- package/dist/chunk-5XIA6KHZ.js +49 -0
- package/dist/chunk-5XIA6KHZ.js.map +1 -0
- package/dist/chunk-66CT2ZRP.js +2229 -0
- package/dist/chunk-66CT2ZRP.js.map +1 -0
- package/dist/chunk-72SYDU4O.js +319 -0
- package/dist/chunk-72SYDU4O.js.map +1 -0
- package/dist/chunk-B2ZJRZDA.js +279 -0
- package/dist/chunk-B2ZJRZDA.js.map +1 -0
- package/dist/chunk-DCTLTNPW.js +2458 -0
- package/dist/chunk-DCTLTNPW.js.map +1 -0
- package/dist/chunk-FGZM4FQ6.js +473 -0
- package/dist/chunk-FGZM4FQ6.js.map +1 -0
- package/dist/chunk-HTJBLBDE.js +591 -0
- package/dist/chunk-HTJBLBDE.js.map +1 -0
- package/dist/chunk-IYFXZQ34.js +5306 -0
- package/dist/chunk-IYFXZQ34.js.map +1 -0
- package/dist/chunk-J6N7IR44.js +172 -0
- package/dist/chunk-J6N7IR44.js.map +1 -0
- package/dist/chunk-N2IHNWBS.js +1002 -0
- package/dist/chunk-N2IHNWBS.js.map +1 -0
- package/dist/chunk-NAG44K67.js +549 -0
- package/dist/chunk-NAG44K67.js.map +1 -0
- package/dist/chunk-O24KTEGP.js +33 -0
- package/dist/chunk-O24KTEGP.js.map +1 -0
- package/dist/chunk-PI2AZVGK.js +2074 -0
- package/dist/chunk-PI2AZVGK.js.map +1 -0
- package/dist/chunk-Q2FFHEY7.js +40 -0
- package/dist/chunk-Q2FFHEY7.js.map +1 -0
- package/dist/chunk-RBGQADXA.js +57 -0
- package/dist/chunk-RBGQADXA.js.map +1 -0
- package/dist/chunk-U5G2BKM2.js +613 -0
- package/dist/chunk-U5G2BKM2.js.map +1 -0
- package/dist/chunk-VOJHMSM3.js +39 -0
- package/dist/chunk-VOJHMSM3.js.map +1 -0
- package/dist/chunk-VUNV25KB.js +16 -0
- package/dist/chunk-VUNV25KB.js.map +1 -0
- package/dist/chunk-XIFUNVBO.js +101 -0
- package/dist/chunk-XIFUNVBO.js.map +1 -0
- package/dist/chunk-ZCJYRJGB.js +489 -0
- package/dist/chunk-ZCJYRJGB.js.map +1 -0
- package/dist/chunk-ZZXPEE4S.js +2260 -0
- package/dist/chunk-ZZXPEE4S.js.map +1 -0
- package/dist/document-docx-api.d.ts +49 -0
- package/dist/document-docx-codec.d.ts +9 -0
- package/dist/document-docx-codec.js +4765 -0
- package/dist/document-docx-codec.js.map +1 -0
- package/dist/document-docx-import.d.ts +261 -0
- package/dist/document-docx-packer.d.ts +18 -0
- package/dist/document-docx-state.d.ts +12 -0
- package/dist/document-render.d.ts +3 -0
- package/dist/document-render.js +492 -0
- package/dist/document-render.js.map +1 -0
- package/dist/document.d.ts +432 -0
- package/dist/document.js +37 -0
- package/dist/document.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/file-blob.d.ts +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +186 -0
- package/dist/index.js.map +1 -0
- package/dist/inspection.d.ts +12 -0
- package/dist/kernel.d.ts +101 -0
- package/dist/lib-GQLPACFK.js +9746 -0
- package/dist/lib-GQLPACFK.js.map +1 -0
- package/dist/materializer-cli-entry.d.ts +2 -0
- package/dist/materializer-cli-entry.js +29 -0
- package/dist/materializer-cli-entry.js.map +1 -0
- package/dist/materializer-cli.d.ts +24 -0
- package/dist/materializer-cli.js +24 -0
- package/dist/materializer-cli.js.map +1 -0
- package/dist/native-raster.d.ts +10 -0
- package/dist/native.d.ts +129 -0
- package/dist/native.js +31 -0
- package/dist/native.js.map +1 -0
- package/dist/office-import.d.ts +48 -0
- package/dist/office-import.js +154 -0
- package/dist/office-import.js.map +1 -0
- package/dist/presentation-pptx-api.d.ts +60 -0
- package/dist/presentation-pptx-import.d.ts +5 -0
- package/dist/presentation-pptx-state-digest-765CZZED.js +10 -0
- package/dist/presentation-pptx-state-digest-765CZZED.js.map +1 -0
- package/dist/presentation-pptx-state-digest.d.ts +3 -0
- package/dist/presentation-pptx-state.d.ts +4 -0
- package/dist/presentation-pptx-xml.d.ts +29 -0
- package/dist/presentation-pptx.d.ts +5 -0
- package/dist/presentation-pptx.js +2365 -0
- package/dist/presentation-pptx.js.map +1 -0
- package/dist/presentation-render.d.ts +29 -0
- package/dist/presentation-render.js +375 -0
- package/dist/presentation-render.js.map +1 -0
- package/dist/presentation.d.ts +565 -0
- package/dist/presentation.js +71 -0
- package/dist/presentation.js.map +1 -0
- package/dist/production-composite.d.ts +68 -0
- package/dist/production-document.d.ts +2 -0
- package/dist/production-document.js +176 -0
- package/dist/production-document.js.map +1 -0
- package/dist/production-native-codecs.d.ts +94 -0
- package/dist/production-presentation.d.ts +2 -0
- package/dist/production-presentation.js +176 -0
- package/dist/production-presentation.js.map +1 -0
- package/dist/production-sha256.d.ts +2 -0
- package/dist/production-spreadsheet-native.d.ts +12 -0
- package/dist/production-spreadsheet.d.ts +2 -0
- package/dist/production-spreadsheet.js +176 -0
- package/dist/production-spreadsheet.js.map +1 -0
- package/dist/production.d.ts +96 -0
- package/dist/raster-image.d.ts +34 -0
- package/dist/reference.d.ts +17 -0
- package/dist/reference.js +574 -0
- package/dist/reference.js.map +1 -0
- package/dist/runtime-cli-entry.d.ts +2 -0
- package/dist/runtime-cli-entry.js +21 -0
- package/dist/runtime-cli-entry.js.map +1 -0
- package/dist/runtime-cli.d.ts +76 -0
- package/dist/runtime-cli.js +26 -0
- package/dist/runtime-cli.js.map +1 -0
- package/dist/runtime-development.d.ts +73 -0
- package/dist/runtime-development.js +30 -0
- package/dist/runtime-development.js.map +1 -0
- package/dist/runtime-receipt.d.ts +19 -0
- package/dist/runtime.d.ts +355 -0
- package/dist/runtime.js +42 -0
- package/dist/runtime.js.map +1 -0
- package/dist/snapshot.d.ts +26 -0
- package/dist/snapshot.js +61 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/spreadsheet-address.d.ts +18 -0
- package/dist/spreadsheet-file.d.ts +17 -0
- package/dist/spreadsheet-formula.d.ts +67 -0
- package/dist/spreadsheet-image.d.ts +17 -0
- package/dist/spreadsheet-render.d.ts +5 -0
- package/dist/spreadsheet-render.js +752 -0
- package/dist/spreadsheet-render.js.map +1 -0
- package/dist/spreadsheet-types.d.ts +155 -0
- package/dist/spreadsheet-xlsx-api.d.ts +88 -0
- package/dist/spreadsheet-xlsx-codec.d.ts +16 -0
- package/dist/spreadsheet-xlsx-codec.js +16 -0
- package/dist/spreadsheet-xlsx-codec.js.map +1 -0
- package/dist/spreadsheet-xlsx-state.d.ts +16 -0
- package/dist/spreadsheet.d.ts +724 -0
- package/dist/spreadsheet.js +67 -0
- package/dist/spreadsheet.js.map +1 -0
- package/package.json +129 -0
- package/src/bounded-zip.ts +450 -0
- package/src/document-docx-api.ts +64 -0
- package/src/document-docx-codec.ts +2253 -0
- package/src/document-docx-import.ts +3814 -0
- package/src/document-docx-packer.ts +72 -0
- package/src/document-docx-state.ts +59 -0
- package/src/document-render.ts +615 -0
- package/src/document.ts +2800 -0
- package/src/errors.ts +45 -0
- package/src/file-blob.ts +43 -0
- package/src/index.ts +4 -0
- package/src/inspection.ts +50 -0
- package/src/kernel.ts +525 -0
- package/src/materializer-cli-entry.ts +11 -0
- package/src/materializer-cli.ts +743 -0
- package/src/native-raster.ts +141 -0
- package/src/native.ts +390 -0
- package/src/office-import.ts +243 -0
- package/src/presentation-pptx-api.ts +107 -0
- package/src/presentation-pptx-import.ts +2065 -0
- package/src/presentation-pptx-state-digest.ts +46 -0
- package/src/presentation-pptx-state.ts +18 -0
- package/src/presentation-pptx-xml.ts +473 -0
- package/src/presentation-pptx.ts +447 -0
- package/src/presentation-render.ts +532 -0
- package/src/presentation.ts +2599 -0
- package/src/production-composite.ts +805 -0
- package/src/production-document.ts +2 -0
- package/src/production-native-codecs.ts +1141 -0
- package/src/production-presentation.ts +2 -0
- package/src/production-sha256.ts +150 -0
- package/src/production-spreadsheet-native.ts +686 -0
- package/src/production-spreadsheet.ts +2 -0
- package/src/production.ts +1022 -0
- package/src/raster-image.ts +586 -0
- package/src/reference.ts +17 -0
- package/src/runtime-cli-entry.ts +13 -0
- package/src/runtime-cli.ts +670 -0
- package/src/runtime-development.ts +689 -0
- package/src/runtime-receipt.ts +169 -0
- package/src/runtime.ts +1463 -0
- package/src/snapshot.ts +88 -0
- package/src/spreadsheet-address.ts +90 -0
- package/src/spreadsheet-file.ts +71 -0
- package/src/spreadsheet-formula.ts +1212 -0
- package/src/spreadsheet-image.ts +242 -0
- package/src/spreadsheet-render.ts +985 -0
- package/src/spreadsheet-types.ts +171 -0
- package/src/spreadsheet-xlsx-api.ts +123 -0
- package/src/spreadsheet-xlsx-codec.ts +3063 -0
- package/src/spreadsheet-xlsx-state.ts +191 -0
- package/src/spreadsheet.ts +5195 -0
|
@@ -0,0 +1,2599 @@
|
|
|
1
|
+
import { FileBlob } from "./file-blob";
|
|
2
|
+
import { boundInspectionRecords, defineHelpEntries, filterHelpEntries } from "./inspection";
|
|
3
|
+
import type { HelpOptions } from "./spreadsheet-types";
|
|
4
|
+
import { presentationLossState } from "./presentation-pptx-state";
|
|
5
|
+
import {
|
|
6
|
+
canonicalizeRasterDataUrl,
|
|
7
|
+
encodeRasterBase64,
|
|
8
|
+
normalizeRasterContentType,
|
|
9
|
+
RasterImageValidationError,
|
|
10
|
+
validateRasterImageBytes,
|
|
11
|
+
} from "./raster-image";
|
|
12
|
+
export {
|
|
13
|
+
PPTX_MEDIA_TYPE,
|
|
14
|
+
PresentationFidelityError,
|
|
15
|
+
PresentationSecurityError,
|
|
16
|
+
type PresentationFidelityIssue,
|
|
17
|
+
type PresentationLossPreservationEnvelope,
|
|
18
|
+
type PresentationPptxExportOptions,
|
|
19
|
+
type PresentationPptxFeature,
|
|
20
|
+
type PresentationPptxImportLimits,
|
|
21
|
+
type PresentationPptxImportOptions,
|
|
22
|
+
} from "./presentation-pptx-api";
|
|
23
|
+
import type {
|
|
24
|
+
PresentationFidelityIssue,
|
|
25
|
+
PresentationLossPreservationEnvelope,
|
|
26
|
+
PresentationPptxExportOptions,
|
|
27
|
+
PresentationPptxImportOptions,
|
|
28
|
+
} from "./presentation-pptx-api";
|
|
29
|
+
|
|
30
|
+
export type PresentationPosition = {
|
|
31
|
+
left: number;
|
|
32
|
+
top: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type PresentationFill = string | { type?: "solid"; color: string };
|
|
38
|
+
|
|
39
|
+
export type PresentationLine = {
|
|
40
|
+
style?: "solid" | "dash" | "dot" | "none";
|
|
41
|
+
fill?: PresentationFill;
|
|
42
|
+
width?: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type PresentationTextStyle = {
|
|
46
|
+
fontFamily?: string;
|
|
47
|
+
fontSize?: number;
|
|
48
|
+
bold?: boolean;
|
|
49
|
+
italic?: boolean;
|
|
50
|
+
underline?: boolean;
|
|
51
|
+
color?: string;
|
|
52
|
+
alignment?: "left" | "center" | "right" | "justify";
|
|
53
|
+
verticalAlignment?: "top" | "middle" | "bottom";
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type PresentationShapeGeometry =
|
|
57
|
+
| "textbox"
|
|
58
|
+
| "rect"
|
|
59
|
+
| "roundRect"
|
|
60
|
+
| "ellipse"
|
|
61
|
+
| "triangle"
|
|
62
|
+
| "rightArrow"
|
|
63
|
+
| "line";
|
|
64
|
+
|
|
65
|
+
export type PresentationShapeConfig = {
|
|
66
|
+
geometry: PresentationShapeGeometry;
|
|
67
|
+
name?: string;
|
|
68
|
+
position?: Partial<PresentationPosition>;
|
|
69
|
+
fill?: PresentationFill;
|
|
70
|
+
line?: PresentationLine;
|
|
71
|
+
text?: string;
|
|
72
|
+
textStyle?: PresentationTextStyle;
|
|
73
|
+
rotation?: number;
|
|
74
|
+
borderRadius?: number | string;
|
|
75
|
+
placeholder?: { type: string; index?: number };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type PresentationChartType =
|
|
79
|
+
| "bar"
|
|
80
|
+
| "line"
|
|
81
|
+
| "area"
|
|
82
|
+
| "pie"
|
|
83
|
+
| "doughnut"
|
|
84
|
+
| "scatter"
|
|
85
|
+
| "bubble"
|
|
86
|
+
| "radar";
|
|
87
|
+
|
|
88
|
+
export type PresentationChartSeriesConfig = {
|
|
89
|
+
name: string;
|
|
90
|
+
categories?: string[];
|
|
91
|
+
values?: number[];
|
|
92
|
+
xValues?: number[];
|
|
93
|
+
bubbleSizes?: number[];
|
|
94
|
+
fill?: PresentationFill;
|
|
95
|
+
line?: PresentationLine;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type PresentationChartConfig = {
|
|
99
|
+
name?: string;
|
|
100
|
+
position?: Partial<PresentationPosition>;
|
|
101
|
+
title?: string;
|
|
102
|
+
categories?: string[];
|
|
103
|
+
series?: PresentationChartSeriesConfig[];
|
|
104
|
+
hasLegend?: boolean;
|
|
105
|
+
legend?: { position?: "left" | "top" | "topRight" | "right" | "bottom"; overlay?: boolean };
|
|
106
|
+
xAxis?: { visible?: boolean; title?: string; min?: number; max?: number };
|
|
107
|
+
yAxis?: { visible?: boolean; title?: string; min?: number; max?: number };
|
|
108
|
+
dataLabels?: {
|
|
109
|
+
showValue?: boolean;
|
|
110
|
+
showSeriesName?: boolean;
|
|
111
|
+
showCategoryName?: boolean;
|
|
112
|
+
showPercent?: boolean;
|
|
113
|
+
position?: "center" | "inEnd" | "outEnd";
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type PresentationTableCellConfig = {
|
|
118
|
+
text: string;
|
|
119
|
+
fill?: PresentationFill;
|
|
120
|
+
textStyle?: PresentationTextStyle;
|
|
121
|
+
rowSpan?: number;
|
|
122
|
+
colSpan?: number;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type PresentationTableCellInput = string | PresentationTableCellConfig | null;
|
|
126
|
+
|
|
127
|
+
export type PresentationTableConfig = {
|
|
128
|
+
name?: string;
|
|
129
|
+
position?: Partial<PresentationPosition>;
|
|
130
|
+
/** null marks a grid position covered by an earlier rowSpan/colSpan anchor. */
|
|
131
|
+
rows: ReadonlyArray<ReadonlyArray<PresentationTableCellInput>>;
|
|
132
|
+
columnWidths?: readonly number[];
|
|
133
|
+
rowHeights?: readonly number[];
|
|
134
|
+
fill?: PresentationFill;
|
|
135
|
+
line?: PresentationLine;
|
|
136
|
+
textStyle?: PresentationTextStyle;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type PresentationGroupChildConfig =
|
|
140
|
+
| { kind: "shape"; config: PresentationShapeConfig }
|
|
141
|
+
| { kind: "chart"; type: PresentationChartType; config?: PresentationChartConfig }
|
|
142
|
+
| { kind: "image"; config: PresentationImageConfig }
|
|
143
|
+
| { kind: "table"; config: PresentationTableConfig }
|
|
144
|
+
| { kind: "group"; config: PresentationGroupConfig };
|
|
145
|
+
|
|
146
|
+
export type PresentationGroupConfig = {
|
|
147
|
+
name?: string;
|
|
148
|
+
position?: Partial<PresentationPosition>;
|
|
149
|
+
childOffset?: { left: number; top: number };
|
|
150
|
+
childExtent?: { width: number; height: number };
|
|
151
|
+
rotation?: number;
|
|
152
|
+
flipHorizontal?: boolean;
|
|
153
|
+
flipVertical?: boolean;
|
|
154
|
+
children?: readonly PresentationGroupChildConfig[];
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type PresentationTemplateElement = Exclude<PresentationGroupChildConfig, { kind: "group" }>;
|
|
158
|
+
|
|
159
|
+
export type PresentationMasterConfig = {
|
|
160
|
+
name?: string;
|
|
161
|
+
background?: PresentationFill;
|
|
162
|
+
elements?: readonly PresentationTemplateElement[];
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export type PresentationLayoutConfig = PresentationMasterConfig & {
|
|
166
|
+
masterId?: string;
|
|
167
|
+
/** Skill-compatible alias for masterId. */
|
|
168
|
+
parentLayoutId?: string;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
type ConcreteImageSource =
|
|
172
|
+
| { blob: ArrayBuffer | Uint8Array; dataUrl?: never; uri?: never }
|
|
173
|
+
| { dataUrl: string; blob?: never; uri?: never }
|
|
174
|
+
| { uri: string; blob?: never; dataUrl?: never };
|
|
175
|
+
|
|
176
|
+
export type PresentationRasterImageContentType =
|
|
177
|
+
| "image/png"
|
|
178
|
+
| "image/jpeg"
|
|
179
|
+
| "image/gif"
|
|
180
|
+
| "image/webp";
|
|
181
|
+
|
|
182
|
+
export type PresentationImageResolution = {
|
|
183
|
+
blob: ArrayBuffer | Uint8Array;
|
|
184
|
+
contentType?: PresentationRasterImageContentType;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export type PresentationImageResolver = (reference: {
|
|
188
|
+
uri: string;
|
|
189
|
+
imageId: string;
|
|
190
|
+
name: string;
|
|
191
|
+
}) => Promise<PresentationImageResolution> | PresentationImageResolution;
|
|
192
|
+
|
|
193
|
+
export type PresentationCreateOptions = {
|
|
194
|
+
slideSize?: { width: number; height: number };
|
|
195
|
+
/** Host-owned resolver; it is invoked only by an explicit image.resolveUri() call. */
|
|
196
|
+
imageResolver?: PresentationImageResolver;
|
|
197
|
+
/** URI schemes the host resolver owns, for example `["asset"]`. Empty by default. */
|
|
198
|
+
allowedImageUriSchemes?: readonly string[];
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export type PresentationImageConfig =
|
|
202
|
+
| (ConcreteImageSource & PresentationImageMetadata)
|
|
203
|
+
| ({ prompt: string; blob?: never; dataUrl?: never; uri?: never } & PresentationImageMetadata);
|
|
204
|
+
|
|
205
|
+
type PresentationImageMetadata = {
|
|
206
|
+
name?: string;
|
|
207
|
+
prompt?: string;
|
|
208
|
+
contentType?: string;
|
|
209
|
+
alt?: string;
|
|
210
|
+
fit?: "contain" | "cover";
|
|
211
|
+
position?: Partial<PresentationPosition>;
|
|
212
|
+
frame?: Partial<PresentationPosition>;
|
|
213
|
+
crop?: { left: number; top: number; right: number; bottom: number };
|
|
214
|
+
geometry?: "rect" | "roundRect" | "ellipse";
|
|
215
|
+
borderRadius?: number | string;
|
|
216
|
+
rotation?: number;
|
|
217
|
+
flipHorizontal?: boolean;
|
|
218
|
+
flipVertical?: boolean;
|
|
219
|
+
lockAspectRatio?: boolean;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export type PresentationInspectOptions = {
|
|
223
|
+
target?: { id: string; beforeLines?: number; afterLines?: number };
|
|
224
|
+
kind?: string;
|
|
225
|
+
include?: string;
|
|
226
|
+
exclude?: string;
|
|
227
|
+
search?: string;
|
|
228
|
+
maxChars?: number;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export type PresentationInspectResult = {
|
|
232
|
+
ndjson: string;
|
|
233
|
+
records: Array<Record<string, unknown>>;
|
|
234
|
+
truncated: boolean;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export type PresentationResolvedObject =
|
|
238
|
+
| Presentation
|
|
239
|
+
| Slide
|
|
240
|
+
| PresentationShape
|
|
241
|
+
| PresentationChart
|
|
242
|
+
| PresentationImage
|
|
243
|
+
| PresentationTable
|
|
244
|
+
| PresentationGroup
|
|
245
|
+
| PresentationMaster
|
|
246
|
+
| PresentationLayout;
|
|
247
|
+
|
|
248
|
+
export type PresentationElement =
|
|
249
|
+
| PresentationShape
|
|
250
|
+
| PresentationChart
|
|
251
|
+
| PresentationImage
|
|
252
|
+
| PresentationTable
|
|
253
|
+
| PresentationGroup;
|
|
254
|
+
|
|
255
|
+
export type PresentationExportOptions = {
|
|
256
|
+
slide?: Slide;
|
|
257
|
+
format: "svg" | "png" | "layout" | "webp";
|
|
258
|
+
scale?: number;
|
|
259
|
+
montage?: boolean;
|
|
260
|
+
columns?: number;
|
|
261
|
+
gap?: number;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export class UnsupportedPresentationFeatureError extends Error {
|
|
265
|
+
readonly code = "UNSUPPORTED_PRESENTATION_FEATURE";
|
|
266
|
+
|
|
267
|
+
constructor(feature: string, detail?: string) {
|
|
268
|
+
super(`Unsupported presentation feature: ${feature}${detail ? `. ${detail}` : ""}`);
|
|
269
|
+
this.name = "UnsupportedPresentationFeatureError";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Untrusted presentation input was rejected before reaching a renderer or file codec. */
|
|
274
|
+
export class InvalidPresentationInputError extends Error {
|
|
275
|
+
readonly code = "INVALID_PRESENTATION_INPUT";
|
|
276
|
+
|
|
277
|
+
constructor(
|
|
278
|
+
readonly field: string,
|
|
279
|
+
detail: string,
|
|
280
|
+
) {
|
|
281
|
+
super(`Invalid presentation ${field}: ${detail}`);
|
|
282
|
+
this.name = "InvalidPresentationInputError";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const PRESENTATION_HELP_ENTRIES = defineHelpEntries([
|
|
287
|
+
["Presentation.create", "Create editable deck", "Presentation.create()"],
|
|
288
|
+
["deck.slides.add", "Add slide", "deck.slides.add()"],
|
|
289
|
+
[
|
|
290
|
+
"slide.shapes.add",
|
|
291
|
+
"Add positioned text or shape",
|
|
292
|
+
'slide.shapes.add({ geometry: "textbox", text, position })',
|
|
293
|
+
],
|
|
294
|
+
[
|
|
295
|
+
"slide.charts.add",
|
|
296
|
+
"Add chart data",
|
|
297
|
+
'slide.charts.add("bar", { categories, series: [{ name, values }] })',
|
|
298
|
+
],
|
|
299
|
+
["slide.tables.add", "Add table", "slide.tables.add({ rows, position })"],
|
|
300
|
+
["slide.images.add", "Add image", "slide.images.add({ dataUrl, name, position })"],
|
|
301
|
+
[
|
|
302
|
+
"deck.inspect",
|
|
303
|
+
"Inspect deck records",
|
|
304
|
+
'await deck.inspect({ kind: "slide,textbox,chart,table" })',
|
|
305
|
+
],
|
|
306
|
+
[
|
|
307
|
+
"deck.export",
|
|
308
|
+
"Export SVG, PNG, WebP, or layout",
|
|
309
|
+
'await deck.export({ format: "png", montage: true })',
|
|
310
|
+
],
|
|
311
|
+
[
|
|
312
|
+
"PresentationFile.importPptx",
|
|
313
|
+
"Import editable PPTX",
|
|
314
|
+
"await PresentationFile.importPptx(blob)",
|
|
315
|
+
],
|
|
316
|
+
["PresentationFile.exportPptx", "Export PPTX", "await PresentationFile.exportPptx(deck)"],
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Skill-compatible TypeScript reference presentation. Native production
|
|
321
|
+
* presentation editing remains unsupported and must fail closed until the
|
|
322
|
+
* Rust modality is feature-complete.
|
|
323
|
+
*/
|
|
324
|
+
export class Presentation {
|
|
325
|
+
readonly slides: SlideCollection;
|
|
326
|
+
readonly masters: PresentationMasterCollection;
|
|
327
|
+
readonly layouts: PresentationLayoutCollection;
|
|
328
|
+
readonly slideSize: { width: number; height: number };
|
|
329
|
+
private readonly objects = new Map<string, unknown>();
|
|
330
|
+
private readonly imageResolver: PresentationImageResolver | undefined;
|
|
331
|
+
private readonly allowedImageUriSchemes: ReadonlySet<string>;
|
|
332
|
+
private nextId = 1;
|
|
333
|
+
|
|
334
|
+
private constructor(options: PresentationCreateOptions = {}) {
|
|
335
|
+
this.slideSize = options.slideSize ?? { width: 1280, height: 720 };
|
|
336
|
+
validatePosition(
|
|
337
|
+
{ left: 0, top: 0, width: this.slideSize.width, height: this.slideSize.height },
|
|
338
|
+
"slideSize",
|
|
339
|
+
);
|
|
340
|
+
this.imageResolver = options.imageResolver;
|
|
341
|
+
this.allowedImageUriSchemes = validateImageUriPolicy(
|
|
342
|
+
options.imageResolver,
|
|
343
|
+
options.allowedImageUriSchemes,
|
|
344
|
+
);
|
|
345
|
+
this.slides = new SlideCollection(this);
|
|
346
|
+
this.masters = new PresentationMasterCollection(this);
|
|
347
|
+
this.layouts = new PresentationLayoutCollection(this);
|
|
348
|
+
this.register("pr", this);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
static create(options: PresentationCreateOptions = {}): Presentation {
|
|
352
|
+
return new Presentation(options);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
assertImageUriAccepted(uri: string): void {
|
|
356
|
+
assertSafeText(uri, "image uri", MAX_IMAGE_URI_LENGTH);
|
|
357
|
+
const scheme = imageUriScheme(uri);
|
|
358
|
+
if (!this.imageResolver || !this.allowedImageUriSchemes.has(scheme)) {
|
|
359
|
+
throw new InvalidPresentationInputError(
|
|
360
|
+
"image uri",
|
|
361
|
+
"requires an injected resolver and an explicitly allowed URI scheme",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async resolveImageUri(reference: {
|
|
367
|
+
uri: string;
|
|
368
|
+
imageId: string;
|
|
369
|
+
name: string;
|
|
370
|
+
}): Promise<PresentationImageResolution> {
|
|
371
|
+
this.assertImageUriAccepted(reference.uri);
|
|
372
|
+
if (!this.imageResolver) {
|
|
373
|
+
throw new InvalidPresentationInputError("image resolver", "is not configured");
|
|
374
|
+
}
|
|
375
|
+
return await this.imageResolver(reference);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
allocateId(
|
|
379
|
+
prefix: "sl" | "sh" | "ch" | "im" | "tb" | "gp" | "mt" | "ly",
|
|
380
|
+
value: unknown,
|
|
381
|
+
): string {
|
|
382
|
+
return this.register(prefix, value);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
resolve<T extends PresentationResolvedObject = PresentationResolvedObject>(id: string): T {
|
|
386
|
+
const value = this.objects.get(id);
|
|
387
|
+
if (value === undefined) throw new Error(`Unknown presentation object id: ${id}`);
|
|
388
|
+
return value as T;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async inspect(options: PresentationInspectOptions = {}): Promise<PresentationInspectResult> {
|
|
392
|
+
const requested = new Set(
|
|
393
|
+
(options.kind ?? "deck,master,layout,slide,textbox,shape,image,chart,table,group,notes")
|
|
394
|
+
.split(",")
|
|
395
|
+
.map((value) => value.trim())
|
|
396
|
+
.filter(Boolean)
|
|
397
|
+
.map((value) => (value === "comments" ? "thread" : value)),
|
|
398
|
+
);
|
|
399
|
+
const records: Array<Record<string, unknown>> = [];
|
|
400
|
+
if (requested.has("deck")) {
|
|
401
|
+
records.push({
|
|
402
|
+
kind: "deck",
|
|
403
|
+
id: "pr/1",
|
|
404
|
+
slides: this.slides.items.length,
|
|
405
|
+
width: this.slideSize.width,
|
|
406
|
+
height: this.slideSize.height,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (requested.has("master")) {
|
|
410
|
+
for (const master of this.masters.items) records.push(master.inspectRecord());
|
|
411
|
+
}
|
|
412
|
+
if (requested.has("layout")) {
|
|
413
|
+
for (const layout of this.layouts.items) records.push(layout.inspectRecord());
|
|
414
|
+
}
|
|
415
|
+
for (const [index, slide] of this.slides.items.entries()) {
|
|
416
|
+
if (requested.has("slide")) records.push(slide.inspectRecord(index));
|
|
417
|
+
for (const shape of slide.shapes.items) {
|
|
418
|
+
const kind = shape.geometry === "textbox" ? "textbox" : "shape";
|
|
419
|
+
if (requested.has(kind) || (kind === "textbox" && requested.has("shape"))) {
|
|
420
|
+
records.push(shape.inspectRecord(index));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (requested.has("chart")) {
|
|
424
|
+
for (const chart of slide.charts.items) records.push(chart.inspectRecord(index));
|
|
425
|
+
}
|
|
426
|
+
if (requested.has("image")) {
|
|
427
|
+
for (const image of slide.images.items) records.push(image.inspectRecord(index));
|
|
428
|
+
}
|
|
429
|
+
if (requested.has("table")) {
|
|
430
|
+
for (const table of slide.tables.items) records.push(table.inspectRecord(index));
|
|
431
|
+
}
|
|
432
|
+
if (requested.has("group")) {
|
|
433
|
+
for (const group of slide.groups.items) records.push(group.inspectRecord(index));
|
|
434
|
+
}
|
|
435
|
+
if (requested.has("notes") && slide.notes.toString().length > 0) {
|
|
436
|
+
records.push({
|
|
437
|
+
kind: "notes",
|
|
438
|
+
id: `${slide.id}/notes`,
|
|
439
|
+
slide: index + 1,
|
|
440
|
+
text: slide.notes.toString(),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const search = options.search?.toLocaleLowerCase();
|
|
446
|
+
let filtered = search
|
|
447
|
+
? records.filter((record) => JSON.stringify(record).toLocaleLowerCase().includes(search))
|
|
448
|
+
: records;
|
|
449
|
+
if (options.target) filtered = filtered.filter((record) => record.id === options.target!.id);
|
|
450
|
+
filtered = filtered.map((record) => projectRecord(record, options.include, options.exclude));
|
|
451
|
+
return boundPresentationRecords(filtered, options.maxChars ?? 20_000);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
help(query: string, options: HelpOptions = {}): PresentationInspectResult {
|
|
455
|
+
if (typeof query !== "string") {
|
|
456
|
+
throw new InvalidPresentationInputError("help query", "must be a string");
|
|
457
|
+
}
|
|
458
|
+
const records = filterHelpEntries(PRESENTATION_HELP_ENTRIES, query, options.search);
|
|
459
|
+
return boundPresentationRecords(records, options.maxChars ?? 6_000);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async export(options: PresentationExportOptions): Promise<FileBlob> {
|
|
463
|
+
if (!options || typeof options !== "object") {
|
|
464
|
+
throw new InvalidPresentationInputError("export options", "must be an object");
|
|
465
|
+
}
|
|
466
|
+
if (!(["svg", "png", "layout", "webp"] as const).includes(options.format)) {
|
|
467
|
+
throw new InvalidPresentationInputError("export format", "is not supported");
|
|
468
|
+
}
|
|
469
|
+
validatePresentation(this, options.format !== "layout");
|
|
470
|
+
if (options.format === "layout") {
|
|
471
|
+
if (options.slide) {
|
|
472
|
+
this.assertOwns(options.slide);
|
|
473
|
+
return jsonBlob(
|
|
474
|
+
options.slide.layoutSnapshot(this.slides.items.indexOf(options.slide)),
|
|
475
|
+
`slide-${this.slides.items.indexOf(options.slide) + 1}.layout.json`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
return jsonBlob(this.layoutSnapshot(), "presentation.layout.json");
|
|
479
|
+
}
|
|
480
|
+
const scale = options.scale ?? 1;
|
|
481
|
+
validateRenderScale(scale);
|
|
482
|
+
if (options.montage) {
|
|
483
|
+
const dimensions = montageDimensions(this, options.columns, options.gap);
|
|
484
|
+
if (options.format !== "svg") {
|
|
485
|
+
assertRenderPixelBudget(dimensions.width, dimensions.height, scale);
|
|
486
|
+
}
|
|
487
|
+
const { executePresentationRender } =
|
|
488
|
+
await import("@opengeni/artifact-tool/presentation/render");
|
|
489
|
+
return await executePresentationRender({
|
|
490
|
+
kind: "montage",
|
|
491
|
+
presentation: this,
|
|
492
|
+
format: options.format,
|
|
493
|
+
scale,
|
|
494
|
+
name: `presentation-montage.${options.format}`,
|
|
495
|
+
dimensions,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const slide =
|
|
499
|
+
options.slide ?? (this.slides.items.length === 1 ? this.slides.items[0] : undefined);
|
|
500
|
+
if (!slide) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
"A slide is required when exporting a multi-slide presentation without montage: true",
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
this.assertOwns(slide);
|
|
506
|
+
if (options.format !== "svg") {
|
|
507
|
+
assertRenderPixelBudget(this.slideSize.width, this.slideSize.height, scale);
|
|
508
|
+
}
|
|
509
|
+
const { executePresentationRender } =
|
|
510
|
+
await import("@opengeni/artifact-tool/presentation/render");
|
|
511
|
+
return await executePresentationRender({
|
|
512
|
+
kind: "slide",
|
|
513
|
+
slide,
|
|
514
|
+
format: options.format,
|
|
515
|
+
scale,
|
|
516
|
+
name: `slide-${this.slides.items.indexOf(slide) + 1}.${options.format}`,
|
|
517
|
+
dimensions: this.slideSize,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
layoutSnapshot(): Record<string, unknown> {
|
|
522
|
+
return {
|
|
523
|
+
kind: "presentation-layout",
|
|
524
|
+
slideSize: { ...this.slideSize },
|
|
525
|
+
masters: this.masters.items.map((master) => master.toProto()),
|
|
526
|
+
layouts: this.layouts.items.map((layout) => layout.toProto()),
|
|
527
|
+
slides: this.slides.items.map((slide, index) => slide.layoutSnapshot(index)),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private register(prefix: string, value: unknown): string {
|
|
532
|
+
const id = `${prefix}/${this.nextId++}`;
|
|
533
|
+
this.objects.set(id, value);
|
|
534
|
+
return id;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private assertOwns(slide: Slide): void {
|
|
538
|
+
if (slide.presentation !== this) throw new Error("Slide belongs to another presentation");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
class PresentationPlaceholderCollection {
|
|
543
|
+
constructor(private readonly elements: readonly PresentationTemplateElement[]) {}
|
|
544
|
+
|
|
545
|
+
summary(): { count: number; names: string[] } {
|
|
546
|
+
const names = this.elements
|
|
547
|
+
.filter(
|
|
548
|
+
(element): element is Extract<PresentationTemplateElement, { kind: "shape" }> =>
|
|
549
|
+
element.kind === "shape" && element.config.placeholder !== undefined,
|
|
550
|
+
)
|
|
551
|
+
.map((element) => element.config.name ?? element.config.placeholder?.type ?? "Placeholder");
|
|
552
|
+
return { count: names.length, names };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export class PresentationMaster {
|
|
557
|
+
readonly id: string;
|
|
558
|
+
name: string;
|
|
559
|
+
background: PresentationFill;
|
|
560
|
+
readonly elements: PresentationTemplateElement[];
|
|
561
|
+
readonly placeholders: PresentationPlaceholderCollection;
|
|
562
|
+
|
|
563
|
+
constructor(
|
|
564
|
+
readonly presentation: Presentation,
|
|
565
|
+
config: PresentationMasterConfig = {},
|
|
566
|
+
) {
|
|
567
|
+
this.id = presentation.allocateId("mt", this);
|
|
568
|
+
this.name = config.name ?? this.id;
|
|
569
|
+
assertSafeText(this.name, "master name", 512);
|
|
570
|
+
this.background = config.background ?? "white";
|
|
571
|
+
colorValue(this.background);
|
|
572
|
+
this.elements = (config.elements ?? []).map(cloneTemplateElement);
|
|
573
|
+
this.placeholders = new PresentationPlaceholderCollection(this.elements);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
inspectRecord(): Record<string, unknown> {
|
|
577
|
+
return {
|
|
578
|
+
kind: "master",
|
|
579
|
+
id: this.id,
|
|
580
|
+
name: this.name,
|
|
581
|
+
elements: this.elements.length,
|
|
582
|
+
placeholders: this.placeholders.summary(),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
toProto(): Record<string, unknown> {
|
|
587
|
+
return {
|
|
588
|
+
id: this.id,
|
|
589
|
+
type: "master",
|
|
590
|
+
name: this.name,
|
|
591
|
+
background: this.background,
|
|
592
|
+
elements: this.elements.map(cloneTemplateElement),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export class PresentationLayout {
|
|
598
|
+
readonly id: string;
|
|
599
|
+
name: string;
|
|
600
|
+
masterId: string | undefined;
|
|
601
|
+
background: PresentationFill;
|
|
602
|
+
readonly elements: PresentationTemplateElement[];
|
|
603
|
+
readonly placeholders: PresentationPlaceholderCollection;
|
|
604
|
+
|
|
605
|
+
get parentLayoutId(): string | undefined {
|
|
606
|
+
return this.masterId;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
constructor(
|
|
610
|
+
readonly presentation: Presentation,
|
|
611
|
+
config: PresentationLayoutConfig = {},
|
|
612
|
+
) {
|
|
613
|
+
this.id = presentation.allocateId("ly", this);
|
|
614
|
+
this.name = config.name ?? this.id;
|
|
615
|
+
assertSafeText(this.name, "layout name", 512);
|
|
616
|
+
if (
|
|
617
|
+
config.masterId !== undefined &&
|
|
618
|
+
config.parentLayoutId !== undefined &&
|
|
619
|
+
config.masterId !== config.parentLayoutId
|
|
620
|
+
) {
|
|
621
|
+
throw new InvalidPresentationInputError(
|
|
622
|
+
"layout masterId",
|
|
623
|
+
"conflicts with the parentLayoutId compatibility alias",
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
this.masterId = config.masterId ?? config.parentLayoutId;
|
|
627
|
+
if (this.masterId !== undefined) assertSafeText(this.masterId, "layout master id", 512);
|
|
628
|
+
this.background = config.background ?? "white";
|
|
629
|
+
colorValue(this.background);
|
|
630
|
+
this.elements = (config.elements ?? []).map(cloneTemplateElement);
|
|
631
|
+
this.placeholders = new PresentationPlaceholderCollection(this.elements);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
inspectRecord(): Record<string, unknown> {
|
|
635
|
+
return {
|
|
636
|
+
kind: "layout",
|
|
637
|
+
id: this.id,
|
|
638
|
+
name: this.name,
|
|
639
|
+
masterId: this.masterId,
|
|
640
|
+
parentLayoutId: this.masterId,
|
|
641
|
+
elements: this.elements.length,
|
|
642
|
+
placeholders: this.placeholders.summary(),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
toProto(): Record<string, unknown> {
|
|
647
|
+
return {
|
|
648
|
+
id: this.id,
|
|
649
|
+
type: "layout",
|
|
650
|
+
name: this.name,
|
|
651
|
+
masterId: this.masterId,
|
|
652
|
+
parentLayoutId: this.masterId,
|
|
653
|
+
background: this.background,
|
|
654
|
+
elements: this.elements.map(cloneTemplateElement),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export class PresentationMasterCollection {
|
|
660
|
+
readonly items: PresentationMaster[] = [];
|
|
661
|
+
|
|
662
|
+
constructor(private readonly presentation: Presentation) {}
|
|
663
|
+
|
|
664
|
+
add(config: PresentationMasterConfig = {}): PresentationMaster {
|
|
665
|
+
const master = new PresentationMaster(this.presentation, config);
|
|
666
|
+
this.items.push(master);
|
|
667
|
+
return master;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
getItem(index: number): PresentationMaster {
|
|
671
|
+
const master = this.items[index];
|
|
672
|
+
if (!master) throw new Error(`Presentation master index out of range: ${index}`);
|
|
673
|
+
return master;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export class PresentationLayoutCollection {
|
|
678
|
+
readonly items: PresentationLayout[] = [];
|
|
679
|
+
|
|
680
|
+
constructor(private readonly presentation: Presentation) {}
|
|
681
|
+
|
|
682
|
+
add(config: PresentationLayoutConfig = {}): PresentationLayout {
|
|
683
|
+
if (
|
|
684
|
+
(config.masterId ?? config.parentLayoutId) !== undefined &&
|
|
685
|
+
!this.presentation.masters.items.some(
|
|
686
|
+
(master) => master.id === (config.masterId ?? config.parentLayoutId),
|
|
687
|
+
)
|
|
688
|
+
) {
|
|
689
|
+
throw new InvalidPresentationInputError(
|
|
690
|
+
"layout parentLayoutId",
|
|
691
|
+
"must reference a master in the same presentation",
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
const layout = new PresentationLayout(this.presentation, config);
|
|
695
|
+
this.items.push(layout);
|
|
696
|
+
return layout;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
getItem(index: number): PresentationLayout {
|
|
700
|
+
const layout = this.items[index];
|
|
701
|
+
if (!layout) throw new Error(`Presentation layout index out of range: ${index}`);
|
|
702
|
+
return layout;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export class SlideCollection {
|
|
707
|
+
readonly items: Slide[] = [];
|
|
708
|
+
|
|
709
|
+
constructor(private readonly presentation: Presentation) {}
|
|
710
|
+
|
|
711
|
+
add(): Slide {
|
|
712
|
+
const slide = new Slide(this.presentation);
|
|
713
|
+
this.items.push(slide);
|
|
714
|
+
return slide;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
getItem(index: number): Slide {
|
|
718
|
+
const slide = this.items[index];
|
|
719
|
+
if (!slide) throw new Error(`Slide index out of range: ${index}`);
|
|
720
|
+
return slide;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export class Slide {
|
|
725
|
+
readonly id: string;
|
|
726
|
+
readonly shapes: PresentationShapeCollection;
|
|
727
|
+
readonly charts: PresentationChartCollection;
|
|
728
|
+
readonly images: PresentationImageCollection;
|
|
729
|
+
readonly tables: PresentationTableCollection;
|
|
730
|
+
readonly groups: PresentationGroupCollection;
|
|
731
|
+
readonly notes = new PresentationText();
|
|
732
|
+
readonly placeholders: SlidePlaceholderCollection;
|
|
733
|
+
/** Canonical cross-type z-order. Typed collections are filtered indexes over this list. */
|
|
734
|
+
readonly elements: PresentationElement[] = [];
|
|
735
|
+
readonly background: { fill: PresentationFill } = { fill: "white" };
|
|
736
|
+
title = "";
|
|
737
|
+
layout: PresentationLayout | undefined;
|
|
738
|
+
|
|
739
|
+
constructor(readonly presentation: Presentation) {
|
|
740
|
+
this.id = presentation.allocateId("sl", this);
|
|
741
|
+
this.shapes = new PresentationShapeCollection(this);
|
|
742
|
+
this.charts = new PresentationChartCollection(this);
|
|
743
|
+
this.images = new PresentationImageCollection(this);
|
|
744
|
+
this.tables = new PresentationTableCollection(this);
|
|
745
|
+
this.groups = new PresentationGroupCollection(this);
|
|
746
|
+
this.placeholders = new SlidePlaceholderCollection(this);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
get speakerNotes(): PresentationText {
|
|
750
|
+
return this.notes;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
setLayout(layout: PresentationLayout | undefined): void {
|
|
754
|
+
if (layout && layout.presentation !== this.presentation) {
|
|
755
|
+
throw new InvalidPresentationInputError("slide layout", "belongs to another presentation");
|
|
756
|
+
}
|
|
757
|
+
this.layout = layout;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
appendElement(element: PresentationElement): void {
|
|
761
|
+
if (element.slide !== this) {
|
|
762
|
+
throw new InvalidPresentationInputError("slide element", "belongs to another slide");
|
|
763
|
+
}
|
|
764
|
+
this.elements.push(element);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async export(options: Omit<PresentationExportOptions, "slide" | "montage">): Promise<FileBlob> {
|
|
768
|
+
return await this.presentation.export({ ...options, slide: this });
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
inspectRecord(index: number): Record<string, unknown> {
|
|
772
|
+
const textShapes = this.shapes.items.filter((shape) => shape.text.toString().length > 0);
|
|
773
|
+
return {
|
|
774
|
+
kind: "slide",
|
|
775
|
+
id: this.id,
|
|
776
|
+
slide: index + 1,
|
|
777
|
+
title: this.title,
|
|
778
|
+
textShapes: textShapes.length,
|
|
779
|
+
charts: this.charts.items.length,
|
|
780
|
+
images: this.images.items.length,
|
|
781
|
+
tables: this.tables.items.length,
|
|
782
|
+
groups: this.groups.items.length,
|
|
783
|
+
layoutId: this.layout?.id,
|
|
784
|
+
notesChars: this.notes.toString().length,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
layoutSnapshot(index: number): Record<string, unknown> {
|
|
789
|
+
return {
|
|
790
|
+
id: this.id,
|
|
791
|
+
slide: index + 1,
|
|
792
|
+
background: { ...this.background },
|
|
793
|
+
layoutId: this.layout?.id,
|
|
794
|
+
notes: this.notes.toString(),
|
|
795
|
+
elements: [...this.elements.map((element) => element.layoutSnapshot())],
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export class SlidePlaceholderCollection {
|
|
801
|
+
constructor(private readonly slide: Slide) {}
|
|
802
|
+
|
|
803
|
+
get items(): PresentationShape[] {
|
|
804
|
+
return this.slide.shapes.items.filter((shape) => shape.placeholder !== undefined);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
summary(): { count: number; types: Record<string, number> } {
|
|
808
|
+
const types: Record<string, number> = {};
|
|
809
|
+
for (const shape of this.items) {
|
|
810
|
+
const type = shape.placeholder?.type ?? "unknown";
|
|
811
|
+
types[type] = (types[type] ?? 0) + 1;
|
|
812
|
+
}
|
|
813
|
+
return { count: this.items.length, types };
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export class PresentationText {
|
|
818
|
+
private value = "";
|
|
819
|
+
private styleValue: PresentationTextStyle = {};
|
|
820
|
+
|
|
821
|
+
constructor(initialValue = "", style: PresentationTextStyle = {}) {
|
|
822
|
+
this.set(initialValue);
|
|
823
|
+
this.style = style;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
get style(): PresentationTextStyle {
|
|
827
|
+
return this.styleValue;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
set style(value: PresentationTextStyle) {
|
|
831
|
+
validateTextStyle(value, "textStyle");
|
|
832
|
+
this.styleValue = { ...value };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
set(value: string): void {
|
|
836
|
+
assertSafeText(value, "text");
|
|
837
|
+
this.value = value;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
replace(searchValue: string | RegExp, replaceValue: string): number {
|
|
841
|
+
const before = this.value;
|
|
842
|
+
let next: string;
|
|
843
|
+
if (typeof searchValue === "string") {
|
|
844
|
+
if (searchValue.length === 0) throw new Error("Text replacement search cannot be empty");
|
|
845
|
+
next = this.value.split(searchValue).join(replaceValue);
|
|
846
|
+
} else {
|
|
847
|
+
next = this.value.replace(searchValue, replaceValue);
|
|
848
|
+
}
|
|
849
|
+
assertSafeText(next, "text");
|
|
850
|
+
this.value = next;
|
|
851
|
+
return before === next ? 0 : 1;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
toString(): string {
|
|
855
|
+
return this.value;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
toJSON(): string {
|
|
859
|
+
return this.value;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export class PresentationShape {
|
|
864
|
+
readonly id: string;
|
|
865
|
+
readonly geometry: PresentationShapeGeometry;
|
|
866
|
+
name: string;
|
|
867
|
+
position: PresentationPosition;
|
|
868
|
+
fill: PresentationFill;
|
|
869
|
+
line: PresentationLine;
|
|
870
|
+
rotation: number;
|
|
871
|
+
borderRadius: number | string | undefined;
|
|
872
|
+
placeholder: { type: string; index?: number } | undefined;
|
|
873
|
+
private readonly textValue: PresentationText;
|
|
874
|
+
|
|
875
|
+
constructor(
|
|
876
|
+
readonly slide: Slide,
|
|
877
|
+
config: PresentationShapeConfig,
|
|
878
|
+
) {
|
|
879
|
+
if (!PRESENTATION_GEOMETRIES.has(config.geometry)) {
|
|
880
|
+
throw new InvalidPresentationInputError("shape geometry", "is not supported");
|
|
881
|
+
}
|
|
882
|
+
this.id = slide.presentation.allocateId("sh", this);
|
|
883
|
+
this.geometry = config.geometry;
|
|
884
|
+
this.name = config.name ?? this.id;
|
|
885
|
+
assertSafeText(this.name, "shape name");
|
|
886
|
+
this.position = normalizePosition(config.position);
|
|
887
|
+
this.fill =
|
|
888
|
+
config.fill ??
|
|
889
|
+
(config.geometry === "textbox" || config.geometry === "line" ? "none" : "white");
|
|
890
|
+
this.line = {
|
|
891
|
+
style: "solid",
|
|
892
|
+
fill: config.geometry === "textbox" ? "none" : "slate-900",
|
|
893
|
+
width: 1,
|
|
894
|
+
...config.line,
|
|
895
|
+
};
|
|
896
|
+
colorValue(this.fill);
|
|
897
|
+
validateLine(this.line, "shape line");
|
|
898
|
+
this.rotation = finiteOr(config.rotation, 0);
|
|
899
|
+
validateRotation(this.rotation, "shape rotation");
|
|
900
|
+
this.borderRadius = config.borderRadius;
|
|
901
|
+
validateBorderRadius(this.borderRadius, "shape borderRadius");
|
|
902
|
+
this.placeholder = normalizePlaceholder(config.placeholder);
|
|
903
|
+
this.textValue = new PresentationText(config.text, config.textStyle);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
get text(): PresentationText {
|
|
907
|
+
return this.textValue;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
set text(value: string | PresentationText) {
|
|
911
|
+
this.textValue.set(value.toString());
|
|
912
|
+
if (value instanceof PresentationText) this.textValue.style = value.style;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
inspectRecord(slideIndex: number): Record<string, unknown> {
|
|
916
|
+
const text = this.text.toString();
|
|
917
|
+
return {
|
|
918
|
+
kind: this.geometry === "textbox" ? "textbox" : "shape",
|
|
919
|
+
id: this.id,
|
|
920
|
+
slide: slideIndex + 1,
|
|
921
|
+
name: this.name,
|
|
922
|
+
geometry: this.geometry,
|
|
923
|
+
text,
|
|
924
|
+
textPreview: truncate(text, 240),
|
|
925
|
+
textChars: text.length,
|
|
926
|
+
textLines: text.length === 0 ? 0 : text.split("\n").length,
|
|
927
|
+
bbox: positionTuple(this.position),
|
|
928
|
+
bboxUnit: "px",
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
layoutSnapshot(): Record<string, unknown> {
|
|
933
|
+
return {
|
|
934
|
+
kind: this.geometry === "textbox" ? "textbox" : "shape",
|
|
935
|
+
id: this.id,
|
|
936
|
+
name: this.name,
|
|
937
|
+
geometry: this.geometry,
|
|
938
|
+
position: { ...this.position },
|
|
939
|
+
fill: this.fill,
|
|
940
|
+
line: { ...this.line },
|
|
941
|
+
rotation: this.rotation,
|
|
942
|
+
text: this.text.toString(),
|
|
943
|
+
textStyle: { ...this.text.style },
|
|
944
|
+
placeholder: this.placeholder ? { ...this.placeholder } : undefined,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export class PresentationShapeCollection {
|
|
950
|
+
readonly items: PresentationShape[] = [];
|
|
951
|
+
|
|
952
|
+
constructor(private readonly slide: Slide) {}
|
|
953
|
+
|
|
954
|
+
add(config: PresentationShapeConfig): PresentationShape {
|
|
955
|
+
const shape = new PresentationShape(this.slide, config);
|
|
956
|
+
this.items.push(shape);
|
|
957
|
+
this.slide.appendElement(shape);
|
|
958
|
+
return shape;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export class PresentationChartSeries {
|
|
963
|
+
name: string;
|
|
964
|
+
categories: string[];
|
|
965
|
+
values: number[];
|
|
966
|
+
xValues: number[];
|
|
967
|
+
bubbleSizes: number[];
|
|
968
|
+
fill: PresentationFill | undefined;
|
|
969
|
+
line: PresentationLine | undefined;
|
|
970
|
+
|
|
971
|
+
constructor(config: PresentationChartSeriesConfig) {
|
|
972
|
+
this.name = config.name;
|
|
973
|
+
assertSafeText(this.name, "chart series name");
|
|
974
|
+
this.categories = [...(config.categories ?? [])];
|
|
975
|
+
for (const category of this.categories) assertSafeText(category, "chart series category");
|
|
976
|
+
this.values = [...(config.values ?? [])];
|
|
977
|
+
this.xValues = [...(config.xValues ?? [])];
|
|
978
|
+
this.bubbleSizes = [...(config.bubbleSizes ?? [])];
|
|
979
|
+
this.fill = config.fill;
|
|
980
|
+
this.line = config.line ? { ...config.line } : undefined;
|
|
981
|
+
validateFiniteArray(this.values, "chart series values");
|
|
982
|
+
validateFiniteArray(this.xValues, "chart series xValues");
|
|
983
|
+
validateFiniteArray(this.bubbleSizes, "chart series bubbleSizes");
|
|
984
|
+
if (this.fill !== undefined) colorValue(this.fill);
|
|
985
|
+
if (this.line !== undefined) validateLine(this.line, "chart series line");
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
export class PresentationChartSeriesCollection {
|
|
990
|
+
readonly items: PresentationChartSeries[];
|
|
991
|
+
|
|
992
|
+
constructor(series: PresentationChartSeriesConfig[]) {
|
|
993
|
+
this.items = series.map((config) => new PresentationChartSeries(config));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
getItemAt(index: number): PresentationChartSeries {
|
|
997
|
+
const series = this.items[index];
|
|
998
|
+
if (!series) throw new Error(`Chart series index out of range: ${index}`);
|
|
999
|
+
return series;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export class PresentationChart {
|
|
1004
|
+
readonly id: string;
|
|
1005
|
+
readonly type: PresentationChartType;
|
|
1006
|
+
name: string;
|
|
1007
|
+
position: PresentationPosition;
|
|
1008
|
+
title: string;
|
|
1009
|
+
categories: string[];
|
|
1010
|
+
readonly series: PresentationChartSeriesCollection;
|
|
1011
|
+
hasLegend: boolean;
|
|
1012
|
+
legend: PresentationChartConfig["legend"];
|
|
1013
|
+
xAxis: PresentationChartConfig["xAxis"];
|
|
1014
|
+
yAxis: PresentationChartConfig["yAxis"];
|
|
1015
|
+
dataLabels: PresentationChartConfig["dataLabels"];
|
|
1016
|
+
|
|
1017
|
+
constructor(
|
|
1018
|
+
readonly slide: Slide,
|
|
1019
|
+
type: PresentationChartType,
|
|
1020
|
+
config: PresentationChartConfig,
|
|
1021
|
+
) {
|
|
1022
|
+
if (!CHART_TYPES.has(type)) {
|
|
1023
|
+
throw new InvalidPresentationInputError("chart type", "is not supported");
|
|
1024
|
+
}
|
|
1025
|
+
this.id = slide.presentation.allocateId("ch", this);
|
|
1026
|
+
this.type = type;
|
|
1027
|
+
this.name = config.name ?? this.id;
|
|
1028
|
+
assertSafeText(this.name, "chart name");
|
|
1029
|
+
this.position = normalizePosition(config.position, {
|
|
1030
|
+
left: 72,
|
|
1031
|
+
top: 120,
|
|
1032
|
+
width: 640,
|
|
1033
|
+
height: 360,
|
|
1034
|
+
});
|
|
1035
|
+
this.title = config.title ?? "";
|
|
1036
|
+
assertSafeText(this.title, "chart title");
|
|
1037
|
+
this.categories = [...(config.categories ?? [])];
|
|
1038
|
+
for (const category of this.categories) assertSafeText(category, "chart category");
|
|
1039
|
+
this.series = new PresentationChartSeriesCollection(config.series ?? []);
|
|
1040
|
+
this.hasLegend = config.hasLegend ?? this.series.items.length > 1;
|
|
1041
|
+
this.legend = config.legend ? { ...config.legend } : undefined;
|
|
1042
|
+
this.xAxis = config.xAxis ? { ...config.xAxis } : undefined;
|
|
1043
|
+
this.yAxis = config.yAxis ? { ...config.yAxis } : undefined;
|
|
1044
|
+
validateOptionalFinite(this.xAxis?.min, "chart xAxis.min");
|
|
1045
|
+
validateOptionalFinite(this.xAxis?.max, "chart xAxis.max");
|
|
1046
|
+
validateOptionalFinite(this.yAxis?.min, "chart yAxis.min");
|
|
1047
|
+
validateOptionalFinite(this.yAxis?.max, "chart yAxis.max");
|
|
1048
|
+
this.dataLabels = config.dataLabels ? { ...config.dataLabels } : undefined;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
inspectRecord(slideIndex: number): Record<string, unknown> {
|
|
1052
|
+
return {
|
|
1053
|
+
kind: "chart",
|
|
1054
|
+
id: this.id,
|
|
1055
|
+
slide: slideIndex + 1,
|
|
1056
|
+
name: this.name,
|
|
1057
|
+
chartType: this.type,
|
|
1058
|
+
title: this.title,
|
|
1059
|
+
series: this.series.items.map((series) => ({ name: series.name, values: series.values })),
|
|
1060
|
+
bbox: positionTuple(this.position),
|
|
1061
|
+
bboxUnit: "px",
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
layoutSnapshot(): Record<string, unknown> {
|
|
1066
|
+
return {
|
|
1067
|
+
kind: "chart",
|
|
1068
|
+
id: this.id,
|
|
1069
|
+
name: this.name,
|
|
1070
|
+
chartType: this.type,
|
|
1071
|
+
title: this.title,
|
|
1072
|
+
position: { ...this.position },
|
|
1073
|
+
categories: [...this.categories],
|
|
1074
|
+
series: this.series.items.map((series) => ({
|
|
1075
|
+
name: series.name,
|
|
1076
|
+
categories: [...series.categories],
|
|
1077
|
+
values: [...series.values],
|
|
1078
|
+
xValues: [...series.xValues],
|
|
1079
|
+
bubbleSizes: [...series.bubbleSizes],
|
|
1080
|
+
fill: series.fill,
|
|
1081
|
+
line: series.line,
|
|
1082
|
+
})),
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
export class PresentationChartCollection {
|
|
1088
|
+
readonly items: PresentationChart[] = [];
|
|
1089
|
+
|
|
1090
|
+
constructor(private readonly slide: Slide) {}
|
|
1091
|
+
|
|
1092
|
+
add(type: PresentationChartType, config: PresentationChartConfig = {}): PresentationChart {
|
|
1093
|
+
const chart = new PresentationChart(this.slide, type, config);
|
|
1094
|
+
this.items.push(chart);
|
|
1095
|
+
this.slide.appendElement(chart);
|
|
1096
|
+
return chart;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export class PresentationTableCell {
|
|
1101
|
+
readonly text: PresentationText;
|
|
1102
|
+
fill: PresentationFill;
|
|
1103
|
+
rowSpan: number;
|
|
1104
|
+
colSpan: number;
|
|
1105
|
+
|
|
1106
|
+
constructor(
|
|
1107
|
+
config: string | PresentationTableCellConfig,
|
|
1108
|
+
defaults: Pick<PresentationTableConfig, "fill" | "textStyle">,
|
|
1109
|
+
) {
|
|
1110
|
+
const normalized = typeof config === "string" ? { text: config } : config;
|
|
1111
|
+
this.text = new PresentationText(normalized.text, normalized.textStyle ?? defaults.textStyle);
|
|
1112
|
+
this.fill = normalized.fill ?? defaults.fill ?? "white";
|
|
1113
|
+
colorValue(this.fill);
|
|
1114
|
+
this.rowSpan = normalized.rowSpan ?? 1;
|
|
1115
|
+
this.colSpan = normalized.colSpan ?? 1;
|
|
1116
|
+
for (const [field, value] of [
|
|
1117
|
+
["rowSpan", this.rowSpan],
|
|
1118
|
+
["colSpan", this.colSpan],
|
|
1119
|
+
] as const) {
|
|
1120
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_TABLE_SPAN) {
|
|
1121
|
+
throw new InvalidPresentationInputError(
|
|
1122
|
+
`table cell ${field}`,
|
|
1123
|
+
`must be 1-${MAX_TABLE_SPAN}`,
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
toConfig(): PresentationTableCellConfig {
|
|
1130
|
+
return {
|
|
1131
|
+
text: this.text.toString(),
|
|
1132
|
+
fill: this.fill,
|
|
1133
|
+
textStyle: { ...this.text.style },
|
|
1134
|
+
rowSpan: this.rowSpan,
|
|
1135
|
+
colSpan: this.colSpan,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
export class PresentationTable {
|
|
1141
|
+
readonly id: string;
|
|
1142
|
+
name: string;
|
|
1143
|
+
position: PresentationPosition;
|
|
1144
|
+
rows: Array<Array<PresentationTableCell | null>>;
|
|
1145
|
+
columnWidths: number[];
|
|
1146
|
+
rowHeights: number[];
|
|
1147
|
+
fill: PresentationFill;
|
|
1148
|
+
line: PresentationLine;
|
|
1149
|
+
textStyle: PresentationTextStyle;
|
|
1150
|
+
|
|
1151
|
+
constructor(
|
|
1152
|
+
readonly slide: Slide,
|
|
1153
|
+
config: PresentationTableConfig,
|
|
1154
|
+
) {
|
|
1155
|
+
if (
|
|
1156
|
+
!Array.isArray(config.rows) ||
|
|
1157
|
+
config.rows.length === 0 ||
|
|
1158
|
+
config.rows.length > MAX_TABLE_ROWS
|
|
1159
|
+
) {
|
|
1160
|
+
throw new InvalidPresentationInputError(
|
|
1161
|
+
"table rows",
|
|
1162
|
+
`must contain 1-${MAX_TABLE_ROWS} rows`,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
const columns = config.rows[0]?.length ?? 0;
|
|
1166
|
+
if (columns === 0 || columns > MAX_TABLE_COLUMNS) {
|
|
1167
|
+
throw new InvalidPresentationInputError(
|
|
1168
|
+
"table columns",
|
|
1169
|
+
`must contain 1-${MAX_TABLE_COLUMNS} columns`,
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
if (config.rows.some((row) => !Array.isArray(row) || row.length !== columns)) {
|
|
1173
|
+
throw new InvalidPresentationInputError("table rows", "must form a rectangular matrix");
|
|
1174
|
+
}
|
|
1175
|
+
this.id = slide.presentation.allocateId("tb", this);
|
|
1176
|
+
this.name = config.name ?? this.id;
|
|
1177
|
+
assertSafeText(this.name, "table name", 512);
|
|
1178
|
+
this.position = normalizePosition(config.position, {
|
|
1179
|
+
left: 72,
|
|
1180
|
+
top: 120,
|
|
1181
|
+
width: 640,
|
|
1182
|
+
height: Math.max(48, config.rows.length * 36),
|
|
1183
|
+
});
|
|
1184
|
+
this.fill = config.fill ?? "white";
|
|
1185
|
+
colorValue(this.fill);
|
|
1186
|
+
this.line = { style: "solid", fill: "slate-300", width: 1, ...config.line };
|
|
1187
|
+
validateLine(this.line, "table line");
|
|
1188
|
+
this.textStyle = { fontSize: 16, ...config.textStyle };
|
|
1189
|
+
validateTextStyle(this.textStyle, "table textStyle");
|
|
1190
|
+
this.rows = config.rows.map((row: readonly PresentationTableCellInput[]) =>
|
|
1191
|
+
row.map((cell: PresentationTableCellInput) =>
|
|
1192
|
+
cell === null ? null : new PresentationTableCell(cell, this),
|
|
1193
|
+
),
|
|
1194
|
+
);
|
|
1195
|
+
validateTableOccupancy(this.rows);
|
|
1196
|
+
this.columnWidths = normalizeTableDimensions(config.columnWidths, columns, "columnWidths");
|
|
1197
|
+
this.rowHeights = normalizeTableDimensions(config.rowHeights, config.rows.length, "rowHeights");
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
inspectRecord(slideIndex: number): Record<string, unknown> {
|
|
1201
|
+
return {
|
|
1202
|
+
kind: "table",
|
|
1203
|
+
id: this.id,
|
|
1204
|
+
slide: slideIndex + 1,
|
|
1205
|
+
name: this.name,
|
|
1206
|
+
rows: this.rows.length,
|
|
1207
|
+
columns: this.rows[0]?.length ?? 0,
|
|
1208
|
+
textPreview: truncate(
|
|
1209
|
+
this.rows
|
|
1210
|
+
.map((row) => row.map((cell) => cell?.text.toString() ?? "").join(" | "))
|
|
1211
|
+
.join("\n"),
|
|
1212
|
+
240,
|
|
1213
|
+
),
|
|
1214
|
+
bbox: positionTuple(this.position),
|
|
1215
|
+
bboxUnit: "px",
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
layoutSnapshot(): Record<string, unknown> {
|
|
1220
|
+
return {
|
|
1221
|
+
kind: "table",
|
|
1222
|
+
id: this.id,
|
|
1223
|
+
name: this.name,
|
|
1224
|
+
position: { ...this.position },
|
|
1225
|
+
rows: this.rows.map((row) => row.map((cell) => cell?.toConfig() ?? null)),
|
|
1226
|
+
columnWidths: [...this.columnWidths],
|
|
1227
|
+
rowHeights: [...this.rowHeights],
|
|
1228
|
+
fill: this.fill,
|
|
1229
|
+
line: { ...this.line },
|
|
1230
|
+
textStyle: { ...this.textStyle },
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
export class PresentationTableCollection {
|
|
1236
|
+
readonly items: PresentationTable[] = [];
|
|
1237
|
+
|
|
1238
|
+
constructor(private readonly slide: Slide) {}
|
|
1239
|
+
|
|
1240
|
+
add(config: PresentationTableConfig): PresentationTable {
|
|
1241
|
+
const table = new PresentationTable(this.slide, config);
|
|
1242
|
+
this.items.push(table);
|
|
1243
|
+
this.slide.appendElement(table);
|
|
1244
|
+
return table;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
export type PresentationGroupChild =
|
|
1249
|
+
| PresentationShape
|
|
1250
|
+
| PresentationChart
|
|
1251
|
+
| PresentationImage
|
|
1252
|
+
| PresentationTable
|
|
1253
|
+
| PresentationGroup;
|
|
1254
|
+
|
|
1255
|
+
export class PresentationGroup {
|
|
1256
|
+
readonly id: string;
|
|
1257
|
+
name: string;
|
|
1258
|
+
position: PresentationPosition;
|
|
1259
|
+
childOffset: { left: number; top: number };
|
|
1260
|
+
childExtent: { width: number; height: number };
|
|
1261
|
+
rotation: number;
|
|
1262
|
+
flipHorizontal: boolean;
|
|
1263
|
+
flipVertical: boolean;
|
|
1264
|
+
readonly children: PresentationGroupChild[] = [];
|
|
1265
|
+
|
|
1266
|
+
constructor(
|
|
1267
|
+
readonly slide: Slide,
|
|
1268
|
+
config: PresentationGroupConfig = {},
|
|
1269
|
+
depth = 1,
|
|
1270
|
+
) {
|
|
1271
|
+
if (depth > MAX_GROUP_DEPTH) {
|
|
1272
|
+
throw new InvalidPresentationInputError("group nesting", `exceeds ${MAX_GROUP_DEPTH} levels`);
|
|
1273
|
+
}
|
|
1274
|
+
this.id = slide.presentation.allocateId("gp", this);
|
|
1275
|
+
this.name = config.name ?? this.id;
|
|
1276
|
+
assertSafeText(this.name, "group name", 512);
|
|
1277
|
+
this.position = normalizePosition(config.position);
|
|
1278
|
+
this.childOffset = config.childOffset
|
|
1279
|
+
? { ...config.childOffset }
|
|
1280
|
+
: { left: this.position.left, top: this.position.top };
|
|
1281
|
+
this.childExtent = config.childExtent
|
|
1282
|
+
? { ...config.childExtent }
|
|
1283
|
+
: { width: this.position.width, height: this.position.height };
|
|
1284
|
+
validatePosition(
|
|
1285
|
+
{
|
|
1286
|
+
left: this.childOffset.left,
|
|
1287
|
+
top: this.childOffset.top,
|
|
1288
|
+
width: this.childExtent.width,
|
|
1289
|
+
height: this.childExtent.height,
|
|
1290
|
+
},
|
|
1291
|
+
"group child coordinate system",
|
|
1292
|
+
);
|
|
1293
|
+
this.rotation = finiteOr(config.rotation, 0);
|
|
1294
|
+
validateRotation(this.rotation, "group rotation");
|
|
1295
|
+
this.flipHorizontal = config.flipHorizontal ?? false;
|
|
1296
|
+
this.flipVertical = config.flipVertical ?? false;
|
|
1297
|
+
for (const child of config.children ?? []) {
|
|
1298
|
+
if (this.children.length >= MAX_GROUP_CHILDREN) {
|
|
1299
|
+
throw new InvalidPresentationInputError(
|
|
1300
|
+
"group children",
|
|
1301
|
+
`exceeds ${MAX_GROUP_CHILDREN} children`,
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
switch (child.kind) {
|
|
1305
|
+
case "shape":
|
|
1306
|
+
this.children.push(new PresentationShape(slide, child.config));
|
|
1307
|
+
break;
|
|
1308
|
+
case "chart":
|
|
1309
|
+
this.children.push(new PresentationChart(slide, child.type, child.config ?? {}));
|
|
1310
|
+
break;
|
|
1311
|
+
case "image":
|
|
1312
|
+
this.children.push(new PresentationImage(slide, child.config));
|
|
1313
|
+
break;
|
|
1314
|
+
case "table":
|
|
1315
|
+
this.children.push(new PresentationTable(slide, child.config));
|
|
1316
|
+
break;
|
|
1317
|
+
case "group":
|
|
1318
|
+
this.children.push(new PresentationGroup(slide, child.config, depth + 1));
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
inspectRecord(slideIndex: number): Record<string, unknown> {
|
|
1325
|
+
return {
|
|
1326
|
+
kind: "group",
|
|
1327
|
+
id: this.id,
|
|
1328
|
+
slide: slideIndex + 1,
|
|
1329
|
+
name: this.name,
|
|
1330
|
+
children: this.children.length,
|
|
1331
|
+
bbox: positionTuple(this.position),
|
|
1332
|
+
bboxUnit: "px",
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
layoutSnapshot(): Record<string, unknown> {
|
|
1337
|
+
return {
|
|
1338
|
+
kind: "group",
|
|
1339
|
+
id: this.id,
|
|
1340
|
+
name: this.name,
|
|
1341
|
+
position: { ...this.position },
|
|
1342
|
+
childOffset: { ...this.childOffset },
|
|
1343
|
+
childExtent: { ...this.childExtent },
|
|
1344
|
+
rotation: this.rotation,
|
|
1345
|
+
flipHorizontal: this.flipHorizontal,
|
|
1346
|
+
flipVertical: this.flipVertical,
|
|
1347
|
+
children: this.children.map((child) => child.layoutSnapshot()),
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
export class PresentationGroupCollection {
|
|
1353
|
+
readonly items: PresentationGroup[] = [];
|
|
1354
|
+
|
|
1355
|
+
constructor(private readonly slide: Slide) {}
|
|
1356
|
+
|
|
1357
|
+
add(config: PresentationGroupConfig = {}): PresentationGroup {
|
|
1358
|
+
const group = new PresentationGroup(this.slide, config);
|
|
1359
|
+
this.items.push(group);
|
|
1360
|
+
this.slide.appendElement(group);
|
|
1361
|
+
return group;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
export class PresentationImage {
|
|
1366
|
+
readonly id: string;
|
|
1367
|
+
name: string;
|
|
1368
|
+
position: PresentationPosition;
|
|
1369
|
+
fit: "contain" | "cover";
|
|
1370
|
+
contentType: string | undefined;
|
|
1371
|
+
alt: string;
|
|
1372
|
+
prompt: string | undefined;
|
|
1373
|
+
crop: { left: number; top: number; right: number; bottom: number } | undefined;
|
|
1374
|
+
geometry: "rect" | "roundRect" | "ellipse";
|
|
1375
|
+
borderRadius: number | string | undefined;
|
|
1376
|
+
rotation: number;
|
|
1377
|
+
flipHorizontal: boolean;
|
|
1378
|
+
flipVertical: boolean;
|
|
1379
|
+
lockAspectRatio: boolean;
|
|
1380
|
+
private blob: Uint8Array | undefined;
|
|
1381
|
+
private dataUrl: string | undefined;
|
|
1382
|
+
private uri: string | undefined;
|
|
1383
|
+
private validatedContentType: PresentationRasterImageContentType | undefined;
|
|
1384
|
+
private cachedCanonicalDataUrl: string | undefined;
|
|
1385
|
+
|
|
1386
|
+
constructor(
|
|
1387
|
+
readonly slide: Slide,
|
|
1388
|
+
config: PresentationImageConfig,
|
|
1389
|
+
) {
|
|
1390
|
+
this.id = slide.presentation.allocateId("im", this);
|
|
1391
|
+
this.name = config.name ?? this.id;
|
|
1392
|
+
assertSafeText(this.name, "image name");
|
|
1393
|
+
this.position = normalizePosition(config.position ?? config.frame, {
|
|
1394
|
+
left: 72,
|
|
1395
|
+
top: 120,
|
|
1396
|
+
width: 480,
|
|
1397
|
+
height: 270,
|
|
1398
|
+
});
|
|
1399
|
+
this.fit = config.fit ?? "contain";
|
|
1400
|
+
if (this.fit !== "contain" && this.fit !== "cover") {
|
|
1401
|
+
throw new InvalidPresentationInputError("image fit", "must be contain or cover");
|
|
1402
|
+
}
|
|
1403
|
+
this.contentType = config.contentType;
|
|
1404
|
+
this.alt = config.alt ?? "";
|
|
1405
|
+
assertSafeText(this.alt, "image alt");
|
|
1406
|
+
this.prompt = config.prompt;
|
|
1407
|
+
if (this.prompt !== undefined) assertSafeText(this.prompt, "image prompt");
|
|
1408
|
+
this.crop = config.crop ? normalizeCrop(config.crop) : undefined;
|
|
1409
|
+
this.geometry = config.geometry ?? "rect";
|
|
1410
|
+
if (!(["rect", "roundRect", "ellipse"] as const).includes(this.geometry)) {
|
|
1411
|
+
throw new InvalidPresentationInputError("image geometry", "is not supported");
|
|
1412
|
+
}
|
|
1413
|
+
this.borderRadius = config.borderRadius;
|
|
1414
|
+
validateBorderRadius(this.borderRadius, "image borderRadius");
|
|
1415
|
+
this.rotation = finiteOr(config.rotation, 0);
|
|
1416
|
+
validateRotation(this.rotation, "image rotation");
|
|
1417
|
+
this.flipHorizontal = config.flipHorizontal ?? false;
|
|
1418
|
+
this.flipVertical = config.flipVertical ?? false;
|
|
1419
|
+
this.lockAspectRatio = config.lockAspectRatio ?? false;
|
|
1420
|
+
this.assignSource(config);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
get frame(): PresentationPosition {
|
|
1424
|
+
return this.position;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
set frame(value: PresentationPosition) {
|
|
1428
|
+
this.position = normalizePosition(value);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
replace(config: PresentationImageConfig): void {
|
|
1432
|
+
if (config.alt !== undefined) assertSafeText(config.alt, "image alt");
|
|
1433
|
+
if (config.prompt !== undefined) assertSafeText(config.prompt, "image prompt");
|
|
1434
|
+
if (config.fit !== undefined && config.fit !== "contain" && config.fit !== "cover") {
|
|
1435
|
+
throw new InvalidPresentationInputError("image fit", "must be contain or cover");
|
|
1436
|
+
}
|
|
1437
|
+
this.assignSource(config);
|
|
1438
|
+
if (config.alt !== undefined) this.alt = config.alt;
|
|
1439
|
+
if (config.fit !== undefined) this.fit = config.fit;
|
|
1440
|
+
if (config.prompt !== undefined) this.prompt = config.prompt;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* Explicitly materializes a host-owned URI into trusted raster bytes.
|
|
1445
|
+
*
|
|
1446
|
+
* Render and file export never invoke resolvers, fetch, or read files. A host
|
|
1447
|
+
* must call this method deliberately, and returned bytes still pass the same
|
|
1448
|
+
* MIME/signature validation as direct `blob` input.
|
|
1449
|
+
*/
|
|
1450
|
+
async resolveUri(): Promise<void> {
|
|
1451
|
+
if (!this.uri) {
|
|
1452
|
+
throw new InvalidPresentationInputError(
|
|
1453
|
+
"image uri",
|
|
1454
|
+
`Image ${this.id} has no URI to resolve`,
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
const uri = this.uri;
|
|
1458
|
+
assertSafeText(this.name, `image ${this.id} name`);
|
|
1459
|
+
let resolution: PresentationImageResolution;
|
|
1460
|
+
try {
|
|
1461
|
+
resolution = await this.slide.presentation.resolveImageUri({
|
|
1462
|
+
uri,
|
|
1463
|
+
imageId: this.id,
|
|
1464
|
+
name: this.name,
|
|
1465
|
+
});
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
const failure = new InvalidPresentationInputError(
|
|
1468
|
+
"image resolver",
|
|
1469
|
+
`failed to resolve image ${this.id}`,
|
|
1470
|
+
);
|
|
1471
|
+
Object.defineProperty(failure, "cause", { value: error, enumerable: false });
|
|
1472
|
+
throw failure;
|
|
1473
|
+
}
|
|
1474
|
+
if (!resolution || typeof resolution !== "object" || !("blob" in resolution)) {
|
|
1475
|
+
throw new InvalidPresentationInputError(
|
|
1476
|
+
"image resolver",
|
|
1477
|
+
"must return { blob, contentType? }",
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
this.assignSource({
|
|
1481
|
+
blob: resolution.blob,
|
|
1482
|
+
...(resolution.contentType ? { contentType: resolution.contentType } : {}),
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
sourceForSvg(): string | undefined {
|
|
1487
|
+
if (this.uri) throw unresolvedImageUriError(this.id);
|
|
1488
|
+
if (this.dataUrl) {
|
|
1489
|
+
if (!this.contentType) {
|
|
1490
|
+
throw new InvalidPresentationInputError(
|
|
1491
|
+
"image contentType",
|
|
1492
|
+
`Image ${this.id} has no validated raster MIME type`,
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
const contentType = normalizeRasterMime(this.contentType);
|
|
1496
|
+
if (
|
|
1497
|
+
contentType !== this.validatedContentType ||
|
|
1498
|
+
!this.dataUrl.startsWith(`data:${contentType};base64,`)
|
|
1499
|
+
) {
|
|
1500
|
+
throw new InvalidPresentationInputError(
|
|
1501
|
+
"image contentType",
|
|
1502
|
+
`Image ${this.id} MIME type no longer matches its data URL`,
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
return (this.cachedCanonicalDataUrl ??= this.dataUrl);
|
|
1506
|
+
}
|
|
1507
|
+
if (this.blob) {
|
|
1508
|
+
if (!this.contentType) {
|
|
1509
|
+
throw new InvalidPresentationInputError(
|
|
1510
|
+
"image contentType",
|
|
1511
|
+
`Image ${this.id} has no validated raster MIME type`,
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
const contentType = normalizeRasterMime(this.contentType);
|
|
1515
|
+
if (contentType !== this.validatedContentType) {
|
|
1516
|
+
throw new InvalidPresentationInputError(
|
|
1517
|
+
"image contentType",
|
|
1518
|
+
`Image ${this.id} MIME type no longer matches its validated bytes`,
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
return (this.cachedCanonicalDataUrl ??= `data:${contentType};base64,${encodeRasterBase64(this.blob)}`);
|
|
1522
|
+
}
|
|
1523
|
+
return undefined;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
sourceForPptx(): { data: string } {
|
|
1527
|
+
const source = this.sourceForSvg();
|
|
1528
|
+
if (!source) {
|
|
1529
|
+
throw new UnsupportedPresentationFeatureError(
|
|
1530
|
+
"prompt-only image PPTX export",
|
|
1531
|
+
`Image ${this.id} must be regenerated to validated raster bytes or a raster data URL before export`,
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
return { data: source };
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
inspectRecord(slideIndex: number): Record<string, unknown> {
|
|
1538
|
+
return {
|
|
1539
|
+
kind: "image",
|
|
1540
|
+
id: this.id,
|
|
1541
|
+
slide: slideIndex + 1,
|
|
1542
|
+
name: this.name,
|
|
1543
|
+
alt: this.alt,
|
|
1544
|
+
prompt: this.prompt,
|
|
1545
|
+
fit: this.fit,
|
|
1546
|
+
geometry: this.geometry,
|
|
1547
|
+
source: this.blob ? "blob" : this.dataUrl ? "dataUrl" : this.uri ? "uri" : "prompt",
|
|
1548
|
+
bbox: positionTuple(this.position),
|
|
1549
|
+
bboxUnit: "px",
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
layoutSnapshot(): Record<string, unknown> {
|
|
1554
|
+
return {
|
|
1555
|
+
kind: "image",
|
|
1556
|
+
id: this.id,
|
|
1557
|
+
name: this.name,
|
|
1558
|
+
alt: this.alt,
|
|
1559
|
+
prompt: this.prompt,
|
|
1560
|
+
fit: this.fit,
|
|
1561
|
+
geometry: this.geometry,
|
|
1562
|
+
position: { ...this.position },
|
|
1563
|
+
crop: this.crop ? { ...this.crop } : undefined,
|
|
1564
|
+
rotation: this.rotation,
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
private assignSource(config: PresentationImageConfig): void {
|
|
1569
|
+
const concrete = [
|
|
1570
|
+
"blob" in config && config.blob !== undefined,
|
|
1571
|
+
"dataUrl" in config && config.dataUrl !== undefined,
|
|
1572
|
+
"uri" in config && config.uri !== undefined,
|
|
1573
|
+
].filter(Boolean).length;
|
|
1574
|
+
if (concrete > 1)
|
|
1575
|
+
throw new InvalidPresentationInputError(
|
|
1576
|
+
"image source",
|
|
1577
|
+
"accepts exactly one concrete source: blob, dataUrl, or uri",
|
|
1578
|
+
);
|
|
1579
|
+
if (concrete === 0 && !config.prompt)
|
|
1580
|
+
throw new InvalidPresentationInputError(
|
|
1581
|
+
"image source",
|
|
1582
|
+
"requires blob, dataUrl, uri, or prompt",
|
|
1583
|
+
);
|
|
1584
|
+
|
|
1585
|
+
let blob: Uint8Array | undefined;
|
|
1586
|
+
let dataUrl: string | undefined;
|
|
1587
|
+
let uri: string | undefined;
|
|
1588
|
+
let contentType: PresentationRasterImageContentType | undefined;
|
|
1589
|
+
|
|
1590
|
+
if ("blob" in config && config.blob !== undefined) {
|
|
1591
|
+
const bytes = copyImageBytes(config.blob);
|
|
1592
|
+
blob = bytes;
|
|
1593
|
+
contentType = validateRasterBytes(bytes, config.contentType, "image blob");
|
|
1594
|
+
} else if ("dataUrl" in config && config.dataUrl !== undefined) {
|
|
1595
|
+
const parsed = parseRasterDataUrl(config.dataUrl);
|
|
1596
|
+
if (
|
|
1597
|
+
config.contentType !== undefined &&
|
|
1598
|
+
normalizeRasterMime(config.contentType) !== parsed.type
|
|
1599
|
+
) {
|
|
1600
|
+
throw new InvalidPresentationInputError(
|
|
1601
|
+
"image contentType",
|
|
1602
|
+
"does not match the data URL MIME type",
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
dataUrl = parsed.canonical;
|
|
1606
|
+
contentType = parsed.type;
|
|
1607
|
+
} else if ("uri" in config && config.uri !== undefined) {
|
|
1608
|
+
this.slide.presentation.assertImageUriAccepted(config.uri);
|
|
1609
|
+
uri = config.uri;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
this.blob = blob;
|
|
1613
|
+
this.dataUrl = dataUrl;
|
|
1614
|
+
this.uri = uri;
|
|
1615
|
+
this.contentType = contentType;
|
|
1616
|
+
this.validatedContentType = contentType;
|
|
1617
|
+
this.cachedCanonicalDataUrl = undefined;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
export class PresentationImageCollection {
|
|
1622
|
+
readonly items: PresentationImage[] = [];
|
|
1623
|
+
|
|
1624
|
+
constructor(private readonly slide: Slide) {}
|
|
1625
|
+
|
|
1626
|
+
add(config: PresentationImageConfig): PresentationImage {
|
|
1627
|
+
const image = new PresentationImage(this.slide, config);
|
|
1628
|
+
this.items.push(image);
|
|
1629
|
+
this.slide.appendElement(image);
|
|
1630
|
+
return image;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// oxlint-disable-next-line typescript/no-extraneous-class -- skill-compatible codec facade
|
|
1635
|
+
export class PresentationFile {
|
|
1636
|
+
static async exportPptx(
|
|
1637
|
+
presentation: Presentation,
|
|
1638
|
+
options: PresentationPptxExportOptions = {},
|
|
1639
|
+
): Promise<FileBlob> {
|
|
1640
|
+
validatePresentation(presentation, true);
|
|
1641
|
+
const { exportPresentationPptx } = await import("@opengeni/artifact-tool/presentation/pptx");
|
|
1642
|
+
return await exportPresentationPptx(presentation, options);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
static async importPptx(
|
|
1646
|
+
input: FileBlob | Blob | ArrayBuffer | Uint8Array,
|
|
1647
|
+
options: PresentationPptxImportOptions = {},
|
|
1648
|
+
): Promise<Presentation> {
|
|
1649
|
+
const { importPresentationPptx } = await import("@opengeni/artifact-tool/presentation/pptx");
|
|
1650
|
+
return await importPresentationPptx(input, options);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
static fidelityReport(presentation: Presentation): readonly PresentationFidelityIssue[] {
|
|
1654
|
+
const issues: PresentationFidelityIssue[] = [];
|
|
1655
|
+
if (
|
|
1656
|
+
presentation.masters.items.length > 1 ||
|
|
1657
|
+
presentation.masters.items.some((master) => master.elements.length > 0)
|
|
1658
|
+
) {
|
|
1659
|
+
issues.push({
|
|
1660
|
+
code: "content-preserved-in-source",
|
|
1661
|
+
severity: "warning",
|
|
1662
|
+
feature: "master",
|
|
1663
|
+
message:
|
|
1664
|
+
"Master hierarchy is editable but requires the package-native PPTX writer for regeneration",
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
if (
|
|
1668
|
+
presentation.layouts.items.length > 1 ||
|
|
1669
|
+
presentation.layouts.items.some((layout) => layout.elements.length > 0)
|
|
1670
|
+
) {
|
|
1671
|
+
issues.push({
|
|
1672
|
+
code: "content-preserved-in-source",
|
|
1673
|
+
severity: "warning",
|
|
1674
|
+
feature: "layout",
|
|
1675
|
+
message:
|
|
1676
|
+
"Layout inheritance is editable but requires the package-native PPTX writer for regeneration",
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
if (presentation.slides.items.some((slide) => slide.groups.items.length > 0)) {
|
|
1680
|
+
issues.push({
|
|
1681
|
+
code: "group-will-be-flattened",
|
|
1682
|
+
severity: "warning",
|
|
1683
|
+
feature: "group",
|
|
1684
|
+
message:
|
|
1685
|
+
"PptxGenJS cannot emit group scene nodes; explicit lossy export flattens their children",
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
for (const slide of presentation.slides.items) {
|
|
1689
|
+
for (const chart of slide.charts.items) {
|
|
1690
|
+
if (
|
|
1691
|
+
(chart.type === "scatter" || chart.type === "bubble") &&
|
|
1692
|
+
!chartSeriesShareXValues(chart)
|
|
1693
|
+
) {
|
|
1694
|
+
issues.push({
|
|
1695
|
+
code: "style-approximated",
|
|
1696
|
+
severity: "warning",
|
|
1697
|
+
feature: "unsupported-chart",
|
|
1698
|
+
message: `Chart ${chart.id} has different X values per series; the fallback PPTX writer only supports one shared X vector`,
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
const provenance = presentationLossState(presentation);
|
|
1704
|
+
if (provenance?.unsupportedParts.length) {
|
|
1705
|
+
issues.push({
|
|
1706
|
+
code: "content-preserved-in-source",
|
|
1707
|
+
severity: "warning",
|
|
1708
|
+
feature: "theme",
|
|
1709
|
+
message: "Validated source-only OOXML parts cannot be regenerated after model edits",
|
|
1710
|
+
parts: [...provenance.unsupportedParts],
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
return issues;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
static lossPreservationEnvelope(
|
|
1717
|
+
presentation: Presentation,
|
|
1718
|
+
): PresentationLossPreservationEnvelope | null {
|
|
1719
|
+
const envelope = presentationLossState(presentation);
|
|
1720
|
+
return envelope
|
|
1721
|
+
? {
|
|
1722
|
+
...envelope,
|
|
1723
|
+
sourceBytes: envelope.sourceBytes.slice(),
|
|
1724
|
+
unsupportedParts: [...envelope.unsupportedParts],
|
|
1725
|
+
}
|
|
1726
|
+
: null;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function chartSeriesShareXValues(chart: PresentationChart): boolean {
|
|
1731
|
+
const reference = chart.series.items[0]?.xValues ?? [];
|
|
1732
|
+
if (reference.length === 0) return false;
|
|
1733
|
+
return chart.series.items.every(
|
|
1734
|
+
(series) =>
|
|
1735
|
+
series.xValues.length === reference.length &&
|
|
1736
|
+
series.xValues.every((value, index) => Object.is(value, reference[index])),
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
type MontageDimensions = {
|
|
1741
|
+
columns: number;
|
|
1742
|
+
rows: number;
|
|
1743
|
+
gap: number;
|
|
1744
|
+
width: number;
|
|
1745
|
+
height: number;
|
|
1746
|
+
};
|
|
1747
|
+
|
|
1748
|
+
function montageDimensions(
|
|
1749
|
+
presentation: Presentation,
|
|
1750
|
+
requestedColumns?: number,
|
|
1751
|
+
requestedGap?: number,
|
|
1752
|
+
): MontageDimensions {
|
|
1753
|
+
const count = presentation.slides.items.length;
|
|
1754
|
+
if (count === 0) {
|
|
1755
|
+
throw new InvalidPresentationInputError("montage", "cannot export an empty presentation");
|
|
1756
|
+
}
|
|
1757
|
+
const columns = requestedColumns ?? Math.ceil(Math.sqrt(count));
|
|
1758
|
+
if (!Number.isInteger(columns) || columns <= 0) {
|
|
1759
|
+
throw new InvalidPresentationInputError("montage columns", "must be a positive integer");
|
|
1760
|
+
}
|
|
1761
|
+
const gap = requestedGap ?? 24;
|
|
1762
|
+
if (!Number.isFinite(gap) || gap < 0) {
|
|
1763
|
+
throw new InvalidPresentationInputError("montage gap", "must be a non-negative number");
|
|
1764
|
+
}
|
|
1765
|
+
const rows = Math.ceil(count / columns);
|
|
1766
|
+
const width = columns * presentation.slideSize.width + (columns + 1) * gap;
|
|
1767
|
+
const height = rows * presentation.slideSize.height + (rows + 1) * gap;
|
|
1768
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) {
|
|
1769
|
+
throw new InvalidPresentationInputError("montage geometry", "exceeds finite numeric bounds");
|
|
1770
|
+
}
|
|
1771
|
+
return { columns, rows, gap, width, height };
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
function validateRenderScale(scale: number): void {
|
|
1775
|
+
if (!Number.isFinite(scale) || scale <= 0 || scale > 8) {
|
|
1776
|
+
throw new InvalidPresentationInputError("export scale", "must be greater than 0 and at most 8");
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function assertRenderPixelBudget(width: number, height: number, scale: number): void {
|
|
1781
|
+
const scaledWidth = Math.ceil(width * scale);
|
|
1782
|
+
const scaledHeight = Math.ceil(height * scale);
|
|
1783
|
+
if (
|
|
1784
|
+
!Number.isSafeInteger(scaledWidth) ||
|
|
1785
|
+
!Number.isSafeInteger(scaledHeight) ||
|
|
1786
|
+
scaledWidth <= 0 ||
|
|
1787
|
+
scaledHeight <= 0
|
|
1788
|
+
) {
|
|
1789
|
+
throw new InvalidPresentationInputError(
|
|
1790
|
+
"render geometry",
|
|
1791
|
+
"scaled dimensions must be positive safe integers",
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
if (scaledWidth > MAX_RENDER_DIMENSION || scaledHeight > MAX_RENDER_DIMENSION) {
|
|
1795
|
+
throw new InvalidPresentationInputError(
|
|
1796
|
+
"render dimension limit",
|
|
1797
|
+
`${scaledWidth}x${scaledHeight} exceeds maximum dimension ${MAX_RENDER_DIMENSION}`,
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
if (scaledWidth > Math.floor(MAX_RENDER_PIXELS / scaledHeight)) {
|
|
1801
|
+
throw new InvalidPresentationInputError(
|
|
1802
|
+
"render pixel limit",
|
|
1803
|
+
`${scaledWidth}x${scaledHeight} exceeds maximum ${MAX_RENDER_PIXELS} pixels`,
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const MAX_GEOMETRY_VALUE = 1_000_000;
|
|
1809
|
+
const MAX_TEXT_LENGTH = 1_000_000;
|
|
1810
|
+
const MAX_FONT_SIZE = 4_096;
|
|
1811
|
+
const MAX_IMAGE_BYTES = 32 * 1024 * 1024;
|
|
1812
|
+
const MAX_IMAGE_URI_LENGTH = 4_096;
|
|
1813
|
+
const MAX_RENDER_DIMENSION = 32_768;
|
|
1814
|
+
const MAX_RENDER_PIXELS = 67_108_864;
|
|
1815
|
+
const MAX_TABLE_ROWS = 10_000;
|
|
1816
|
+
const MAX_TABLE_COLUMNS = 1_024;
|
|
1817
|
+
const MAX_TABLE_SPAN = 1_024;
|
|
1818
|
+
const MAX_GROUP_DEPTH = 32;
|
|
1819
|
+
const MAX_GROUP_CHILDREN = 100_000;
|
|
1820
|
+
|
|
1821
|
+
const PRESENTATION_GEOMETRIES = new Set<PresentationShapeGeometry>([
|
|
1822
|
+
"textbox",
|
|
1823
|
+
"rect",
|
|
1824
|
+
"roundRect",
|
|
1825
|
+
"ellipse",
|
|
1826
|
+
"triangle",
|
|
1827
|
+
"rightArrow",
|
|
1828
|
+
"line",
|
|
1829
|
+
]);
|
|
1830
|
+
const CHART_TYPES = new Set<PresentationChartType>([
|
|
1831
|
+
"bar",
|
|
1832
|
+
"line",
|
|
1833
|
+
"area",
|
|
1834
|
+
"pie",
|
|
1835
|
+
"doughnut",
|
|
1836
|
+
"scatter",
|
|
1837
|
+
"bubble",
|
|
1838
|
+
"radar",
|
|
1839
|
+
]);
|
|
1840
|
+
const LINE_STYLES: ReadonlySet<unknown> = new Set(["solid", "dash", "dot", "none"]);
|
|
1841
|
+
const TEXT_ALIGNMENTS: ReadonlySet<unknown> = new Set(["left", "center", "right", "justify"]);
|
|
1842
|
+
const VERTICAL_ALIGNMENTS: ReadonlySet<unknown> = new Set(["top", "middle", "bottom"]);
|
|
1843
|
+
const BORDER_RADIUS_TOKENS: Readonly<Record<string, number>> = {
|
|
1844
|
+
rounded: 4,
|
|
1845
|
+
"rounded-lg": 8,
|
|
1846
|
+
"rounded-xl": 12,
|
|
1847
|
+
"rounded-2xl": 16,
|
|
1848
|
+
"rounded-3xl": 24,
|
|
1849
|
+
};
|
|
1850
|
+
const FORBIDDEN_IMAGE_URI_SCHEMES = new Set([
|
|
1851
|
+
"blob",
|
|
1852
|
+
"data",
|
|
1853
|
+
"file",
|
|
1854
|
+
"ftp",
|
|
1855
|
+
"ftps",
|
|
1856
|
+
"http",
|
|
1857
|
+
"https",
|
|
1858
|
+
"javascript",
|
|
1859
|
+
]);
|
|
1860
|
+
|
|
1861
|
+
function cloneTemplateElement(element: PresentationTemplateElement): PresentationTemplateElement {
|
|
1862
|
+
if (!element || typeof element !== "object") {
|
|
1863
|
+
throw new InvalidPresentationInputError("template element", "must be an object");
|
|
1864
|
+
}
|
|
1865
|
+
switch (element.kind) {
|
|
1866
|
+
case "shape":
|
|
1867
|
+
return {
|
|
1868
|
+
kind: "shape",
|
|
1869
|
+
config: {
|
|
1870
|
+
...element.config,
|
|
1871
|
+
...(element.config.position ? { position: { ...element.config.position } } : {}),
|
|
1872
|
+
...(element.config.line ? { line: { ...element.config.line } } : {}),
|
|
1873
|
+
...(element.config.textStyle ? { textStyle: { ...element.config.textStyle } } : {}),
|
|
1874
|
+
},
|
|
1875
|
+
};
|
|
1876
|
+
case "chart":
|
|
1877
|
+
return {
|
|
1878
|
+
kind: "chart",
|
|
1879
|
+
type: element.type,
|
|
1880
|
+
...(element.config
|
|
1881
|
+
? {
|
|
1882
|
+
config: {
|
|
1883
|
+
...element.config,
|
|
1884
|
+
...(element.config.position ? { position: { ...element.config.position } } : {}),
|
|
1885
|
+
...(element.config.categories
|
|
1886
|
+
? { categories: [...element.config.categories] }
|
|
1887
|
+
: {}),
|
|
1888
|
+
...(element.config.series
|
|
1889
|
+
? {
|
|
1890
|
+
series: element.config.series.map((series) => ({
|
|
1891
|
+
...series,
|
|
1892
|
+
...(series.categories ? { categories: [...series.categories] } : {}),
|
|
1893
|
+
...(series.values ? { values: [...series.values] } : {}),
|
|
1894
|
+
...(series.xValues ? { xValues: [...series.xValues] } : {}),
|
|
1895
|
+
...(series.bubbleSizes ? { bubbleSizes: [...series.bubbleSizes] } : {}),
|
|
1896
|
+
...(series.line ? { line: { ...series.line } } : {}),
|
|
1897
|
+
})),
|
|
1898
|
+
}
|
|
1899
|
+
: {}),
|
|
1900
|
+
...(element.config.legend ? { legend: { ...element.config.legend } } : {}),
|
|
1901
|
+
...(element.config.xAxis ? { xAxis: { ...element.config.xAxis } } : {}),
|
|
1902
|
+
...(element.config.yAxis ? { yAxis: { ...element.config.yAxis } } : {}),
|
|
1903
|
+
...(element.config.dataLabels
|
|
1904
|
+
? { dataLabels: { ...element.config.dataLabels } }
|
|
1905
|
+
: {}),
|
|
1906
|
+
},
|
|
1907
|
+
}
|
|
1908
|
+
: {}),
|
|
1909
|
+
};
|
|
1910
|
+
case "image": {
|
|
1911
|
+
const config = element.config;
|
|
1912
|
+
return {
|
|
1913
|
+
kind: "image",
|
|
1914
|
+
config: {
|
|
1915
|
+
...config,
|
|
1916
|
+
...(config.blob !== undefined
|
|
1917
|
+
? {
|
|
1918
|
+
blob:
|
|
1919
|
+
config.blob instanceof Uint8Array ? config.blob.slice() : config.blob.slice(0),
|
|
1920
|
+
}
|
|
1921
|
+
: {}),
|
|
1922
|
+
...(config.position ? { position: { ...config.position } } : {}),
|
|
1923
|
+
...(config.frame ? { frame: { ...config.frame } } : {}),
|
|
1924
|
+
...(config.crop ? { crop: { ...config.crop } } : {}),
|
|
1925
|
+
} as PresentationImageConfig,
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
case "table":
|
|
1929
|
+
return {
|
|
1930
|
+
kind: "table",
|
|
1931
|
+
config: {
|
|
1932
|
+
...element.config,
|
|
1933
|
+
...(element.config.position ? { position: { ...element.config.position } } : {}),
|
|
1934
|
+
rows: element.config.rows.map((row) =>
|
|
1935
|
+
row.map((cell) =>
|
|
1936
|
+
cell === null || typeof cell === "string"
|
|
1937
|
+
? cell
|
|
1938
|
+
: {
|
|
1939
|
+
...cell,
|
|
1940
|
+
...(cell.textStyle ? { textStyle: { ...cell.textStyle } } : {}),
|
|
1941
|
+
},
|
|
1942
|
+
),
|
|
1943
|
+
),
|
|
1944
|
+
...(element.config.columnWidths
|
|
1945
|
+
? { columnWidths: [...element.config.columnWidths] }
|
|
1946
|
+
: {}),
|
|
1947
|
+
...(element.config.rowHeights ? { rowHeights: [...element.config.rowHeights] } : {}),
|
|
1948
|
+
...(element.config.line ? { line: { ...element.config.line } } : {}),
|
|
1949
|
+
...(element.config.textStyle ? { textStyle: { ...element.config.textStyle } } : {}),
|
|
1950
|
+
},
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
function normalizeTableDimensions(
|
|
1956
|
+
values: readonly number[] | undefined,
|
|
1957
|
+
expected: number,
|
|
1958
|
+
field: string,
|
|
1959
|
+
): number[] {
|
|
1960
|
+
if (values === undefined) return [];
|
|
1961
|
+
if (!Array.isArray(values) || values.length !== expected) {
|
|
1962
|
+
throw new InvalidPresentationInputError(`table ${field}`, `must contain ${expected} values`);
|
|
1963
|
+
}
|
|
1964
|
+
const output = [...values];
|
|
1965
|
+
if (output.some((value) => !Number.isFinite(value) || value <= 0 || value > MAX_GEOMETRY_VALUE)) {
|
|
1966
|
+
throw new InvalidPresentationInputError(
|
|
1967
|
+
`table ${field}`,
|
|
1968
|
+
"must contain bounded positive numbers",
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
return output;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
function validateTableOccupancy(
|
|
1975
|
+
rows: readonly (readonly (PresentationTableCell | null)[])[],
|
|
1976
|
+
): void {
|
|
1977
|
+
const rowCount = rows.length;
|
|
1978
|
+
const columnCount = rows[0]?.length ?? 0;
|
|
1979
|
+
const covered = Array.from({ length: rowCount }, () => new Uint8Array(columnCount));
|
|
1980
|
+
for (let row = 0; row < rowCount; row += 1) {
|
|
1981
|
+
for (let column = 0; column < columnCount; column += 1) {
|
|
1982
|
+
const cell = rows[row]![column];
|
|
1983
|
+
if (cell === undefined) {
|
|
1984
|
+
throw new InvalidPresentationInputError("table rows", "must form a rectangular matrix");
|
|
1985
|
+
}
|
|
1986
|
+
if (cell === null) {
|
|
1987
|
+
if (covered[row]![column] === 0) {
|
|
1988
|
+
throw new InvalidPresentationInputError(
|
|
1989
|
+
"table cell occupancy",
|
|
1990
|
+
`uncovered null at row ${row + 1}, column ${column + 1}`,
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
if (covered[row]![column] !== 0) {
|
|
1996
|
+
throw new InvalidPresentationInputError(
|
|
1997
|
+
"table cell occupancy",
|
|
1998
|
+
`spanned position at row ${row + 1}, column ${column + 1} must be null`,
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
if (row + cell.rowSpan > rowCount || column + cell.colSpan > columnCount) {
|
|
2002
|
+
throw new InvalidPresentationInputError(
|
|
2003
|
+
"table cell span",
|
|
2004
|
+
`cell at row ${row + 1}, column ${column + 1} extends outside the table`,
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
for (let coveredRow = row; coveredRow < row + cell.rowSpan; coveredRow += 1) {
|
|
2008
|
+
for (
|
|
2009
|
+
let coveredColumn = column;
|
|
2010
|
+
coveredColumn < column + cell.colSpan;
|
|
2011
|
+
coveredColumn += 1
|
|
2012
|
+
) {
|
|
2013
|
+
if (coveredRow === row && coveredColumn === column) continue;
|
|
2014
|
+
if (covered[coveredRow]![coveredColumn] !== 0) {
|
|
2015
|
+
throw new InvalidPresentationInputError("table cell span", "overlaps another span");
|
|
2016
|
+
}
|
|
2017
|
+
covered[coveredRow]![coveredColumn] = 1;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
function normalizePlaceholder(
|
|
2025
|
+
value: PresentationShapeConfig["placeholder"],
|
|
2026
|
+
): { type: string; index?: number } | undefined {
|
|
2027
|
+
if (value === undefined) return undefined;
|
|
2028
|
+
if (!value || typeof value !== "object") {
|
|
2029
|
+
throw new InvalidPresentationInputError("shape placeholder", "must be an object");
|
|
2030
|
+
}
|
|
2031
|
+
assertSafeText(value.type, "shape placeholder type", 128);
|
|
2032
|
+
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(value.type)) {
|
|
2033
|
+
throw new InvalidPresentationInputError("shape placeholder type", "is invalid");
|
|
2034
|
+
}
|
|
2035
|
+
if (value.index !== undefined && (!Number.isSafeInteger(value.index) || value.index < 0)) {
|
|
2036
|
+
throw new InvalidPresentationInputError(
|
|
2037
|
+
"shape placeholder index",
|
|
2038
|
+
"must be a non-negative safe integer",
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
return value.index === undefined
|
|
2042
|
+
? { type: value.type }
|
|
2043
|
+
: { type: value.type, index: value.index };
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
function validateImageUriPolicy(
|
|
2047
|
+
resolver: PresentationImageResolver | undefined,
|
|
2048
|
+
schemes: readonly string[] | undefined,
|
|
2049
|
+
): ReadonlySet<string> {
|
|
2050
|
+
if (schemes !== undefined && !Array.isArray(schemes)) {
|
|
2051
|
+
throw new InvalidPresentationInputError("allowedImageUriSchemes", "must be an array");
|
|
2052
|
+
}
|
|
2053
|
+
const normalized = new Set<string>();
|
|
2054
|
+
for (const value of schemes ?? []) {
|
|
2055
|
+
if (typeof value !== "string" || !/^[a-z][a-z0-9+.-]*$/i.test(value)) {
|
|
2056
|
+
throw new InvalidPresentationInputError(
|
|
2057
|
+
"allowedImageUriSchemes",
|
|
2058
|
+
"contains an invalid URI scheme",
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
const scheme = value.toLowerCase();
|
|
2062
|
+
if (FORBIDDEN_IMAGE_URI_SCHEMES.has(scheme)) {
|
|
2063
|
+
throw new InvalidPresentationInputError(
|
|
2064
|
+
"allowedImageUriSchemes",
|
|
2065
|
+
`${scheme}: is reserved for active, local, or remote resources`,
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
normalized.add(scheme);
|
|
2069
|
+
}
|
|
2070
|
+
if (!resolver && normalized.size > 0) {
|
|
2071
|
+
throw new InvalidPresentationInputError(
|
|
2072
|
+
"allowedImageUriSchemes",
|
|
2073
|
+
"requires an injected imageResolver",
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
return normalized;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
function imageUriScheme(uri: string): string {
|
|
2080
|
+
const match = /^([a-z][a-z0-9+.-]*):/i.exec(uri);
|
|
2081
|
+
if (!match) {
|
|
2082
|
+
throw new InvalidPresentationInputError(
|
|
2083
|
+
"image uri",
|
|
2084
|
+
"must use a host-owned, explicitly allowed URI scheme",
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
return match[1]!.toLowerCase();
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function validatePresentation(presentation: Presentation, requireResolvedImages: boolean): void {
|
|
2091
|
+
validatePosition(
|
|
2092
|
+
{
|
|
2093
|
+
left: 0,
|
|
2094
|
+
top: 0,
|
|
2095
|
+
width: presentation.slideSize.width,
|
|
2096
|
+
height: presentation.slideSize.height,
|
|
2097
|
+
},
|
|
2098
|
+
"slideSize",
|
|
2099
|
+
);
|
|
2100
|
+
for (const master of presentation.masters.items) {
|
|
2101
|
+
assertSafeText(master.name, `master ${master.id} name`, 512);
|
|
2102
|
+
colorValue(master.background);
|
|
2103
|
+
}
|
|
2104
|
+
for (const layout of presentation.layouts.items) {
|
|
2105
|
+
assertSafeText(layout.name, `layout ${layout.id} name`, 512);
|
|
2106
|
+
colorValue(layout.background);
|
|
2107
|
+
if (
|
|
2108
|
+
layout.masterId !== undefined &&
|
|
2109
|
+
!presentation.masters.items.some((master) => master.id === layout.masterId)
|
|
2110
|
+
) {
|
|
2111
|
+
throw new InvalidPresentationInputError(
|
|
2112
|
+
`layout ${layout.id} masterId`,
|
|
2113
|
+
"must reference a master in this presentation",
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
for (const [slideIndex, slide] of presentation.slides.items.entries()) {
|
|
2118
|
+
assertSafeText(slide.title, `slides[${slideIndex}].title`);
|
|
2119
|
+
assertSafeText(slide.notes.toString(), `slides[${slideIndex}].notes`);
|
|
2120
|
+
colorValue(slide.background.fill);
|
|
2121
|
+
for (const shape of slide.shapes.items) {
|
|
2122
|
+
if (!PRESENTATION_GEOMETRIES.has(shape.geometry)) {
|
|
2123
|
+
throw new InvalidPresentationInputError("shape geometry", "is not supported");
|
|
2124
|
+
}
|
|
2125
|
+
assertSafeText(shape.name, `shape ${shape.id} name`);
|
|
2126
|
+
validatePosition(shape.position, `shape ${shape.id} position`);
|
|
2127
|
+
colorValue(shape.fill);
|
|
2128
|
+
validateLine(shape.line, `shape ${shape.id} line`);
|
|
2129
|
+
validateRotation(shape.rotation, `shape ${shape.id} rotation`);
|
|
2130
|
+
validateBorderRadius(shape.borderRadius, `shape ${shape.id} borderRadius`);
|
|
2131
|
+
normalizePlaceholder(shape.placeholder);
|
|
2132
|
+
assertSafeText(shape.text.toString(), `shape ${shape.id} text`);
|
|
2133
|
+
validateTextStyle(shape.text.style, `shape ${shape.id} textStyle`);
|
|
2134
|
+
}
|
|
2135
|
+
for (const chart of slide.charts.items) {
|
|
2136
|
+
if (!CHART_TYPES.has(chart.type)) {
|
|
2137
|
+
throw new InvalidPresentationInputError("chart type", "is not supported");
|
|
2138
|
+
}
|
|
2139
|
+
assertSafeText(chart.name, `chart ${chart.id} name`);
|
|
2140
|
+
assertSafeText(chart.title, `chart ${chart.id} title`);
|
|
2141
|
+
validatePosition(chart.position, `chart ${chart.id} position`);
|
|
2142
|
+
for (const [index, category] of chart.categories.entries()) {
|
|
2143
|
+
assertSafeText(category, `chart ${chart.id} categories[${index}]`);
|
|
2144
|
+
}
|
|
2145
|
+
validateOptionalFinite(chart.xAxis?.min, `chart ${chart.id} xAxis.min`);
|
|
2146
|
+
validateOptionalFinite(chart.xAxis?.max, `chart ${chart.id} xAxis.max`);
|
|
2147
|
+
validateOptionalFinite(chart.yAxis?.min, `chart ${chart.id} yAxis.min`);
|
|
2148
|
+
validateOptionalFinite(chart.yAxis?.max, `chart ${chart.id} yAxis.max`);
|
|
2149
|
+
for (const [seriesIndex, series] of chart.series.items.entries()) {
|
|
2150
|
+
assertSafeText(series.name, `chart ${chart.id} series[${seriesIndex}].name`);
|
|
2151
|
+
for (const [index, category] of series.categories.entries()) {
|
|
2152
|
+
assertSafeText(category, `chart ${chart.id} series[${seriesIndex}].categories[${index}]`);
|
|
2153
|
+
}
|
|
2154
|
+
validateFiniteArray(series.values, `chart ${chart.id} series[${seriesIndex}].values`);
|
|
2155
|
+
validateFiniteArray(series.xValues, `chart ${chart.id} series[${seriesIndex}].xValues`);
|
|
2156
|
+
validateFiniteArray(
|
|
2157
|
+
series.bubbleSizes,
|
|
2158
|
+
`chart ${chart.id} series[${seriesIndex}].bubbleSizes`,
|
|
2159
|
+
);
|
|
2160
|
+
if (series.fill !== undefined) colorValue(series.fill);
|
|
2161
|
+
if (series.line !== undefined) {
|
|
2162
|
+
validateLine(series.line, `chart ${chart.id} series[${seriesIndex}].line`);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
for (const image of slide.images.items) {
|
|
2167
|
+
assertSafeText(image.name, `image ${image.id} name`);
|
|
2168
|
+
assertSafeText(image.alt, `image ${image.id} alt`);
|
|
2169
|
+
if (image.prompt !== undefined) assertSafeText(image.prompt, `image ${image.id} prompt`);
|
|
2170
|
+
validatePosition(image.position, `image ${image.id} position`);
|
|
2171
|
+
if (image.fit !== "contain" && image.fit !== "cover") {
|
|
2172
|
+
throw new InvalidPresentationInputError(
|
|
2173
|
+
`image ${image.id} fit`,
|
|
2174
|
+
"must be contain or cover",
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
if (!(["rect", "roundRect", "ellipse"] as const).includes(image.geometry)) {
|
|
2178
|
+
throw new InvalidPresentationInputError(`image ${image.id} geometry`, "is not supported");
|
|
2179
|
+
}
|
|
2180
|
+
validateRotation(image.rotation, `image ${image.id} rotation`);
|
|
2181
|
+
validateBorderRadius(image.borderRadius, `image ${image.id} borderRadius`);
|
|
2182
|
+
if (image.crop !== undefined) normalizeCrop(image.crop);
|
|
2183
|
+
for (const [key, value] of [
|
|
2184
|
+
["flipHorizontal", image.flipHorizontal],
|
|
2185
|
+
["flipVertical", image.flipVertical],
|
|
2186
|
+
["lockAspectRatio", image.lockAspectRatio],
|
|
2187
|
+
] as const) {
|
|
2188
|
+
if (typeof value !== "boolean") {
|
|
2189
|
+
throw new InvalidPresentationInputError(`image ${image.id} ${key}`, "must be boolean");
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
if (requireResolvedImages) image.sourceForSvg();
|
|
2193
|
+
}
|
|
2194
|
+
for (const table of slide.tables.items) validatePresentationTable(table);
|
|
2195
|
+
for (const group of slide.groups.items) {
|
|
2196
|
+
validatePresentationGroup(group, requireResolvedImages, new Set());
|
|
2197
|
+
}
|
|
2198
|
+
const indexedElements: PresentationElement[] = [
|
|
2199
|
+
...slide.shapes.items,
|
|
2200
|
+
...slide.charts.items,
|
|
2201
|
+
...slide.images.items,
|
|
2202
|
+
...slide.tables.items,
|
|
2203
|
+
...slide.groups.items,
|
|
2204
|
+
];
|
|
2205
|
+
if (
|
|
2206
|
+
slide.elements.length !== indexedElements.length ||
|
|
2207
|
+
new Set(slide.elements).size !== slide.elements.length ||
|
|
2208
|
+
indexedElements.some((element) => !slide.elements.includes(element))
|
|
2209
|
+
) {
|
|
2210
|
+
throw new InvalidPresentationInputError(
|
|
2211
|
+
`slides[${slideIndex}].elements`,
|
|
2212
|
+
"must contain each typed top-level element exactly once",
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
function validatePresentationTable(table: PresentationTable): void {
|
|
2219
|
+
assertSafeText(table.name, `table ${table.id} name`, 512);
|
|
2220
|
+
validatePosition(table.position, `table ${table.id} position`);
|
|
2221
|
+
colorValue(table.fill);
|
|
2222
|
+
validateLine(table.line, `table ${table.id} line`);
|
|
2223
|
+
validateTextStyle(table.textStyle, `table ${table.id} textStyle`);
|
|
2224
|
+
validateTableOccupancy(table.rows);
|
|
2225
|
+
for (const row of table.rows) {
|
|
2226
|
+
for (const cell of row) {
|
|
2227
|
+
if (!cell) continue;
|
|
2228
|
+
assertSafeText(cell.text.toString(), `table ${table.id} cell text`);
|
|
2229
|
+
validateTextStyle(cell.text.style, `table ${table.id} cell textStyle`);
|
|
2230
|
+
colorValue(cell.fill);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
normalizeTableDimensions(
|
|
2234
|
+
table.columnWidths.length === 0 ? undefined : table.columnWidths,
|
|
2235
|
+
table.rows[0]?.length ?? 0,
|
|
2236
|
+
"columnWidths",
|
|
2237
|
+
);
|
|
2238
|
+
normalizeTableDimensions(
|
|
2239
|
+
table.rowHeights.length === 0 ? undefined : table.rowHeights,
|
|
2240
|
+
table.rows.length,
|
|
2241
|
+
"rowHeights",
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
function validatePresentationGroup(
|
|
2246
|
+
group: PresentationGroup,
|
|
2247
|
+
requireResolvedImages: boolean,
|
|
2248
|
+
ancestors: Set<PresentationGroup>,
|
|
2249
|
+
): void {
|
|
2250
|
+
if (ancestors.has(group)) {
|
|
2251
|
+
throw new InvalidPresentationInputError(`group ${group.id}`, "contains a cycle");
|
|
2252
|
+
}
|
|
2253
|
+
ancestors.add(group);
|
|
2254
|
+
assertSafeText(group.name, `group ${group.id} name`, 512);
|
|
2255
|
+
validatePosition(group.position, `group ${group.id} position`);
|
|
2256
|
+
validatePosition(
|
|
2257
|
+
{
|
|
2258
|
+
left: group.childOffset.left,
|
|
2259
|
+
top: group.childOffset.top,
|
|
2260
|
+
width: group.childExtent.width,
|
|
2261
|
+
height: group.childExtent.height,
|
|
2262
|
+
},
|
|
2263
|
+
`group ${group.id} child coordinate system`,
|
|
2264
|
+
);
|
|
2265
|
+
validateRotation(group.rotation, `group ${group.id} rotation`);
|
|
2266
|
+
for (const child of group.children) {
|
|
2267
|
+
if (child.slide !== group.slide) {
|
|
2268
|
+
throw new InvalidPresentationInputError(
|
|
2269
|
+
`group ${group.id}`,
|
|
2270
|
+
"contains an element from another slide",
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
if (child instanceof PresentationGroup) {
|
|
2274
|
+
validatePresentationGroup(child, requireResolvedImages, ancestors);
|
|
2275
|
+
} else if (child instanceof PresentationTable) {
|
|
2276
|
+
validatePresentationTable(child);
|
|
2277
|
+
} else if (child instanceof PresentationShape) {
|
|
2278
|
+
validatePosition(child.position, `shape ${child.id} position`);
|
|
2279
|
+
assertSafeText(child.text.toString(), `shape ${child.id} text`);
|
|
2280
|
+
validateTextStyle(child.text.style, `shape ${child.id} textStyle`);
|
|
2281
|
+
colorValue(child.fill);
|
|
2282
|
+
validateLine(child.line, `shape ${child.id} line`);
|
|
2283
|
+
} else if (child instanceof PresentationChart) {
|
|
2284
|
+
validatePosition(child.position, `chart ${child.id} position`);
|
|
2285
|
+
for (const series of child.series.items)
|
|
2286
|
+
validateFiniteArray(series.values, `chart ${child.id} values`);
|
|
2287
|
+
} else if (child instanceof PresentationImage) {
|
|
2288
|
+
validatePosition(child.position, `image ${child.id} position`);
|
|
2289
|
+
if (requireResolvedImages) child.sourceForSvg();
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
ancestors.delete(group);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
function validatePosition(position: PresentationPosition, field: string): void {
|
|
2296
|
+
const values = [position.left, position.top, position.width, position.height];
|
|
2297
|
+
if (!values.every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
2298
|
+
throw new InvalidPresentationInputError(field, "values must be finite numbers");
|
|
2299
|
+
}
|
|
2300
|
+
if (position.width <= 0 || position.height <= 0) {
|
|
2301
|
+
throw new InvalidPresentationInputError(field, "width and height must be positive");
|
|
2302
|
+
}
|
|
2303
|
+
if (values.some((value) => Math.abs(value) > MAX_GEOMETRY_VALUE)) {
|
|
2304
|
+
throw new InvalidPresentationInputError(field, "values exceed the supported geometry bounds");
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function validateRotation(value: number, field: string): void {
|
|
2309
|
+
if (!Number.isFinite(value) || Math.abs(value) > MAX_GEOMETRY_VALUE) {
|
|
2310
|
+
throw new InvalidPresentationInputError(field, "must be a bounded finite number");
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function validateBorderRadius(value: number | string | undefined, field: string): void {
|
|
2315
|
+
if (value === undefined) return;
|
|
2316
|
+
if (typeof value === "number") {
|
|
2317
|
+
if (!Number.isFinite(value) || value < 0 || value > MAX_GEOMETRY_VALUE) {
|
|
2318
|
+
throw new InvalidPresentationInputError(field, "must be a bounded non-negative number");
|
|
2319
|
+
}
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
if (value !== "rounded-full" && !Object.hasOwn(BORDER_RADIUS_TOKENS, value)) {
|
|
2323
|
+
throw new InvalidPresentationInputError(field, "uses an unsupported radius token");
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
function validateLine(line: PresentationLine, field: string): void {
|
|
2328
|
+
if (!line || typeof line !== "object") {
|
|
2329
|
+
throw new InvalidPresentationInputError(field, "must be an object");
|
|
2330
|
+
}
|
|
2331
|
+
if (line.style !== undefined && !LINE_STYLES.has(line.style)) {
|
|
2332
|
+
throw new InvalidPresentationInputError(field, "uses an unsupported line style");
|
|
2333
|
+
}
|
|
2334
|
+
if (line.fill !== undefined) colorValue(line.fill);
|
|
2335
|
+
if (
|
|
2336
|
+
line.width !== undefined &&
|
|
2337
|
+
(!Number.isFinite(line.width) || line.width < 0 || line.width > MAX_GEOMETRY_VALUE)
|
|
2338
|
+
) {
|
|
2339
|
+
throw new InvalidPresentationInputError(field, "width must be a bounded non-negative number");
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
function validateTextStyle(style: PresentationTextStyle, field: string): void {
|
|
2344
|
+
if (!style || typeof style !== "object") {
|
|
2345
|
+
throw new InvalidPresentationInputError(field, "must be an object");
|
|
2346
|
+
}
|
|
2347
|
+
if (style.fontFamily !== undefined) assertSafeText(style.fontFamily, `${field}.fontFamily`, 512);
|
|
2348
|
+
if (
|
|
2349
|
+
style.fontSize !== undefined &&
|
|
2350
|
+
(!Number.isFinite(style.fontSize) || style.fontSize <= 0 || style.fontSize > MAX_FONT_SIZE)
|
|
2351
|
+
) {
|
|
2352
|
+
throw new InvalidPresentationInputError(
|
|
2353
|
+
`${field}.fontSize`,
|
|
2354
|
+
`must be greater than 0 and at most ${MAX_FONT_SIZE}`,
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
if (style.color !== undefined) colorValue(style.color);
|
|
2358
|
+
if (style.alignment !== undefined && !TEXT_ALIGNMENTS.has(style.alignment)) {
|
|
2359
|
+
throw new InvalidPresentationInputError(`${field}.alignment`, "is not supported");
|
|
2360
|
+
}
|
|
2361
|
+
if (style.verticalAlignment !== undefined && !VERTICAL_ALIGNMENTS.has(style.verticalAlignment)) {
|
|
2362
|
+
throw new InvalidPresentationInputError(`${field}.verticalAlignment`, "is not supported");
|
|
2363
|
+
}
|
|
2364
|
+
for (const key of ["bold", "italic", "underline"] as const) {
|
|
2365
|
+
if (style[key] !== undefined && typeof style[key] !== "boolean") {
|
|
2366
|
+
throw new InvalidPresentationInputError(`${field}.${key}`, "must be boolean");
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
function validateOptionalFinite(value: number | undefined, field: string): void {
|
|
2372
|
+
if (value !== undefined && (!Number.isFinite(value) || Math.abs(value) > MAX_GEOMETRY_VALUE)) {
|
|
2373
|
+
throw new InvalidPresentationInputError(field, "must be a bounded finite number");
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function validateFiniteArray(values: number[], field: string): void {
|
|
2378
|
+
if (!Array.isArray(values)) {
|
|
2379
|
+
throw new InvalidPresentationInputError(field, "must be an array");
|
|
2380
|
+
}
|
|
2381
|
+
for (const value of values) validateOptionalFinite(value, field);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
function assertSafeText(value: string, field: string, maxLength = MAX_TEXT_LENGTH): void {
|
|
2385
|
+
if (typeof value !== "string") {
|
|
2386
|
+
throw new InvalidPresentationInputError(field, "must be a string");
|
|
2387
|
+
}
|
|
2388
|
+
if (value.length > maxLength) {
|
|
2389
|
+
throw new InvalidPresentationInputError(field, `exceeds ${maxLength} UTF-16 code units`);
|
|
2390
|
+
}
|
|
2391
|
+
for (const character of value) {
|
|
2392
|
+
const codePoint = character.codePointAt(0)!;
|
|
2393
|
+
if (
|
|
2394
|
+
(codePoint < 0x20 && codePoint !== 0x09 && codePoint !== 0x0a && codePoint !== 0x0d) ||
|
|
2395
|
+
(codePoint >= 0xd800 && codePoint <= 0xdfff) ||
|
|
2396
|
+
codePoint === 0xfffe ||
|
|
2397
|
+
codePoint === 0xffff
|
|
2398
|
+
) {
|
|
2399
|
+
throw new InvalidPresentationInputError(field, "contains characters forbidden by XML 1.0");
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function copyImageBytes(value: ArrayBuffer | Uint8Array): Uint8Array {
|
|
2405
|
+
if (!(value instanceof ArrayBuffer) && !(value instanceof Uint8Array)) {
|
|
2406
|
+
throw new InvalidPresentationInputError("image blob", "must be an ArrayBuffer or Uint8Array");
|
|
2407
|
+
}
|
|
2408
|
+
const bytes =
|
|
2409
|
+
value instanceof Uint8Array ? new Uint8Array(value) : new Uint8Array(value.slice(0));
|
|
2410
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_IMAGE_BYTES) {
|
|
2411
|
+
throw new InvalidPresentationInputError(
|
|
2412
|
+
"image blob",
|
|
2413
|
+
`must contain between 1 and ${MAX_IMAGE_BYTES} bytes`,
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
return bytes;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function normalizeRasterMime(value: string): PresentationRasterImageContentType {
|
|
2420
|
+
try {
|
|
2421
|
+
return normalizeRasterContentType(value);
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
if (error instanceof RasterImageValidationError) {
|
|
2424
|
+
throw new InvalidPresentationInputError("image contentType", error.detail);
|
|
2425
|
+
}
|
|
2426
|
+
throw error;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
function validateRasterBytes(
|
|
2431
|
+
bytes: Uint8Array,
|
|
2432
|
+
declaredType: string | undefined,
|
|
2433
|
+
field: string,
|
|
2434
|
+
): PresentationRasterImageContentType {
|
|
2435
|
+
try {
|
|
2436
|
+
return validateRasterImageBytes(bytes, declaredType);
|
|
2437
|
+
} catch (error) {
|
|
2438
|
+
if (error instanceof RasterImageValidationError) {
|
|
2439
|
+
throw new InvalidPresentationInputError(field, error.detail);
|
|
2440
|
+
}
|
|
2441
|
+
throw error;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
function parseRasterDataUrl(value: string): {
|
|
2446
|
+
type: PresentationRasterImageContentType;
|
|
2447
|
+
canonical: string;
|
|
2448
|
+
} {
|
|
2449
|
+
try {
|
|
2450
|
+
const result = canonicalizeRasterDataUrl(value, MAX_IMAGE_BYTES);
|
|
2451
|
+
return { type: result.contentType, canonical: result.canonical };
|
|
2452
|
+
} catch (error) {
|
|
2453
|
+
if (error instanceof RasterImageValidationError) {
|
|
2454
|
+
throw new InvalidPresentationInputError("image dataUrl", error.detail);
|
|
2455
|
+
}
|
|
2456
|
+
throw error;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
function unresolvedImageUriError(id: string): UnsupportedPresentationFeatureError {
|
|
2461
|
+
return new UnsupportedPresentationFeatureError(
|
|
2462
|
+
"unresolved image URI",
|
|
2463
|
+
`Image ${id} is a host reference. Call image.resolveUri() explicitly before render or export`,
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function normalizePosition(
|
|
2468
|
+
input: Partial<PresentationPosition> | undefined,
|
|
2469
|
+
defaults: PresentationPosition = { left: 0, top: 0, width: 100, height: 100 },
|
|
2470
|
+
): PresentationPosition {
|
|
2471
|
+
const result = {
|
|
2472
|
+
left: input?.left ?? defaults.left,
|
|
2473
|
+
top: input?.top ?? defaults.top,
|
|
2474
|
+
width: input?.width ?? defaults.width,
|
|
2475
|
+
height: input?.height ?? defaults.height,
|
|
2476
|
+
};
|
|
2477
|
+
validatePosition(result, "position");
|
|
2478
|
+
return result;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function normalizeCrop(crop: {
|
|
2482
|
+
left: number;
|
|
2483
|
+
top: number;
|
|
2484
|
+
right: number;
|
|
2485
|
+
bottom: number;
|
|
2486
|
+
}): typeof crop {
|
|
2487
|
+
const normalized = { ...crop };
|
|
2488
|
+
if (
|
|
2489
|
+
!Object.values(normalized).every(
|
|
2490
|
+
(value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1,
|
|
2491
|
+
)
|
|
2492
|
+
) {
|
|
2493
|
+
throw new InvalidPresentationInputError("image crop", "values must be between 0 and 1");
|
|
2494
|
+
}
|
|
2495
|
+
if (normalized.left + normalized.right >= 1 || normalized.top + normalized.bottom >= 1) {
|
|
2496
|
+
throw new InvalidPresentationInputError(
|
|
2497
|
+
"image crop",
|
|
2498
|
+
"cannot remove the complete source image",
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
return normalized;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
function colorValue(fill: PresentationFill): string {
|
|
2505
|
+
if (typeof fill !== "string") {
|
|
2506
|
+
if (!fill || typeof fill !== "object" || (fill.type !== undefined && fill.type !== "solid")) {
|
|
2507
|
+
throw new InvalidPresentationInputError("fill", "must be a string or a solid color object");
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
const value = typeof fill === "string" ? fill : fill.color;
|
|
2511
|
+
if (typeof value !== "string") {
|
|
2512
|
+
throw new InvalidPresentationInputError("color", "must be a string");
|
|
2513
|
+
}
|
|
2514
|
+
if (value === "none") return "none";
|
|
2515
|
+
const token = COLOR_TOKENS[value];
|
|
2516
|
+
if (token) return token;
|
|
2517
|
+
if (/^#[0-9a-f]{6}$/i.test(value)) return value.toLowerCase();
|
|
2518
|
+
if (/^#[0-9a-f]{3}$/i.test(value)) {
|
|
2519
|
+
return `#${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}`.toLowerCase();
|
|
2520
|
+
}
|
|
2521
|
+
throw new InvalidPresentationInputError("color", "must be a known token, none, #RGB, or #RRGGBB");
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
const COLOR_TOKENS: Readonly<Record<string, string>> = {
|
|
2525
|
+
white: "#ffffff",
|
|
2526
|
+
black: "#000000",
|
|
2527
|
+
accent1: "#2563eb",
|
|
2528
|
+
accent2: "#0f766e",
|
|
2529
|
+
"slate-50": "#f8fafc",
|
|
2530
|
+
"slate-100": "#f1f5f9",
|
|
2531
|
+
"slate-200": "#e2e8f0",
|
|
2532
|
+
"slate-300": "#cbd5e1",
|
|
2533
|
+
"slate-400": "#94a3b8",
|
|
2534
|
+
"slate-500": "#64748b",
|
|
2535
|
+
"slate-600": "#475569",
|
|
2536
|
+
"slate-700": "#334155",
|
|
2537
|
+
"slate-800": "#1e293b",
|
|
2538
|
+
"slate-900": "#0f172a",
|
|
2539
|
+
"slate-950": "#020617",
|
|
2540
|
+
};
|
|
2541
|
+
|
|
2542
|
+
function projectRecord(
|
|
2543
|
+
record: Record<string, unknown>,
|
|
2544
|
+
includeText: string | undefined,
|
|
2545
|
+
excludeText: string | undefined,
|
|
2546
|
+
): Record<string, unknown> {
|
|
2547
|
+
const include = parseProjection(includeText);
|
|
2548
|
+
const exclude = parseProjection(excludeText);
|
|
2549
|
+
if (include.size === 0 && exclude.size === 0) return record;
|
|
2550
|
+
const projected: Record<string, unknown> = {};
|
|
2551
|
+
for (const [key, value] of Object.entries(record)) {
|
|
2552
|
+
if ((include.size === 0 || include.has(key)) && !exclude.has(key)) projected[key] = value;
|
|
2553
|
+
}
|
|
2554
|
+
return projected;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function parseProjection(value: string | undefined): Set<string> {
|
|
2558
|
+
return new Set(
|
|
2559
|
+
(value ?? "")
|
|
2560
|
+
.split(",")
|
|
2561
|
+
.map((part) => part.trim())
|
|
2562
|
+
.filter(Boolean),
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
function boundPresentationRecords(
|
|
2567
|
+
records: Array<Record<string, unknown>>,
|
|
2568
|
+
maxChars: number,
|
|
2569
|
+
): PresentationInspectResult {
|
|
2570
|
+
if (!Number.isInteger(maxChars) || maxChars <= 0)
|
|
2571
|
+
throw new Error("inspect maxChars must be a positive integer");
|
|
2572
|
+
return boundInspectionRecords(records, maxChars);
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
function jsonBlob(value: unknown, name: string): FileBlob {
|
|
2576
|
+
return new FileBlob([JSON.stringify(value, null, 2)], { name, type: "application/json" });
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
function positionTuple(position: PresentationPosition): [number, number, number, number] {
|
|
2580
|
+
return [position.left, position.top, position.width, position.height];
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
function finiteOr(value: number | undefined, fallback: number): number {
|
|
2584
|
+
if (value === undefined) return fallback;
|
|
2585
|
+
if (!Number.isFinite(value)) {
|
|
2586
|
+
throw new InvalidPresentationInputError("number", "must be finite");
|
|
2587
|
+
}
|
|
2588
|
+
return value;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
function truncate(value: string, max: number): string {
|
|
2592
|
+
return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
export {
|
|
2596
|
+
assertRenderPixelBudget as assertPresentationRenderPixelBudget,
|
|
2597
|
+
colorValue as presentationColorValue,
|
|
2598
|
+
truncate as truncatePresentationText,
|
|
2599
|
+
};
|