How to automate svg base64 encoding for json apis and build scripts
- Step 1Encode one file in Node —
Buffer.from(fs.readFileSync('icon.svg', 'utf-8'), 'utf-8').toString('base64')returns the bare Base64. Prefix it yourself with `data:image/svg+xml;base64,${b64}` to match the web tool's prefix-on output. - Step 2Loop a folder into a manifest object — Read the directory, filter
.svg, map each file to[name, base64], andObject.fromEntriesthe pairs. Write the object to a.jsonfile — one pass, no dependencies. - Step 3Decide prefix per target at generation time — For an
img/CSS/React Native manifest, include thedata:image/svg+xml;base64,prefix in each value. For a manifest the client will re-wrap, store bare strings. There's no runtime flag — bake the decision into the generated file. - Step 4Or pair the runner and POST locally — Install @jadapps/runner, then POST
{ "input": "<svg…>", "options": { "includePrefix": false } }tohttp://127.0.0.1:9789/v1/tools/svg-to-base64/run. The file never leaves your machine. Fetch the option schema first fromGET /api/v1/tools/svg-to-base64. - Step 5Wire it into your build step — Run the generator in a
prebuildnpm script or a CI job so the manifest is regenerated whenever theicons/folder changes. Commit the generated.jsonor.tsso runtime never re-encodes. - Step 6Minify the sources before the batch encode — Add a minify pass over the folder first — svg-pro-minifier and svg-metadata-scrubber — so every encoded string is as small as possible. Base64 multiplies whatever you don't strip.
Equivalent encode across runtimes
Every snippet produces the same standard RFC 4648 Base64 the web tool emits. UTF-8 is handled correctly in all of them.
| Runtime | Bare Base64 | Notes |
|---|---|---|
| Node / Bun | Buffer.from(svg, 'utf-8').toString('base64') | Reads files with fs.readFileSync; no dependencies |
| Deno | btoa(unescape(encodeURIComponent(svg))) | Same call the JAD browser tool uses; or encodeBase64 from std/encoding |
| Browser | btoa(unescape(encodeURIComponent(svg))) | Identical to the web tool; encodeURIComponent step is required for non-ASCII |
| Add the prefix (any runtime) | data:image/svg+xml;base64,${b64} | This is exactly what Include data URI prefix = on produces |
Runner API contract for svg-to-base64
The hosted API never accepts uploads; execution runs through your paired @jadapps/runner on localhost. Verified against the route handlers.
| Item | Value |
|---|---|
| Schema endpoint (hosted) | GET /api/v1/tools/svg-to-base64 — returns the option schema and execution shape |
| Run endpoint (local runner) | POST http://127.0.0.1:9789/v1/tools/svg-to-base64/run |
| Only option | includePrefix (boolean, default true) — no include_prefix snake_case, no other fields |
| Execution mode | engine — svg-to-base64 is in SERVER_SAFE_SVG_SLUGS, so the runner executes it in-process (no headless browser needed) |
| Hosted /run with a payload | Returns HTTP 400 with pairing instructions — the API layer never receives file content |
| Output | text/plain — the Base64 string (with or without prefix) |
Cookbook
Drop-in scripts and the runner workflow. Each produces the same Base64 as the web tool.
Node — encode a folder into a JSON manifest
Read every .svg in a directory, encode each with the prefix on, and write a name→data-URI map. Zero dependencies, byte-identical to the web tool.
const fs = require("fs");
const path = require("path");
const dir = "./icons";
const manifest = Object.fromEntries(
fs.readdirSync(dir)
.filter((f) => f.endsWith(".svg"))
.map((f) => {
const svg = fs.readFileSync(path.join(dir, f), "utf-8");
const b64 = Buffer.from(svg, "utf-8").toString("base64");
const name = path.basename(f, ".svg");
return [name, `data:image/svg+xml;base64,${b64}`];
})
);
fs.writeFileSync("icons.json", JSON.stringify(manifest, null, 2));Typed TS manifest with as const
Generate a .ts module so consumers get autocomplete and a literal union of icon names. Store bare Base64 here and let the component add the prefix, or include it — your call.
// generate-icons.ts (run with tsx / ts-node)
import fs from "node:fs";
const entries = fs.readdirSync("icons")
.filter((f) => f.endsWith(".svg"))
.map((f) => {
const b64 = Buffer.from(fs.readFileSync(`icons/${f}`)).toString("base64");
return ` "${f.replace(/\.svg$/, "")}": "${b64}",`;
});
fs.writeFileSync(
"icons.ts",
`export const Icons = {\n${entries.join("\n")}\n} as const;\n` +
`export type IconName = keyof typeof Icons;\n`
);Runner — POST a single SVG locally
Pair @jadapps/runner, then dispatch to localhost. The file never touches JAD servers. Note the option name is includePrefix exactly.
// Fetch the schema first (hosted), then run locally:
// GET https://<host>/api/v1/tools/svg-to-base64 (auth) → schema
// POST http://127.0.0.1:9789/v1/tools/svg-to-base64/run
await fetch("http://127.0.0.1:9789/v1/tools/svg-to-base64/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
input: "<svg xmlns='http://www.w3.org/2000/svg'>…</svg>",
options: { includePrefix: false } // bare Base64
})
});
// Output: text/plain Base64 stringFeed a PDF library a Base64 SVG
pdfmake / jsPDF accept image data as Base64. Encode each SVG (prefix usually on for jsPDF addImage, off where the lib expects raw) and pass it straight in.
const b64 = Buffer.from(fs.readFileSync("logo.svg")).toString("base64");
// jsPDF (expects a data URI):
doc.addImage(`data:image/svg+xml;base64,${b64}`, "SVG", 10, 10, 40, 12);
// pdfmake (svg property takes raw SVG markup, not Base64 —
// check your library: some want the data URI, some want the source)Minify the folder, then batch-encode
Run a size pass before encoding so every string is minimal. Conceptually: scrub + minify each file, then encode. Each removed byte is ~1.33 fewer Base64 bytes across the whole manifest.
Pipeline (per file):
scrub metadata → /svg-tools/svg-metadata-scrubber
minify → /svg-tools/svg-pro-minifier
encode base64 → Buffer.from(min, 'utf-8').toString('base64')
Result: a manifest that's as small as Base64 allows.
Estimate the win first with /svg-tools/svg-compression-estimator.Edge cases and what actually happens
Expecting a hosted /api/svg/to-code batch endpoint
Does not existThere is no hosted batch upload endpoint for SVG encoding. The JAD API is upload-free by design — POST /api/v1/tools/svg-to-base64/run with content returns HTTP 400 and pairing instructions. Batch either with a local Node/Deno script or by POSTing to your paired runner at 127.0.0.1:9789, one file per call.
Using include_prefix (snake_case) as the option name
RejectedThe only option is includePrefix (camelCase, boolean). A snake_case include_prefix is not recognised by the schema validator and is ignored — you'd silently get the default (prefix on). Match the exact casing the schema endpoint reports.
Naive btoa(svg) throws on non-ASCII
TypeErrorCalling btoa(svgString) directly throws InvalidCharacterError when the SVG contains characters outside Latin-1 (accents, CJK in <text>). Always do btoa(unescape(encodeURIComponent(svg))) in the browser, or Buffer.from(svg, 'utf-8') in Node. The JAD tool already does this; replicate it in your scripts.
Reading the file as a Buffer without utf-8
Subtle bugBuffer.from(fs.readFileSync('icon.svg')) (no encoding on readFileSync) still works because the raw bytes are already UTF-8 on disk. But if you read with a wrong encoding (e.g. 'latin1') and then re-encode, multi-byte glyphs corrupt. Read as 'utf-8' (or as a raw Buffer) consistently.
Per-file size limit on the runner path
Tier-limitedThe runner respects the same SVG tier limits as the web tool (5 MB free, 50 MB Pro, 2 GB Developer per job). A pure Node script you run yourself has no such cap — it's bounded only by memory. For a folder of icons you'll never hit either limit; the cap matters only for unusually large single files.
Manifest JSON is ~33% bigger than the source folder
ExpectedA Base64 manifest is inherently ~33% larger than the raw SVGs (more with prefixes). That's the cost of inlining. If the manifest ships to the client, weigh it against serving the icons as cacheable files. For build-time use it's usually fine; for a giant icon set, reconsider inlining everything.
Mixing prefixed and bare strings in one manifest
Consistency bugDecide once whether the generated manifest stores full data: URIs or bare Base64, and apply it to every entry. A mix breaks consumers that blindly prepend the prefix (double data:) or blindly use the value (missing scheme). The web tool's checkbox makes this choice per run — in a script, make it once for the whole batch.
Encoding non-SVG files in the folder
Garbage inThe encoder is byte-faithful and won't validate that a .svg file is actually SVG. A PNG accidentally renamed .svg, or a half-written file, encodes to valid-looking Base64 that renders nothing. Filter strictly on extension and, if untrusted, validate with a DOM parse before encoding.
Frequently asked questions
What's the fastest way to Base64-encode SVGs in Node?
Buffer.from(svgString, 'utf-8').toString('base64') — native, zero dependencies. For files: Buffer.from(fs.readFileSync('icon.svg')).toString('base64'). This produces the exact same standard RFC 4648 string the JAD web tool emits, so a script and a manual encode are interchangeable.
Is there a hosted batch API I can POST a folder to?
No. JAD's API is upload-free — it never receives file content. POST /api/v1/tools/svg-to-base64/run with a payload returns 400 with pairing instructions. To automate, either run a local Node/Deno script or pair the @jadapps/runner and POST one file at a time to 127.0.0.1:9789/v1/tools/svg-to-base64/run.
What's the exact option name for the prefix?
includePrefix (camelCase, boolean, default true). There is no include_prefix snake_case variant — that's ignored and you'd get the default. Confirm the schema any time via GET /api/v1/tools/svg-to-base64, which returns the single-option schema the UI and runner share.
Can I pre-encode SVGs and bundle them with my app?
Yes — generate a JSON or TS manifest at build time and import it. This moves the encoding cost out of runtime entirely. Remember the manifest is ~33% larger than the raw SVGs; for large icon sets, weigh that against serving cacheable files. Regenerate the manifest in a prebuild script whenever the icons change.
How do I feed batch-encoded SVGs to a PDF library?
Most PDF libraries take image data as Base64. For jsPDF: doc.addImage(data:image/svg+xml;base64,${b64}, 'SVG', x, y, w, h). Some libraries (certain pdfmake configs) want raw SVG markup instead of Base64 — check your library's SVG input expectation before encoding.
Does the output match the web tool exactly?
Yes. The web tool runs btoa(unescape(encodeURIComponent(svg))) and the server engine uses the equivalent unescape(encodeURIComponent()) + btoa/Buffer fallback — both standard RFC 4648 with no line breaks. Buffer.from(svg, 'utf-8').toString('base64') in Node yields the identical string.
Should I include the data URI prefix in my manifest?
Include it if consumers use the value directly (img/CSS/React Native). Store bare Base64 if the consumer rebuilds the URI. Whatever you choose, be consistent across the whole manifest — a mix causes double-prefix or missing-scheme bugs. There's no runtime toggle in a static manifest; bake the decision in at generation.
Will the runner upload my files to JAD?
No. The runner executes locally on your machine — the hosted API never receives file content. You POST to 127.0.0.1:9789, the runner runs the in-process engine (svg-to-base64 is server-safe, so no headless browser), and returns the string. Files never traverse JAD's network.
How do I encode correctly for non-ASCII SVG content?
In Node, read as UTF-8 and Buffer.from(svg, 'utf-8').toString('base64') handles it. In the browser/Deno, use btoa(unescape(encodeURIComponent(svg))) — never bare btoa(svg), which throws on characters outside Latin-1. The JAD tool already wraps with encodeURIComponent; mirror that.
Can I minify as part of the batch?
Yes — add a minify pass before encoding. Conceptually: scrub metadata, minify, then encode each file. Use svg-metadata-scrubber and svg-pro-minifier; estimate the saving with svg-compression-estimator. Each byte removed is ~1.33 fewer Base64 bytes across the manifest.
Is there a row/file count limit for batching?
A pure local script has no count limit — it's bounded only by memory and disk. The runner path applies the per-job size cap (5 MB free / 50 MB Pro / 2 GB Developer) per file, not a count cap, and the hosted API rate-limits requests. For typical icon sets you won't hit any of these.
Does the manifest need to be re-encoded if I edit one icon?
Regenerate the whole manifest — it's cheap and guarantees consistency. Wire the generator into a watch/prebuild step so editing any .svg triggers a rebuild. Because the encode is deterministic, unchanged icons produce identical strings, so diffs stay small and review-friendly.
Privacy first
Every JAD SVG tool runs entirely in your browser using the DOM API and Canvas. Your SVG files never leave your device — verified by zero outbound network requests during processing.