图片转 Base64 — data URL、img 标签还是裸字符串,外加真实的体积代价

从这台设备上挑一张图,拿回你要的那种形态:data URL、裸 base64、img 标签、CSS 背景规则或 JSON 值。类型取自文件开头的字节签名,不是扩展名,所以一个其实是 PNG 的 .jpg 会被说破。每份输出都被量过:字节数、字符数、体积变化,还有这串字符究竟贴得进哪里的对照表。粘回来还能拆开、检查补齐、看图、存成文件。你的文件不会离开这台设备。

把 base64 还原成图片

六十四个字符与体积代价

为什么偏偏是六十四个字符

不少通道坚持只带文本,对另外 192 个字节值却很不放心。MIME 邮件正文、配置文件、URL、日志行里的 JSON、Cookie 的值——它们各自都在说“给我可打印的字符”。base64 的回答是每个字符花掉 6 位:A-Z、a-z、0-9,再加上 + 和 /,一共六十四个符号,而且正好六十四个,因为六十四个就是 2^6。

三个字节是 24 位,四个字符也是 24 位,所以打包是均分的,什么都不丢。文件末尾剩下一两个字节时,最后那一组就不满,等号记下差了多少。这就是全部设计,也解释了输出旁边打印的两件事:字符串的长度永远是 4 的倍数,而且它永远比文件大三分之一。

那三分之一是价钱,而且不总是值得付

一个 12,000 字节的图标变成 16,000 个字符。输出框下面那张表把这个数字拿去和真正会卡住它的上限逐个对照,并且刻意区分了“写进规范的”和“只是软件默认带的”:Cookie 的 4,096 字节是浏览器的行为,可以指望;MySQL 的 TEXT 写死 65,535;nginx 的 1 MB 请求体是个谁都能改的默认值;而 localStorage 在任何规范里都没有大小——几家引擎只是恰好都收敛在 5 MB 上下。任何一句“装得下”,都只对你一分钟前挑的那个文件成立。

还有一种代价是字节计数器照不出来的。data URL 写进 HTML 就是 HTML 的一部分:它没法单独被缓存,所以每次浏览都要重发一遍;它没法和文档其余部分并行加载;它让文档变得更难解析;而如果你的站点有 CSP,那它多半写着 img-src 却没有 data:,于是图片干脆不出现。对十张 1 KB 图标的雪碧图,这是笔好买卖——一次请求换十次。对一张 2 MB 的主视觉照片,这是笔坏买卖,而上面的数字会在你把它提交之前,先让你看清这个错误长什么形状。

两套字母表,各自合法在哪里

+ 和 / 不是 URL 安全的:+ 在查询串里表示空格,/ 表示路径分隔符。URL 安全的变体把它们换成 - 和 _,JWT、文件名和数据库键用的是这一种。它不是浏览器在 data: URL 里认的那一种,所以三种要进文档的形态拿到的是标准字母表,只有在你要求一个自己去搬运的字符串时,才给换过的那一种。

为什么开头的字节胜过文件名

一个其实是 PNG 的 .jpg,名字是对的,头是错的。读的是头部——PNG 的 ‰PNG、JPEG 的 FFD8、RIFF….WEBP,如此等等——并且当两者不一致时,把声称的和证据一起说出来,因为写进前缀里的那个类型,才是你的浏览器会相信的。手工改过名字的截图、出自转换器却留着旧扩展名的图、以及从网页上带着错误 Content-Type 到来的文件,全都是这个样子。

解码器拒绝去猜的东西

把字符串粘回来,拆开之前会先检查一遍:长度对四取余、补齐符的个数、它们有没有落在中间、既不属于任何一种字母表的字符及其位置、还有最后一组里那几个 base64 装不下的位——规范的字符串一定让它们保持为零。如果那几个位不是零,会讲出来,因为你粘进来的这串在上游被折腾过,重新编码不会再拿回同样的字符。

它不会悄悄收下没有标成 base64 的 data: URL,因为那一种装的是百分号编码的文本,把它还原是另一件事。它也从不修补被截断的字符串:一张半途而废的图片解码出来,是你的浏览器拒绝描绘的文件,而诚实的答案是——这份拷贝丢了尾巴。

问与答

问:我的图片会被上传吗? 不会。字节是由这个标签页自己的文件读取器读进来的,再由脚本里的循环走完:图片不会被发出去,没有服务器看到它,你复制的那串字符,是由从未离开这台设备的字节构造的。

问:为什么输出比文件长? 因为每三个字节都要以四个字符离开。base64 不是压缩,它是一次翻译,翻成一套胆小的通道愿意携带的字符。

问:我能编码比 8 MB 上限更大的图片吗? 拦下来的是上限,不是格式——一个 40 MB 的字符串放进文本框里,也不是你贴得进任何有用地方的东西。去指一个真正的编码器,或者按路径引用那个文件。

微信
微信
支付宝
支付宝