Base64 Decode Shows Garbled Text or Black Question Marks for Chinese — How to Fix
Symptom: garbage or question marks after decoding
You feed a Base64 string into a decoder expecting Chinese, but get:
䏿–‡
or a row of black question-mark boxes �, sometimes �. This means the decoding succeeded, but the bytes were interpreted with the wrong character set.
Root cause: Base64 encodes bytes, not characters
This is the trap almost everyone hits. Base64 only translates a byte sequence into printable characters — it has no idea what text those bytes represented.
- Chinese is usually 3 bytes in UTF-8, but 2 bytes in GBK;
- If the encoder turned "你好" into bytes using GBK, but the decoder reads those bytes as UTF-8, you get mojibake;
- And vice versa.
So the garble is not a Base64 problem — it is an encoding mismatch between the encode and decode ends.
How to fix it
Method 1: online tool (auto UTF-8 by default)
Open the Base64 Encode/Decode tool on ToolVault:
- Paste the content into the input;
- The tool restores bytes to characters as UTF-8 by default, so Chinese displays correctly;
- If the original data was GBK, switch the charset option before decoding;
- Everything runs locally in your browser — no upload, safe for sensitive strings.
Method 2: command line
# Standard decode (shows Chinese when terminal locale is UTF-8)
echo '5ZG95ZG9' | base64 -d
# output: 你好
# Force UTF-8 if the locale is off
echo '5ZG95ZG9' | base64 -d | iconv -f UTF-8 -t UTF-8
Method 3: specify the charset explicitly in code
import base64
raw = base64.b64decode("5ZG95ZG9")
text = raw.decode("utf-8") # be explicit, don't rely on defaults
print(text) # 你好
// Browser / Node: TextDecoder with explicit encoding
const bytes = Uint8Array.from(atob('5ZG95ZG9'), c => c.charCodeAt(0));
const text = new TextDecoder('utf-8').decode(bytes);
console.log(text); // 你好
Why you should not "guess" the encoding
When mojibake appears, some people try GBK, GB2312, UTF-8 one by one. It偶尔 works on tiny samples but is unreliable in production:
- Mixed Chinese/English data may look "mostly fine" with a few boxes — very hard to debug;
- The correct approach is to fix and declare the charset at the encode end (UTF-8 is the default best practice) and keep the decode end consistent.
Tracing the mojibake to its hop
Garbled Chinese after Base64 round-trip is always a UTF-8 assumption breaking somewhere. Locate the hop by what the garbage looks like: 䏿–‡ (ä-style) means UTF-8 bytes were interpreted as Latin-1 somewhere — the classic Java new String(bytes) default-charset trap or an HTTP header declaring ISO-8859-1. 涓枃 (E6B8-style) is the reverse: GBK bytes read as UTF-8. ??? means lossy conversion already happened — the original bytes are gone and no re-decode can recover them. Each pattern indicts a specific layer (transport, storage, or display), which is why the fix starts with identifying the pattern, not with changing encodings at random.
The three checkpoints that catch 90% of cases
(1) Encode side: TextEncoder in JS, -E UTF-8 in iconv, charset declared in the HTTP header — one declaration, checked once. (2) Transport: URLs force percent-encoding of non-ASCII, so Base64-with-Chinese in a query string needs encodeURIComponent — forgetting it silently decodes to garbage on the server. (3) Decode side: the mirror of encode — atob gives Latin-1 bytes in JS, so the correct decode is TextDecoder on the Uint8Array, not string concatenation. The Base64 tool does the TextDecoder path correctly and shows the round-trip — a 10-second differential against your own pipeline isolates the broken hop.
When the data is already garbled
If ??? or U+FFFD replacement characters have replaced the original bytes, the information is destroyed — set expectations accordingly. If the damage is the ä-style double-encoding, recovery is mechanical: encode the string as Latin-1 bytes, decode those bytes as UTF-8. That one-liner has rescued countless mis-migrated databases.
Prognosis differs by corruption source
Not all mojibake is equal — where the bytes got mangled determines whether recovery is possible:
| Source | Signature | Recoverable? |
|---|---|---|
| Transport (charset header mismatch) | consistent pattern, whole document | Yes — re-decode with the right charset |
| Storage (DB column truncation) | cut at a byte boundary, tail missing | No — data past the cut is gone |
| Generation (double-encoding) | w6E-style sequences appearing twice | Yes — decode repeatedly until stable |
The diagnostic shortcut: garbled throughout means transport (fixable), garbled ending means truncation (lost), garbled stacked means double-encoding (fixable). Triage before promising anyone anything.
Do not confuse charset mojibake with Base64URL / PEM dialect errors
Three failure classes look similar in a chat screenshot and need different first moves:
| What you see | Likely class | First move |
|---|---|---|
䏿–‡, æ¸, or � after a successful decode | Charset (UTF-8 / GBK / Latin-1) | This article |
Decoder throws / refuses alphabet with - _ or missing = | Base64URL vs standard | Base64 vs Base64URL, toggle URL-safe |
String has two dots (a.b.c) | JWT segments | JWT decoder — do not MIME-decode the whole token |
BEGIN CERTIFICATE / broken after “removing newlines” | PEM packaging | Certificate decoder |
If the Base64 layer already produced the correct UTF-8 bytes and the UI still shows garbage, the bug is downstream (terminal locale, HTTP charset, DB column collation) — not another Base64 pass.
FAQ
What are the black boxes (�)?
That is the Unicode replacement character U+FFFD, meaning the decoder hit a byte it could not represent in the current charset. It is a charset mismatch, not data loss.
I typed Chinese when encoding, why is decoding garbled?
When you "type Chinese", the program first turns it into bytes using some charset, then Base64. If that charset differs from the decoder's, it breaks. Standardizing on UTF-8 eliminates this class of bugs.
Are online tools safe?
If you use a local, no-upload tool (like the ToolVault one above), data is processed only in your own browser and never leaves your device — safe for sensitive strings.
Is garbled Chinese the same as a JWT that “won’t decode”?
Usually no. JWT failures are alphabet/padding/segment issues (Base64URL). Chinese mojibake means the bytes after Base64 were read with the wrong charset. Fix the dialect first, then the charset — in that order.
Related reading
- Base64 vs Base64URL: JWT segments and PEM
- Base64 encoding explained
- Encoding troubleshooting handbook
- JWT explained
Implementation notes
For Base64url debugging such as JWTs, the site's decoder accepts a deep link — /en/tools/jwt-decoder?token=... — so you can jump in from api-tester or other tools with the token in hand instead of copying back and forth. The parameter is read exactly once on mount; after that the input belongs entirely to the user, so a deep link and a manual paste never overwrite each other.
Provided by ToolVault. Related tools: URL Encode/Decode, JWT Decoder, Regex Tester. Visit the home page for more developer tools.
Related Tools
Related Articles
PDF to JPG: How to Pick the Right Resolution (DPI Guide + 3 Ways)
A complete guide to converting PDF pages to JPG: how DPI maps to sharpness, two-page and batch conversion, three approaches compared (in-browser local, command line, desktop software), plus a quality settings cheat sheet.
PDF Too Large for Email? How to Choose the Right Compression Quality (3 Local Levels)
How PDF compression works: lossy rasterization vs. lossless structure optimization, which of the three quality levels fits which use, and how to compare sizes before and after — entirely in your browser.
How to Split a PDF and Extract Pages: Range Syntax, Common Cases, and a Local Workflow
A complete guide to splitting PDFs by page range: the 1-3,5 range syntax, page-by-page extraction, and split-then-merge workflows — all in your browser, nothing uploaded.