@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,532 @@
1
+ import { FileBlob } from "./file-blob";
2
+ import { rasterizeSvgToPng } from "./native-raster";
3
+ import {
4
+ assertPresentationRenderPixelBudget,
5
+ InvalidPresentationInputError,
6
+ presentationColorValue,
7
+ truncatePresentationText,
8
+ UnsupportedPresentationFeatureError,
9
+ type Presentation,
10
+ type PresentationChart,
11
+ type PresentationExportOptions,
12
+ type PresentationImage,
13
+ type PresentationLine,
14
+ type PresentationPosition,
15
+ type PresentationGroup,
16
+ type PresentationShape,
17
+ type PresentationTable,
18
+ type PresentationText,
19
+ type Slide,
20
+ } from "./presentation";
21
+
22
+ const colorValue = presentationColorValue;
23
+ const truncate = truncatePresentationText;
24
+
25
+ type MontageDimensions = {
26
+ columns: number;
27
+ rows: number;
28
+ gap: number;
29
+ width: number;
30
+ height: number;
31
+ };
32
+
33
+ export type PresentationRenderJob =
34
+ | {
35
+ kind: "slide";
36
+ slide: Slide;
37
+ format: Exclude<PresentationExportOptions["format"], "layout">;
38
+ scale: number;
39
+ name: string;
40
+ dimensions: { width: number; height: number };
41
+ }
42
+ | {
43
+ kind: "montage";
44
+ presentation: Presentation;
45
+ format: Exclude<PresentationExportOptions["format"], "layout">;
46
+ scale: number;
47
+ name: string;
48
+ dimensions: MontageDimensions;
49
+ };
50
+
51
+ export async function executePresentationRender(job: PresentationRenderJob): Promise<FileBlob> {
52
+ const svg =
53
+ job.kind === "slide"
54
+ ? renderSlideSvg(job.slide)
55
+ : renderMontageSvg(job.presentation, job.dimensions);
56
+ return await imageBlob(svg, job.format, job.scale, job.name, job.dimensions);
57
+ }
58
+
59
+ function renderSlideSvg(slide: Slide): string {
60
+ const { width, height } = slide.presentation.slideSize;
61
+ const defs: string[] = [];
62
+ const elements: string[] = [];
63
+ for (const element of slide.elements) {
64
+ if ("geometry" in element && "text" in element) elements.push(renderShapeSvg(element));
65
+ else if ("series" in element) elements.push(renderChartSvg(element));
66
+ else if ("sourceForSvg" in element) elements.push(renderImageSvg(element, defs));
67
+ else if ("rows" in element) elements.push(renderTableSvg(element));
68
+ else if ("children" in element) elements.push(renderGroupSvg(element, defs));
69
+ }
70
+ return svgDocument(
71
+ width,
72
+ height,
73
+ colorValue(slide.background.fill),
74
+ defs.join(""),
75
+ elements.join(""),
76
+ );
77
+ }
78
+
79
+ function renderGroupSvg(group: PresentationGroup, defs: string[]): string {
80
+ const children = group.children.map((child) => {
81
+ if ("geometry" in child && "text" in child) return renderShapeSvg(child);
82
+ if ("series" in child) return renderChartSvg(child);
83
+ if ("sourceForSvg" in child) return renderImageSvg(child, defs);
84
+ if ("rows" in child) return renderTableSvg(child);
85
+ if ("children" in child) return renderGroupSvg(child, defs);
86
+ return "";
87
+ });
88
+ const sx = group.position.width / group.childExtent.width;
89
+ const sy = group.position.height / group.childExtent.height;
90
+ const translatedX = group.position.left - group.childOffset.left * sx;
91
+ const translatedY = group.position.top - group.childOffset.top * sy;
92
+ const centerX = group.position.left + group.position.width / 2;
93
+ const centerY = group.position.top + group.position.height / 2;
94
+ const outer = [
95
+ `translate(${centerX} ${centerY})`,
96
+ group.rotation === 0 ? "" : `rotate(${group.rotation})`,
97
+ `scale(${group.flipHorizontal ? -1 : 1} ${group.flipVertical ? -1 : 1})`,
98
+ `translate(${-centerX} ${-centerY})`,
99
+ ]
100
+ .filter(Boolean)
101
+ .join(" ");
102
+ return `<g data-presentation-group="${escapeXml(group.id)}" transform="${outer}"><g transform="translate(${translatedX} ${translatedY}) scale(${sx} ${sy})">${children.join("")}</g></g>`;
103
+ }
104
+
105
+ function renderTableSvg(table: PresentationTable): string {
106
+ const position = table.position;
107
+ const rowCount = table.rows.length;
108
+ const columnCount = table.rows[0]?.length ?? 0;
109
+ if (rowCount === 0 || columnCount === 0) return "";
110
+ const columnWidths = normalizeTracks(table.columnWidths, columnCount, position.width);
111
+ const rowHeights = normalizeTracks(table.rowHeights, rowCount, position.height);
112
+ const fragments: string[] = [];
113
+ let y = position.top;
114
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
115
+ let x = position.left;
116
+ const row = table.rows[rowIndex]!;
117
+ for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
118
+ const cell = row[columnIndex]!;
119
+ const width = columnWidths[columnIndex]!;
120
+ const height = rowHeights[rowIndex]!;
121
+ if (cell === null) {
122
+ x += width;
123
+ continue;
124
+ }
125
+ const cellPosition = {
126
+ left: x + 4,
127
+ top: y + 2,
128
+ width: Math.max(1, width - 8),
129
+ height: Math.max(1, height - 4),
130
+ };
131
+ fragments.push(
132
+ `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${colorValue(cell.fill)}" ${lineSvgAttributes(table.line)}/>${renderTextSvg(cell.text, cellPosition)}`,
133
+ );
134
+ x += width;
135
+ }
136
+ y += rowHeights[rowIndex]!;
137
+ }
138
+ return `<g data-presentation-table="${escapeXml(table.id)}">${fragments.join("")}</g>`;
139
+ }
140
+
141
+ function normalizeTracks(values: readonly number[], count: number, total: number): number[] {
142
+ if (values.length !== count) return Array.from({ length: count }, () => total / count);
143
+ const sum = values.reduce((value, next) => value + next, 0);
144
+ return values.map((value) => (value / sum) * total);
145
+ }
146
+
147
+ function renderShapeSvg(shape: PresentationShape): string {
148
+ const p = shape.position;
149
+ const fill = colorValue(shape.fill);
150
+ const stroke = lineSvgAttributes(shape.line);
151
+ const transform =
152
+ shape.rotation === 0
153
+ ? ""
154
+ : ` transform="rotate(${shape.rotation} ${p.left + p.width / 2} ${p.top + p.height / 2})"`;
155
+ let geometry: string;
156
+ switch (shape.geometry) {
157
+ case "textbox":
158
+ geometry = "";
159
+ break;
160
+ case "rect":
161
+ geometry = `<rect x="${p.left}" y="${p.top}" width="${p.width}" height="${p.height}" fill="${fill}" ${stroke}/>`;
162
+ break;
163
+ case "roundRect": {
164
+ const radius = resolveRadius(shape.borderRadius, p);
165
+ geometry = `<rect x="${p.left}" y="${p.top}" width="${p.width}" height="${p.height}" rx="${radius}" fill="${fill}" ${stroke}/>`;
166
+ break;
167
+ }
168
+ case "ellipse":
169
+ geometry = `<ellipse cx="${p.left + p.width / 2}" cy="${p.top + p.height / 2}" rx="${p.width / 2}" ry="${p.height / 2}" fill="${fill}" ${stroke}/>`;
170
+ break;
171
+ case "triangle":
172
+ geometry = `<path d="M ${p.left + p.width / 2} ${p.top} L ${p.left + p.width} ${p.top + p.height} L ${p.left} ${p.top + p.height} Z" fill="${fill}" ${stroke}/>`;
173
+ break;
174
+ case "rightArrow": {
175
+ const shaft = p.width * 0.62;
176
+ geometry = `<path d="M ${p.left} ${p.top + p.height * 0.25} H ${p.left + shaft} V ${p.top} L ${p.left + p.width} ${p.top + p.height / 2} L ${p.left + shaft} ${p.top + p.height} V ${p.top + p.height * 0.75} H ${p.left} Z" fill="${fill}" ${stroke}/>`;
177
+ break;
178
+ }
179
+ case "line":
180
+ geometry = `<line x1="${p.left}" y1="${p.top}" x2="${p.left + p.width}" y2="${p.top + p.height}" ${stroke}/>`;
181
+ break;
182
+ default:
183
+ throw new UnsupportedPresentationFeatureError(`SVG shape geometry ${String(shape.geometry)}`);
184
+ }
185
+ const text = renderTextSvg(shape.text, p);
186
+ return `<g${transform}>${geometry}${text}</g>`;
187
+ }
188
+
189
+ function renderTextSvg(text: PresentationText, position: PresentationPosition): string {
190
+ const value = text.toString();
191
+ if (!value) return "";
192
+ const style = text.style;
193
+ const fontSize = style.fontSize ?? 18;
194
+ const lineHeight = fontSize * 1.2;
195
+ const lines = wrapText(value, position.width, fontSize);
196
+ const anchor =
197
+ style.alignment === "center" ? "middle" : style.alignment === "right" ? "end" : "start";
198
+ const x =
199
+ style.alignment === "center"
200
+ ? position.left + position.width / 2
201
+ : style.alignment === "right"
202
+ ? position.left + position.width
203
+ : position.left;
204
+ const textHeight = Math.max(lineHeight, lines.length * lineHeight);
205
+ const top =
206
+ style.verticalAlignment === "middle"
207
+ ? position.top + (position.height - textHeight) / 2
208
+ : style.verticalAlignment === "bottom"
209
+ ? position.top + position.height - textHeight
210
+ : position.top;
211
+ const spans = lines
212
+ .map(
213
+ (line, index) =>
214
+ `<tspan x="${x}" y="${top + fontSize + index * lineHeight}">${escapeXml(line)}</tspan>`,
215
+ )
216
+ .join("");
217
+ return `<text font-family="${escapeXml(style.fontFamily ?? "Arial")}" font-size="${fontSize}" font-weight="${style.bold ? 700 : 400}" font-style="${style.italic ? "italic" : "normal"}" text-decoration="${style.underline ? "underline" : "none"}" text-anchor="${anchor}" fill="${colorValue(style.color ?? "slate-950")}">${spans}</text>`;
218
+ }
219
+
220
+ function renderChartSvg(chart: PresentationChart): string {
221
+ const p = chart.position;
222
+ const pad = { left: 52, right: 20, top: chart.title ? 46 : 24, bottom: 40 };
223
+ const plot = {
224
+ left: p.left + pad.left,
225
+ top: p.top + pad.top,
226
+ width: Math.max(1, p.width - pad.left - pad.right),
227
+ height: Math.max(1, p.height - pad.top - pad.bottom),
228
+ };
229
+ const values = chart.series.items.flatMap((series) => series.values).filter(Number.isFinite);
230
+ const min = Math.min(0, chart.yAxis?.min ?? Math.min(...values, 0));
231
+ const max = Math.max(1, chart.yAxis?.max ?? Math.max(...values, 1));
232
+ const palette = ["#2563eb", "#0f766e", "#f59e0b", "#dc2626", "#7c3aed"];
233
+ const title = chart.title
234
+ ? `<text x="${p.left + p.width / 2}" y="${p.top + 24}" text-anchor="middle" font-family="Arial" font-size="18" font-weight="700" fill="#0f172a">${escapeXml(chart.title)}</text>`
235
+ : "";
236
+ const frame = `<rect x="${p.left}" y="${p.top}" width="${p.width}" height="${p.height}" fill="#ffffff" stroke="#e2e8f0"/>`;
237
+ const axes = `<line x1="${plot.left}" y1="${plot.top + plot.height}" x2="${plot.left + plot.width}" y2="${plot.top + plot.height}" stroke="#94a3b8"/><line x1="${plot.left}" y1="${plot.top}" x2="${plot.left}" y2="${plot.top + plot.height}" stroke="#94a3b8"/>`;
238
+ let marks: string;
239
+ switch (chart.type) {
240
+ case "bar":
241
+ marks = renderBarMarks(chart, plot, min, max, palette);
242
+ break;
243
+ case "line":
244
+ case "area":
245
+ marks = renderLineMarks(chart, plot, min, max, palette, chart.type === "area");
246
+ break;
247
+ case "pie":
248
+ case "doughnut":
249
+ marks = renderPieMarks(chart, plot, palette, chart.type === "doughnut");
250
+ break;
251
+ case "scatter":
252
+ case "bubble":
253
+ marks = renderScatterMarks(chart, plot, palette, chart.type === "bubble");
254
+ break;
255
+ case "radar":
256
+ throw new UnsupportedPresentationFeatureError(
257
+ "SVG radar chart export",
258
+ `Chart ${chart.id} remains editable and exportable to PPTX`,
259
+ );
260
+ default:
261
+ throw new UnsupportedPresentationFeatureError(`SVG chart type ${String(chart.type)}`);
262
+ }
263
+ return `<g>${frame}${title}${chart.type === "pie" || chart.type === "doughnut" ? "" : axes}${marks}</g>`;
264
+ }
265
+
266
+ function renderBarMarks(
267
+ chart: PresentationChart,
268
+ plot: PresentationPosition,
269
+ min: number,
270
+ max: number,
271
+ palette: string[],
272
+ ): string {
273
+ const count = Math.max(1, ...chart.series.items.map((series) => series.values.length));
274
+ const groupWidth = plot.width / count;
275
+ const barWidth = Math.max(2, (groupWidth * 0.72) / Math.max(1, chart.series.items.length));
276
+ return chart.series.items
277
+ .flatMap((series, seriesIndex) =>
278
+ series.values.map((value, valueIndex) => {
279
+ const height = Math.max(0, ((value - min) / (max - min)) * plot.height);
280
+ const x = plot.left + valueIndex * groupWidth + groupWidth * 0.14 + seriesIndex * barWidth;
281
+ const y = plot.top + plot.height - height;
282
+ const label = chart.dataLabels?.showValue
283
+ ? `<text x="${x + barWidth / 2}" y="${Math.max(plot.top + 12, y - 4)}" text-anchor="middle" font-family="Arial" font-size="12" fill="#334155">${value}</text>`
284
+ : "";
285
+ return `<rect x="${x}" y="${y}" width="${barWidth}" height="${height}" fill="${colorValue(series.fill ?? palette[seriesIndex % palette.length]!)}"/>${label}`;
286
+ }),
287
+ )
288
+ .join("");
289
+ }
290
+
291
+ function renderLineMarks(
292
+ chart: PresentationChart,
293
+ plot: PresentationPosition,
294
+ min: number,
295
+ max: number,
296
+ palette: string[],
297
+ area: boolean,
298
+ ): string {
299
+ return chart.series.items
300
+ .map((series, seriesIndex) => {
301
+ const divisor = Math.max(1, series.values.length - 1);
302
+ const points = series.values.map((value, index) => ({
303
+ x: plot.left + (index / divisor) * plot.width,
304
+ y: plot.top + plot.height - ((value - min) / (max - min)) * plot.height,
305
+ }));
306
+ if (points.length === 0) return "";
307
+ const polyline = points.map((point) => `${point.x},${point.y}`).join(" ");
308
+ const color = colorValue(
309
+ series.line?.fill ?? series.fill ?? palette[seriesIndex % palette.length]!,
310
+ );
311
+ const fill = area
312
+ ? `<path d="M ${plot.left} ${plot.top + plot.height} L ${polyline.replaceAll(",", " ")} L ${plot.left + plot.width} ${plot.top + plot.height} Z" fill="${color}" opacity="0.22"/>`
313
+ : "";
314
+ const dots = points
315
+ .map((point) => `<circle cx="${point.x}" cy="${point.y}" r="3" fill="${color}"/>`)
316
+ .join("");
317
+ return `${fill}<polyline points="${polyline}" fill="none" stroke="${color}" stroke-width="${series.line?.width ?? 3}"/>${dots}`;
318
+ })
319
+ .join("");
320
+ }
321
+
322
+ function renderPieMarks(
323
+ chart: PresentationChart,
324
+ plot: PresentationPosition,
325
+ palette: string[],
326
+ doughnut: boolean,
327
+ ): string {
328
+ const values = chart.series.items[0]?.values ?? [];
329
+ const total = values.reduce((sum, value) => sum + Math.max(0, value), 0);
330
+ if (total <= 0) return "";
331
+ const radius = Math.min(plot.width, plot.height) * 0.42;
332
+ const cx = plot.left + plot.width / 2;
333
+ const cy = plot.top + plot.height / 2;
334
+ let angle = -Math.PI / 2;
335
+ const slices = values
336
+ .map((value, index) => {
337
+ const next = angle + (Math.max(0, value) / total) * Math.PI * 2;
338
+ const large = next - angle > Math.PI ? 1 : 0;
339
+ const x1 = cx + radius * Math.cos(angle);
340
+ const y1 = cy + radius * Math.sin(angle);
341
+ const x2 = cx + radius * Math.cos(next);
342
+ const y2 = cy + radius * Math.sin(next);
343
+ const path = `<path d="M ${cx} ${cy} L ${x1} ${y1} A ${radius} ${radius} 0 ${large} 1 ${x2} ${y2} Z" fill="${palette[index % palette.length]}"/>`;
344
+ angle = next;
345
+ return path;
346
+ })
347
+ .join("");
348
+ const hole = doughnut
349
+ ? `<circle cx="${cx}" cy="${cy}" r="${radius * 0.55}" fill="#ffffff"/>`
350
+ : "";
351
+ return `${slices}${hole}`;
352
+ }
353
+
354
+ function renderScatterMarks(
355
+ chart: PresentationChart,
356
+ plot: PresentationPosition,
357
+ palette: string[],
358
+ bubble: boolean,
359
+ ): string {
360
+ const allX = chart.series.items.flatMap((series) => series.xValues);
361
+ const allY = chart.series.items.flatMap((series) => series.values);
362
+ const minX = Math.min(...allX, 0);
363
+ const maxX = Math.max(...allX, 1);
364
+ const minY = Math.min(...allY, 0);
365
+ const maxY = Math.max(...allY, 1);
366
+ return chart.series.items
367
+ .map((series, seriesIndex) =>
368
+ series.values
369
+ .map((value, index) => {
370
+ const xValue = series.xValues[index] ?? index;
371
+ const x = plot.left + ((xValue - minX) / (maxX - minX)) * plot.width;
372
+ const y = plot.top + plot.height - ((value - minY) / (maxY - minY)) * plot.height;
373
+ const radius = bubble
374
+ ? Math.max(3, Math.sqrt(Math.max(0, series.bubbleSizes[index] ?? 9)))
375
+ : 4;
376
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="${colorValue(series.fill ?? palette[seriesIndex % palette.length]!)}" opacity="0.8"/>`;
377
+ })
378
+ .join(""),
379
+ )
380
+ .join("");
381
+ }
382
+
383
+ function renderImageSvg(image: PresentationImage, defs: string[]): string {
384
+ const p = image.position;
385
+ const source = image.sourceForSvg();
386
+ if (!source) {
387
+ return `<g><rect x="${p.left}" y="${p.top}" width="${p.width}" height="${p.height}" fill="#f1f5f9" stroke="#94a3b8" stroke-dasharray="8 6"/><text x="${p.left + 16}" y="${p.top + 28}" font-family="Arial" font-size="14" fill="#475569">Image prompt: ${escapeXml(truncate(image.prompt ?? "", 100))}</text></g>`;
388
+ }
389
+ const clipId = `clip-${image.id.replace("/", "-")}`;
390
+ const radius = resolveRadius(image.borderRadius, p);
391
+ const clipShape =
392
+ image.geometry === "ellipse"
393
+ ? `<ellipse cx="${p.left + p.width / 2}" cy="${p.top + p.height / 2}" rx="${p.width / 2}" ry="${p.height / 2}"/>`
394
+ : `<rect x="${p.left}" y="${p.top}" width="${p.width}" height="${p.height}" rx="${image.geometry === "roundRect" ? radius : 0}"/>`;
395
+ defs.push(`<clipPath id="${clipId}">${clipShape}</clipPath>`);
396
+ const preserve = image.fit === "cover" ? "xMidYMid slice" : "xMidYMid meet";
397
+ const transform = [
398
+ image.flipHorizontal ? `translate(${2 * p.left + p.width} 0) scale(-1 1)` : "",
399
+ image.flipVertical ? `translate(0 ${2 * p.top + p.height}) scale(1 -1)` : "",
400
+ image.rotation
401
+ ? `rotate(${image.rotation} ${p.left + p.width / 2} ${p.top + p.height / 2})`
402
+ : "",
403
+ ]
404
+ .filter(Boolean)
405
+ .join(" ");
406
+ return `<image href="${escapeXml(source)}" x="${p.left}" y="${p.top}" width="${p.width}" height="${p.height}" preserveAspectRatio="${preserve}" clip-path="url(#${clipId})"${transform ? ` transform="${transform}"` : ""}/>`;
407
+ }
408
+
409
+ function renderMontageSvg(presentation: Presentation, dimensions: MontageDimensions): string {
410
+ const { columns, gap, width, height } = dimensions;
411
+ const slides = presentation.slides.items
412
+ .map((slide, index) => {
413
+ const x = gap + (index % columns) * (presentation.slideSize.width + gap);
414
+ const y = gap + Math.floor(index / columns) * (presentation.slideSize.height + gap);
415
+ const inner = renderSlideSvg(slide)
416
+ .replace(/^<svg[^>]*>/, "")
417
+ .replace(/<\/svg>$/, "");
418
+ return `<g transform="translate(${x} ${y})">${inner}</g>`;
419
+ })
420
+ .join("");
421
+ return svgDocument(width, height, "#cbd5e1", "", slides);
422
+ }
423
+
424
+ async function imageBlob(
425
+ svg: string,
426
+ format: "svg" | "png" | "webp",
427
+ scale = 1,
428
+ name: string,
429
+ dimensions: { width: number; height: number },
430
+ ): Promise<FileBlob> {
431
+ if (!Number.isFinite(scale) || scale <= 0 || scale > 8) {
432
+ throw new InvalidPresentationInputError("export scale", "must be greater than 0 and at most 8");
433
+ }
434
+ if (format === "svg") return new FileBlob([svg], { name, type: "image/svg+xml" });
435
+ assertPresentationRenderPixelBudget(dimensions.width, dimensions.height, scale);
436
+ const png = await rasterizeSvgToPng(svg, { zoom: scale });
437
+ if (format === "png") return FileBlob.fromBytes(png, { name, type: "image/png" });
438
+ const { default: sharp } = await importNativeRuntime<SharpModuleRuntime>("sharp");
439
+ const webp = await sharp(png).webp({ quality: 90, smartSubsample: true }).toBuffer();
440
+ return FileBlob.fromBytes(webp, { name, type: "image/webp" });
441
+ }
442
+
443
+ function svgDocument(
444
+ width: number,
445
+ height: number,
446
+ background: string,
447
+ defs: string,
448
+ body: string,
449
+ ): string {
450
+ return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><defs>${defs}</defs><rect width="${width}" height="${height}" fill="${background}"/>${body}</svg>`;
451
+ }
452
+ function lineSvgAttributes(line: PresentationLine): string {
453
+ if (
454
+ line.style === "none" ||
455
+ colorValue(line.fill ?? "none") === "none" ||
456
+ (line.width ?? 1) === 0
457
+ ) {
458
+ return 'stroke="none"';
459
+ }
460
+ const dash =
461
+ line.style === "dash"
462
+ ? ' stroke-dasharray="8 5"'
463
+ : line.style === "dot"
464
+ ? ' stroke-dasharray="2 4"'
465
+ : "";
466
+ return `stroke="${colorValue(line.fill ?? "slate-900")}" stroke-width="${line.width ?? 1}"${dash}`;
467
+ }
468
+
469
+ function resolveRadius(
470
+ radius: number | string | undefined,
471
+ position: PresentationPosition,
472
+ ): number {
473
+ if (typeof radius === "number") return Math.max(0, radius);
474
+ const token = radius ?? "rounded-xl";
475
+ if (token === "rounded-full") return Math.min(position.width, position.height) / 2;
476
+ const resolved = BORDER_RADIUS_TOKENS[token];
477
+ if (resolved === undefined) {
478
+ throw new InvalidPresentationInputError("borderRadius", "uses an unsupported radius token");
479
+ }
480
+ return resolved;
481
+ }
482
+
483
+ function wrapText(value: string, width: number, fontSize: number): string[] {
484
+ const maxCharacters = Math.max(1, Math.floor(width / Math.max(1, fontSize * 0.56)));
485
+ const lines: string[] = [];
486
+ for (const hardLine of value.split("\n")) {
487
+ const words = hardLine.split(/\s+/).filter(Boolean);
488
+ if (words.length === 0) {
489
+ lines.push("");
490
+ continue;
491
+ }
492
+ let current = "";
493
+ for (const word of words) {
494
+ if (!current || current.length + 1 + word.length <= maxCharacters) {
495
+ current = current ? `${current} ${word}` : word;
496
+ } else {
497
+ lines.push(current);
498
+ current = word;
499
+ }
500
+ }
501
+ lines.push(current);
502
+ }
503
+ return lines;
504
+ }
505
+ function escapeXml(value: string): string {
506
+ return value
507
+ .replaceAll("&", "&amp;")
508
+ .replaceAll("<", "&lt;")
509
+ .replaceAll(">", "&gt;")
510
+ .replaceAll('"', "&quot;")
511
+ .replaceAll("'", "&apos;");
512
+ }
513
+
514
+ const BORDER_RADIUS_TOKENS: Readonly<Record<string, number>> = {
515
+ rounded: 4,
516
+ "rounded-lg": 8,
517
+ "rounded-xl": 12,
518
+ "rounded-2xl": 16,
519
+ "rounded-3xl": 24,
520
+ };
521
+
522
+ type SharpModuleRuntime = {
523
+ default: (input: Uint8Array) => {
524
+ webp(options: { quality: number; smartSubsample: boolean }): {
525
+ toBuffer(): Promise<Uint8Array>;
526
+ };
527
+ };
528
+ };
529
+
530
+ async function importNativeRuntime<T>(specifier: string): Promise<T> {
531
+ return (await import(/* @vite-ignore */ specifier)) as T;
532
+ }