Three ways a Base64 image string arrives damaged
Every broken string comes down to one of three things, and they do not share a fix. Telling them apart is the whole job.
Cut short
Bytes at the end never made it. A chat app truncated the message, a terminal buffer ran out, a copy stopped early, a line got dropped when the text was re-wrapped.
Lost a character
One character went missing somewhere in the middle — a joined line, a stray edit, a find-and-replace that ate a +. Everything after the gap shifts, so the damage is not confined to where the character went.
Padding out of place
Base64 pads the final group with = so the length divides by four. When that = is missing, or sits somewhere it cannot, the decoder refuses the string outright. This is often a symptom rather than the fault itself: it is what losing a character looks like on a string that ended in padding.
A preview is not evidence
Every decoder you have used shows you a picture when it can and an error when it cannot. That sounds like a test. It is not. I took one 640 × 360 PNG — 5,879 bytes, 7,840 Base64 characters — and damaged copies of it in a script, then asked Chromium to load each one and compared every rendered row against the intact picture, pixel by pixel.
| What I did to the string | What the browser did | What the bytes say | Rows still identical |
|---|---|---|---|
Removed the final character (the =) | loads, reports 640 × 360 | 5,879 bytes — nothing lost | 360 of 360 |
| Removed 10 characters | loads, reports 640 × 360 | 5,872 bytes, IEND chunk gone | 360 of 360 |
| Removed 40 characters | loads, reports 640 × 360 | 5,850 bytes | 358 of 360 |
| Cut 2% off the end | loads | 5,762 bytes | 340 of 360 |
| Cut 10% off the end | loads | 5,292 bytes | 287 of 360 |
| Cut 25% off the end | loads | 4,410 bytes | 280 of 360 |
| Cut 50% off the end | loads | 2,940 bytes | 268 of 360 |
| Cut 75% off the end | loads | 1,470 bytes | 144 of 360 |
| Changed one character, at position 3,920 | loads | 5,879 bytes, IDAT checksum fails | 268 of 360 |
| Deleted one character, at 10% in | decoder refuses the string | — | — |
Two rows in that table are the reason this page exists. Removing the last ten characters deletes the PNG end marker, and the picture still comes back identical in all 360 rows. Cutting half the file away still loads and still gets 268 rows right.
So "it rendered" tells you almost nothing. A decoder that draws a picture has only told you the header was readable.
The shape of the decay is worth noting too: content disappears far more slowly than bytes do. Losing half the bytes cost me 26% of the rows, not 50%.
JPEG behaves the same way in outline and worse in one specific spot. Same picture, re-encoded at quality 0.92: 15,084 bytes, 20,112 Base64 characters, and — because the byte count divides by three — no padding at all.
| What I did to the string | What the browser did | What the bytes say | Rows still identical |
|---|---|---|---|
| Removed 1 character | loads | 15,083 bytes, FF D9 marker gone | 350 of 360 |
| Cut 10% off the end | loads | 13,575 bytes | 286 of 360 |
| Cut 25% off the end | loads | 11,313 bytes | 270 of 360 |
| Cut 50% off the end | loads | 7,542 bytes | 254 of 360 |
| Cut 75% off the end | loads | 3,771 bytes | 158 of 360 |
| Deleted one character, at 10% in | loads, all 360 rows painted | 15,083 bytes | 63 of 360 |
| Deleted one character, at 50% in | loads, all 360 rows painted | 15,083 bytes | 255 of 360 |
| Deleted one character, at 90% in | loads, all 360 rows painted | 15,083 bytes | 287 of 360 |
Those last three are the nasty case. Nothing errors. The image box fills completely. And depending on where the character went missing, up to 83% of what you are looking at is wrong.
The five things this page checks
1. Length over four
Base64 turns three bytes into four characters, so an undamaged string always divides by four. A remainder of one is fatal and unconditional: I appended each of the 64 alphabet characters in turn to a string sitting at remainder one, and all 64 came back refused. Remainders of two and three still decode — with something missing.
2. Where the = sits
Padding is only ever the last one or two characters, and removing it has to leave a length divisible by four. A 7,839-character string ending in = is refused; the same length without that = decodes cleanly. Three or more = at the end is refused too. This check is what catches a lost character on any string that ended in padding — knock one character out of the middle and the = is left standing somewhere it is not allowed to be.
3. The signature at the front and the marker at the end
A PNG opens with 89 50 4E 47 0D 0A 1A 0A and closes with its IEND chunk. A JPEG opens with FF D8 FF and closes with FF D9. Missing end marker means missing bytes.
For PNG you can be very exact about this. Across twelve PNGs I generated, from 1 × 1 up to 640 × 360, the Base64 always began with iVBORw0KGgo, and it always ended with one of exactly three strings — one per value of the byte length modulo 3:
| Bytes mod 3 | The string always ends with |
|---|---|
| 0 | AAAAAElFTkSuQmCC |
| 1 | AABJRU5ErkJggg== |
| 2 | AAAASUVORK5CYII= |
If your PNG string ends in anything else, the tail is gone. JPEG has no equivalent: the FF D9 lands on different characters depending on the byte count, so the only honest test is to decode and look at the last two bytes. My JPEG samples all opened with /9j/ followed by 4AAQSkZJ, but those eight characters are the JFIF header and every sample came from the same Chromium encoder, so do not expect them everywhere.
4. Every PNG chunk's own checksum
Each PNG chunk carries a CRC32, and browsers do not check them. Change one character in the middle of a PNG string and Chromium loads it happily and draws 268 rows correctly before sliding off — while the IDAT chunk's CRC32 fails on the spot. This page walks the chunks and verifies each one, so that class of damage gets caught instead of being shown to you as a picture.
5. Declared rows versus painted rows
The header states a height; this page draws the result and counts how many rows actually came back with pixels. When the two disagree, the image ran out partway. Cut half the bytes off a PNG and Chromium still reports 640 × 360 and still fires its load event, while only 268 rows have anything in them.
How each one is fixed
- Wrapper, stray whitespace, URL-safe alphabet. Fixed before the checks even run. Wrappers come off, invisible characters are pulled out,
-and_go back to+and/. A no-break space is the one worth knowing about: a plain space or a line break inside a string is ignored by the decoder, but a no-break space makes it reject the whole thing. That single character accounts for a lot of "this Base64 is invalid" reports. - Missing padding. Put it back. Padding carries no image data, so restoring it costs nothing — I measured zero bytes lost when the final
=was dropped and restored. - A
=in an illegal position. A character is missing. You can force a decode by dropping the padding, but every byte past the gap is then wrong and nothing will tell you where the gap is. Copy the string again. - Remainder of one. Copy the string again. Nothing else works; no decoder accepts that length.
- End marker gone. Those bytes are not in your string, so no tool anywhere can rebuild them. Fetch the string again from its source. What you are looking at above is what survived.
- A failed chunk checksum. Re-export or re-copy the original. Your browser will keep drawing the wrong version without complaint, which is exactly why this one is worth catching.
What this page cannot tell you
- A changed character inside a JPEG. JPEG has no per-chunk checksum, so there is nothing to verify against. Deleting one character 10% into a 20,112-character JPEG produced no error at all and filled all 360 rows, and only 63 of them matched the original. If you need certainty on a JPEG, compare it against the original file.
- Whether it is the image you meant to paste. This only tells you whether the bytes are a well-formed image.
- Transparent rows versus missing rows. A PNG whose bottom rows are genuinely transparent looks exactly like one that was cut off. That check is a warning, not a verdict.
How these numbers were produced
One 640 × 360 PNG, 5,879 bytes, encoded to 7,840 Base64 characters ending in a single =. The same picture re-encoded as JPEG at quality 0.92, 15,084 bytes, 20,112 characters, no padding. Every damaged copy was generated by script rather than by hand, so the damage is exactly what it says it is.
Each copy was decoded, loaded into an image element, drawn to a canvas, and compared row by row against the intact render. "Rows still identical" counts rows from the top that matched the intact picture exactly; "painted" counts rows holding any non-transparent pixel. Tests ran in Chromium on Windows. It is one image in one browser, so treat the percentages as the shape of the problem rather than as constants.
Related
Image to Base64 converter — the other direction: turn a file into Base64, a data URI, an img tag or a CSS background.