Skip to content

Commit c68054c

Browse files
authored
Support PNG generation
Support PNG generation
1 parent 09d9a08 commit c68054c

File tree

9 files changed

+284
-24
lines changed

9 files changed

+284
-24
lines changed

README.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# svg.tscircuit.com
22

3-
A server that takes encoded tscircuit code or circuit JSON and renders it into an SVG suitable for use in docs.
3+
A server that takes encoded tscircuit code or circuit JSON and renders it into SVG or PNG assets suitable for use in docs.
44

55
## API Overview
66

7-
This service converts TSCircuit code or pre-generated circuit JSON into various SVG formats (PCB, schematic, and 3D views) via HTTP requests. It's designed to be used for generating circuit diagrams, PCB layouts, and 3D visualizations for documentation and web applications.
7+
This service converts TSCircuit code or pre-generated circuit JSON into various visual formats (PCB, schematic, and 3D views) via HTTP requests. It's designed to be used for generating circuit diagrams, PCB layouts, and 3D visualizations for documentation and web applications.
88

99
## API Endpoints
1010

@@ -18,14 +18,17 @@ This service converts TSCircuit code or pre-generated circuit JSON into various
1818
- `schematic` - Circuit schematic view
1919
- `pinout` - Pinout diagram view
2020
- `3d` - 3D visualization view
21+
- `format` (optional): Output format. Defaults to `svg`. Set `format=png` to receive a PNG rasterized version. PNG-specific query parameters:
22+
- `png_width` / `png_height`
23+
- `png_density`
2124

2225
**Input Methods:**
2326
- `code` (GET/POST query parameter): Base64-encoded and compressed TSCircuit code
2427
- `circuit_json` (POST body only): Raw circuit JSON object - pass as `{"circuit_json": {...}}`
2528

2629
Either `code` or `circuit_json` must be provided.
2730

28-
**Response:** SVG content with `image/svg+xml` content type
31+
**Response:** SVG content with `image/svg+xml` content type (default) or PNG content with `image/png` when `format=png`
2932

3033
**Example Request with Code:**
3134
```bash
@@ -91,6 +94,11 @@ export default () => (
9194
curl "https://svg.tscircuit.com/?svg_type=pcb&code=YOUR_ENCODED_CODE"
9295
```
9396

97+
**PNG Output:**
98+
```bash
99+
curl "https://svg.tscircuit.com/?svg_type=pcb&format=png&code=YOUR_ENCODED_CODE"
100+
```
101+
94102
**Schematic View:**
95103
```bash
96104
curl "https://svg.tscircuit.com/?svg_type=schematic&code=YOUR_ENCODED_CODE"
@@ -135,19 +143,19 @@ const apiUrl = `https://svg.tscircuit.com/?svg_type=pcb&circuit_json=${encodeURI
135143

136144
## Response Headers
137145

138-
Successful SVG responses include:
139-
- `Content-Type: image/svg+xml`
146+
Successful responses include:
147+
- `Content-Type: image/svg+xml` (SVG) or `image/png` (PNG)
140148
- `Cache-Control: public, max-age=86400, s-maxage=31536000, immutable`
141149

142150
## Error Handling
143151

144-
When errors occur, the API returns an SVG with error information instead of the requested circuit SVG. Error responses include:
145-
- `Content-Type: image/svg+xml`
152+
When errors occur, the API returns an image with error information instead of the requested circuit asset. Error responses include:
153+
- `Content-Type: image/svg+xml` (or `image/png` when `format=png`)
146154
- `Cache-Control: public, max-age=86400, s-maxage=86400`
147155

148156
## Caching
149157

150-
The API implements aggressive caching for generated SVGs:
158+
The API implements aggressive caching for generated assets:
151159
- Browser cache: 24 hours (`max-age=86400`)
152160
- CDN cache: 1 year (`s-maxage=31536000`)
153161
- Error responses: 24 hours

bun.lock

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"circuit-json-to-simple-3d": "^0.0.9",
99
"circuit-to-svg": "^0.0.200",
1010
"jscad-fiber": "^0.0.85",
11+
"sharp": "^0.34.4",
1112
},
1213
"devDependencies": {
1314
"@biomejs/biome": "^1.9.4",
@@ -62,6 +63,8 @@
6263

6364
"@edge-runtime/vm": ["@edge-runtime/[email protected]", "", { "dependencies": { "@edge-runtime/primitives": "4.1.0" } }, "sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw=="],
6465

66+
"@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
67+
6568
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
6669

6770
"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
@@ -112,6 +115,52 @@
112115

113116
"@flatten-js/interval-tree": ["@flatten-js/[email protected]", "", {}, "sha512-xhFWUBoHJFF77cJO1D6REjdgJEMRf2Y2Z+eKEPav8evGKcLSnj1ud5pLXQSbGuxF3VSvT1rWhMfVpXEKJLTL+A=="],
114117

118+
"@img/colour": ["@img/[email protected]", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
119+
120+
"@img/sharp-darwin-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA=="],
121+
122+
"@img/sharp-darwin-x64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.3" }, "os": "darwin", "cpu": "x64" }, "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg=="],
123+
124+
"@img/sharp-libvips-darwin-arm64": ["@img/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw=="],
125+
126+
"@img/sharp-libvips-darwin-x64": ["@img/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA=="],
127+
128+
"@img/sharp-libvips-linux-arm": ["@img/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA=="],
129+
130+
"@img/sharp-libvips-linux-arm64": ["@img/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ=="],
131+
132+
"@img/sharp-libvips-linux-ppc64": ["@img/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg=="],
133+
134+
"@img/sharp-libvips-linux-s390x": ["@img/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w=="],
135+
136+
"@img/sharp-libvips-linux-x64": ["@img/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg=="],
137+
138+
"@img/sharp-libvips-linuxmusl-arm64": ["@img/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw=="],
139+
140+
"@img/sharp-libvips-linuxmusl-x64": ["@img/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g=="],
141+
142+
"@img/sharp-linux-arm": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.3" }, "os": "linux", "cpu": "arm" }, "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA=="],
143+
144+
"@img/sharp-linux-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ=="],
145+
146+
"@img/sharp-linux-ppc64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ=="],
147+
148+
"@img/sharp-linux-s390x": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.3" }, "os": "linux", "cpu": "s390x" }, "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw=="],
149+
150+
"@img/sharp-linux-x64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A=="],
151+
152+
"@img/sharp-linuxmusl-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA=="],
153+
154+
"@img/sharp-linuxmusl-x64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg=="],
155+
156+
"@img/sharp-wasm32": ["@img/[email protected]", "", { "dependencies": { "@emnapi/runtime": "^1.5.0" }, "cpu": "none" }, "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA=="],
157+
158+
"@img/sharp-win32-arm64": ["@img/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA=="],
159+
160+
"@img/sharp-win32-ia32": ["@img/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw=="],
161+
162+
"@img/sharp-win32-x64": ["@img/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig=="],
163+
115164
"@isaacs/cliui": ["@isaacs/[email protected]", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
116165

117166
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
@@ -344,7 +393,7 @@
344393

345394
"deep-rename-keys": ["[email protected]", "", { "dependencies": { "kind-of": "^3.0.2", "rename-keys": "^1.1.2" } }, "sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A=="],
346395

347-
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
396+
"detect-libc": ["detect-libc@2.1.0", "", {}, "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg=="],
348397

349398
"dettle": ["[email protected]", "", {}, "sha512-ZVyjhAJ7sCe1PNXEGveObOH9AC8QvMga3HJIghHawtG7mE4K5pW9nz/vDGAr/U7a3LWgdOzEE7ac9MURnyfaTA=="],
350399

@@ -620,7 +669,7 @@
620669

621670
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
622671

623-
"sharp": ["[email protected]", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.2", "node-addon-api": "^6.1.0", "prebuild-install": "^7.1.1", "semver": "^7.5.4", "simple-get": "^4.0.1", "tar-fs": "^3.0.4", "tunnel-agent": "^0.6.0" } }, "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w=="],
672+
"sharp": ["[email protected]", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="],
624673

625674
"shebang-command": ["[email protected]", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
626675

@@ -770,8 +819,12 @@
770819

771820
"log-symbols/is-unicode-supported": ["[email protected]", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
772821

822+
"looks-same/sharp": ["[email protected]", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.2", "node-addon-api": "^6.1.0", "prebuild-install": "^7.1.1", "semver": "^7.5.4", "simple-get": "^4.0.1", "tar-fs": "^3.0.4", "tunnel-agent": "^0.6.0" } }, "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w=="],
823+
773824
"parse-color/color-convert": ["[email protected]", "", {}, "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling=="],
774825

826+
"prebuild-install/detect-libc": ["[email protected]", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
827+
775828
"prebuild-install/tar-fs": ["[email protected]", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
776829

777830
"react-reconciler/scheduler": ["[email protected]", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
@@ -826,6 +879,8 @@
826879

827880
"connectivity-map/@biomejs/biome/@biomejs/cli-win32-x64": ["@biomejs/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-gvCpewE7mBwBIpqk1YrUqNR4mCiyJm6UI3YWQQXkedSSEwzRdodRpaKhbdbHw1/hmTWOVXQ+Eih5Qctf4TCVOQ=="],
828881

882+
"looks-same/sharp/detect-libc": ["[email protected]", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
883+
829884
"prebuild-install/tar-fs/tar-stream": ["[email protected]", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
830885

831886
"string-width-cjs/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],

endpoint.ts

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import {
77
} from "circuit-to-svg"
88
import { convertCircuitJsonToSimple3dSvg } from "circuit-json-to-simple-3d/dist/index.js"
99
import { getHtmlForGeneratedUrlPage } from "./get-html-for-generated-url-page"
10-
import { getErrorSvg } from "./getErrorSvg"
1110
import { getIndexPageHtml } from "./get-index-page-html"
11+
import { errorResponse } from "./lib/errorResponse"
12+
import { getOutputFormat } from "./lib/getOutputFormat"
13+
import { parsePositiveInt } from "./lib/parsePositiveInt"
14+
import { svgToPng } from "./lib/svgToPng"
1215

1316
export default async (req: Request) => {
1417
const url = new URL(req.url.replace("/api", "/"))
@@ -50,6 +53,10 @@ export default async (req: Request) => {
5053
background_color: body.background_color,
5154
background_opacity: body.background_opacity,
5255
zoom_multiplier: body.zoom_multiplier,
56+
output_format: body.output_format || body.format,
57+
png_width: body.png_width,
58+
png_height: body.png_height,
59+
png_density: body.png_density,
5360
}
5461
} catch (err) {
5562
return new Response(
@@ -74,6 +81,17 @@ export default async (req: Request) => {
7481
})
7582
}
7683

84+
const outputFormat = getOutputFormat(url, postBodyParams)
85+
if (!outputFormat) {
86+
return new Response(
87+
JSON.stringify({
88+
ok: false,
89+
error: "Invalid format parameter",
90+
}),
91+
{ status: 400, headers: { "Content-Type": "application/json" } },
92+
)
93+
}
94+
7795
if (!compressedCode && !circuitJsonFromPost) {
7896
return new Response(
7997
JSON.stringify({
@@ -93,7 +111,7 @@ export default async (req: Request) => {
93111
try {
94112
userCode = getUncompressedSnippetString(compressedCode)
95113
} catch (err) {
96-
return errorResponse(err as Error)
114+
return await errorResponse(err as Error, outputFormat)
97115
}
98116

99117
const worker = new CircuitRunner()
@@ -120,7 +138,7 @@ export default async (req: Request) => {
120138
await worker.renderUntilSettled()
121139
circuitJson = await worker.getCircuitJson()
122140
} catch (err) {
123-
return errorResponse(err as Error)
141+
return await errorResponse(err as Error, outputFormat)
124142
}
125143
}
126144

@@ -171,7 +189,33 @@ export default async (req: Request) => {
171189
})
172190
}
173191
} catch (err) {
174-
return errorResponse(err as Error)
192+
return await errorResponse(err as Error, outputFormat)
193+
}
194+
195+
if (outputFormat === "png") {
196+
try {
197+
const pngBuffer = await svgToPng(svgContent, {
198+
density: parsePositiveInt(
199+
url.searchParams.get("png_density") ?? postBodyParams.png_density,
200+
),
201+
width: parsePositiveInt(
202+
url.searchParams.get("png_width") ?? postBodyParams.png_width,
203+
),
204+
height: parsePositiveInt(
205+
url.searchParams.get("png_height") ?? postBodyParams.png_height,
206+
),
207+
})
208+
209+
return new Response(pngBuffer, {
210+
headers: {
211+
"Content-Type": "image/png",
212+
"Cache-Control":
213+
"public, max-age=86400, s-maxage=31536000, immutable",
214+
},
215+
})
216+
} catch (err) {
217+
return await errorResponse(err as Error, outputFormat)
218+
}
175219
}
176220

177221
return new Response(svgContent, {
@@ -181,12 +225,3 @@ export default async (req: Request) => {
181225
},
182226
})
183227
}
184-
185-
function errorResponse(err: Error) {
186-
return new Response(getErrorSvg(err.message), {
187-
headers: {
188-
"Content-Type": "image/svg+xml",
189-
"Cache-Control": "public, max-age=86400, s-maxage=86400",
190-
},
191-
})
192-
}

lib/errorResponse.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { getErrorSvg } from "../getErrorSvg"
2+
import { svgToPng } from "./svgToPng"
3+
4+
export async function errorResponse(err: Error, format: "svg" | "png") {
5+
const errorSvg = getErrorSvg(err.message)
6+
7+
if (format === "png") {
8+
try {
9+
const pngBuffer = await svgToPng(errorSvg, {})
10+
11+
return new Response(pngBuffer, {
12+
headers: {
13+
"Content-Type": "image/png",
14+
"Cache-Control": "public, max-age=86400, s-maxage=86400",
15+
},
16+
})
17+
} catch (_) {
18+
return new Response(JSON.stringify({ ok: false, error: err.message }), {
19+
status: 500,
20+
headers: { "Content-Type": "application/json" },
21+
})
22+
}
23+
}
24+
25+
return new Response(errorSvg, {
26+
headers: {
27+
"Content-Type": "image/svg+xml",
28+
"Cache-Control": "public, max-age=86400, s-maxage=86400",
29+
},
30+
})
31+
}

lib/getOutputFormat.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export type OutputFormat = "svg" | "png"
2+
3+
export function getOutputFormat(
4+
url: URL,
5+
postBodyParams: Record<string, any>,
6+
): OutputFormat | null {
7+
const rawFormat =
8+
url.searchParams.get("format") ||
9+
url.searchParams.get("output") ||
10+
url.searchParams.get("response_format") ||
11+
postBodyParams.output_format ||
12+
"svg"
13+
14+
if (typeof rawFormat !== "string") {
15+
return null
16+
}
17+
18+
const normalized = rawFormat.toLowerCase()
19+
20+
if (normalized === "svg") {
21+
return "svg"
22+
}
23+
24+
if (normalized === "png") {
25+
return "png"
26+
}
27+
28+
return null
29+
}

0 commit comments

Comments
 (0)