@pollium/vyva-convert 0.0.0-stage → 0.1.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.
- package/README.md +88 -2
- package/dist/attachment.d.ts +3 -0
- package/dist/attachment.js +8 -0
- package/dist/clean-content.d.ts +6 -0
- package/dist/clean-content.js +58 -0
- package/dist/clean-html.d.ts +3 -0
- package/dist/clean-html.js +161 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +11 -0
- package/dist/export/bundle.d.ts +34 -0
- package/dist/export/bundle.js +41 -0
- package/dist/export/docx.d.ts +5 -0
- package/dist/export/docx.js +382 -0
- package/dist/export/fonts.d.ts +16 -0
- package/dist/export/fonts.js +37 -0
- package/dist/export/html-blocks.d.ts +58 -0
- package/dist/export/html-blocks.js +170 -0
- package/dist/export/html.d.ts +8 -0
- package/dist/export/html.js +31 -0
- package/dist/export/image-size.d.ts +8 -0
- package/dist/export/image-size.js +83 -0
- package/dist/export/pdf.d.ts +28 -0
- package/dist/export/pdf.js +84 -0
- package/dist/export/pptx.d.ts +5 -0
- package/dist/export/pptx.js +310 -0
- package/dist/export.d.ts +19 -0
- package/dist/export.js +39 -0
- package/dist/font-widths.d.ts +8 -0
- package/dist/font-widths.js +3430 -0
- package/dist/gotenberg.d.ts +8 -0
- package/dist/gotenberg.js +39 -0
- package/dist/import/documents.d.ts +12 -0
- package/dist/import/documents.js +44 -0
- package/dist/import/html-to-blocks.d.ts +6 -0
- package/dist/import/html-to-blocks.js +58 -0
- package/dist/import/images.d.ts +10 -0
- package/dist/import/images.js +23 -0
- package/dist/import/pptx-package.d.ts +22 -0
- package/dist/import/pptx-package.js +61 -0
- package/dist/import/pptx-placeholders.d.ts +11 -0
- package/dist/import/pptx-placeholders.js +44 -0
- package/dist/import/pptx-slide.d.ts +19 -0
- package/dist/import/pptx-slide.js +324 -0
- package/dist/import/pptx-theme.d.ts +18 -0
- package/dist/import/pptx-theme.js +161 -0
- package/dist/import/pptx-xml.d.ts +23 -0
- package/dist/import/pptx-xml.js +27 -0
- package/dist/import/pptx.d.ts +9 -0
- package/dist/import/pptx.js +112 -0
- package/dist/import/titles.d.ts +3 -0
- package/dist/import/titles.js +6 -0
- package/dist/import.d.ts +14 -0
- package/dist/import.js +66 -0
- package/dist/text-measure.d.ts +4 -0
- package/dist/text-measure.js +98 -0
- package/package.json +49 -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,310 @@
|
|
|
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
|
+
};
|
|
115
|
+
const addShape = (slide, element, deck) => {
|
|
116
|
+
const isLine = element.shape === "line" || element.shape === "arrow";
|
|
117
|
+
const lineColor = element.stroke ?? element.fill;
|
|
118
|
+
const box = {
|
|
119
|
+
x: inch(element.x),
|
|
120
|
+
y: inch(isLine ? element.y + element.h / 2 : element.y),
|
|
121
|
+
w: inch(element.w),
|
|
122
|
+
h: isLine ? 0 : inch(element.h)
|
|
123
|
+
};
|
|
124
|
+
const paint = {
|
|
125
|
+
rotate: element.rotation,
|
|
126
|
+
...isLine || element.fill === null ? { fill: { type: "none" } } : { fill: {
|
|
127
|
+
color: hex(element.fill),
|
|
128
|
+
transparency: transparency(element.opacity)
|
|
129
|
+
} },
|
|
130
|
+
...lineColor === null || element.strokeWidth === 0 && !isLine ? { line: { type: "none" } } : { line: {
|
|
131
|
+
color: hex(lineColor),
|
|
132
|
+
width: Math.max(.5, element.strokeWidth * PT_PER_PX),
|
|
133
|
+
...element.shape === "arrow" ? { endArrowType: "triangle" } : {}
|
|
134
|
+
} },
|
|
135
|
+
...element.shape === "rounded" ? { rectRadius: .18 } : {}
|
|
136
|
+
};
|
|
137
|
+
if (element.text !== void 0 && TEXT_SHAPES.includes(element.shape) && element.text.html.trim() !== "") {
|
|
138
|
+
slide.addText(textRuns(element.text.html, element.text.style), {
|
|
139
|
+
...box,
|
|
140
|
+
...paint,
|
|
141
|
+
...textOptions(element.text.style, deck),
|
|
142
|
+
shape: SHAPES[element.shape],
|
|
143
|
+
margin: 6
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
slide.addShape(SHAPES[element.shape], {
|
|
148
|
+
...box,
|
|
149
|
+
...paint
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
const addCode = (slide, element) => {
|
|
153
|
+
const palette = CODE_PALETTES[element.colorScheme];
|
|
154
|
+
const lines = codeLines(element.code, element.language, element.colorScheme);
|
|
155
|
+
const runs = lines.flatMap((line, index) => {
|
|
156
|
+
const tokens = line.length === 0 ? [{
|
|
157
|
+
text: " ",
|
|
158
|
+
color: palette.foreground
|
|
159
|
+
}] : line;
|
|
160
|
+
return tokens.map((token, at) => ({
|
|
161
|
+
text: token.text,
|
|
162
|
+
options: {
|
|
163
|
+
color: hex(token.color),
|
|
164
|
+
...at === tokens.length - 1 && index < lines.length - 1 ? { breakLine: true } : {}
|
|
165
|
+
}
|
|
166
|
+
}));
|
|
167
|
+
});
|
|
168
|
+
const padding = Math.round(element.fontSize * CODE_PADDING_EMS * PT_PER_PX);
|
|
169
|
+
slide.addText(runs, {
|
|
170
|
+
x: inch(element.x),
|
|
171
|
+
y: inch(element.y),
|
|
172
|
+
w: inch(element.w),
|
|
173
|
+
h: inch(element.h),
|
|
174
|
+
fontFace: CODE_FONT,
|
|
175
|
+
fontSize: Math.max(6, Math.round(element.fontSize * PT_PER_PX)),
|
|
176
|
+
color: hex(palette.foreground),
|
|
177
|
+
fill: { color: hex(palette.background) },
|
|
178
|
+
rectRadius: .08,
|
|
179
|
+
shape: "roundRect",
|
|
180
|
+
valign: "top",
|
|
181
|
+
align: "left",
|
|
182
|
+
lineSpacingMultiple: CODE_LINE_HEIGHT,
|
|
183
|
+
margin: [
|
|
184
|
+
padding,
|
|
185
|
+
padding,
|
|
186
|
+
padding,
|
|
187
|
+
padding
|
|
188
|
+
],
|
|
189
|
+
wrap: false,
|
|
190
|
+
rotate: element.rotation,
|
|
191
|
+
transparency: transparency(element.opacity)
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
const addTable = (slide, element, deck) => {
|
|
195
|
+
const columns = Math.max(1, ...element.rows.map((row) => row.length));
|
|
196
|
+
const rows = element.rows.map((row, index) => Array.from({ length: columns }, (_, column) => {
|
|
197
|
+
const isHeader = element.hasHeader && index === 0;
|
|
198
|
+
return {
|
|
199
|
+
text: row[column] ?? "",
|
|
200
|
+
options: isHeader ? {
|
|
201
|
+
bold: true,
|
|
202
|
+
color: "FFFFFF",
|
|
203
|
+
fill: { color: hex(element.headerFill) }
|
|
204
|
+
} : { color: hex(deck.theme.text) }
|
|
205
|
+
};
|
|
206
|
+
}));
|
|
207
|
+
slide.addTable(rows, {
|
|
208
|
+
x: inch(element.x),
|
|
209
|
+
y: inch(element.y),
|
|
210
|
+
w: inch(element.w),
|
|
211
|
+
colW: Array.from({ length: columns }, () => inch(element.w / columns)),
|
|
212
|
+
fontFace: deck.theme.font,
|
|
213
|
+
fontSize: Math.max(6, Math.round(element.fontSize * PT_PER_PX)),
|
|
214
|
+
border: {
|
|
215
|
+
type: "solid",
|
|
216
|
+
color: hex(element.borderColor),
|
|
217
|
+
pt: 1
|
|
218
|
+
},
|
|
219
|
+
valign: "middle",
|
|
220
|
+
margin: .06
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
const addBackground = (slide, background, deck, bundle) => {
|
|
224
|
+
if (background?.type === "image") {
|
|
225
|
+
const image = bundle.images.get(background.fileId);
|
|
226
|
+
if (image !== void 0) {
|
|
227
|
+
slide.background = { data: dataUrl(image.contentType, image.data) };
|
|
228
|
+
if (background.dim > 0) {
|
|
229
|
+
const { width, height } = DECK_SIZES[deck.format];
|
|
230
|
+
slide.addShape("rect", {
|
|
231
|
+
x: 0,
|
|
232
|
+
y: 0,
|
|
233
|
+
w: inch(width),
|
|
234
|
+
h: inch(height),
|
|
235
|
+
fill: {
|
|
236
|
+
color: "000000",
|
|
237
|
+
transparency: transparency(background.dim)
|
|
238
|
+
},
|
|
239
|
+
line: { type: "none" }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
slide.background = { color: hex(background?.type === "color" ? background.color : deck.theme.background) };
|
|
246
|
+
};
|
|
247
|
+
const addBand = (slide, source, deck) => {
|
|
248
|
+
if (!hasBand(deck.theme, source)) return;
|
|
249
|
+
const { width } = DECK_SIZES[deck.format];
|
|
250
|
+
slide.addShape("rect", {
|
|
251
|
+
x: 0,
|
|
252
|
+
y: 0,
|
|
253
|
+
w: inch(width),
|
|
254
|
+
h: inch(bandHeight(deck.format)),
|
|
255
|
+
fill: { color: hex(deck.theme.accent) },
|
|
256
|
+
line: { type: "none" }
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
const addFooter = (slide, source, deck) => {
|
|
260
|
+
if (!hasFooter(deck.theme, source)) return;
|
|
261
|
+
const { width, height } = DECK_SIZES[deck.format];
|
|
262
|
+
const tall = footerHeight(deck.format);
|
|
263
|
+
const third = width / 3;
|
|
264
|
+
footerSegments(source, deck).forEach((segment, index) => {
|
|
265
|
+
slide.addText(segment.text, {
|
|
266
|
+
x: inch(third * index),
|
|
267
|
+
y: inch(height - tall),
|
|
268
|
+
w: inch(third),
|
|
269
|
+
h: inch(tall),
|
|
270
|
+
fill: { color: hex(segment.fill) },
|
|
271
|
+
color: hex(inkOn(segment.fill)),
|
|
272
|
+
fontFace: deck.theme.font,
|
|
273
|
+
fontSize: Math.round(tall * .48 * PT_PER_PX),
|
|
274
|
+
align: segment.align,
|
|
275
|
+
valign: "middle",
|
|
276
|
+
margin: Math.round(tall * .5 * PT_PER_PX)
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
const buildPptx = async (deck, bundle) => {
|
|
281
|
+
const { width, height } = DECK_SIZES[deck.format];
|
|
282
|
+
const pptx = new PptxGenJS();
|
|
283
|
+
pptx.defineLayout({
|
|
284
|
+
name: LAYOUT,
|
|
285
|
+
width: inch(width),
|
|
286
|
+
height: inch(height)
|
|
287
|
+
});
|
|
288
|
+
pptx.layout = LAYOUT;
|
|
289
|
+
pptx.title = bundle.title;
|
|
290
|
+
for (const source of deck.slides) {
|
|
291
|
+
const slide = pptx.addSlide();
|
|
292
|
+
addBackground(slide, source.background, deck, bundle);
|
|
293
|
+
addBand(slide, source, deck);
|
|
294
|
+
for (const element of source.elements) if (element.type === "text") addText(slide, element, deck);
|
|
295
|
+
else if (element.type === "image") addImage(slide, element, bundle);
|
|
296
|
+
else if (element.type === "shape") addShape(slide, element, deck);
|
|
297
|
+
else if (element.type === "code") addCode(slide, element);
|
|
298
|
+
else addTable(slide, element, deck);
|
|
299
|
+
addFooter(slide, source, deck);
|
|
300
|
+
}
|
|
301
|
+
const data = await pptx.write({ outputType: "uint8array" });
|
|
302
|
+
if (!(data instanceof Uint8Array)) throw new TypeError("PptxGenJS did not write the presentation as bytes");
|
|
303
|
+
return {
|
|
304
|
+
data,
|
|
305
|
+
contentType: PPTX_MIME,
|
|
306
|
+
extension: "pptx"
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
//#endregion
|
|
310
|
+
export { buildPptx };
|
package/dist/export.d.ts
ADDED
|
@@ -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
|