串递出去之后才开始出问题
事情是从一次多余的清理开始的。别人递来的 base64 中间每 76 字符断了行,我怕解码器扛不住,先手工删干净才敢往下走,对方说换行本来就不用管。我拿自己生成的串反着试,试到第二种改动才真出错:有人按 URL-safe 的习惯把 + 和 / 写成 - 和 _,atob 立刻报 InvalidCharacterError。这篇把两端对齐记一次。
整条串是 readAsDataURL 给的,前缀只有五档
模板第 80 行只有一次 readAsDataURL,页面自己不做编码:每个文件恰好新建 1 个 FileReader,产出的 body 与 btoa 编同一批字节逐字全等,连补位符个数都一样。前缀也不按后缀拼,恒为 data: 加 File 对象的 type 再加 ;base64,,22 个样本测下来前缀(含逗号)长度只有 22、23、25、29、37 这五档。
type 为空时浏览器写 application/octet-stream,不嗅探内容:同一段 70 B 的 PNG 字节按空 type 投进去,前缀退到 37 字符那一档;39587 B 的 JPEG 字节声明成 image/webp,前缀就写 image/webp,预览仍解出 1200x900。能否出图由解码器按内容决定,与统计那一栏无关。
图 1 前缀含逗号的五档,末档是空 type 的退路
body 长度恒等于 4*ceil(n/3)
编码交给浏览器之后,字符数就是纯算术。四档输入实测如下,body 长度与公式 4*ceil(n/3) 全等。
| 输入(B) | body(字符) | 前缀含逗号 |
|---|---|---|
| 70 | 96 | 22 |
| 9712 | 12952 | 23 |
| 39587 | 52784 | 23 |
| 4194304 | 5592408 | 37 |
「Base64大小」只数 body,Data URI 的真实串长要再加前缀:96 字符的整串是 118,12952 的是 12975。
复制与下载拿到的是同一条无换行的串
textarea 的 maxlength 实测是 null,也没有「仅显示前 N 字符」的分支,4194304 B 的文件完整持有 5592408 字符,两个模式来回切值逐字相同。把文件拖进 二进制转 Base64 之后取走的两个出口同样不加工:复制收到的字符数与框里一致(12952、5592408 都测过,含换行全为 false),下载走 new Blob,字节数等于字符数,无 BOM 也无末尾换行。22 个样本的整串换行数 0,这一页不替你做 76 列折行。
换行、空格、缺补位符:三条路径都吞
拿这一页的 body 去解,容忍度比直觉宽。以 photo_400 的 12952 字符为基准:每 76 字符插一个换行变 13122,插 CRLF 变 13292,atob、fetch 一个 data: URI、新 Image 都照旧解回 9712 字节;任意位置插空格或制表符、首尾各加空白,结论不变。补位符也不是硬要求:去掉 = 后 12950 字符仍得 9712 字节,那张 70 B 的 PNG 从 96 变 94 也一样。
但连字符和下划线一档都不容
宽容只到空白与补位符为止。把 + 换成 -、/ 换成 _ 之后,atob 报 InvalidCharacterError、fetch 报 TypeError、Image 走 error 事件,三条路径全灭。实测 4 个样本里 3 个直接失败,剩下那个 100 B 纯文本样例本来不含 + 也不含 /,替换后串一个字没变,只是没被改坏。
更隐蔽的是查询串:photo_400 的 body 里 + 出现 119 次、/ 出现 139 次,塞进 URLSearchParams 再取出,长度仍是 12952 却与原串不全等——加号被解成了空格,随后 atob 报错。拿长度做检查在这里完全失效。
图 2 改动后的解码结果,绿色是能解回的四种送法
页面上那几个统计数各自的口径
三个数各按各的算法。formatSize(第 140-142 行)用 1024 进制,KB 与 MB 固定 2 位小数,9712 显示成 9.48 KB;「Base64大小」(第 90-91 行)取 body 的字符数、不含前缀;「增大」(第 92 行)是增量比,实测从 0.0% 摆到 50.0%:16 B 显示 50.0%、70 B 显示 37.1%、4194304 B 显示 33.3%,0 字节被钉成 0.0%。显示值之间不能互推:12.65 除以 9.48 是 1.3344,那一行的增大显示 33.4%。
这一页没有解码入口
上面那组容忍度是浏览器侧对这一页所产出串的表现,不是这一页的功能:真实存在的输入只有第 11 行那个 accept 为 null 的 input type=file,第 35 行的输出框是 readonly,没有第二个可输入文本的控件;拖拽与选择两处都只用第 1 个文件(第 170、155 行),也没有批量。要把一串 base64 转回文件得另找解码方向。至于编码侧为什么会变大、末尾的 = 怎么来,base64 编码为什么会让文件变大 那篇已经记过,不复述。
另外几种会让人以为页面坏了的输入
伪后缀和坏图都拿不到错误:72 B 的坏 PNG 照样产出 96 字符、照样打开预览容器,整串喂 Image 报 error,页面上只留浏览器自带的破图标(第 64 行只判断 type 是否以 image/ 开头,不看解码结果)。反过来,类型写成 text/plain 的 PNG 字节容器是关的,可整串喂给 Image 仍解出 1x1。
0 字节更别扭:纯 Base64 下 body 长为 0,第 117、124 行双双短路;切到 Data URI 才有 37 字符,复制能收到 37,下载是个只含前缀的 37 字节 txt。
还常碰到的几件事
Q:贴到别处时换行要先手动删掉吗?
浏览器侧不用。12952 折成 13122 或 13292 都照旧解回 9712 字节。会出事的是传输那一层:串一进查询串,加号先被解成空格。
Q:顺手改成 URL-safe 形态发出去行不行?
先确认对面按哪套字母表解。这次 4 个样本里 3 个改成 -_ 之后直接报错,浏览器不会替你把它们认回来,接收侧得显式做一次反向替换。
Q:0 字节文件为什么复制按钮没反应?
值长为 0 时第 117 行的短路先命中,剪贴板一次都没被调用;切到 Data URI 才有 37 字符可复制,而那 37 个字符全是前缀。