@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,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("&", "&")
|
|
508
|
+
.replaceAll("<", "<")
|
|
509
|
+
.replaceAll(">", ">")
|
|
510
|
+
.replaceAll('"', """)
|
|
511
|
+
.replaceAll("'", "'");
|
|
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
|
+
}
|