Base64 图片不显示:怎么找到真正的原因
2026-09-30 · 1308 词
Base64 图片不显示,是 Web 开发里信息量最少的故障之一。你得到一个裂图图标、一个空框,或者干脆什么都没有 —— 而浏览器控制台一声不吭。没有异常可捕获,因为浏览器只是照你说的做了:它尝试渲染一个资源,然后安静地放弃。
好消息是,现实中的原因只有六种,按顺序检查都很便宜,而且只要你仔细看,每一种的症状都不一样。
检查 1:这个字符串真的是 Base64 吗
在别的之前,先确认载荷用对了字母表。两项基于计数的检查就能覆盖大部分情况:
const payload = s.includes(',') ? s.slice(s.indexOf(',') + 1) : s;
console.log('长度 % 4 =', payload.length % 4); // 1 说明被截断
console.log('非法字符 =', (payload.match(/[^A-Za-z0-9+/=_-]/g) || []).join(''));
余数为 1 是致命的:任何完整的 Base64 字符串都不可能是这个余数,说明字符在传输中丢了。其他的通常都能修。非法字符会告诉你到底哪里错了 —— 换行意味着 MIME 折行(见 换行与填充),%3D 意味着这个字符串走过 URL,- 或 _ 意味着它是 URL 安全 Base64(见 URL 安全与标准 Base64)。
如果字符串除了 0-9a-f 什么都没有,那它是十六进制。按 Base64 解码它会不报错地返回字节,而那些字节不会是图片。
检查 2:前缀是完整的吗
语法是 data:[<mediatype>][;base64],<data>,三部分都在起作用。三种前缀 bug 对应三种不同症状:
- 没有
;base64标志。data:image/png,iVBORw0KGgo...会让浏览器把载荷当字面文本读。你得到一个空的图片框且没有报错,因为你要的就是一个文本文件。 - 没有逗号。
data:image/png;base64后面直接跟载荷,根本就不是 data URI。如果你的代码"剥掉第一个逗号之前的一切",这一步会静默地什么都不做,然后解码器在头里的:上崩掉。 ;base64写错,比如base64;或;base-64。和第一种一样:载荷被当文本处理。
在严格的解码器里,这些都会在某个具体字符位置大声失败;在浏览器里,它们安静地失败。排查时值得记住这个不对称:浏览器是问"为什么"时最没用的地方。
检查 3:它到底解出字节了吗
零字节的结果是真实存在的,而且有具体成因:载荷里只有填充字符,或者归一化步骤把所有它认为的垃圾都删掉了。渲染之前先把解码长度打出来:
const bytes = Uint8Array.from(atob(clean), (c) => c.charCodeAt(0));
console.log(bytes.length); // 0 → 没东西可渲染,字符串看起来再漂亮也没用
如果长度是几十万字节,那你手里有数据。它是不是图片,看检查 4。
检查 4:这些字节是图片吗 —— 而且是那一张吗
这里要拿声明的媒体类型和现实对账。读开头几个字节来比较:
89 50 PNG
FF D8 FF JPEG
47 49 46 38 GIF
52 49 46 46 WebP(偏移 8 处跟着 "WEBP")
1F 8B gzip —— 不是图片
25 50 44 46 PDF
50 4B 03 04 ZIP
这里有两种不同的故障。第一种是字节根本不是图片 —— 为了走纯文本通道而被 Base64 编码的 gzip JSON 是经典案例,值得明确点名,因为"它能正常解码"会让人停止追查。第二种是字节是图片,但和头里声称的不是同一种:data:image/jpeg;base64,iVBORw0KGgo... 在多数浏览器里能渲染,然后在每一个相信声明类型的工具里失败。两种情况下都是字节说了算,这就是 Base64 转图片 会把声明类型与签名并排打印、而不是替你挑一个的原因。
检查 5:图片是完整的吗
这是最容易被漏掉的原因,它的症状也最迷惑人:图片确实显示了,然后从中间断掉。照片底部出现一条灰带,或者图片在浏览器里正常、却被图片编辑器拒绝打开。
原因是 JPEG 的头里没有总长度字段。一张在任何位置被截断的 JPEG,只要已解析的标记都合法,看起来仍然是一张完全合法的 JPEG,所以解码器把手上的部分渲染出来就停下了。截断非常常见,因为 Base64 会经过那些会截断的地方:有长度上限的日志行、选中文本时丢掉折行的终端、按平均情况设定长度的数据库列、有附件大小限制的聊天消息。
测试方法是算术,不是肉眼:
- 解码字符串,记录字节数。
- 和来源方给出的原始文件大小对比。
- 如果解码字节数更少,你拿到的是一份被截断的副本 —— 回源头重新取,而不是尝试修复它。
对 PNG 同样的检查也成立,而且还有一个额外的结构信号:完整的 PNG 以 IEND 块结尾。读最后十二个字节,找 49 45 4E 44(IEND),就是一个便宜的完整性测试:
const tail = bytes.subarray(-12);
console.log(new TextDecoder().decode(tail).includes('IEND'));
检查 6:浏览器拒绝它,是不是因为与 Base64 无关的原因
最后两个原因与字符串毫无关系,这正是它们被漏掉的原因。
内容安全策略(CSP)。 如果你的页面发送 img-src 'self' 而没有 data:,那么每一个 data URI 图片都会被拦截 —— 安静地拦截,而且 Base64 本身没有任何问题。多数情况下浏览器会把违规报到控制台,但前提是你知道要去那儿看;而一些默认注入自己 CSP 的工具(静态托管、反向代理)会在你毫无察觉的情况下加上这条限制。我们自己的页面正是因此才发送 img-src 'self' blob: data:。
体积。 data URI 是字符串数据,活在包含它的那份文档里。非常大的那些 —— 内联 <style> 或生成的 HTML 里几 MB 的 Base64 —— 会撞上因浏览器而异的解析器与内存上限,而失败的表现就是"什么都没发生"。如果一个 40 KB 的图标能显示、4 MB 的照片不能,这就是答案,修法是换成真正的 URL。
顺序很重要
按这个顺序查,并在第一个失败处停下,因为前面的检查坏掉时,后面的会给出误导性的结果:被截断的字符串看起来"解码正常",而错误的字母表会产出看起来像损坏图片的字节。六项检查,大约一分钟,每一项都告诉你一件不同的、需要修的事。
拿到答案之后,值得掌握的两个修法都是结构性的而不是文本性的:如果余数说明字符串短了,就补填充(安全、可逆);如果余数是 1,或者解码长度与生产者报告的不一致,就回源头重取(不安全、不可逆、不要猜)。其它的做法 —— 重新编码、"试着"换一种字母表、不断追加字节直到它能渲染 —— 都是在对你根本看不见的数据瞎猜。