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>

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

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.