@pollium/vyva-convert 0.0.0-stage → 0.2.0

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 (64) hide show
  1. package/README.md +100 -2
  2. package/dist/attachment.d.ts +3 -0
  3. package/dist/attachment.js +8 -0
  4. package/dist/clean-content.d.ts +6 -0
  5. package/dist/clean-content.js +63 -0
  6. package/dist/clean-html.d.ts +3 -0
  7. package/dist/clean-html.js +161 -0
  8. package/dist/errors.d.ts +7 -0
  9. package/dist/errors.js +11 -0
  10. package/dist/export/bundle.d.ts +34 -0
  11. package/dist/export/bundle.js +41 -0
  12. package/dist/export/docx.d.ts +5 -0
  13. package/dist/export/docx.js +382 -0
  14. package/dist/export/fonts.d.ts +16 -0
  15. package/dist/export/fonts.js +37 -0
  16. package/dist/export/html-blocks.d.ts +58 -0
  17. package/dist/export/html-blocks.js +170 -0
  18. package/dist/export/html.d.ts +8 -0
  19. package/dist/export/html.js +31 -0
  20. package/dist/export/image-size.d.ts +8 -0
  21. package/dist/export/image-size.js +83 -0
  22. package/dist/export/pdf.d.ts +28 -0
  23. package/dist/export/pdf.js +84 -0
  24. package/dist/export/pptx.d.ts +5 -0
  25. package/dist/export/pptx.js +334 -0
  26. package/dist/export.d.ts +19 -0
  27. package/dist/export.js +39 -0
  28. package/dist/font-widths.d.ts +8 -0
  29. package/dist/font-widths.js +3430 -0
  30. package/dist/gotenberg.d.ts +8 -0
  31. package/dist/gotenberg.js +39 -0
  32. package/dist/import/documents.d.ts +12 -0
  33. package/dist/import/documents.js +44 -0
  34. package/dist/import/html-to-blocks.d.ts +6 -0
  35. package/dist/import/html-to-blocks.js +58 -0
  36. package/dist/import/images.d.ts +10 -0
  37. package/dist/import/images.js +23 -0
  38. package/dist/import/pdf-lines.d.ts +4 -0
  39. package/dist/import/pdf-lines.js +206 -0
  40. package/dist/import/pdf-slides.d.ts +7 -0
  41. package/dist/import/pdf-slides.js +21 -0
  42. package/dist/import/pptx-geometry.d.ts +11 -0
  43. package/dist/import/pptx-geometry.js +74 -0
  44. package/dist/import/pptx-package.d.ts +22 -0
  45. package/dist/import/pptx-package.js +61 -0
  46. package/dist/import/pptx-placeholders.d.ts +11 -0
  47. package/dist/import/pptx-placeholders.js +44 -0
  48. package/dist/import/pptx-slide.d.ts +19 -0
  49. package/dist/import/pptx-slide.js +348 -0
  50. package/dist/import/pptx-theme.d.ts +19 -0
  51. package/dist/import/pptx-theme.js +160 -0
  52. package/dist/import/pptx-xml.d.ts +23 -0
  53. package/dist/import/pptx-xml.js +27 -0
  54. package/dist/import/pptx.d.ts +9 -0
  55. package/dist/import/pptx.js +126 -0
  56. package/dist/import/titles.d.ts +3 -0
  57. package/dist/import/titles.js +6 -0
  58. package/dist/import.d.ts +16 -0
  59. package/dist/import.js +68 -0
  60. package/dist/libreoffice.d.ts +8 -0
  61. package/dist/libreoffice.js +39 -0
  62. package/dist/text-measure.d.ts +5 -0
  63. package/dist/text-measure.js +102 -0
  64. package/package.json +50 -3
@@ -0,0 +1,31 @@
1
+ import { fontFaces, fontFilesOf } from "./fonts.js";
2
+ import { DECK_SIZES } from "@pollium/vyva-core/content";
3
+ import { CONTENT_CSS, renderDocument, renderSlide } from "@pollium/vyva-core/render";
4
+ import { escapeHtml } from "@pollium/vyva-core/escape";
5
+ //#region src/export/html.ts
6
+ const SCREEN_PX_PER_INCH = 96;
7
+ const SLIDE_PX_PER_INCH = 144;
8
+ const SCREEN_CSS = "body{margin:0;background:#f3f4f6;display:flex;justify-content:center;padding:24px 0}.vy-doc{box-shadow:0 1px 4px rgba(0,0,0,.12)}";
9
+ const dataUrl = (contentType, data) => `data:${contentType};base64,${data.toString("base64")}`;
10
+ const bodyOf = (content, fileUrl) => {
11
+ if (content.kind === "document") return renderDocument(content, fileUrl);
12
+ const { width, height } = DECK_SIZES[content.format];
13
+ const zoom = SCREEN_PX_PER_INCH / SLIDE_PX_PER_INCH;
14
+ return content.slides.map((slide) => `<div class="vy-print-slide" style="width:${width * zoom}px;height:${height * zoom}px"><div style="width:${width}px;height:${height}px;transform:scale(${zoom});transform-origin:0 0">${renderSlide(slide, content, fileUrl)}</div></div>`).join("");
15
+ };
16
+ const pageHtml = ({ title, language }, css, body) => `<!doctype html><html lang="${escapeHtml(language)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>${css}</style></head><body>${body}</body></html>`;
17
+ const buildHtml = async (content, bundle) => {
18
+ const fonts = fontFaces(await fontFilesOf(bundle.fonts), (file) => dataUrl("font/woff2", file.data));
19
+ const fileUrl = (id) => {
20
+ const image = bundle.images.get(id);
21
+ return image === void 0 ? null : dataUrl(image.contentType, image.data);
22
+ };
23
+ const html = pageHtml(bundle, `${fonts}${CONTENT_CSS}${SCREEN_CSS}`, bodyOf(content, fileUrl));
24
+ return {
25
+ data: Buffer.from(html, "utf8"),
26
+ contentType: "text/html; charset=utf-8",
27
+ extension: "html"
28
+ };
29
+ };
30
+ //#endregion
31
+ export { bodyOf, buildHtml, pageHtml };
@@ -0,0 +1,8 @@
1
+ //#region src/export/image-size.d.ts
2
+ export interface ImageSize {
3
+ width: number;
4
+ height: number;
5
+ }
6
+ export declare const imageSize: (data: Buffer, contentType: string) => ImageSize;
7
+ export declare const fitWithin: (size: ImageSize, maxWidth: number, maxHeight: number) => ImageSize;
8
+ //#endregion
@@ -0,0 +1,83 @@
1
+ //#region src/export/image-size.ts
2
+ const FALLBACK = {
3
+ width: 800,
4
+ height: 600
5
+ };
6
+ const pngSize = (data) => data.length >= 24 ? {
7
+ width: data.readUInt32BE(16),
8
+ height: data.readUInt32BE(20)
9
+ } : null;
10
+ const gifSize = (data) => data.length >= 10 ? {
11
+ width: data.readUInt16LE(6),
12
+ height: data.readUInt16LE(8)
13
+ } : null;
14
+ const JPEG_FRAMES = /* @__PURE__ */ new Set([
15
+ 192,
16
+ 193,
17
+ 194,
18
+ 195,
19
+ 197,
20
+ 198,
21
+ 199,
22
+ 201,
23
+ 202,
24
+ 203,
25
+ 205,
26
+ 206,
27
+ 207
28
+ ]);
29
+ const jpegSize = (data) => {
30
+ let offset = 2;
31
+ while (offset + 9 < data.length) {
32
+ if (data[offset] !== 255) {
33
+ offset += 1;
34
+ continue;
35
+ }
36
+ const marker = data[offset + 1];
37
+ if (JPEG_FRAMES.has(marker)) return {
38
+ height: data.readUInt16BE(offset + 5),
39
+ width: data.readUInt16BE(offset + 7)
40
+ };
41
+ offset += 2 + data.readUInt16BE(offset + 2);
42
+ }
43
+ return null;
44
+ };
45
+ const webpSize = (data) => {
46
+ if (data.length < 30 || data.toString("ascii", 0, 4) !== "RIFF" || data.toString("ascii", 8, 12) !== "WEBP") return null;
47
+ const chunk = data.toString("ascii", 12, 16);
48
+ if (chunk === "VP8X") return {
49
+ width: 1 + data.readUIntLE(24, 3),
50
+ height: 1 + data.readUIntLE(27, 3)
51
+ };
52
+ if (chunk === "VP8L") {
53
+ const bits = data.readUInt32LE(21);
54
+ return {
55
+ width: 1 + (bits & 16383),
56
+ height: 1 + (bits >> 14 & 16383)
57
+ };
58
+ }
59
+ if (chunk === "VP8 ") return {
60
+ width: data.readUInt16LE(26) & 16383,
61
+ height: data.readUInt16LE(28) & 16383
62
+ };
63
+ return null;
64
+ };
65
+ const READERS = {
66
+ "image/png": pngSize,
67
+ "image/gif": gifSize,
68
+ "image/jpeg": jpegSize,
69
+ "image/webp": webpSize
70
+ };
71
+ const imageSize = (data, contentType) => {
72
+ const size = READERS[contentType]?.(data) ?? null;
73
+ return size !== null && size.width > 0 && size.height > 0 ? size : FALLBACK;
74
+ };
75
+ const fitWithin = (size, maxWidth, maxHeight) => {
76
+ const scale = Math.min(1, maxWidth / size.width, maxHeight / size.height);
77
+ return {
78
+ width: Math.round(size.width * scale),
79
+ height: Math.round(size.height * scale)
80
+ };
81
+ };
82
+ //#endregion
83
+ export { fitWithin, imageSize };
@@ -0,0 +1,28 @@
1
+ import { ExportBundle, ExportedFile } from "./bundle.js";
2
+ import { Content } from "@pollium/vyva-core/content";
3
+ //#region src/export/pdf.d.ts
4
+ export interface PdfFile {
5
+ name: string;
6
+ data: Uint8Array;
7
+ contentType: string;
8
+ }
9
+ export interface PdfMargins {
10
+ top: number;
11
+ right: number;
12
+ bottom: number;
13
+ left: number;
14
+ }
15
+ export interface PdfPage {
16
+ width: number;
17
+ height: number;
18
+ margins: PdfMargins;
19
+ }
20
+ export interface PdfJob {
21
+ html: string;
22
+ files: PdfFile[];
23
+ footerHtml: string | null;
24
+ page: PdfPage;
25
+ }
26
+ export type PdfEngine = (job: PdfJob) => Promise<Uint8Array>;
27
+ export declare const buildPdf: (content: Content, bundle: ExportBundle, engine: PdfEngine) => Promise<ExportedFile>;
28
+ //#endregion
@@ -0,0 +1,84 @@
1
+ import { fontFaces, fontFilesOf } from "./fonts.js";
2
+ import { bodyOf, pageHtml } from "./html.js";
3
+ import { DECK_SIZES } from "@pollium/vyva-core/content";
4
+ import { CONTENT_CSS } from "@pollium/vyva-core/render";
5
+ import { escapeHtml } from "@pollium/vyva-core/escape";
6
+ //#region src/export/pdf.ts
7
+ const SLIDE_PX_PER_INCH = 144;
8
+ const MM_PER_INCH = 25.4;
9
+ const A4_INCHES = {
10
+ width: 8.27,
11
+ height: 11.69
12
+ };
13
+ const FOOTER_ROOM_MM = 4;
14
+ const NO_MARGINS = {
15
+ top: 0,
16
+ right: 0,
17
+ bottom: 0,
18
+ left: 0
19
+ };
20
+ const PRINT_CSS = `
21
+ @page{size:auto}
22
+ html,body{margin:0;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact}
23
+ .vy-print-slide{break-after:page;overflow:hidden}
24
+ .vy-print-slide:last-child{break-after:auto}
25
+ .vy-doc{width:auto;min-height:0;padding:0}
26
+ .vy-doc .vy-doc-footer{display:none}
27
+ .vy-doc img{break-inside:avoid}
28
+ .vy-doc tr,.vy-doc pre,.vy-doc blockquote{break-inside:avoid}
29
+ .vy-doc h1,.vy-doc h2,.vy-doc h3,.vy-doc h4{break-after:avoid}
30
+ `;
31
+ const inches = (mm) => mm / MM_PER_INCH;
32
+ const pageOf = (content) => {
33
+ if (content.kind === "deck") {
34
+ const { width, height } = DECK_SIZES[content.format];
35
+ return {
36
+ width: width / SLIDE_PX_PER_INCH,
37
+ height: height / SLIDE_PX_PER_INCH,
38
+ margins: NO_MARGINS
39
+ };
40
+ }
41
+ const { margins } = content.design;
42
+ return {
43
+ ...A4_INCHES,
44
+ margins: {
45
+ top: inches(margins.top),
46
+ right: inches(margins.right),
47
+ bottom: inches(margins.bottom + FOOTER_ROOM_MM),
48
+ left: inches(margins.left)
49
+ }
50
+ };
51
+ };
52
+ const footerOf = (content) => {
53
+ if (content.kind !== "document") return null;
54
+ const { design } = content;
55
+ const text = escapeHtml(design.footerText);
56
+ if (text === "" && !design.showPageNumbers) return null;
57
+ const number = design.showPageNumbers ? "<span><span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></span>" : "<span></span>";
58
+ const side = (mm) => `${inches(mm).toFixed(3)}in`;
59
+ return `<html><head><style>.vy-page-footer{box-sizing:border-box;display:flex;justify-content:space-between;width:100%;padding:0 ${side(design.margins.right)} 0 ${side(design.margins.left)};font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:8pt;color:#6b7280}</style></head><body><div class="vy-page-footer"><span>${text}</span>${number}</div></body></html>`;
60
+ };
61
+ const buildPdf = async (content, bundle, engine) => {
62
+ const fonts = await fontFilesOf(bundle.fonts);
63
+ const css = `${fontFaces(fonts, (file) => file.name)}${CONTENT_CSS}${PRINT_CSS}`;
64
+ return {
65
+ data: await engine({
66
+ html: pageHtml(bundle, css, bodyOf(content, (id) => bundle.images.get(id)?.name ?? null)),
67
+ files: [...[...bundle.images.values()].map(({ name, data, contentType }) => ({
68
+ name,
69
+ data,
70
+ contentType
71
+ })), ...fonts.map(({ name, data }) => ({
72
+ name,
73
+ data,
74
+ contentType: "font/woff2"
75
+ }))],
76
+ footerHtml: footerOf(content),
77
+ page: pageOf(content)
78
+ }),
79
+ contentType: "application/pdf",
80
+ extension: "pdf"
81
+ };
82
+ };
83
+ //#endregion
84
+ export { buildPdf };
@@ -0,0 +1,5 @@
1
+ import { ExportBundle, ExportedFile } from "./bundle.js";
2
+ import { DeckContent } from "@pollium/vyva-core/content";
3
+ //#region src/export/pptx.d.ts
4
+ export declare const buildPptx: (deck: DeckContent, bundle: ExportBundle) => Promise<ExportedFile>;
5
+ //#endregion
@@ -0,0 +1,334 @@
1
+ import { blocksOfHtml } from "./html-blocks.js";
2
+ import { CODE_FONT, CODE_LINE_HEIGHT, CODE_PALETTES, codeLines } from "@pollium/vyva-core/code";
3
+ import { DECK_SIZES, TEXT_SHAPES } from "@pollium/vyva-core/content";
4
+ import { bandHeight, footerHeight, footerSegments, hasBand, hasFooter, inkOn } from "@pollium/vyva-core/deck-frame";
5
+ import PptxGenJS from "pptxgenjs";
6
+ //#region src/export/pptx.ts
7
+ const PPTX_MIME = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
8
+ const LAYOUT = "VYVA";
9
+ const PX_PER_INCH = 144;
10
+ const PT_PER_PX = 72 / PX_PER_INCH;
11
+ const CODE_PADDING_EMS = .8;
12
+ const inch = (px) => Number((px / PX_PER_INCH).toFixed(4));
13
+ const hex = (color) => color.replace("#", "").toUpperCase();
14
+ const transparency = (opacity) => Math.round((1 - Math.min(1, Math.max(0, opacity))) * 100);
15
+ const dataUrl = (contentType, data) => `${contentType};base64,${data.toString("base64")}`;
16
+ const runProps = (run, style) => ({
17
+ bold: run.bold || style.isBold,
18
+ italic: run.italic,
19
+ ...run.underline ? { underline: { style: "sng" } } : {},
20
+ ...run.strike ? { strike: "sngStrike" } : {},
21
+ ...run.color === null ? {} : { color: hex(run.color) },
22
+ ...run.highlight === null ? {} : { highlight: hex(run.highlight) },
23
+ ...run.code ? { fontFace: CODE_FONT } : {},
24
+ ...run.sizePx === null ? {} : { fontSize: Math.max(6, Math.round(run.sizePx * PT_PER_PX)) },
25
+ ...run.link !== null && /^(https?:|mailto:)/i.test(run.link) ? { hyperlink: { url: run.link } } : {}
26
+ });
27
+ const textRuns = (html, style) => {
28
+ const out = [];
29
+ const push = (runs, paragraph) => {
30
+ const visible = runs.filter((run) => run.isBreak !== true);
31
+ if (visible.length === 0) {
32
+ out.push({
33
+ text: "",
34
+ options: {
35
+ ...paragraph,
36
+ breakLine: true
37
+ }
38
+ });
39
+ return;
40
+ }
41
+ visible.forEach((run, index) => out.push({
42
+ text: run.text,
43
+ options: {
44
+ ...runProps(run, style),
45
+ ...index === 0 ? paragraph : {},
46
+ ...index === visible.length - 1 ? { breakLine: true } : {}
47
+ }
48
+ }));
49
+ };
50
+ for (const block of blocksOfHtml(html)) if (block.kind === "paragraph" || block.kind === "heading") push(block.runs, block.align === null ? {} : { align: block.align });
51
+ else if (block.kind === "list") block.items.forEach((item) => push(item.runs, {
52
+ bullet: block.ordered ? { type: "number" } : true,
53
+ indentLevel: item.level
54
+ }));
55
+ else if (block.kind === "code") block.text.split("\n").forEach((line) => out.push({
56
+ text: line,
57
+ options: {
58
+ fontFace: CODE_FONT,
59
+ breakLine: true
60
+ }
61
+ }));
62
+ const last = out.at(-1);
63
+ if (last?.options !== void 0) last.options.breakLine = false;
64
+ return out;
65
+ };
66
+ const textOptions = (style, deck) => ({
67
+ fontFace: deck.theme.font,
68
+ fontSize: Math.max(6, Math.round(style.fontSize * PT_PER_PX)),
69
+ color: hex(style.color),
70
+ align: style.align,
71
+ valign: style.verticalAlign,
72
+ lineSpacingMultiple: style.lineHeight
73
+ });
74
+ const addText = (slide, element, deck) => {
75
+ slide.addText(textRuns(element.html, element.style), {
76
+ x: inch(element.x),
77
+ y: inch(element.y),
78
+ w: inch(element.w),
79
+ h: inch(element.h),
80
+ ...textOptions(element.style, deck),
81
+ margin: 0,
82
+ rotate: element.rotation,
83
+ transparency: transparency(element.opacity),
84
+ ...element.fill === null ? {} : { fill: { color: hex(element.fill) } }
85
+ });
86
+ };
87
+ const addImage = (slide, element, bundle) => {
88
+ const image = bundle.images.get(element.fileId);
89
+ if (image === void 0) return;
90
+ slide.addImage({
91
+ data: dataUrl(image.contentType, image.data),
92
+ x: inch(element.x),
93
+ y: inch(element.y),
94
+ w: inch(element.w),
95
+ h: inch(element.h),
96
+ rotate: element.rotation,
97
+ transparency: transparency(element.opacity),
98
+ rounding: element.radius >= Math.min(element.w, element.h) / 2,
99
+ altText: element.alt,
100
+ sizing: {
101
+ type: element.fit === "contain" ? "contain" : "cover",
102
+ w: inch(element.w),
103
+ h: inch(element.h)
104
+ }
105
+ });
106
+ };
107
+ const SHAPES = {
108
+ rect: "rect",
109
+ rounded: "roundRect",
110
+ ellipse: "ellipse",
111
+ triangle: "triangle",
112
+ line: "line",
113
+ arrow: "line",
114
+ polygon: "custGeom"
115
+ };
116
+ const BOX_CORNERS = [
117
+ {
118
+ x: 0,
119
+ y: 0
120
+ },
121
+ {
122
+ x: 1,
123
+ y: 0
124
+ },
125
+ {
126
+ x: 1,
127
+ y: 1
128
+ },
129
+ {
130
+ x: 0,
131
+ y: 1
132
+ }
133
+ ];
134
+ const outline = (element) => [...(element.points ?? BOX_CORNERS).map((point) => ({
135
+ x: inch(point.x * element.w),
136
+ y: inch(point.y * element.h)
137
+ })), { close: true }];
138
+ const addShape = (slide, element, deck) => {
139
+ const isLine = element.shape === "line" || element.shape === "arrow";
140
+ const lineColor = element.stroke ?? element.fill;
141
+ const box = {
142
+ x: inch(element.x),
143
+ y: inch(isLine ? element.y + element.h / 2 : element.y),
144
+ w: inch(element.w),
145
+ h: isLine ? 0 : inch(element.h)
146
+ };
147
+ const paint = {
148
+ rotate: element.rotation,
149
+ ...isLine || element.fill === null ? { fill: { type: "none" } } : { fill: {
150
+ color: hex(element.fill),
151
+ transparency: transparency(element.opacity)
152
+ } },
153
+ ...lineColor === null || element.strokeWidth === 0 && !isLine ? { line: { type: "none" } } : { line: {
154
+ color: hex(lineColor),
155
+ width: Math.max(.5, element.strokeWidth * PT_PER_PX),
156
+ ...element.shape === "arrow" ? { endArrowType: "triangle" } : {}
157
+ } },
158
+ ...element.shape === "rounded" ? { rectRadius: .18 } : {},
159
+ ...element.shape === "polygon" ? { points: outline(element) } : {}
160
+ };
161
+ if (element.text !== void 0 && TEXT_SHAPES.includes(element.shape) && element.text.html.trim() !== "") {
162
+ slide.addText(textRuns(element.text.html, element.text.style), {
163
+ ...box,
164
+ ...paint,
165
+ ...textOptions(element.text.style, deck),
166
+ shape: SHAPES[element.shape],
167
+ margin: 6
168
+ });
169
+ return;
170
+ }
171
+ slide.addShape(SHAPES[element.shape], {
172
+ ...box,
173
+ ...paint
174
+ });
175
+ };
176
+ const addCode = (slide, element) => {
177
+ const palette = CODE_PALETTES[element.colorScheme];
178
+ const lines = codeLines(element.code, element.language, element.colorScheme);
179
+ const runs = lines.flatMap((line, index) => {
180
+ const tokens = line.length === 0 ? [{
181
+ text: " ",
182
+ color: palette.foreground
183
+ }] : line;
184
+ return tokens.map((token, at) => ({
185
+ text: token.text,
186
+ options: {
187
+ color: hex(token.color),
188
+ ...at === tokens.length - 1 && index < lines.length - 1 ? { breakLine: true } : {}
189
+ }
190
+ }));
191
+ });
192
+ const padding = Math.round(element.fontSize * CODE_PADDING_EMS * PT_PER_PX);
193
+ slide.addText(runs, {
194
+ x: inch(element.x),
195
+ y: inch(element.y),
196
+ w: inch(element.w),
197
+ h: inch(element.h),
198
+ fontFace: CODE_FONT,
199
+ fontSize: Math.max(6, Math.round(element.fontSize * PT_PER_PX)),
200
+ color: hex(palette.foreground),
201
+ fill: { color: hex(palette.background) },
202
+ rectRadius: .08,
203
+ shape: "roundRect",
204
+ valign: "top",
205
+ align: "left",
206
+ lineSpacingMultiple: CODE_LINE_HEIGHT,
207
+ margin: [
208
+ padding,
209
+ padding,
210
+ padding,
211
+ padding
212
+ ],
213
+ wrap: false,
214
+ rotate: element.rotation,
215
+ transparency: transparency(element.opacity)
216
+ });
217
+ };
218
+ const addTable = (slide, element, deck) => {
219
+ const columns = Math.max(1, ...element.rows.map((row) => row.length));
220
+ const rows = element.rows.map((row, index) => Array.from({ length: columns }, (_, column) => {
221
+ const isHeader = element.hasHeader && index === 0;
222
+ return {
223
+ text: row[column] ?? "",
224
+ options: isHeader ? {
225
+ bold: true,
226
+ color: "FFFFFF",
227
+ fill: { color: hex(element.headerFill) }
228
+ } : { color: hex(deck.theme.text) }
229
+ };
230
+ }));
231
+ slide.addTable(rows, {
232
+ x: inch(element.x),
233
+ y: inch(element.y),
234
+ w: inch(element.w),
235
+ colW: Array.from({ length: columns }, () => inch(element.w / columns)),
236
+ fontFace: deck.theme.font,
237
+ fontSize: Math.max(6, Math.round(element.fontSize * PT_PER_PX)),
238
+ border: {
239
+ type: "solid",
240
+ color: hex(element.borderColor),
241
+ pt: 1
242
+ },
243
+ valign: "middle",
244
+ margin: .06
245
+ });
246
+ };
247
+ const addBackground = (slide, background, deck, bundle) => {
248
+ if (background?.type === "image") {
249
+ const image = bundle.images.get(background.fileId);
250
+ if (image !== void 0) {
251
+ slide.background = { data: dataUrl(image.contentType, image.data) };
252
+ if (background.dim > 0) {
253
+ const { width, height } = DECK_SIZES[deck.format];
254
+ slide.addShape("rect", {
255
+ x: 0,
256
+ y: 0,
257
+ w: inch(width),
258
+ h: inch(height),
259
+ fill: {
260
+ color: "000000",
261
+ transparency: transparency(background.dim)
262
+ },
263
+ line: { type: "none" }
264
+ });
265
+ }
266
+ return;
267
+ }
268
+ }
269
+ slide.background = { color: hex(background?.type === "color" ? background.color : deck.theme.background) };
270
+ };
271
+ const addBand = (slide, source, deck) => {
272
+ if (!hasBand(deck.theme, source)) return;
273
+ const { width } = DECK_SIZES[deck.format];
274
+ slide.addShape("rect", {
275
+ x: 0,
276
+ y: 0,
277
+ w: inch(width),
278
+ h: inch(bandHeight(deck.format)),
279
+ fill: { color: hex(deck.theme.accent) },
280
+ line: { type: "none" }
281
+ });
282
+ };
283
+ const addFooter = (slide, source, deck) => {
284
+ if (!hasFooter(deck.theme, source)) return;
285
+ const { width, height } = DECK_SIZES[deck.format];
286
+ const tall = footerHeight(deck.format);
287
+ const third = width / 3;
288
+ footerSegments(source, deck).forEach((segment, index) => {
289
+ slide.addText(segment.text, {
290
+ x: inch(third * index),
291
+ y: inch(height - tall),
292
+ w: inch(third),
293
+ h: inch(tall),
294
+ fill: { color: hex(segment.fill) },
295
+ color: hex(inkOn(segment.fill)),
296
+ fontFace: deck.theme.font,
297
+ fontSize: Math.round(tall * .48 * PT_PER_PX),
298
+ align: segment.align,
299
+ valign: "middle",
300
+ margin: Math.round(tall * .5 * PT_PER_PX)
301
+ });
302
+ });
303
+ };
304
+ const buildPptx = async (deck, bundle) => {
305
+ const { width, height } = DECK_SIZES[deck.format];
306
+ const pptx = new PptxGenJS();
307
+ pptx.defineLayout({
308
+ name: LAYOUT,
309
+ width: inch(width),
310
+ height: inch(height)
311
+ });
312
+ pptx.layout = LAYOUT;
313
+ pptx.title = bundle.title;
314
+ for (const source of deck.slides) {
315
+ const slide = pptx.addSlide();
316
+ addBackground(slide, source.background, deck, bundle);
317
+ addBand(slide, source, deck);
318
+ for (const element of source.elements) if (element.type === "text") addText(slide, element, deck);
319
+ else if (element.type === "image") addImage(slide, element, bundle);
320
+ else if (element.type === "shape") addShape(slide, element, deck);
321
+ else if (element.type === "code") addCode(slide, element);
322
+ else addTable(slide, element, deck);
323
+ addFooter(slide, source, deck);
324
+ }
325
+ const data = await pptx.write({ outputType: "uint8array" });
326
+ if (!(data instanceof Uint8Array)) throw new TypeError("PptxGenJS did not write the presentation as bytes");
327
+ return {
328
+ data,
329
+ contentType: PPTX_MIME,
330
+ extension: "pptx"
331
+ };
332
+ };
333
+ //#endregion
334
+ export { buildPptx };
@@ -0,0 +1,19 @@
1
+ import { ExportedFile, ReadImage } from "./export/bundle.js";
2
+ import { PdfEngine } from "./export/pdf.js";
3
+ import { Content, ContentKind } from "@pollium/vyva-core/content";
4
+ //#region src/export.d.ts
5
+ export declare const EXPORT_FORMATS: {
6
+ readonly document: readonly ["pdf", "docx", "html"];
7
+ readonly deck: readonly ["pptx", "pdf"];
8
+ };
9
+ export type ExportFormat = typeof EXPORT_FORMATS[ContentKind][number];
10
+ export interface ExportOptions {
11
+ title: string;
12
+ readImage: ReadImage;
13
+ pdfEngine?: PdfEngine;
14
+ language?: string;
15
+ }
16
+ export declare const isExportFormat: (value: unknown) => value is ExportFormat;
17
+ export declare const canExport: (kind: ContentKind, format: ExportFormat) => boolean;
18
+ export declare const exportContent: (content: Content, format: ExportFormat, { title, readImage, pdfEngine, language }: ExportOptions) => Promise<ExportedFile>;
19
+ //#endregion
package/dist/export.js ADDED
@@ -0,0 +1,39 @@
1
+ import { ConvertError } from "./errors.js";
2
+ import { bundleOf } from "./export/bundle.js";
3
+ import { buildDocx } from "./export/docx.js";
4
+ import { buildHtml } from "./export/html.js";
5
+ import { buildPdf } from "./export/pdf.js";
6
+ import { buildPptx } from "./export/pptx.js";
7
+ //#region src/export.ts
8
+ const EXPORT_FORMATS = {
9
+ document: [
10
+ "pdf",
11
+ "docx",
12
+ "html"
13
+ ],
14
+ deck: ["pptx", "pdf"]
15
+ };
16
+ const DEFAULT_LANGUAGE = "en";
17
+ const FORMATS = new Set(Object.values(EXPORT_FORMATS).flat());
18
+ const isExportFormat = (value) => typeof value === "string" && FORMATS.has(value);
19
+ const canExport = (kind, format) => {
20
+ return EXPORT_FORMATS[kind].includes(format);
21
+ };
22
+ const engineOf = (engine) => {
23
+ if (engine === void 0) throw new TypeError("Exporting as pdf needs a pdfEngine");
24
+ return engine;
25
+ };
26
+ const exportContent = async (content, format, { title, readImage, pdfEngine, language = DEFAULT_LANGUAGE }) => {
27
+ if (!canExport(content.kind, format)) throw new ConvertError("unsupported", `A ${content.kind} cannot be exported as ${format}`);
28
+ const engine = format === "pdf" ? engineOf(pdfEngine) : null;
29
+ const bundle = await bundleOf(content, {
30
+ title,
31
+ language,
32
+ readImage
33
+ });
34
+ if (engine !== null) return buildPdf(content, bundle, engine);
35
+ if (format === "html") return buildHtml(content, bundle);
36
+ return content.kind === "document" ? buildDocx(content, bundle) : buildPptx(content, bundle);
37
+ };
38
+ //#endregion
39
+ export { EXPORT_FORMATS, canExport, exportContent, isExportFormat };
@@ -0,0 +1,8 @@
1
+ import { FontFamily } from "@pollium/vyva-core/content";
2
+ //#region src/font-widths.d.ts
3
+ export declare const MEASURED_CHARS = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ–—‘’‚“”„•…€→←↑↓≤≥≠×÷✓";
4
+ export declare const FONT_WIDTHS: Record<FontFamily, {
5
+ regular: number[];
6
+ bold: number[];
7
+ }>;
8
+ //#endregion