Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions src/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ Usage: grafex export --file <path> [options]

Options:
--file, -f Path to the composition .tsx file (required)
--out, -o Output file path (default: ./output.png)
--out, -o Output file path (default: ./output.png or ./output.svg)
--props Props to pass as JSON (default: {})
--width Override composition width (pixels)
--height Override composition height (pixels)
--scale Device scale factor, e.g. 2 for retina (default: 1)
--format Output format, must be "png" (default: png)
--browser Browser engine: webkit or chromium (default: webkit)
--scale Scale factor, e.g. 2 for retina PNG or larger SVG canvas (default: 1)
--format Output format: "png" or "svg" (default: png)
--browser Browser engine: webkit or chromium (default: webkit, PNG only)
--help, -h Show this help text
`.trim();

Expand Down Expand Up @@ -44,14 +44,14 @@ export async function runExport(args: string[]): Promise<void> {
process.exit(1);
}

const outPath = values.out ?? './output.png';

const format = values.format ?? 'png';
if (format !== 'png') {
process.stderr.write('Only PNG format is supported in this version.\n');
if (format !== 'png' && format !== 'svg') {
process.stderr.write(`Error: --format must be "png" or "svg", got "${format}".\n`);
process.exit(1);
}

const outPath = values.out ?? (format === 'svg' ? './output.svg' : './output.png');

const browser = values.browser ?? 'webkit';
if (browser !== 'webkit' && browser !== 'chromium') {
process.stderr.write(`Error: --browser must be "webkit" or "chromium", got "${browser}".\n`);
Expand Down Expand Up @@ -107,6 +107,7 @@ export async function runExport(args: string[]): Promise<void> {
width,
height,
scale,
format: format as 'png' | 'svg',
browser: browser as 'webkit' | 'chromium',
});
try {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { pipeline, browserManager as defaultBrowserManager } from './render.js';
import { BrowserManager } from './browser.js';
import type { RenderOptions, RenderResult } from './types.js';

export { h, Fragment, renderToHTML } from './runtime.js';
export { h, Fragment, renderToHTML, renderToSVG } from './runtime.js';
export { BrowserManager } from './browser.js';
export type { RenderOptions, RenderResult } from './types.js';

Expand Down
56 changes: 44 additions & 12 deletions src/render.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
import { resolve } from 'node:path';
import { transpile } from './transpile.js';
import { renderToHTML } from './runtime.js';
import { renderToHTML, renderToSVG } from './runtime.js';
import { BrowserManager } from './browser.js';
import type { RenderOptions, RenderResult, CompositionConfig } from './types.js';

export type { RenderOptions, RenderResult, CompositionConfig };

interface FormatContext {
componentHtml: string;
width: number;
height: number;
scale: number;
fonts?: string[];
manager: Pick<BrowserManager, 'render'>;
}

type FormatHandler = (ctx: FormatContext) => Promise<RenderResult>;

const formatHandlers: Record<string, FormatHandler> = {
png: async ({ componentHtml, width, height, scale, fonts, manager }) => {
const html = renderToHTML(componentHtml, { width, height }, fonts);
const buffer = await manager.render(html, { width, height }, scale);
return {
buffer,
width: Math.round(width * scale),
height: Math.round(height * scale),
scale,
format: 'png',
};
},

svg: async ({ componentHtml, width, height, scale, fonts }) => {
const svgString = renderToSVG(componentHtml, { width, height }, scale, fonts);
return {
buffer: Buffer.from(svgString),
width: Math.round(width * scale),
height: Math.round(height * scale),
scale,
format: 'svg',
};
},
};

// ── Pipeline ──────────────────────────────────────────────────────────────────

export const browserManager = new BrowserManager();

export async function pipeline(
Expand All @@ -26,17 +64,11 @@ export async function pipeline(
const width = options.width ?? config.width ?? 1200;
const height = options.height ?? config.height ?? 630;
const scale = options.scale ?? config.scale ?? 1;
const format = options.format ?? config.format ?? 'png';

const componentHtml = String(component(options.props ?? {}));
const html = renderToHTML(componentHtml, { width, height }, config.fonts);

const buffer = await manager.render(html, { width, height }, scale);
const handler = formatHandlers[format];
if (!handler) throw new Error(`Unsupported format: "${format}"`);

return {
buffer,
width: Math.round(width * scale),
height: Math.round(height * scale),
scale,
format: 'png',
};
const componentHtml = String(component(options.props ?? {}));
return handler({ componentHtml, width, height, scale, fonts: config.fonts, manager });
}
33 changes: 33 additions & 0 deletions src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,36 @@ ${componentHtml}
</body>
</html>`;
}

export function renderToSVG(
componentHtml: string,
viewport: { width: number; height: number },
scale: number = 1,
fonts?: string[],
): string {
const { width, height } = viewport;
const physicalWidth = Math.round(width * scale);
const physicalHeight = Math.round(height * scale);
const fontLinks =
fonts && fonts.length > 0
? `${fonts
.map((url) => `<link rel="stylesheet" href="${escapeHtml(url)}" crossorigin/>`)
.join('\n')}\n`
: '';
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xhtml="http://www.w3.org/1999/xhtml" width="${physicalWidth}" height="${physicalHeight}" viewBox="0 0 ${width} ${height}">
<foreignObject width="${width}" height="${height}">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
${fontLinks}<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { width: ${width}px; height: ${height}px; overflow: hidden; }
</style>
</head>
<body>
${componentHtml}
</body>
</html>
</foreignObject>
</svg>`;
}
8 changes: 5 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export type Format = 'png' | 'svg';

export interface RenderOptions {
props?: Record<string, unknown>;
width?: number;
height?: number;
scale?: number;
format?: 'png';
format?: Format;
browser?: 'webkit' | 'chromium';
}

Expand All @@ -12,13 +14,13 @@ export interface RenderResult {
width: number;
height: number;
scale: number;
format: 'png';
format: Format;
}

export interface CompositionConfig {
width?: number;
height?: number;
scale?: number;
format?: 'png';
format?: Format;
fonts?: string[];
}
Loading
Loading