Skip to content

Commit df6316d

Browse files
committed
image
1 parent 9f58d87 commit df6316d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+4142
-144
lines changed

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"tailwind-merge": "^3.4.0",
5151
"unist-util-visit": "5.1.0",
5252
"vaul": "^1.1.2",
53-
"zod": "^4.0.0"
53+
"zod": "^4.3.6"
5454
},
5555
"devDependencies": {
5656
"@internal/eslint-config": "workspace:*",

examples/chat/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"streamdown": "^2.2.0",
3434
"tailwind-merge": "^3.4.0",
3535
"three": "^0.182.0",
36-
"zod": "4.3.5"
36+
"zod": "4.3.6"
3737
},
3838
"devDependencies": {
3939
"@internal/eslint-config": "workspace:*",

examples/dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"sonner": "^2.0.7",
4040
"tailwind-merge": "^3.4.0",
4141
"vaul": "^1.1.2",
42-
"zod": "^4.0.0"
42+
"zod": "^4.3.6"
4343
},
4444
"devDependencies": {
4545
"@internal/eslint-config": "workspace:*",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { streamText } from "ai";
2+
import { gateway } from "@ai-sdk/gateway";
3+
import { buildUserPrompt, type Spec } from "@json-render/core";
4+
import { imageCatalog } from "@/lib/catalog";
5+
6+
export const maxDuration = 60;
7+
8+
const SYSTEM_PROMPT = imageCatalog.prompt();
9+
10+
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
11+
12+
export async function POST(req: Request) {
13+
const { prompt, startingSpec } = (await req.json()) as {
14+
prompt: string;
15+
startingSpec?: Spec | null;
16+
};
17+
18+
if (!prompt || typeof prompt !== "string") {
19+
return Response.json({ error: "prompt is required" }, { status: 400 });
20+
}
21+
22+
const userPrompt = buildUserPrompt({
23+
prompt,
24+
currentSpec: startingSpec,
25+
});
26+
27+
const result = streamText({
28+
model: gateway(process.env.AI_GATEWAY_MODEL ?? DEFAULT_MODEL),
29+
system: SYSTEM_PROMPT,
30+
prompt: userPrompt,
31+
temperature: 0.7,
32+
});
33+
34+
return result.toTextStreamResponse();
35+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { renderToSvg, renderToPng } from "@json-render/image/render";
2+
import { examples } from "@/lib/examples";
3+
import type { Spec } from "@json-render/core";
4+
import { readFile } from "node:fs/promises";
5+
import { join } from "node:path";
6+
7+
let fontCache: ArrayBuffer | null = null;
8+
9+
async function loadFont(): Promise<ArrayBuffer> {
10+
if (fontCache) return fontCache;
11+
const buffer = await readFile(
12+
join(process.cwd(), "public", "Inter-Regular.ttf"),
13+
);
14+
fontCache = buffer.buffer.slice(
15+
buffer.byteOffset,
16+
buffer.byteOffset + buffer.byteLength,
17+
);
18+
return fontCache;
19+
}
20+
21+
export async function GET(req: Request) {
22+
const { searchParams } = new URL(req.url);
23+
const name = searchParams.get("name") ?? "og-image";
24+
const download = searchParams.get("download") === "1";
25+
26+
const example = examples.find((e) => e.name === name);
27+
if (!example) {
28+
return new Response("Example not found", { status: 404 });
29+
}
30+
31+
return imageResponse(example.spec, name, download);
32+
}
33+
34+
export async function POST(req: Request) {
35+
const { spec, download, filename } = (await req.json()) as {
36+
spec: Spec;
37+
download?: boolean;
38+
filename?: string;
39+
};
40+
41+
if (!spec || !spec.root || !spec.elements) {
42+
return new Response("Invalid spec", { status: 400 });
43+
}
44+
45+
return imageResponse(spec, filename ?? "image", download ?? false);
46+
}
47+
48+
async function imageResponse(spec: Spec, name: string, download: boolean) {
49+
const fontData = await loadFont();
50+
const fonts = [
51+
{
52+
name: "Inter",
53+
data: fontData,
54+
weight: 400 as const,
55+
style: "normal" as const,
56+
},
57+
];
58+
59+
const png = await renderToPng(spec, { fonts });
60+
61+
const disposition = download
62+
? `attachment; filename="${name}.png"`
63+
: `inline; filename="${name}.png"`;
64+
65+
return new Response(png as unknown as ArrayBuffer, {
66+
headers: {
67+
"Content-Type": "image/png",
68+
"Content-Disposition": disposition,
69+
"Cache-Control": "no-store",
70+
},
71+
});
72+
}

examples/image/app/globals.css

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
@import "tailwindcss";
2+
@import "tw-animate-css";
3+
@import "shadcn/tailwind.css";
4+
5+
@custom-variant dark (&:is(.dark *));
6+
7+
@theme inline {
8+
--radius-sm: calc(var(--radius) - 4px);
9+
--radius-md: calc(var(--radius) - 2px);
10+
--radius-lg: var(--radius);
11+
--radius-xl: calc(var(--radius) + 4px);
12+
--radius-2xl: calc(var(--radius) + 8px);
13+
--radius-3xl: calc(var(--radius) + 12px);
14+
--radius-4xl: calc(var(--radius) + 16px);
15+
--color-background: var(--background);
16+
--color-foreground: var(--foreground);
17+
--color-card: var(--card);
18+
--color-card-foreground: var(--card-foreground);
19+
--color-popover: var(--popover);
20+
--color-popover-foreground: var(--popover-foreground);
21+
--color-primary: var(--primary);
22+
--color-primary-foreground: var(--primary-foreground);
23+
--color-secondary: var(--secondary);
24+
--color-secondary-foreground: var(--secondary-foreground);
25+
--color-muted: var(--muted);
26+
--color-muted-foreground: var(--muted-foreground);
27+
--color-accent: var(--accent);
28+
--color-accent-foreground: var(--accent-foreground);
29+
--color-destructive: var(--destructive);
30+
--color-border: var(--border);
31+
--color-input: var(--input);
32+
--color-ring: var(--ring);
33+
--color-chart-1: var(--chart-1);
34+
--color-chart-2: var(--chart-2);
35+
--color-chart-3: var(--chart-3);
36+
--color-chart-4: var(--chart-4);
37+
--color-chart-5: var(--chart-5);
38+
--color-sidebar: var(--sidebar);
39+
--color-sidebar-foreground: var(--sidebar-foreground);
40+
--color-sidebar-primary: var(--sidebar-primary);
41+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
42+
--color-sidebar-accent: var(--sidebar-accent);
43+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
44+
--color-sidebar-border: var(--sidebar-border);
45+
--color-sidebar-ring: var(--sidebar-ring);
46+
}
47+
48+
:root {
49+
--radius: 0.625rem;
50+
--background: oklch(1 0 0);
51+
--foreground: oklch(0.145 0 0);
52+
--card: oklch(1 0 0);
53+
--card-foreground: oklch(0.145 0 0);
54+
--popover: oklch(1 0 0);
55+
--popover-foreground: oklch(0.145 0 0);
56+
--primary: oklch(0.205 0 0);
57+
--primary-foreground: oklch(0.985 0 0);
58+
--secondary: oklch(0.97 0 0);
59+
--secondary-foreground: oklch(0.205 0 0);
60+
--muted: oklch(0.97 0 0);
61+
--muted-foreground: oklch(0.556 0 0);
62+
--accent: oklch(0.97 0 0);
63+
--accent-foreground: oklch(0.205 0 0);
64+
--destructive: oklch(0.577 0.245 27.325);
65+
--border: oklch(0.922 0 0);
66+
--input: oklch(0.922 0 0);
67+
--ring: oklch(0.708 0 0);
68+
--chart-1: oklch(0.646 0.222 41.116);
69+
--chart-2: oklch(0.6 0.118 184.704);
70+
--chart-3: oklch(0.398 0.07 227.392);
71+
--chart-4: oklch(0.828 0.189 84.429);
72+
--chart-5: oklch(0.769 0.188 70.08);
73+
--sidebar: oklch(0.985 0 0);
74+
--sidebar-foreground: oklch(0.145 0 0);
75+
--sidebar-primary: oklch(0.205 0 0);
76+
--sidebar-primary-foreground: oklch(0.985 0 0);
77+
--sidebar-accent: oklch(0.97 0 0);
78+
--sidebar-accent-foreground: oklch(0.205 0 0);
79+
--sidebar-border: oklch(0.922 0 0);
80+
--sidebar-ring: oklch(0.708 0 0);
81+
}
82+
83+
.dark {
84+
--background: oklch(0.145 0 0);
85+
--foreground: oklch(0.985 0 0);
86+
--card: oklch(0.205 0 0);
87+
--card-foreground: oklch(0.985 0 0);
88+
--popover: oklch(0.205 0 0);
89+
--popover-foreground: oklch(0.985 0 0);
90+
--primary: oklch(0.922 0 0);
91+
--primary-foreground: oklch(0.205 0 0);
92+
--secondary: oklch(0.269 0 0);
93+
--secondary-foreground: oklch(0.985 0 0);
94+
--muted: oklch(0.269 0 0);
95+
--muted-foreground: oklch(0.708 0 0);
96+
--accent: oklch(0.269 0 0);
97+
--accent-foreground: oklch(0.985 0 0);
98+
--destructive: oklch(0.704 0.191 22.216);
99+
--border: oklch(1 0 0 / 10%);
100+
--input: oklch(1 0 0 / 15%);
101+
--ring: oklch(0.556 0 0);
102+
--chart-1: oklch(0.488 0.243 264.376);
103+
--chart-2: oklch(0.696 0.17 162.48);
104+
--chart-3: oklch(0.769 0.188 70.08);
105+
--chart-4: oklch(0.627 0.265 303.9);
106+
--chart-5: oklch(0.645 0.246 16.439);
107+
--sidebar: oklch(0.205 0 0);
108+
--sidebar-foreground: oklch(0.985 0 0);
109+
--sidebar-primary: oklch(0.488 0.243 264.376);
110+
--sidebar-primary-foreground: oklch(0.985 0 0);
111+
--sidebar-accent: oklch(0.269 0 0);
112+
--sidebar-accent-foreground: oklch(0.985 0 0);
113+
--sidebar-border: oklch(1 0 0 / 10%);
114+
--sidebar-ring: oklch(0.556 0 0);
115+
}
116+
117+
@layer base {
118+
* {
119+
@apply border-border outline-ring/50;
120+
}
121+
body {
122+
@apply bg-background text-foreground;
123+
}
124+
}

examples/image/app/layout.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Metadata } from "next";
2+
import { Inter } from "next/font/google";
3+
import "./globals.css";
4+
5+
const inter = Inter({ subsets: ["latin"] });
6+
7+
export const metadata: Metadata = {
8+
title: "json-render Image Example",
9+
description: "Generate images from JSON specs with @json-render/image",
10+
};
11+
12+
export default function RootLayout({
13+
children,
14+
}: {
15+
children: React.ReactNode;
16+
}) {
17+
return (
18+
<html lang="en">
19+
<body className={inter.className}>{children}</body>
20+
</html>
21+
);
22+
}

0 commit comments

Comments
 (0)