@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.
Files changed (203) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +104 -0
  3. package/dist/bounded-zip.d.ts +28 -0
  4. package/dist/chunk-4BSENMAE.js +39 -0
  5. package/dist/chunk-4BSENMAE.js.map +1 -0
  6. package/dist/chunk-4FRFNNBM.js +276 -0
  7. package/dist/chunk-4FRFNNBM.js.map +1 -0
  8. package/dist/chunk-5XIA6KHZ.js +49 -0
  9. package/dist/chunk-5XIA6KHZ.js.map +1 -0
  10. package/dist/chunk-66CT2ZRP.js +2229 -0
  11. package/dist/chunk-66CT2ZRP.js.map +1 -0
  12. package/dist/chunk-72SYDU4O.js +319 -0
  13. package/dist/chunk-72SYDU4O.js.map +1 -0
  14. package/dist/chunk-B2ZJRZDA.js +279 -0
  15. package/dist/chunk-B2ZJRZDA.js.map +1 -0
  16. package/dist/chunk-DCTLTNPW.js +2458 -0
  17. package/dist/chunk-DCTLTNPW.js.map +1 -0
  18. package/dist/chunk-FGZM4FQ6.js +473 -0
  19. package/dist/chunk-FGZM4FQ6.js.map +1 -0
  20. package/dist/chunk-HTJBLBDE.js +591 -0
  21. package/dist/chunk-HTJBLBDE.js.map +1 -0
  22. package/dist/chunk-IYFXZQ34.js +5306 -0
  23. package/dist/chunk-IYFXZQ34.js.map +1 -0
  24. package/dist/chunk-J6N7IR44.js +172 -0
  25. package/dist/chunk-J6N7IR44.js.map +1 -0
  26. package/dist/chunk-N2IHNWBS.js +1002 -0
  27. package/dist/chunk-N2IHNWBS.js.map +1 -0
  28. package/dist/chunk-NAG44K67.js +549 -0
  29. package/dist/chunk-NAG44K67.js.map +1 -0
  30. package/dist/chunk-O24KTEGP.js +33 -0
  31. package/dist/chunk-O24KTEGP.js.map +1 -0
  32. package/dist/chunk-PI2AZVGK.js +2074 -0
  33. package/dist/chunk-PI2AZVGK.js.map +1 -0
  34. package/dist/chunk-Q2FFHEY7.js +40 -0
  35. package/dist/chunk-Q2FFHEY7.js.map +1 -0
  36. package/dist/chunk-RBGQADXA.js +57 -0
  37. package/dist/chunk-RBGQADXA.js.map +1 -0
  38. package/dist/chunk-U5G2BKM2.js +613 -0
  39. package/dist/chunk-U5G2BKM2.js.map +1 -0
  40. package/dist/chunk-VOJHMSM3.js +39 -0
  41. package/dist/chunk-VOJHMSM3.js.map +1 -0
  42. package/dist/chunk-VUNV25KB.js +16 -0
  43. package/dist/chunk-VUNV25KB.js.map +1 -0
  44. package/dist/chunk-XIFUNVBO.js +101 -0
  45. package/dist/chunk-XIFUNVBO.js.map +1 -0
  46. package/dist/chunk-ZCJYRJGB.js +489 -0
  47. package/dist/chunk-ZCJYRJGB.js.map +1 -0
  48. package/dist/chunk-ZZXPEE4S.js +2260 -0
  49. package/dist/chunk-ZZXPEE4S.js.map +1 -0
  50. package/dist/document-docx-api.d.ts +49 -0
  51. package/dist/document-docx-codec.d.ts +9 -0
  52. package/dist/document-docx-codec.js +4765 -0
  53. package/dist/document-docx-codec.js.map +1 -0
  54. package/dist/document-docx-import.d.ts +261 -0
  55. package/dist/document-docx-packer.d.ts +18 -0
  56. package/dist/document-docx-state.d.ts +12 -0
  57. package/dist/document-render.d.ts +3 -0
  58. package/dist/document-render.js +492 -0
  59. package/dist/document-render.js.map +1 -0
  60. package/dist/document.d.ts +432 -0
  61. package/dist/document.js +37 -0
  62. package/dist/document.js.map +1 -0
  63. package/dist/errors.d.ts +23 -0
  64. package/dist/file-blob.d.ts +11 -0
  65. package/dist/index.d.ts +4 -0
  66. package/dist/index.js +186 -0
  67. package/dist/index.js.map +1 -0
  68. package/dist/inspection.d.ts +12 -0
  69. package/dist/kernel.d.ts +101 -0
  70. package/dist/lib-GQLPACFK.js +9746 -0
  71. package/dist/lib-GQLPACFK.js.map +1 -0
  72. package/dist/materializer-cli-entry.d.ts +2 -0
  73. package/dist/materializer-cli-entry.js +29 -0
  74. package/dist/materializer-cli-entry.js.map +1 -0
  75. package/dist/materializer-cli.d.ts +24 -0
  76. package/dist/materializer-cli.js +24 -0
  77. package/dist/materializer-cli.js.map +1 -0
  78. package/dist/native-raster.d.ts +10 -0
  79. package/dist/native.d.ts +129 -0
  80. package/dist/native.js +31 -0
  81. package/dist/native.js.map +1 -0
  82. package/dist/office-import.d.ts +48 -0
  83. package/dist/office-import.js +154 -0
  84. package/dist/office-import.js.map +1 -0
  85. package/dist/presentation-pptx-api.d.ts +60 -0
  86. package/dist/presentation-pptx-import.d.ts +5 -0
  87. package/dist/presentation-pptx-state-digest-765CZZED.js +10 -0
  88. package/dist/presentation-pptx-state-digest-765CZZED.js.map +1 -0
  89. package/dist/presentation-pptx-state-digest.d.ts +3 -0
  90. package/dist/presentation-pptx-state.d.ts +4 -0
  91. package/dist/presentation-pptx-xml.d.ts +29 -0
  92. package/dist/presentation-pptx.d.ts +5 -0
  93. package/dist/presentation-pptx.js +2365 -0
  94. package/dist/presentation-pptx.js.map +1 -0
  95. package/dist/presentation-render.d.ts +29 -0
  96. package/dist/presentation-render.js +375 -0
  97. package/dist/presentation-render.js.map +1 -0
  98. package/dist/presentation.d.ts +565 -0
  99. package/dist/presentation.js +71 -0
  100. package/dist/presentation.js.map +1 -0
  101. package/dist/production-composite.d.ts +68 -0
  102. package/dist/production-document.d.ts +2 -0
  103. package/dist/production-document.js +176 -0
  104. package/dist/production-document.js.map +1 -0
  105. package/dist/production-native-codecs.d.ts +94 -0
  106. package/dist/production-presentation.d.ts +2 -0
  107. package/dist/production-presentation.js +176 -0
  108. package/dist/production-presentation.js.map +1 -0
  109. package/dist/production-sha256.d.ts +2 -0
  110. package/dist/production-spreadsheet-native.d.ts +12 -0
  111. package/dist/production-spreadsheet.d.ts +2 -0
  112. package/dist/production-spreadsheet.js +176 -0
  113. package/dist/production-spreadsheet.js.map +1 -0
  114. package/dist/production.d.ts +96 -0
  115. package/dist/raster-image.d.ts +34 -0
  116. package/dist/reference.d.ts +17 -0
  117. package/dist/reference.js +574 -0
  118. package/dist/reference.js.map +1 -0
  119. package/dist/runtime-cli-entry.d.ts +2 -0
  120. package/dist/runtime-cli-entry.js +21 -0
  121. package/dist/runtime-cli-entry.js.map +1 -0
  122. package/dist/runtime-cli.d.ts +76 -0
  123. package/dist/runtime-cli.js +26 -0
  124. package/dist/runtime-cli.js.map +1 -0
  125. package/dist/runtime-development.d.ts +73 -0
  126. package/dist/runtime-development.js +30 -0
  127. package/dist/runtime-development.js.map +1 -0
  128. package/dist/runtime-receipt.d.ts +19 -0
  129. package/dist/runtime.d.ts +355 -0
  130. package/dist/runtime.js +42 -0
  131. package/dist/runtime.js.map +1 -0
  132. package/dist/snapshot.d.ts +26 -0
  133. package/dist/snapshot.js +61 -0
  134. package/dist/snapshot.js.map +1 -0
  135. package/dist/spreadsheet-address.d.ts +18 -0
  136. package/dist/spreadsheet-file.d.ts +17 -0
  137. package/dist/spreadsheet-formula.d.ts +67 -0
  138. package/dist/spreadsheet-image.d.ts +17 -0
  139. package/dist/spreadsheet-render.d.ts +5 -0
  140. package/dist/spreadsheet-render.js +752 -0
  141. package/dist/spreadsheet-render.js.map +1 -0
  142. package/dist/spreadsheet-types.d.ts +155 -0
  143. package/dist/spreadsheet-xlsx-api.d.ts +88 -0
  144. package/dist/spreadsheet-xlsx-codec.d.ts +16 -0
  145. package/dist/spreadsheet-xlsx-codec.js +16 -0
  146. package/dist/spreadsheet-xlsx-codec.js.map +1 -0
  147. package/dist/spreadsheet-xlsx-state.d.ts +16 -0
  148. package/dist/spreadsheet.d.ts +724 -0
  149. package/dist/spreadsheet.js +67 -0
  150. package/dist/spreadsheet.js.map +1 -0
  151. package/package.json +129 -0
  152. package/src/bounded-zip.ts +450 -0
  153. package/src/document-docx-api.ts +64 -0
  154. package/src/document-docx-codec.ts +2253 -0
  155. package/src/document-docx-import.ts +3814 -0
  156. package/src/document-docx-packer.ts +72 -0
  157. package/src/document-docx-state.ts +59 -0
  158. package/src/document-render.ts +615 -0
  159. package/src/document.ts +2800 -0
  160. package/src/errors.ts +45 -0
  161. package/src/file-blob.ts +43 -0
  162. package/src/index.ts +4 -0
  163. package/src/inspection.ts +50 -0
  164. package/src/kernel.ts +525 -0
  165. package/src/materializer-cli-entry.ts +11 -0
  166. package/src/materializer-cli.ts +743 -0
  167. package/src/native-raster.ts +141 -0
  168. package/src/native.ts +390 -0
  169. package/src/office-import.ts +243 -0
  170. package/src/presentation-pptx-api.ts +107 -0
  171. package/src/presentation-pptx-import.ts +2065 -0
  172. package/src/presentation-pptx-state-digest.ts +46 -0
  173. package/src/presentation-pptx-state.ts +18 -0
  174. package/src/presentation-pptx-xml.ts +473 -0
  175. package/src/presentation-pptx.ts +447 -0
  176. package/src/presentation-render.ts +532 -0
  177. package/src/presentation.ts +2599 -0
  178. package/src/production-composite.ts +805 -0
  179. package/src/production-document.ts +2 -0
  180. package/src/production-native-codecs.ts +1141 -0
  181. package/src/production-presentation.ts +2 -0
  182. package/src/production-sha256.ts +150 -0
  183. package/src/production-spreadsheet-native.ts +686 -0
  184. package/src/production-spreadsheet.ts +2 -0
  185. package/src/production.ts +1022 -0
  186. package/src/raster-image.ts +586 -0
  187. package/src/reference.ts +17 -0
  188. package/src/runtime-cli-entry.ts +13 -0
  189. package/src/runtime-cli.ts +670 -0
  190. package/src/runtime-development.ts +689 -0
  191. package/src/runtime-receipt.ts +169 -0
  192. package/src/runtime.ts +1463 -0
  193. package/src/snapshot.ts +88 -0
  194. package/src/spreadsheet-address.ts +90 -0
  195. package/src/spreadsheet-file.ts +71 -0
  196. package/src/spreadsheet-formula.ts +1212 -0
  197. package/src/spreadsheet-image.ts +242 -0
  198. package/src/spreadsheet-render.ts +985 -0
  199. package/src/spreadsheet-types.ts +171 -0
  200. package/src/spreadsheet-xlsx-api.ts +123 -0
  201. package/src/spreadsheet-xlsx-codec.ts +3063 -0
  202. package/src/spreadsheet-xlsx-state.ts +191 -0
  203. package/src/spreadsheet.ts +5195 -0
@@ -0,0 +1,1141 @@
1
+ import type {
2
+ DocumentPageGeometry,
3
+ DocumentParagraphFormat,
4
+ DocumentParagraphStyle,
5
+ DocumentTableStyle,
6
+ DocumentTextStyle,
7
+ SerializedDocument,
8
+ SerializedDocumentBlock,
9
+ SerializedDocumentComment,
10
+ SerializedDocumentSection,
11
+ SerializedParagraph,
12
+ SerializedStory,
13
+ SerializedTable,
14
+ SerializedTextRun,
15
+ SerializedTrackedChange,
16
+ } from "./document";
17
+ import {
18
+ Presentation,
19
+ PresentationChart,
20
+ PresentationGroup,
21
+ PresentationImage,
22
+ PresentationShape,
23
+ PresentationTable,
24
+ presentationColorValue,
25
+ type PresentationElement,
26
+ type PresentationFill,
27
+ type PresentationLine,
28
+ type PresentationPosition,
29
+ type PresentationTemplateElement,
30
+ type PresentationText,
31
+ type PresentationTextStyle,
32
+ } from "./presentation";
33
+ import { UnsupportedArtifactFeatureError } from "./errors";
34
+ import { decodeRasterBase64, inspectRasterImage } from "./raster-image";
35
+ import { sha256Bytes } from "./production-sha256";
36
+
37
+ const textEncoder = new TextEncoder();
38
+ const DOCUMENT_COMMAND_MAX_BYTES = 64 * 1024 * 1024;
39
+ const DOCUMENT_COMMAND_MAX_COUNT = 4_096;
40
+ const PRESENTATION_COMMAND_MAX_BYTES = 4 * 1024 * 1024;
41
+ const PRESENTATION_COMMAND_MAX_COUNT = 10_000;
42
+ const HEADER_BYTES = 24;
43
+ const CHECKSUM_BYTES = 8;
44
+ const EMU_PER_CSS_PIXEL = 9_525;
45
+ const PRESENTATION_MAX_COORDINATE_EMU = 9_525_000_000_000;
46
+
47
+ export function encodeDocumentProjectionCommands(snapshot: SerializedDocument): Uint8Array {
48
+ const payload = new ByteWriter(DOCUMENT_COMMAND_MAX_BYTES - HEADER_BYTES - CHECKSUM_BYTES);
49
+ let commandCount = 0;
50
+ const command = (write: () => void): void => {
51
+ commandCount += 1;
52
+ if (commandCount > DOCUMENT_COMMAND_MAX_COUNT) {
53
+ throw new RangeError("Document projection exceeds the native command-count limit");
54
+ }
55
+ write();
56
+ };
57
+
58
+ const firstSection = snapshot.sections[0];
59
+ if (!firstSection || firstSection.startBlockIndex !== 0) {
60
+ throw new Error("Document projection requires one initial section at body offset zero");
61
+ }
62
+ assertInitialDocumentSectionIds(firstSection, snapshot.idNamespace);
63
+
64
+ if (snapshot.evenAndOddHeaders !== undefined || snapshot.trackRevisions !== undefined) {
65
+ command(() => {
66
+ payload.u8(0);
67
+ payload.u8(optionalOptionalBool(snapshot.evenAndOddHeaders));
68
+ payload.u8(optionalOptionalBool(snapshot.trackRevisions));
69
+ });
70
+ }
71
+ if (firstSection.titlePage !== undefined) {
72
+ command(() => {
73
+ payload.u8(9);
74
+ payload.documentId(firstSection.id, "sec");
75
+ payload.u8(optionalBool(firstSection.titlePage));
76
+ });
77
+ }
78
+ if (!isDefaultNativeDocumentPage(firstSection.page)) {
79
+ command(() => {
80
+ payload.u8(14);
81
+ payload.documentId(firstSection.id, "sec");
82
+ payload.pageWithExtras(firstSection.page);
83
+ });
84
+ }
85
+
86
+ const laterSections = new Map<number, SerializedDocumentSection>();
87
+ for (const section of snapshot.sections.slice(1)) {
88
+ if (laterSections.has(section.startBlockIndex)) {
89
+ throw new Error("Document projection contains adjacent sections without a body block");
90
+ }
91
+ laterSections.set(section.startBlockIndex, section);
92
+ }
93
+ for (let index = 0; index <= snapshot.blocks.length; index += 1) {
94
+ const section = laterSections.get(index);
95
+ if (section) {
96
+ command(() => writeAddSection(payload, section));
97
+ if (hasNonDefaultDocumentPageExtras(section.page)) {
98
+ command(() => {
99
+ payload.u8(14);
100
+ payload.documentId(section.id, "sec");
101
+ payload.pageWithExtras(section.page);
102
+ });
103
+ }
104
+ }
105
+ const block = snapshot.blocks[index];
106
+ if (block) command(() => writeDocumentBlock(payload, block, { kind: "body" }));
107
+ }
108
+ if ([...laterSections.keys()].some((index) => index > snapshot.blocks.length)) {
109
+ throw new Error("Document section starts beyond the body block list");
110
+ }
111
+
112
+ for (const section of snapshot.sections) {
113
+ writeDocumentStoryCommands(
114
+ payload,
115
+ command,
116
+ section,
117
+ "header",
118
+ "default",
119
+ section.headers.default,
120
+ );
121
+ writeDocumentStoryCommands(payload, command, section, "header", "first", section.headers.first);
122
+ writeDocumentStoryCommands(payload, command, section, "header", "even", section.headers.even);
123
+ writeDocumentStoryCommands(
124
+ payload,
125
+ command,
126
+ section,
127
+ "footer",
128
+ "default",
129
+ section.footers.default,
130
+ );
131
+ writeDocumentStoryCommands(payload, command, section, "footer", "first", section.footers.first);
132
+ writeDocumentStoryCommands(payload, command, section, "footer", "even", section.footers.even);
133
+ }
134
+
135
+ for (const thread of snapshot.comments) {
136
+ const root = thread.replies[0];
137
+ if (!root) throw new Error(`Document comment ${thread.id} has no root reply`);
138
+ command(() => {
139
+ payload.u8(10);
140
+ payload.documentId(thread.id, "dc");
141
+ payload.documentId(thread.blockId, "p");
142
+ payload.u32(thread.start);
143
+ payload.u32(thread.end);
144
+ payload.bool(thread.resolved);
145
+ payload.commentReply(root);
146
+ });
147
+ for (const reply of thread.replies.slice(1)) {
148
+ command(() => {
149
+ payload.u8(11);
150
+ payload.documentId(thread.id, "dc");
151
+ payload.commentReply(reply);
152
+ });
153
+ }
154
+ }
155
+ for (const change of snapshot.changes) {
156
+ command(() => {
157
+ payload.u8(13);
158
+ payload.documentId(change.id, "chg");
159
+ payload.documentId(change.blockId, "p");
160
+ payload.u32(change.start);
161
+ payload.u32(change.end);
162
+ payload.u8(change.kind === "insert" ? 1 : 2);
163
+ payload.string(change.author);
164
+ payload.string(change.createdAt);
165
+ });
166
+ }
167
+
168
+ return envelope("OGADC001", commandCount, payload.finish(), DOCUMENT_COMMAND_MAX_BYTES);
169
+ }
170
+
171
+ export type DocumentProjectionTarget =
172
+ | { kind: "body" }
173
+ | {
174
+ kind: "story";
175
+ sectionId: string;
176
+ storyKind: "header" | "footer";
177
+ variant: "default" | "first" | "even";
178
+ };
179
+
180
+ export type DocumentIncrementalCommand =
181
+ | { kind: "block.add"; target: DocumentProjectionTarget; block: SerializedDocumentBlock }
182
+ | {
183
+ kind: "paragraph.edit";
184
+ id: string;
185
+ start: number;
186
+ end: number;
187
+ text: string;
188
+ style?: DocumentTextStyle;
189
+ }
190
+ | { kind: "paragraph.format"; id: string; format: DocumentParagraphFormat }
191
+ | { kind: "paragraph.style"; id: string; style: DocumentParagraphStyle }
192
+ | { kind: "table.style"; id: string; style: DocumentTableStyle }
193
+ | { kind: "document.flags"; evenAndOddHeaders?: boolean; trackRevisions?: boolean }
194
+ | { kind: "section.add"; section: SerializedDocumentSection }
195
+ | { kind: "section.title"; id: string; titlePage?: boolean }
196
+ | { kind: "comment.add"; comment: SerializedDocumentComment }
197
+ | { kind: "comment.reply"; id: string; reply: SerializedDocumentComment["replies"][number] }
198
+ | { kind: "comment.resolved"; id: string; resolved: boolean }
199
+ | { kind: "tracked.add"; change: SerializedTrackedChange };
200
+
201
+ /** Encodes only touched document nodes using the existing OGADC001 protocol. */
202
+ export function encodeDocumentIncrementalCommands(
203
+ commands: readonly DocumentIncrementalCommand[],
204
+ ): Uint8Array {
205
+ const payload = new ByteWriter(DOCUMENT_COMMAND_MAX_BYTES - HEADER_BYTES - CHECKSUM_BYTES);
206
+ if (commands.length === 0 || commands.length > DOCUMENT_COMMAND_MAX_COUNT) {
207
+ throw new RangeError("Document incremental command count is outside its limit");
208
+ }
209
+ for (const command of commands) {
210
+ switch (command.kind) {
211
+ case "block.add":
212
+ writeDocumentBlock(payload, command.block, command.target);
213
+ break;
214
+ case "paragraph.edit":
215
+ payload.u8(2);
216
+ payload.documentId(command.id, "p");
217
+ payload.u32(command.start);
218
+ payload.u32(command.end);
219
+ payload.string(command.text);
220
+ payload.bool(command.style !== undefined);
221
+ if (command.style) payload.textStyle(command.style);
222
+ break;
223
+ case "paragraph.format":
224
+ payload.u8(3);
225
+ payload.documentId(command.id, "p");
226
+ payload.u32(command.format.start);
227
+ payload.u32(command.format.end);
228
+ payload.textStylePatch(command.format.style);
229
+ break;
230
+ case "paragraph.style":
231
+ payload.u8(4);
232
+ payload.documentId(command.id, "p");
233
+ payload.paragraphStyle(command.style);
234
+ break;
235
+ case "table.style":
236
+ payload.u8(6);
237
+ payload.documentId(command.id, "dt");
238
+ payload.tableStyle(command.style);
239
+ break;
240
+ case "document.flags":
241
+ payload.u8(0);
242
+ payload.u8(optionalOptionalBool(command.evenAndOddHeaders));
243
+ payload.u8(optionalOptionalBool(command.trackRevisions));
244
+ break;
245
+ case "section.add":
246
+ writeAddSection(payload, command.section);
247
+ if (hasNonDefaultDocumentPageExtras(command.section.page)) {
248
+ payload.u8(14);
249
+ payload.documentId(command.section.id, "sec");
250
+ payload.pageWithExtras(command.section.page);
251
+ }
252
+ break;
253
+ case "section.title":
254
+ payload.u8(9);
255
+ payload.documentId(command.id, "sec");
256
+ payload.u8(optionalBool(command.titlePage));
257
+ break;
258
+ case "comment.add": {
259
+ const root = command.comment.replies[0];
260
+ if (!root) throw new Error(`Document comment ${command.comment.id} has no root reply`);
261
+ payload.u8(10);
262
+ payload.documentId(command.comment.id, "dc");
263
+ payload.documentId(command.comment.blockId, "p");
264
+ payload.u32(command.comment.start);
265
+ payload.u32(command.comment.end);
266
+ payload.bool(command.comment.resolved);
267
+ payload.commentReply(root);
268
+ break;
269
+ }
270
+ case "comment.reply":
271
+ payload.u8(11);
272
+ payload.documentId(command.id, "dc");
273
+ payload.commentReply(command.reply);
274
+ break;
275
+ case "comment.resolved":
276
+ payload.u8(12);
277
+ payload.documentId(command.id, "dc");
278
+ payload.bool(command.resolved);
279
+ break;
280
+ case "tracked.add":
281
+ payload.u8(13);
282
+ payload.documentId(command.change.id, "chg");
283
+ payload.documentId(command.change.blockId, "p");
284
+ payload.u32(command.change.start);
285
+ payload.u32(command.change.end);
286
+ payload.u8(command.change.kind === "insert" ? 1 : 2);
287
+ payload.string(command.change.author);
288
+ payload.string(command.change.createdAt);
289
+ break;
290
+ }
291
+ }
292
+ // Section page extras are a second command in the same atomic batch.
293
+ const commandCount =
294
+ commands.length +
295
+ commands.filter(
296
+ (command) =>
297
+ command.kind === "section.add" && hasNonDefaultDocumentPageExtras(command.section.page),
298
+ ).length;
299
+ return envelope("OGADC001", commandCount, payload.finish(), DOCUMENT_COMMAND_MAX_BYTES);
300
+ }
301
+
302
+ export function encodePresentationProjectionCommands(
303
+ presentation: Presentation,
304
+ namespace: bigint,
305
+ ): Uint8Array {
306
+ const hasCustomSlideSize =
307
+ presentation.slideSize.width !== 1_280 || presentation.slideSize.height !== 720;
308
+ if (hasCustomSlideSize) {
309
+ assertFinitePositive(presentation.slideSize.width, "presentation slide width");
310
+ assertFinitePositive(presentation.slideSize.height, "presentation slide height");
311
+ }
312
+ const payload = new ByteWriter(PRESENTATION_COMMAND_MAX_BYTES - HEADER_BYTES - CHECKSUM_BYTES);
313
+ let commandCount = 0;
314
+ const command = (write: () => void): void => {
315
+ commandCount += 1;
316
+ if (commandCount > PRESENTATION_COMMAND_MAX_COUNT) {
317
+ throw new RangeError("Presentation projection exceeds the native command-count limit");
318
+ }
319
+ write();
320
+ };
321
+ if (hasCustomSlideSize) {
322
+ command(() => {
323
+ payload.u8(15);
324
+ payload.i64(presentationSizeEmu(presentation.slideSize.width, "presentation slide width"));
325
+ payload.i64(presentationSizeEmu(presentation.slideSize.height, "presentation slide height"));
326
+ });
327
+ }
328
+ let templateElementCounter = 0xffff_ffff_ffff_ffffn;
329
+ const nextTemplateElementId = (): string => {
330
+ if (templateElementCounter === 0n) {
331
+ throw new RangeError("Presentation template element id space is exhausted");
332
+ }
333
+ return `sh/${templateElementCounter--}`;
334
+ };
335
+
336
+ for (const master of presentation.masters.items) {
337
+ command(() => {
338
+ payload.u8(0);
339
+ payload.presentationId(master.id, namespace);
340
+ payload.string(master.name);
341
+ payload.fill(master.background);
342
+ });
343
+ for (const [index, template] of master.elements.entries()) {
344
+ const element = materializeTemplateElement(template);
345
+ writePresentationElement(
346
+ payload,
347
+ command,
348
+ element,
349
+ namespace,
350
+ { tag: 0, id: master.id },
351
+ undefined,
352
+ index,
353
+ nextTemplateElementId(),
354
+ );
355
+ }
356
+ }
357
+ for (const layout of presentation.layouts.items) {
358
+ command(() => {
359
+ payload.u8(1);
360
+ payload.presentationId(layout.id, namespace);
361
+ payload.string(layout.name);
362
+ payload.optionalPresentationId(layout.masterId, namespace);
363
+ payload.fill(layout.background);
364
+ });
365
+ for (const [index, template] of layout.elements.entries()) {
366
+ const element = materializeTemplateElement(template);
367
+ writePresentationElement(
368
+ payload,
369
+ command,
370
+ element,
371
+ namespace,
372
+ { tag: 1, id: layout.id },
373
+ undefined,
374
+ index,
375
+ nextTemplateElementId(),
376
+ );
377
+ }
378
+ }
379
+ for (const [slideIndex, slide] of presentation.slides.items.entries()) {
380
+ command(() => {
381
+ payload.u8(2);
382
+ payload.presentationId(slide.id, namespace);
383
+ payload.u32(slideIndex);
384
+ payload.string(slide.title);
385
+ payload.optionalPresentationId(slide.layout?.id, namespace);
386
+ payload.fill(slide.background.fill);
387
+ });
388
+ if (slide.notes.toString().length > 0) {
389
+ command(() => {
390
+ payload.u8(8);
391
+ payload.presentationId(slide.id, namespace);
392
+ payload.richText(slide.notes);
393
+ });
394
+ }
395
+ for (const [index, element] of slide.elements.entries()) {
396
+ writePresentationElement(
397
+ payload,
398
+ command,
399
+ element,
400
+ namespace,
401
+ { tag: 2, id: slide.id },
402
+ undefined,
403
+ index,
404
+ );
405
+ }
406
+ }
407
+ return envelope("OGAPC001", commandCount, payload.finish(), PRESENTATION_COMMAND_MAX_BYTES);
408
+ }
409
+
410
+ export type PresentationIncrementalCommand =
411
+ | { kind: "slide.create"; slide: Presentation["slides"]["items"][number]; index: number }
412
+ | { kind: "slide.title"; id: string; title: string }
413
+ | { kind: "slide.layout"; id: string; layoutId?: string }
414
+ | { kind: "slide.notes"; id: string; notes: PresentationText }
415
+ | { kind: "node.insert"; slideId: string; index: number; element: PresentationElement }
416
+ | { kind: "node.update"; element: PresentationElement }
417
+ | { kind: "presentation.size"; width: number; height: number };
418
+
419
+ /** Encodes touched presentation nodes using the existing OGAPC001 protocol. */
420
+ export function encodePresentationIncrementalCommands(
421
+ commands: readonly PresentationIncrementalCommand[],
422
+ namespace: bigint,
423
+ ): Uint8Array {
424
+ const payload = new ByteWriter(PRESENTATION_COMMAND_MAX_BYTES - HEADER_BYTES - CHECKSUM_BYTES);
425
+ let commandCount = 0;
426
+ const command = (write: () => void): void => {
427
+ commandCount += 1;
428
+ if (commandCount > PRESENTATION_COMMAND_MAX_COUNT) {
429
+ throw new RangeError("Presentation incremental command count is outside its limit");
430
+ }
431
+ write();
432
+ };
433
+ for (const entry of commands) {
434
+ switch (entry.kind) {
435
+ case "slide.create":
436
+ command(() => {
437
+ payload.u8(2);
438
+ payload.presentationId(entry.slide.id, namespace);
439
+ payload.u32(entry.index);
440
+ payload.string(entry.slide.title);
441
+ payload.optionalPresentationId(entry.slide.layout?.id, namespace);
442
+ payload.fill(entry.slide.background.fill);
443
+ });
444
+ break;
445
+ case "slide.title":
446
+ command(() => {
447
+ payload.u8(6);
448
+ payload.presentationId(entry.id, namespace);
449
+ payload.string(entry.title);
450
+ });
451
+ break;
452
+ case "slide.layout":
453
+ command(() => {
454
+ payload.u8(7);
455
+ payload.presentationId(entry.id, namespace);
456
+ payload.optionalPresentationId(entry.layoutId, namespace);
457
+ });
458
+ break;
459
+ case "slide.notes":
460
+ command(() => {
461
+ payload.u8(8);
462
+ payload.presentationId(entry.id, namespace);
463
+ payload.richText(entry.notes);
464
+ });
465
+ break;
466
+ case "node.insert":
467
+ writePresentationElement(
468
+ payload,
469
+ command,
470
+ entry.element,
471
+ namespace,
472
+ { tag: 2, id: entry.slideId },
473
+ undefined,
474
+ entry.index,
475
+ );
476
+ break;
477
+ case "node.update":
478
+ command(() => {
479
+ payload.u8(12);
480
+ payload.presentationId(entry.element.id, namespace);
481
+ payload.presentationRect(entry.element.position);
482
+ });
483
+ command(() => {
484
+ payload.u8(13);
485
+ payload.presentationId(entry.element.id, namespace);
486
+ payload.presentationTransform(
487
+ "rotation" in entry.element ? entry.element.rotation : 0,
488
+ "flipHorizontal" in entry.element ? entry.element.flipHorizontal : false,
489
+ "flipVertical" in entry.element ? entry.element.flipVertical : false,
490
+ );
491
+ });
492
+ command(() => {
493
+ payload.u8(14);
494
+ payload.presentationId(entry.element.id, namespace);
495
+ payload.presentationNode(entry.element);
496
+ });
497
+ break;
498
+ case "presentation.size":
499
+ command(() => {
500
+ payload.u8(15);
501
+ payload.i64(presentationSizeEmu(entry.width, "presentation slide width"));
502
+ payload.i64(presentationSizeEmu(entry.height, "presentation slide height"));
503
+ });
504
+ break;
505
+ }
506
+ }
507
+ if (commandCount === 0) throw new RangeError("Presentation incremental command batch is empty");
508
+ return envelope("OGAPC001", commandCount, payload.finish(), PRESENTATION_COMMAND_MAX_BYTES);
509
+ }
510
+
511
+ type NativePresentationSceneOwner = Readonly<{ tag: 0 | 1 | 2; id: string }>;
512
+
513
+ /**
514
+ * Template configs intentionally are not scene objects in the public model.
515
+ * Materialize them through the same constructors used by ordinary slides so
516
+ * native projection receives the exact same validation and normalization.
517
+ */
518
+ function materializeTemplateElement(template: PresentationTemplateElement): PresentationElement {
519
+ const scratch = Presentation.create();
520
+ const slide = scratch.slides.add();
521
+ switch (template.kind) {
522
+ case "shape":
523
+ return slide.shapes.add(template.config);
524
+ case "chart":
525
+ return slide.charts.add(template.type, template.config);
526
+ case "image":
527
+ return slide.images.add(template.config);
528
+ case "table":
529
+ return slide.tables.add(template.config);
530
+ }
531
+ }
532
+
533
+ function writeDocumentStoryCommands(
534
+ writer: ByteWriter,
535
+ command: (write: () => void) => void,
536
+ section: SerializedDocumentSection,
537
+ kind: "header" | "footer",
538
+ variant: "default" | "first" | "even",
539
+ story: SerializedStory,
540
+ ): void {
541
+ const expectedPrefix = kind === "header" ? "hdr" : "ftr";
542
+ writer.assertDocumentId(story.id, expectedPrefix);
543
+ for (const block of story.blocks) {
544
+ command(() =>
545
+ writeDocumentBlock(writer, block, {
546
+ kind: "story",
547
+ sectionId: section.id,
548
+ storyKind: kind,
549
+ variant,
550
+ }),
551
+ );
552
+ }
553
+ }
554
+
555
+ type DocumentTarget = DocumentProjectionTarget;
556
+
557
+ function writeDocumentBlock(
558
+ writer: ByteWriter,
559
+ block: SerializedDocumentBlock | SerializedParagraph | SerializedTable,
560
+ target: DocumentTarget,
561
+ ): void {
562
+ if (block.kind === "paragraph") {
563
+ writer.u8(1);
564
+ writer.documentTarget(target);
565
+ writer.documentId(block.id, "p");
566
+ writer.textRuns(block.runs);
567
+ writer.paragraphStyle(block.style);
568
+ return;
569
+ }
570
+ if (block.kind === "table") {
571
+ writer.u8(5);
572
+ writer.documentTarget(target);
573
+ writer.documentId(block.id, "dt");
574
+ writer.u32(block.rows.length);
575
+ for (const row of block.rows) {
576
+ writer.u32(row.length);
577
+ for (const cell of row) writer.textRuns(cell);
578
+ }
579
+ writer.tableStyle(block.style);
580
+ return;
581
+ }
582
+ if (target.kind !== "body") {
583
+ throw new Error("Document story cannot contain a page break");
584
+ }
585
+ writer.u8(7);
586
+ writer.documentId(block.id, "pb");
587
+ }
588
+
589
+ function writeAddSection(writer: ByteWriter, section: SerializedDocumentSection): void {
590
+ writer.u8(8);
591
+ writer.documentId(section.id, "sec");
592
+ writer.documentId(section.headers.default.id, "hdr");
593
+ writer.documentId(section.headers.first.id, "hdr");
594
+ writer.documentId(section.headers.even.id, "hdr");
595
+ writer.documentId(section.footers.default.id, "ftr");
596
+ writer.documentId(section.footers.first.id, "ftr");
597
+ writer.documentId(section.footers.even.id, "ftr");
598
+ writer.page(section.page);
599
+ writer.u8(optionalBool(section.titlePage));
600
+ }
601
+
602
+ function assertInitialDocumentSectionIds(
603
+ section: SerializedDocumentSection,
604
+ namespace: string,
605
+ ): void {
606
+ const expected = [
607
+ [section.id, "sec", 1n],
608
+ [section.headers.default.id, "hdr", 2n],
609
+ [section.headers.first.id, "hdr", 3n],
610
+ [section.headers.even.id, "hdr", 4n],
611
+ [section.footers.default.id, "ftr", 5n],
612
+ [section.footers.first.id, "ftr", 6n],
613
+ [section.footers.even.id, "ftr", 7n],
614
+ ] as const;
615
+ for (const [id, prefix, counter] of expected) {
616
+ const parsed = parseDocumentId(id, prefix);
617
+ if (parsed.namespace !== BigInt(`0x${namespace}`) || parsed.counter !== counter) {
618
+ throw new Error("Initial document structural ids do not match the native document allocator");
619
+ }
620
+ }
621
+ }
622
+
623
+ function isDefaultNativeDocumentPage(page: DocumentPageGeometry): boolean {
624
+ return (
625
+ page.widthPt === 612 &&
626
+ page.heightPt === 792 &&
627
+ page.marginTopPt === 72 &&
628
+ page.marginRightPt === 72 &&
629
+ page.marginBottomPt === 72 &&
630
+ page.marginLeftPt === 72 &&
631
+ !hasNonDefaultDocumentPageExtras(page)
632
+ );
633
+ }
634
+
635
+ function hasNonDefaultDocumentPageExtras(page: DocumentPageGeometry): boolean {
636
+ return (page.headerPt ?? 36) !== 36 || (page.footerPt ?? 36) !== 36 || (page.gutterPt ?? 0) !== 0;
637
+ }
638
+
639
+ function writePresentationElement(
640
+ writer: ByteWriter,
641
+ command: (write: () => void) => void,
642
+ element: PresentationElement,
643
+ namespace: bigint,
644
+ owner: NativePresentationSceneOwner,
645
+ parentId: string | undefined,
646
+ index: number,
647
+ idOverride?: string,
648
+ ): void {
649
+ const elementId = idOverride ?? element.id;
650
+ command(() => {
651
+ writer.u8(9);
652
+ writer.u8(owner.tag);
653
+ writer.presentationId(owner.id, namespace);
654
+ writer.optionalPresentationId(parentId, namespace);
655
+ writer.u32(index);
656
+ writer.presentationId(elementId, namespace);
657
+ writer.string(element.name);
658
+ writer.presentationRect(element.position);
659
+ writer.presentationTransform(
660
+ "rotation" in element ? element.rotation : 0,
661
+ "flipHorizontal" in element ? element.flipHorizontal : false,
662
+ "flipVertical" in element ? element.flipVertical : false,
663
+ );
664
+ writer.presentationNode(element);
665
+ });
666
+ if (element instanceof PresentationGroup) {
667
+ for (const [childIndex, child] of element.children.entries()) {
668
+ writePresentationElement(writer, command, child, namespace, owner, elementId, childIndex);
669
+ }
670
+ }
671
+ }
672
+
673
+ function unsupportedPresentation(feature: string): never {
674
+ throw new UnsupportedArtifactFeatureError("presentation", feature, "native Rust projection");
675
+ }
676
+
677
+ class ByteWriter {
678
+ readonly #bytes: number[] = [];
679
+ constructor(private readonly maximum: number) {}
680
+
681
+ finish(): Uint8Array {
682
+ return Uint8Array.from(this.#bytes);
683
+ }
684
+ bytes(value: Uint8Array): void {
685
+ if (this.#bytes.length + value.byteLength > this.maximum) {
686
+ throw new RangeError("Native projection command envelope exceeds its byte limit");
687
+ }
688
+ this.#bytes.push(...value);
689
+ }
690
+ u8(value: number): void {
691
+ this.bytes(Uint8Array.of(value));
692
+ }
693
+ bool(value: boolean): void {
694
+ this.u8(value ? 1 : 0);
695
+ }
696
+ u16(value: number): void {
697
+ const bytes = new Uint8Array(2);
698
+ new DataView(bytes.buffer).setUint16(0, value, true);
699
+ this.bytes(bytes);
700
+ }
701
+ u32(value: number): void {
702
+ if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff_ffff) {
703
+ throw new RangeError("Native projection integer is outside u32");
704
+ }
705
+ const bytes = new Uint8Array(4);
706
+ new DataView(bytes.buffer).setUint32(0, value, true);
707
+ this.bytes(bytes);
708
+ }
709
+ i32(value: number): void {
710
+ if (!Number.isSafeInteger(value) || value < -0x8000_0000 || value > 0x7fff_ffff) {
711
+ throw new RangeError("Native projection integer is outside i32");
712
+ }
713
+ const bytes = new Uint8Array(4);
714
+ new DataView(bytes.buffer).setInt32(0, value, true);
715
+ this.bytes(bytes);
716
+ }
717
+ u64(value: bigint): void {
718
+ if (value < 0n || value > 0xffff_ffff_ffff_ffffn) {
719
+ throw new RangeError("Native projection integer is outside u64");
720
+ }
721
+ const bytes = new Uint8Array(8);
722
+ new DataView(bytes.buffer).setBigUint64(0, value, true);
723
+ this.bytes(bytes);
724
+ }
725
+ i64(value: bigint): void {
726
+ const bytes = new Uint8Array(8);
727
+ new DataView(bytes.buffer).setBigInt64(0, value, true);
728
+ this.bytes(bytes);
729
+ }
730
+ f64(value: number): void {
731
+ if (!Number.isFinite(value) || Object.is(value, -0)) {
732
+ throw new TypeError("Native projection numbers must be finite canonical doubles");
733
+ }
734
+ const bytes = new Uint8Array(8);
735
+ new DataView(bytes.buffer).setFloat64(0, value, true);
736
+ this.bytes(bytes);
737
+ }
738
+ string(value: string): void {
739
+ const encoded = textEncoder.encode(value);
740
+ this.u32(encoded.byteLength);
741
+ this.bytes(encoded);
742
+ }
743
+ optionalString(value: string | undefined): void {
744
+ this.bool(value !== undefined);
745
+ if (value !== undefined) this.string(value);
746
+ }
747
+ assertDocumentId(id: string, expectedPrefix: string): void {
748
+ parseDocumentId(id, expectedPrefix);
749
+ }
750
+ documentId(id: string, expectedPrefix: string): void {
751
+ const parsed = parseDocumentId(id, expectedPrefix);
752
+ this.u8(documentKindTag(expectedPrefix));
753
+ this.u64(parsed.namespace);
754
+ this.u64(parsed.counter);
755
+ }
756
+ documentTarget(target: DocumentTarget): void {
757
+ if (target.kind === "body") {
758
+ this.u8(0);
759
+ return;
760
+ }
761
+ this.u8(1);
762
+ this.documentId(target.sectionId, "sec");
763
+ this.u8(target.storyKind === "header" ? 1 : 2);
764
+ this.u8(target.variant === "default" ? 1 : target.variant === "first" ? 2 : 3);
765
+ }
766
+ page(page: DocumentPageGeometry): void {
767
+ this.f64(page.widthPt);
768
+ this.f64(page.heightPt);
769
+ this.f64(page.marginTopPt);
770
+ this.f64(page.marginRightPt);
771
+ this.f64(page.marginBottomPt);
772
+ this.f64(page.marginLeftPt);
773
+ }
774
+ pageWithExtras(page: DocumentPageGeometry): void {
775
+ this.page(page);
776
+ this.f64(page.headerPt ?? 36);
777
+ this.f64(page.footerPt ?? 36);
778
+ this.f64(page.gutterPt ?? 0);
779
+ }
780
+ textRuns(runs: readonly SerializedTextRun[]): void {
781
+ this.u32(runs.length);
782
+ for (const run of runs) {
783
+ this.string(run.text);
784
+ this.textStyle(run.style);
785
+ }
786
+ }
787
+ textStyle(style: DocumentTextStyle): void {
788
+ const mask =
789
+ Number(style.fontFamily !== undefined) |
790
+ (Number(style.fontSizePt !== undefined) << 1) |
791
+ (Number(style.color !== undefined) << 2) |
792
+ (Number(style.bold !== undefined) << 3) |
793
+ (Number(style.italic !== undefined) << 4) |
794
+ (Number(style.underline !== undefined) << 5) |
795
+ (Number(style.strike !== undefined) << 6);
796
+ this.u8(mask);
797
+ if (style.fontFamily !== undefined) this.string(style.fontFamily);
798
+ if (style.fontSizePt !== undefined) this.f64(style.fontSizePt);
799
+ if (style.color !== undefined) this.string(style.color);
800
+ for (const value of [style.bold, style.italic, style.underline, style.strike]) {
801
+ if (value !== undefined) this.bool(value);
802
+ }
803
+ }
804
+ textStylePatch(style: DocumentParagraphFormat["style"]): void {
805
+ this.patchString(style.fontFamily);
806
+ this.patchF64(style.fontSizePt);
807
+ this.patchString(style.color);
808
+ this.patchBool(style.bold);
809
+ this.patchBool(style.italic);
810
+ this.patchBool(style.underline);
811
+ this.patchBool(style.strike);
812
+ }
813
+ private patchString(value: string | null | undefined): void {
814
+ this.u8(value === undefined ? 0 : value === null ? 1 : 2);
815
+ if (typeof value === "string") this.string(value);
816
+ }
817
+ private patchF64(value: number | null | undefined): void {
818
+ this.u8(value === undefined ? 0 : value === null ? 1 : 2);
819
+ if (typeof value === "number") this.f64(value);
820
+ }
821
+ private patchBool(value: boolean | null | undefined): void {
822
+ this.u8(value === undefined ? 0 : value === null ? 1 : value ? 3 : 2);
823
+ }
824
+ paragraphStyle(style: DocumentParagraphStyle): void {
825
+ const mask =
826
+ Number(style.headingLevel !== undefined) |
827
+ (Number(style.alignment !== undefined) << 1) |
828
+ (Number(style.spaceBeforePt !== undefined) << 2) |
829
+ (Number(style.spaceAfterPt !== undefined) << 3) |
830
+ (Number(style.lineHeight !== undefined) << 4) |
831
+ (Number(style.keepNext !== undefined) << 5) |
832
+ (Number(style.pageBreakBefore !== undefined) << 6) |
833
+ (Number(style.list !== undefined) << 7);
834
+ this.u16(mask);
835
+ if (style.headingLevel !== undefined) this.u8(style.headingLevel);
836
+ if (style.alignment !== undefined) {
837
+ this.u8({ left: 1, center: 2, right: 3, justify: 4 }[style.alignment]);
838
+ }
839
+ for (const value of [style.spaceBeforePt, style.spaceAfterPt, style.lineHeight]) {
840
+ if (value !== undefined) this.f64(value);
841
+ }
842
+ for (const value of [style.keepNext, style.pageBreakBefore]) {
843
+ if (value !== undefined) this.bool(value);
844
+ }
845
+ if (style.list) {
846
+ this.u8(style.list.kind === "bullet" ? 1 : 2);
847
+ this.bool(style.list.level !== undefined);
848
+ if (style.list.level !== undefined) this.u8(style.list.level);
849
+ this.optionalString(style.list.instanceId);
850
+ }
851
+ }
852
+ tableStyle(style: DocumentTableStyle): void {
853
+ const mask =
854
+ Number(style.widthPt !== undefined) |
855
+ (Number(style.columnWidthsPt !== undefined) << 1) |
856
+ (Number(style.headerRows !== undefined) << 2) |
857
+ (Number(style.cellPaddingPt !== undefined) << 3) |
858
+ (Number(style.borderColor !== undefined) << 4) |
859
+ (Number(style.headerFill !== undefined) << 5) |
860
+ (Number(style.allowRowSplit !== undefined) << 6);
861
+ this.u8(mask);
862
+ if (style.widthPt !== undefined) this.f64(style.widthPt);
863
+ if (style.columnWidthsPt !== undefined) {
864
+ this.u32(style.columnWidthsPt.length);
865
+ for (const width of style.columnWidthsPt) this.f64(width);
866
+ }
867
+ if (style.headerRows !== undefined) this.u32(style.headerRows);
868
+ if (style.cellPaddingPt !== undefined) this.f64(style.cellPaddingPt);
869
+ if (style.borderColor !== undefined) this.string(style.borderColor);
870
+ if (style.headerFill !== undefined) this.string(style.headerFill);
871
+ if (style.allowRowSplit !== undefined) this.bool(style.allowRowSplit);
872
+ }
873
+ commentReply(reply: { author: string; text: string; createdAt: string }): void {
874
+ this.string(reply.author);
875
+ this.string(reply.text);
876
+ this.string(reply.createdAt);
877
+ }
878
+ presentationId(id: string, namespace: bigint): void {
879
+ const counter = presentationCounter(id);
880
+ this.u64(counter);
881
+ this.u64(namespace);
882
+ }
883
+ optionalPresentationId(id: string | undefined, namespace: bigint): void {
884
+ this.bool(id !== undefined);
885
+ if (id !== undefined) this.presentationId(id, namespace);
886
+ }
887
+ fill(fill: PresentationFill): void {
888
+ const value = presentationColorValue(fill);
889
+ if (value === "none") {
890
+ this.u8(0);
891
+ return;
892
+ }
893
+ this.u8(1);
894
+ const rgb = Number.parseInt(value.slice(1), 16);
895
+ this.u32((rgb * 256 + 0xff) >>> 0);
896
+ }
897
+ line(line: PresentationLine): void {
898
+ this.fill(line.fill ?? "black");
899
+ this.i64(cssPixels(line.width ?? 1));
900
+ this.u8(line.style === "dash" ? 1 : line.style === "dot" ? 2 : 0);
901
+ }
902
+ presentationRect(position: PresentationPosition): void {
903
+ this.i64(cssPixels(position.left));
904
+ this.i64(cssPixels(position.top));
905
+ this.i64(cssPixels(position.width));
906
+ this.i64(cssPixels(position.height));
907
+ }
908
+ presentationTransform(rotation: number, flipHorizontal: boolean, flipVertical: boolean): void {
909
+ this.i32(Math.round(rotation * 60_000));
910
+ this.bool(flipHorizontal);
911
+ this.bool(flipVertical);
912
+ }
913
+ richText(text: PresentationText, fallbackStyle: PresentationTextStyle = {}): void {
914
+ const style = { ...fallbackStyle, ...text.style };
915
+ this.u8(
916
+ style.verticalAlignment === "middle" ? 1 : style.verticalAlignment === "bottom" ? 2 : 0,
917
+ );
918
+ this.u32(1);
919
+ this.u8(
920
+ style.alignment === "center"
921
+ ? 1
922
+ : style.alignment === "right"
923
+ ? 2
924
+ : style.alignment === "justify"
925
+ ? 3
926
+ : 0,
927
+ );
928
+ this.u32(1);
929
+ this.string(text.toString());
930
+ this.presentationTextStyle(style);
931
+ }
932
+ plainRichText(value: string, style: PresentationTextStyle = {}): void {
933
+ this.u8(
934
+ style.verticalAlignment === "middle" ? 1 : style.verticalAlignment === "bottom" ? 2 : 0,
935
+ );
936
+ this.u32(1);
937
+ this.u8(
938
+ style.alignment === "center"
939
+ ? 1
940
+ : style.alignment === "right"
941
+ ? 2
942
+ : style.alignment === "justify"
943
+ ? 3
944
+ : 0,
945
+ );
946
+ this.u32(1);
947
+ this.string(value);
948
+ this.presentationTextStyle(style);
949
+ }
950
+ presentationTextStyle(style: PresentationTextStyle): void {
951
+ this.string(style.fontFamily ?? "Arial");
952
+ this.u32(Math.round((style.fontSize ?? 18) * 100));
953
+ const color = presentationColorValue(style.color ?? "black");
954
+ const rgb = color === "none" ? 0 : Number.parseInt(color.slice(1), 16);
955
+ this.u32((rgb * 256 + (color === "none" ? 0 : 0xff)) >>> 0);
956
+ this.bool(style.bold ?? false);
957
+ this.bool(style.italic ?? false);
958
+ this.bool(style.underline ?? false);
959
+ this.bool(false);
960
+ }
961
+ presentationNode(element: PresentationElement): void {
962
+ if (element instanceof PresentationShape) {
963
+ this.u8(0);
964
+ this.u8(
965
+ { textbox: 0, rect: 1, roundRect: 2, ellipse: 3, triangle: 4, rightArrow: 5, line: 6 }[
966
+ element.geometry
967
+ ],
968
+ );
969
+ this.fill(element.fill);
970
+ this.line(element.line);
971
+ const hasText = element.text.toString().length > 0;
972
+ this.bool(hasText);
973
+ if (hasText) this.richText(element.text);
974
+ this.bool(element.placeholder !== undefined);
975
+ if (element.placeholder) {
976
+ this.string(element.placeholder.type);
977
+ this.bool(element.placeholder.index !== undefined);
978
+ if (element.placeholder.index !== undefined) this.u32(element.placeholder.index);
979
+ }
980
+ return;
981
+ }
982
+ if (element instanceof PresentationGroup) {
983
+ this.u8(1);
984
+ this.i64(cssPixels(element.childOffset.left));
985
+ this.i64(cssPixels(element.childOffset.top));
986
+ this.i64(cssPixels(element.childExtent.width));
987
+ this.i64(cssPixels(element.childExtent.height));
988
+ this.u32(0);
989
+ return;
990
+ }
991
+ if (element instanceof PresentationChart) {
992
+ this.u8(3);
993
+ this.u8(
994
+ { bar: 0, line: 1, area: 2, pie: 3, doughnut: 4, scatter: 5, bubble: 6, radar: 7 }[
995
+ element.type
996
+ ],
997
+ );
998
+ this.plainRichText(element.title);
999
+ this.u32(element.series.items.length);
1000
+ for (const series of element.series.items) {
1001
+ this.string(series.name);
1002
+ this.stringVector(series.categories.length > 0 ? series.categories : element.categories);
1003
+ this.numberVector(series.values);
1004
+ this.numberVector(series.xValues);
1005
+ this.numberVector(series.bubbleSizes);
1006
+ }
1007
+ this.bool(element.hasLegend);
1008
+ return;
1009
+ }
1010
+ if (element instanceof PresentationTable) {
1011
+ this.u8(4);
1012
+ this.u32(element.rows.length);
1013
+ this.u32(element.rows[0]?.length ?? 0);
1014
+ for (const row of element.rows) {
1015
+ for (const cell of row) {
1016
+ this.bool(cell !== null);
1017
+ if (cell) {
1018
+ this.richText(cell.text, element.textStyle);
1019
+ this.fill(cell.fill);
1020
+ this.u16(cell.rowSpan);
1021
+ this.u16(cell.colSpan);
1022
+ }
1023
+ }
1024
+ }
1025
+ this.u32(element.columnWidths.length);
1026
+ for (const width of element.columnWidths) this.i64(cssPixels(width));
1027
+ this.u32(element.rowHeights.length);
1028
+ for (const height of element.rowHeights) this.i64(cssPixels(height));
1029
+ this.line(element.line);
1030
+ return;
1031
+ }
1032
+ if (element instanceof PresentationImage) {
1033
+ const source = element.sourceForSvg();
1034
+ if (!source) {
1035
+ unsupportedPresentation("prompt-only raster image scene nodes");
1036
+ }
1037
+ const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/u.exec(source);
1038
+ if (!match) throw new Error(`Presentation image ${element.id} has a non-canonical source`);
1039
+ const bytes = decodeRasterBase64(match[2]!);
1040
+ const metadata = inspectRasterImage(bytes);
1041
+ if (metadata.contentType !== match[1] || metadata.contentType !== element.contentType) {
1042
+ throw new Error(`Presentation image ${element.id} MIME metadata diverged`);
1043
+ }
1044
+ this.u8(5);
1045
+ this.bytes(sha256Bytes(bytes));
1046
+ this.string(metadata.contentType);
1047
+ this.string(element.alt);
1048
+ this.u8(element.fit === "contain" ? 0 : 1);
1049
+ this.u32(metadata.width);
1050
+ this.u32(metadata.height);
1051
+ return;
1052
+ }
1053
+ unsupportedPresentation("unknown scene node");
1054
+ }
1055
+ stringVector(values: readonly string[]): void {
1056
+ this.u32(values.length);
1057
+ for (const value of values) this.string(value);
1058
+ }
1059
+ numberVector(values: readonly number[]): void {
1060
+ this.u32(values.length);
1061
+ for (const value of values) this.f64(value);
1062
+ }
1063
+ }
1064
+
1065
+ function envelope(magic: string, count: number, payload: Uint8Array, maximum: number): Uint8Array {
1066
+ const output = new Uint8Array(HEADER_BYTES + payload.byteLength + CHECKSUM_BYTES);
1067
+ if (output.byteLength > maximum) throw new RangeError(`${magic} envelope exceeds its byte limit`);
1068
+ output.set(textEncoder.encode(magic), 0);
1069
+ const view = new DataView(output.buffer);
1070
+ view.setUint16(8, 1, true);
1071
+ view.setUint32(12, count, true);
1072
+ view.setBigUint64(16, BigInt(payload.byteLength), true);
1073
+ output.set(payload, HEADER_BYTES);
1074
+ view.setBigUint64(HEADER_BYTES + payload.byteLength, fnv1a64(output.subarray(0, -8)), true);
1075
+ return output;
1076
+ }
1077
+
1078
+ function parseDocumentId(
1079
+ id: string,
1080
+ expectedPrefix: string,
1081
+ ): { namespace: bigint; counter: bigint } {
1082
+ const match = /^([a-z]{1,8})\/([0-9a-f]{16})([0-9a-f]{16})$/u.exec(id);
1083
+ if (!match || match[1] !== expectedPrefix) {
1084
+ throw new Error(`Document id ${id} is not a canonical ${expectedPrefix} id`);
1085
+ }
1086
+ const counter = BigInt(`0x${match[3]}`);
1087
+ if (counter === 0n) throw new Error(`Document id ${id} uses the reserved zero counter`);
1088
+ return { namespace: BigInt(`0x${match[2]}`), counter };
1089
+ }
1090
+
1091
+ function documentKindTag(prefix: string): number {
1092
+ const tag = { p: 1, dt: 2, pb: 3, sec: 4, hdr: 5, ftr: 6, dc: 7, chg: 8 }[prefix];
1093
+ if (!tag) throw new Error(`Unknown document id prefix: ${prefix}`);
1094
+ return tag;
1095
+ }
1096
+
1097
+ function presentationCounter(id: string): bigint {
1098
+ const match = /^[a-z]{2}\/([1-9][0-9]*)$/u.exec(id);
1099
+ if (!match) throw new Error(`Presentation object id is not canonical: ${id}`);
1100
+ const counter = BigInt(match[1]!);
1101
+ if (counter > 0xffff_ffff_ffff_ffffn) throw new RangeError("Presentation object id is exhausted");
1102
+ return counter;
1103
+ }
1104
+
1105
+ function optionalBool(value: boolean | undefined): number {
1106
+ return value === undefined ? 0 : value ? 2 : 1;
1107
+ }
1108
+
1109
+ function optionalOptionalBool(value: boolean | undefined): number {
1110
+ return value === undefined ? 0 : value ? 3 : 2;
1111
+ }
1112
+
1113
+ function cssPixels(value: number): bigint {
1114
+ if (!Number.isFinite(value) || Object.is(value, -0)) {
1115
+ throw new TypeError("Presentation geometry must contain finite canonical numbers");
1116
+ }
1117
+ return BigInt(Math.round(value * EMU_PER_CSS_PIXEL));
1118
+ }
1119
+
1120
+ function assertFinitePositive(value: number, label: string): void {
1121
+ if (!Number.isFinite(value) || value <= 0)
1122
+ throw new TypeError(`${label} must be positive and finite`);
1123
+ }
1124
+
1125
+ function presentationSizeEmu(value: number, label: string): bigint {
1126
+ assertFinitePositive(value, label);
1127
+ const rounded = Math.round(value * EMU_PER_CSS_PIXEL);
1128
+ if (!Number.isSafeInteger(rounded) || rounded <= 0 || rounded > PRESENTATION_MAX_COORDINATE_EMU) {
1129
+ throw new RangeError(`${label} exceeds the native presentation-coordinate bound`);
1130
+ }
1131
+ return BigInt(rounded);
1132
+ }
1133
+
1134
+ function fnv1a64(bytes: Uint8Array): bigint {
1135
+ let hash = 0xcbf29ce484222325n;
1136
+ for (const byte of bytes) {
1137
+ hash ^= BigInt(byte);
1138
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
1139
+ }
1140
+ return hash;
1141
+ }