Guides · Utiltown
Base64 and Data URI Cheat Sheet: MIME and Pitfalls
Base64 is the plumbing of the web: JWTs, data URIs, email attachments,
CSS-embedded fonts, API payloads. The encoding itself is simple — every
3 bytes become 4 characters from A–Z a–z 0–9 + /. The
questions below are the ones that actually come up in a working session:
garbage after decoding, the right MIME prefix, a JWT that refuses to
decode, and whether to inline an image at all.
Data URI grammar at a glance
data:[<MIME type>][;base64],<payload>
data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...
data:text/plain;charset=utf-8,Hello%20world
data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'...</svg> - No
;base64flag → the payload is literal text (percent-encoded where needed). - With the flag → the payload is base64; the MIME type and
charsetparameter tell the browser how to interpret the decoded bytes. - MIME type omitted → defaults to
text/plain;charset=US-ASCII— the classic cause of "my inlined SVG renders as text".
The MIME prefixes you actually use
| Prefix | Typical use | Recognizable start |
|---|---|---|
data:image/png;base64, | Icons, screenshots, lossless images | iVBORw |
data:image/jpeg;base64, | Photos | /9j/ |
data:image/webp;base64, | Modern compressed images | UklGR |
data:image/svg+xml, (URL-encoded) | Vector icons | %3Csvg |
data:application/pdf;base64, | Embedded PDF viewers | JVBERi0 |
data:text/plain;charset=utf-8, | Tiny text payloads | — (set the charset explicitly) |
data:application/octet-stream;base64, | Forced downloads (pair with the download attribute) | — |
Those "recognizable starts" are the base64 of each format's magic bytes:
iVBORw encodes PNG's \x89PNG,
/9j/ encodes JPEG's \xFF\xD8\xFF,
UklGR encodes WebP's RIFF,
JVBERi0 encodes PDF's %PDF, and
eyJ encodes {" — the universal first two
characters of every JWT segment. Recognizing them makes decode errors
diagnosable on sight: a payload that should be a PNG but starts
differently means the MIME prefix and the payload disagree.
Padding and the URL-safe alphabet
- Padding: base64 output length is always a multiple of
4;
=characters pad the final quantum. Some decoders require it, some tolerate its absence. When a decode fails with "invalid length", adding or stripping padding is the first check. - base64url replaces
+→-and/→_. JWT segments must also drop the=padding (RFC 7515 §3.2); other base64url uses may keep it. JWTs mandate the URL-safe alphabet; URL parameters should use it too. The Base64 Encode / Decode tool handles text conversions with both alphabets in mind, and the JWT Decoder applies the URL-safe alphabet per segment automatically.
The Unicode pitfall (JavaScript)
btoa throws InvalidCharacterError for any
character above U+007F — which includes every CJK character and emoji.
The string must be UTF-8 encoded to bytes first:
// Encode: text → UTF-8 bytes → base64
const bytes = new TextEncoder().encode(text);
const b64 = btoa(String.fromCharCode(...bytes));
// Decode: base64 → bytes → UTF-8 text
const bin = atob(b64);
const text = new TextDecoder().decode(Uint8Array.from(bin, c => c.charCodeAt(0)));
The historical one-liner
btoa(unescape(encodeURIComponent(s))) does the same thing
but leans on a deprecated function. Symptom of getting it wrong:
é becomes é, CJK text becomes mojibake, and
the round-trip is not stable.
Inline or separate file? A decision table
| Scenario | Recommendation | Why |
|---|---|---|
| Small icons (< 2 KB) in CSS/HTML | Inline data URI | Saves a request; avoids cache overhead for tiny assets. |
| Hero images / photos | Separate file | +33% bytes, no independent caching, no lazy loading, bloats every load. |
| Fonts | Usually separate; inline only critical subsets | Fonts are large and benefit from caching and font-display. |
| Single-file HTML export (report, email) | Inline | The deliverable must be self-contained; size is secondary. |
| Vision API payloads (OpenAI, Claude, Gemini) | Inline base64 (protocol) | These APIs accept images as base64 inside JSON — OpenAI via data: URLs, Claude and Gemini via structured base64 fields. Conversion is required by the interface. |
| Frequently updated assets | Separate file | The inline form re-downloads the whole host document on every change. |
Quick recipes
// CSS: inline icon
background-image: url("data:image/svg+xml;base64,PHN2Zy4uLg==");
// HTML: inline image
<img src="data:image/png;base64,iVBORw0KGgo..." alt="logo" />
// JS: file → data URI (browser)
const uri = await new Promise((res, rej) => {
const r = new FileReader();
r.onload = () => res(r.result);
r.readAsDataURL(file);
});
// CLI: file → base64
base64 -w0 image.png # Linux (single line)
base64 -i image.png # macOS (-i = input file; output is single-line by default)
certutil -encode in.png out.txt # Windows Convert and verify locally
The Base64 Encode / Decode tool handles text round-trips with full Unicode correctness, and the Image to Base64 Converter produces ready-to-paste data URIs and vision-API JSON snippets from real image files — both entirely in the browser, so nothing you encode is uploaded. A useful final check for any inlined asset: decode it back and compare the first bytes against the magic-number table above; a mismatch means the MIME prefix does not match the actual bytes — the usual "broken inline image" bug.
Frequently Asked Questions
What is the exact syntax of a data URI?
data:[<media type>][;base64],<data>. The MIME type is optional (text/plain;charset=US-ASCII is the default), the ;base64 flag must come immediately before the comma, and everything after the comma is the payload. Example: data:image/png;base64,iVBORw0KGgo... Without the flag, the payload is parsed as plain text (percent-encoded where needed).
Why is base64 output longer than the input?
Base64 encodes every 3 bytes as 4 characters, so binary data grows by exactly one third (about 33%), plus line-break padding in some encoders. A 100 KB image becomes ~133 KB of text. That overhead is why data URIs belong to small inline assets — icons, tiny logos — and not to large images, where they bloat every page load and defeat caching.
What is URL-safe base64 and when do I need it?
Standard base64 uses + and /, which are meaningful in URLs, plus = padding. base64url replaces + with - and / with _, often dropping the padding entirely. JWTs are base64url — that is why token strings never contain +, /, or =. Feeding a JWT segment to a decoder that only accepts the standard alphabet is a common decode failure; the Utiltown JWT Decoder applies the URL-safe alphabet per segment automatically.
Why does my Unicode text decode to garbage?
Because btoa()/atob() in JavaScript work on bytes, not characters: btoa("héllo") throws InvalidCharacterError for anything above U+007F. The correct pattern UTF-8 encodes first with TextEncoder, then converts the bytes; decoding reverses the process with TextDecoder. The Utiltown Base64 tool implements full Unicode support, so CJK text and emoji round-trip correctly.
When should I inline an image as a data URI?
For small assets where an extra request costs more than the bytes: icons, tiny logos, and single-file HTML deliverables. Not for large or frequently changing images — the +33% overhead, lost caching granularity, and lost lazy-loading outweigh the request savings. Vision API payloads (OpenAI, Claude, Gemini) are a separate case: these APIs accept images as base64 inside JSON (OpenAI via data: URLs, Claude and Gemini via structured base64 fields), so conversion is a protocol requirement.