@openpresentation/opf-render 0.0.2 → 0.1.1

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/dist/raster.js ADDED
@@ -0,0 +1,191 @@
1
+ import {OPFRenderError,packageName} from './svg.js';
2
+ const DEFAULT_DIMENSIONS = { width: 1280, height: 720 };
3
+ const DEFAULT_RASTER_SCALE = 1;
4
+ const DEFAULT_RASTER_BACKGROUND = "#FFFFFF";
5
+ let bundledFontFilesCache=null;
6
+
7
+ export async function svgToPng(svg, options = {}) {
8
+ const rendered = await rasterizeSvg(svg, options);
9
+ return rendered.png;
10
+ }
11
+
12
+ export async function svgToPdf(svgs, options = {}) {
13
+ const pdfInputs = normalizeSvgList(svgs);
14
+ if (!pdfInputs.length) {
15
+ throw new OPFRenderError("empty-pdf", "svgToPdf requires at least one SVG slide.");
16
+ }
17
+
18
+ const { PDFDocument } = await loadPdfLib();
19
+ const pdf = await PDFDocument.create({ updateMetadata: false });
20
+ pdf.setCreator(packageName);
21
+ pdf.setProducer(packageName);
22
+
23
+ for (const input of pdfInputs) {
24
+ const svg = normalizeSvgInput(input);
25
+ const pageSize = svgPageSize(svg);
26
+ const rendered = await rasterizeSvg(svg, options);
27
+ const image = await pdf.embedPng(rendered.png);
28
+ const page = pdf.addPage([pageSize.width, pageSize.height]);
29
+ page.drawImage(image, {
30
+ x: 0,
31
+ y: 0,
32
+ width: pageSize.width,
33
+ height: pageSize.height
34
+ });
35
+ }
36
+
37
+ return pdf.save({ addDefaultPage: false, useObjectStreams: false });
38
+ }
39
+
40
+ async function rasterizeSvg(svgInput, options) {
41
+ const { Resvg } = await loadResvg();
42
+ const svg = normalizeSvgInput(svgInput);
43
+ const scale = positiveNumber(options.scale, "scale", DEFAULT_RASTER_SCALE);
44
+ const fontFiles = [
45
+ ...(options.useBundledFonts === false ? [] : await bundledFontFiles()),
46
+ ...stringArray(options.fontFiles)
47
+ ];
48
+ const renderOptions = {
49
+ fitTo: { mode: "zoom", value: scale },
50
+ background: options.background ?? DEFAULT_RASTER_BACKGROUND,
51
+ font: {
52
+ loadSystemFonts: options.loadSystemFonts === true,
53
+ fontFiles,
54
+ fontDirs: stringArray(options.fontDirs),
55
+ defaultFontFamily: options.defaultFontFamily ?? "Roboto",
56
+ sansSerifFamily: options.sansSerifFamily ?? options.defaultFontFamily ?? "Roboto",
57
+ monospaceFamily: options.monospaceFamily ?? "Roboto Mono"
58
+ },
59
+ logLevel: "off"
60
+ };
61
+
62
+ if (options.dpi !== undefined) renderOptions.dpi = positiveNumber(options.dpi, "dpi", 96);
63
+
64
+ try {
65
+ const image = new Resvg(svg, renderOptions).render();
66
+ return {
67
+ png: new Uint8Array(image.asPng()),
68
+ width: image.width,
69
+ height: image.height
70
+ };
71
+ } catch (error) {
72
+ throw new OPFRenderError("png-render-failed", "SVG to PNG conversion failed.", {
73
+ cause: error instanceof Error ? error.message : String(error)
74
+ });
75
+ }
76
+ }
77
+
78
+ async function loadResvg() {
79
+ try {
80
+ return await import("@resvg/resvg-js");
81
+ } catch (error) {
82
+ throw new OPFRenderError("png-renderer-unavailable", "PNG/PDF conversion requires the Node runtime dependency @resvg/resvg-js.", {
83
+ cause: error instanceof Error ? error.message : String(error)
84
+ });
85
+ }
86
+ }
87
+
88
+ async function loadPdfLib() {
89
+ try {
90
+ return await import("pdf-lib");
91
+ } catch (error) {
92
+ throw new OPFRenderError("pdf-writer-unavailable", "PDF conversion requires the runtime dependency pdf-lib.", {
93
+ cause: error instanceof Error ? error.message : String(error)
94
+ });
95
+ }
96
+ }
97
+
98
+ async function bundledFontFiles() {
99
+ if (!bundledFontFilesCache) {
100
+ bundledFontFilesCache = resolveBundledFontFiles();
101
+ }
102
+ return bundledFontFilesCache;
103
+ }
104
+
105
+ async function resolveBundledFontFiles() {
106
+ try {
107
+ const { createRequire } = await import("node:module");
108
+ const path = await import("node:path");
109
+ const require = createRequire(import.meta.url);
110
+ const roboto = path.dirname(require.resolve("@expo-google-fonts/roboto/package.json"));
111
+ const robotoMono = path.dirname(require.resolve("@expo-google-fonts/roboto-mono/package.json"));
112
+ return [
113
+ path.join(roboto, "400Regular/Roboto_400Regular.ttf"),
114
+ path.join(roboto, "500Medium/Roboto_500Medium.ttf"),
115
+ path.join(roboto, "700Bold/Roboto_700Bold.ttf"),
116
+ path.join(roboto, "800ExtraBold/Roboto_800ExtraBold.ttf"),
117
+ path.join(robotoMono, "400Regular/RobotoMono_400Regular.ttf"),
118
+ path.join(robotoMono, "700Bold/RobotoMono_700Bold.ttf")
119
+ ];
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+
125
+ function normalizeSvgList(value) {
126
+ if (Array.isArray(value)) return value;
127
+ return [value];
128
+ }
129
+
130
+ function normalizeSvgInput(value) {
131
+ if (typeof value === "string") return value;
132
+ if (value instanceof Uint8Array) return new TextDecoder().decode(value);
133
+ throw new OPFRenderError("invalid-svg-input", "SVG input must be a string or Uint8Array.");
134
+ }
135
+
136
+ function positiveNumber(value, name, fallback) {
137
+ if (value === undefined || value === null) return fallback;
138
+ const number = Number(value);
139
+ if (Number.isFinite(number) && number > 0) return number;
140
+ throw new OPFRenderError("invalid-conversion-option", `${name} must be a positive finite number.`, {
141
+ option: name,
142
+ value
143
+ });
144
+ }
145
+
146
+ function stringArray(value) {
147
+ if (!Array.isArray(value)) return [];
148
+ return value.filter((item) => typeof item === "string");
149
+ }
150
+
151
+ function svgPageSize(svg) {
152
+ const attrs = svgRootAttrs(svg);
153
+ const viewBox = parseViewBox(attrs.viewBox);
154
+ return {
155
+ width: positiveDimension(parseSvgLength(attrs.width) ?? viewBox?.width, DEFAULT_DIMENSIONS.width),
156
+ height: positiveDimension(parseSvgLength(attrs.height) ?? viewBox?.height, DEFAULT_DIMENSIONS.height)
157
+ };
158
+ }
159
+
160
+ function svgRootAttrs(svg) {
161
+ const match = svg.match(/<svg\s+([^>]*?)>/i);
162
+ if (!match) {
163
+ throw new OPFRenderError("invalid-svg", "SVG input must contain a root <svg> element.");
164
+ }
165
+
166
+ const attrs = {};
167
+ const attrPattern = /([\w:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
168
+ for (const attr of match[1].matchAll(attrPattern)) {
169
+ attrs[attr[1]] = attr[2] ?? attr[3] ?? "";
170
+ }
171
+ return attrs;
172
+ }
173
+
174
+ function parseViewBox(value) {
175
+ if (typeof value !== "string") return null;
176
+ const parts = value.trim().split(/[\s,]+/).map(Number);
177
+ if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) return null;
178
+ return { width: parts[2], height: parts[3] };
179
+ }
180
+
181
+ function parseSvgLength(value) {
182
+ if (typeof value !== "string" && typeof value !== "number") return null;
183
+ const match = String(value).trim().match(/^-?\d+(?:\.\d+)?/);
184
+ if (!match) return null;
185
+ const number = Number(match[0]);
186
+ return Number.isFinite(number) ? number : null;
187
+ }
188
+
189
+ function positiveDimension(value, fallback) {
190
+ return Number.isFinite(value) && value > 0 ? value : fallback;
191
+ }
package/dist/svg.d.ts ADDED
@@ -0,0 +1,112 @@
1
+ import type { EmbeddedFont } from "./fonts.js";
2
+ import type { SlideComposition, LayoutDiagnostic, TextMeasurement } from "@openpresentation/opf/composition";
3
+ export declare const packageName = "@openpresentation/opf-render";
4
+
5
+ export declare const releaseLane: Readonly<{
6
+ githubRepository: "OpenPresentation/opf-render";
7
+ npmPackage: "@openpresentation/opf-render";
8
+ compatibilityPackage: "@openpresentation/opf";
9
+ }>;
10
+
11
+ export declare const runtimePolicy: Readonly<{
12
+ hostedServiceInCriticalPath: false;
13
+ telemetry: false;
14
+ commercialSdkInCriticalPath: false;
15
+ requiredNetworkCalls: false;
16
+ deterministicLocalExecution: true;
17
+ }>;
18
+
19
+ export declare const engineDefaults: Readonly<{
20
+ catalogs: Readonly<Record<string, Readonly<{ source: string }>>>;
21
+ theme: "minimal";
22
+ colorScheme: "cool-horizon";
23
+ language: "english";
24
+ narrative: "classic-story";
25
+ tone: "formal";
26
+ audience: "executives";
27
+ fontScheme: Readonly<{
28
+ pptx: Readonly<{ latin: "aptos"; ea: "microsoft-yahei"; cs: "nirmala-ui" }>;
29
+ google: Readonly<{ latin: "roboto"; ea: "noto-sans-sc"; cs: "noto-sans" }>;
30
+ }>;
31
+ chartTypes: readonly ["stacked-column-3x", "stacked-area-3x", "line-with-markers-3x"];
32
+ }>;
33
+
34
+ export interface RenderSvgOptions {
35
+ textMeasurement?: TextMeasurement;
36
+ embeddedFonts?: EmbeddedFont[];
37
+ strictAssets?: boolean;
38
+ imageResolver?: (src: string | undefined, context: { asset: unknown; path: string }) => string | null | undefined;
39
+ onDiagnostic?: (diagnostic: LayoutDiagnostic) => void;
40
+ slideIndex?: number;
41
+ trace?: boolean;
42
+ catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
43
+ catalogSources?: Record<string, { records?: unknown[] } | unknown[]>;
44
+ }
45
+
46
+ export interface SvgToPngOptions {
47
+ scale?: number;
48
+ background?: string;
49
+ dpi?: number;
50
+ useBundledFonts?: boolean;
51
+ loadSystemFonts?: boolean;
52
+ fontFiles?: string[];
53
+ fontDirs?: string[];
54
+ defaultFontFamily?: string;
55
+ sansSerifFamily?: string;
56
+ monospaceFamily?: string;
57
+ }
58
+
59
+ export interface SvgToPdfOptions extends SvgToPngOptions {}
60
+
61
+ export interface ResolvedPresentation {
62
+ presentation: unknown;
63
+ engineDefaults: typeof engineDefaults;
64
+ slides: ResolvedSlide[];
65
+ }
66
+
67
+ export interface ResolvedSlide {
68
+ geometry: SlideComposition;
69
+ index: number;
70
+ path: string;
71
+ slide: unknown;
72
+ layout: unknown;
73
+ design: {
74
+ theme: unknown;
75
+ colorScheme: unknown;
76
+ fontScheme: unknown;
77
+ dimensions: { width: number; height: number };
78
+ background: unknown;
79
+ backgroundColor: string | null;
80
+ colors: Record<string, string>;
81
+ fonts: Record<"heading" | "body" | "code", string>;
82
+ };
83
+ titleBindings: RenderBinding[];
84
+ contentItems: RenderBinding[];
85
+ blocks: RenderBinding[];
86
+ }
87
+
88
+ export interface RenderBinding {
89
+ type: string;
90
+ field?: string;
91
+ slot?: string;
92
+ value: unknown;
93
+ path: string;
94
+ placeholderIndex?: number;
95
+ regionKey?: string;
96
+ }
97
+
98
+ export declare class OPFRenderError extends Error {
99
+ readonly code: string;
100
+ readonly details: Record<string, unknown>;
101
+ readonly issues?: unknown[];
102
+ readonly path?: string;
103
+ constructor(code: string, message: string, details?: Record<string, unknown>);
104
+ }
105
+
106
+ export declare function resolvePresentation(input: unknown, options?: RenderSvgOptions): ResolvedPresentation;
107
+
108
+ export declare function renderSvg(input: unknown, options?: RenderSvgOptions): string;
109
+
110
+ export declare function renderSvgDeck(input: unknown, options?: RenderSvgOptions): string[];
111
+
112
+