Base64 Encoder
Encode and decode text or a dropped file, with data-URI output for images.
Drop a file here
You get the Base64 and a data: URL you can paste into an img tag, a stylesheet or a small JSON fixture.
Choose a file Any file up to 12 MB- File
- —
- Size
- —
- Base64 size
- —
- Overhead
- —
A file this size makes a long string. Base64 adds a third to it, and a textarea holding several million characters is slow to scroll and worse to select by hand. Copy the result straight to where it is going.
Three bytes in, four characters out
The encoder reads the input 24 bits at a time and cuts those 24 bits into four 6-bit numbers. Six bits names a value from 0 to 63, and 64 values is exactly what the alphabet has: 26 capitals, 26 lowercase letters, 10 digits, and two punctuation marks left over. Four characters per three bytes, which is where the growth comes from — every group of three costs four.
The tail is where the arithmetic shows. Take the three-character string cat:
c a t
0x63 0x61 0x74
01100011 01100001 01110100
011000 110110 000101 110100
24 54 5 52
Y m F 0 -> "YmF0"
Now take ca, two bytes instead of three. Sixteen bits do not divide into four groups of six; they divide into two groups of six with four bits left over. The encoder pads that leftover group with zeroes to make a third character, then writes one = to mark the place a character would have gone. Three bytes take no padding at all, two bytes take one sign, one byte takes two — which is why the number of = signs at the end of a value is a statement about how many bytes the original ended on. It is not decoration, and it is not part of the data.
Every value is a whole number of four-character groups, or it is broken. Because four characters carry three bytes, a valid encoding is always a multiple of four in length — padding included. A string that arrives one or two characters short of a multiple of four has been trimmed somewhere, and typing the missing = signs back in does not recover the bytes that were cut with it. The length rule is the fastest way to tell a damaged value from a valid one before you start reading the characters.
Why btoa throws on an accent
The browser function walks the string one code unit at a time and treats each one as a byte. That is fine for ASCII and a hard failure above it: btoa('café') throws an InvalidCharacterError, and so does every curly quote, every CJK character and every emoji. It is not picking on accented letters — it is refusing a value that is not a byte, because writing code point 233 into a byte array would produce a value no decoder could read back into the same string.
The fix is to decide on an encoding first and encode the bytes rather than the characters. TextEncoder turns a string into UTF-8 bytes, and é becomes two of them, C3 A9. Those two bytes go through Base64 as ordinary input and come out as w6k=. A decoder handed that value gets the two bytes back and reads them as UTF-8, which gives the é again. Encoding the code point directly would have written 6Q==, which decodes to byte E9 — Latin-1, not UTF-8 — and shows up downstream as a mojibake character in whatever reads it next.
The two are not interchangeable, and nothing in the value itself says which one produced it. A short accented string is the classic case: both encodings are valid Base64, both decode without error, and only one of them gives back what you put in.
Where Base64 does not belong
It is a transport, and it is worth being strict about that. It is not encryption: the alphabet is public, there is no key anywhere in the process, and anyone holding the string can turn it back into the original bytes. A password or a token put through it is exactly as readable as it was before, and a private key pasted into an encoder has been copied somewhere else for no gain.
It is also not compression. The output is larger than the input every time, by about a third, and the reason to accept that is always the same: the channel at the far end carries text and mangles anything else. Mail bodies, JSON strings, XML attributes, header values and data URLs will all destroy raw bytes and all carry Base64 unchanged. That is the entire job.
The place it gets used and should not is storage. A photograph in a database column as Base64 costs a third more space, cannot be indexed as an image, cannot be resized by the thing serving it, and has to be decoded in full before a single pixel can be drawn. For a favicon or a one-colour logo the trade is fine and saves a request; past a few kilobytes a separate file with a long cache header wins on every count. If the bytes are already a file, the Image Compressor will shrink them, which is a real reduction rather than a re-spelling.
When the alphabet in front of you is in doubt, decode rather than guess. A few readable bytes confirm the standard alphabet, and a value that opens with eyJ and carries a hyphen or an underscore further along is base64url. If the output is nonsense under both alphabets the string was damaged in transit rather than encoded strangely, and the URL Encoder is where the damage usually happens.
Reference
The two alphabets, and which one a system is expecting
| Position or case | Standard alphabet | URL-safe alphabet | Where each one is required |
|---|---|---|---|
| 62 | + | - | The standard + is a space the moment it sits in a query string, which is why a token pasted straight into a URL comes back corrupted. The - form is what a JWT segment and a PKCE code_challenge are defined to carry. |
| 63 | / | _ | The standard / ends a path segment, so a value holding one needs percent-encoding before it goes into a URL. _ is a legal filename character and survives a path unchanged. |
| end | One = after a two-byte tail, two after a one-byte tail | Usually left off, and the length gives it back | A strict decoder — Java's Base64.getDecoder(), Go's base64.StdEncoding — refuses a length that is not a multiple of four. RFC 7515 tells a JWT producer to leave the signs off. |
| 76 | MIME puts a line break here (RFC 2045) | Usually one unbroken line, because the value is going into a URL | Mail bodies and YAML blocks arrive wrapped. A JSON string, a header value and a URL do not, and a decoder that rejects whitespace turns the wrapped form down. |
| empty | The empty string | The empty string | Nothing in, nothing out, with no group to pad: a lone = is not the encoding of anything, and a decoder rejects it. |
| case | A–Z at 0–25, a–z at 26–51 | The same characters in the same order | A proxy or a router that lowercases a value turns one valid string into a different valid string, and the decoder has nothing to complain about. |
The two alphabets differ in two characters and in how they treat the padding sign, and a value that has crossed one of those boundaries is what most decoder complaints turn out to be about. The remaining rows are a question of tolerance: whether the decoder at the far end accepts a line that arrived wrapped, a sign that was stripped, or a value that was folded to one case on the way through.
Base64 is not encryption and it is not compression. There is no key and nothing to guess: anyone holding the text can turn it back into the bytes it came from, and the output is about a third larger than the input. It exists because mail bodies, JSON strings, headers and URLs carry text and mangle anything else, so a value crossing one of those places is safer spelled in letters, digits and two punctuation marks. That is the whole job: a transport, not a hiding place.
Most errors that say invalid base64 come from a row above. A plus sign that travelled through a query string has already been read as a space, a slash from a path was cut at the segment boundary, and a value that passed through a system with no use for padding arrives a character or two short. The failure rarely appears where the damage happened, which is why the position a decoder reports can look fine.
When the alphabet is in doubt, decode the value and read the result. A few readable bytes confirm the standard alphabet, and a JWT that opens with eyJ and carries a hyphen or an underscore further along is base64url. If the output is nonsense under both, the string was truncated in transit, and padding typed back in will not recover what went missing.
Questions
Base64, answered plainly
Does Base64 hide or protect anything?
No. It is a reversible re-spelling of the same bytes with no key anywhere in it, so a password, an API token or a private key put through it is exactly as readable as it was before. The alphabet is public, the padding rule is public, and the only reason it exists is that some channels — mail bodies, JSON strings, XML attributes, data URLs — cannot carry raw bytes without mangling them.
Why did btoa() throw on my text?
Because the string held a character above U+00FF, or a Latin-1 character that was already the wrong byte. btoa() walks the string one code unit at a time and rejects anything over 255, which means every accented letter, every CJK character and every curly quote is a hard failure rather than a wrong answer. Encoding UTF-8 bytes instead gives a result any decoder can read back, and that is what the box above does.
How much bigger is the result?
About a third. Four characters carry three bytes, so the output is 4/3 of the input, rounded up to a whole group and then rounded up again to a multiple of four characters. A 1 MB file comes back at roughly 1.37 MB, and a 10 MB one at 13.7 MB. If a payload grows by more than a third, a newline or a carriage return has been added every 76 characters by whatever produced it — MIME mail does this — and the decoder here ignores whitespace entirely.
When do I need the URL-safe variant?
Whenever the string travels inside a URL, a filename or a JWT. + and / survive a query string only if somebody remembers to percent-encode them, and a slash inside a path segment is a different path. The URL-safe alphabet replaces them with - and _ and drops the = padding, which decoders restore from the length. Swapping between the two is a straight substitution of four characters and the padding, not a re-encode.
Why do I get question marks when I decode?
The bytes decoded, but they are not UTF-8 text. The replacement character marks each byte sequence the text decoder could not read, which is what an image, a zip archive or an encrypted blob looks like after a round trip through a text box. Paste a whole data URL in, header and all: the data: prefix and the media type in front of the comma are stripped before decoding, so data:text/plain;base64,… gives you the sentence back and data:image/png;base64,… gives you a page of replacement characters.
Does a file I drop on this page go anywhere?
It is read by the page you are looking at, through the browser's own file reader, and the encoding runs in the same tab. There is no upload step to audit and no request to inspect, because the site has nowhere to send anything: no analytics, no API, no back end. Drag in a photograph and watch the network panel stay empty if you would rather check than take my word for it.
Why does it say invalid base64 when the string looks fine?
The damage is usually a character you cannot see. A + that came out of a query string has already been read as a space, a / may have been cut at a segment boundary by whoever built the link, and a value that passed through a system with no use for the padding arrives a character or two short of a length the decoder will accept. The decoder on this page names the character it stopped at and its position, and that position is where the string stopped being valid, which is not always where it stopped being correct. Read it before you go hunting for a missing character by eye.
Can I use Base64 for an image on a page?
Yes for something small — an icon, a favicon, a one-colour logo. It stops being a good idea as the image grows, because a data URI is part of the document that carries it: the browser cannot cache it on its own, it is downloaded again with every copy of that document, and the server cannot resize it or hand a different format to a different client. The document also gains about a third of the image's size, and the browser has to parse the whole string before it can draw a pixel. Past a few kilobytes, a separate file with a long cache header wins on every count.
Does a data URI keep the file type?
Yes. The media type is written in front of the data — data:image/png;base64, — so the browser knows what the bytes are without inspecting them. Write the wrong type there and the browser either refuses to draw it or saves the file instead of showing it, which is the usual reason a data URI downloads rather than displays. The bytes are untouched by any of this, so a mismatch is something the person who wrote the header got wrong, not something the encoding did.