一张损坏JPEG文件的背后排查

11 MB 的"损坏 JPEG" 文件背后:原始位图的一场误会

一次扫描件打不开的排查记录。读完你会发现:它不是文件损坏,也不是加密,而是扫描仪把未压缩原始位图交给了调用方,但调用方把它当成 JPEG 存盘了。


1. 事发

某天收到同事反馈:用佳能文档扫描仪(imageFORMULA DR-M160 类似机型)扫出来的文件,在系统里打不开。

文件线索:

  • 扩展名:.jpg
  • 大小:约 11 MB(11,594,142 字节)
  • 创建软件:扫描仪配套软件 / 业务系统集成扫描接口
  • 同一个扫描任务,扫描仪自带软件的"另存为 JPG"只有 300 KB
  • 业务系统落盘的文件却始终 11 MB

2. 初判:它不是 JPEG

拿到文件后先看标准文件签名。JPEG 的开头应该是 FF D8 FF:

文件类型 标准文件头
JPEG FF D8 FF
PNG 89 50 4E 47
BMP 42 4D
GIF 47 49 46 38

但该文件的前 16 字节是:

bash 复制代码
1e1 e6 e9 da df e2 d5 da dd df de ec e2 e1 ef e8

完全没有标准签名。用常见工具打开自然失败。

进一步扫描文件内部:

Marker 次数 说明
FF D8 FF 0 JPEG 头不存在
FF D9 78 JPEG 结束标记偶发出现,但位置随机
PNG 签名 89 50 4E 47 0 不是 PNG
BMP 头 42 4D 几例 疑似偶发,不是真正 BMP

结论:这不是被截断的 JPEG,也不是常见图像格式。


3. 字节分布透露的信息

3.1 熵很低

  • 信息熵:4.63 bit/字节(完全随机约 7.8~7.9)
  • 说明数据有结构、有规律,不是加密或强压缩的结果。

3.2 字节高度集中在高位

Top 高频字节:

字节 占比
0xFA 11.46%
0xF9 10.55%
0xFB 9.80%
0xF8 9.10%
0xFC 8.21%

基本都在 0xF0~0xFF 区间。这说明图像内容以接近白色的亮部为主,非常符合扫描白纸文档的特征。

3.3 存在 3 字节周期

相隔 3 字节的字节相同率高达 27.83% ,远高于其他步长。文件末尾则是 fa fd fc fa fd fc ... 的 3 字节重复。

这强烈暗示数据是每 3 字节一组,也就是 24-bit 彩色(RGB/BGR 每像素 3 字节)。


4. 关键计算:11 MB 是什么

如果这是一张 24-bit 彩色图像,每个像素 3 字节,那么总像素数为:

ini 复制代码
111,594,142 ÷ 3 = 3,864,714 像素

已知这台扫描仪扫描设置的分辨率是 200 dpi。A4 纸的尺寸是 210 mm × 297 mm,换算到 200 dpi:

yaml 复制代码
1宽  = 210 mm × 200 / 25.4 ≈ 1653 px2高  = 297 mm × 200 / 25.4 ≈ 2338 px3总像素 = 1653 × 2338 = 3,862,714

再乘以 3 字节:

ini 复制代码
11653 × 2338 × 3 = 11,594,142 字节

完全吻合。

所以这 11 MB 的文件,本质上就是:

200 dpi、A4、24-bit 彩色、未压缩的 RAW 位图数据。

只是它没有 BMP/TIFF 文件头,也没有 JPEG 压缩,却被写上了 .jpg 扩展名。


5. 还原图像验证

用 Python 按 1653 × 2338 × 3 解析数据并写入 PNG:

scss 复制代码
1import zlib, struct, binascii23W, H = 1653, 23384raw = open('sample-scan.bin', 'rb').read()5assert W * H * 3 == len(raw)67def chunk(typ, payload):8    c = struct.pack('>I', len(payload)) + typ + payload9    crc = binascii.crc32(typ + payload) & 0xffffffff10    return c + struct.pack('>I', crc)1112ihdr = struct.pack('>IIBBBBB', W, H, 8, 2, 0, 0, 0)13filtered = bytearray()14stride = W * 315for y in range(H):16    filtered.append(0)17    filtered += raw[y*stride:(y+1)*stride]1819png = b'\x89PNG\r\n\x1a\n'20png += chunk(b'IHDR', ihdr)21png += chunk(b'IDAT', zlib.compress(filtered))22png += chunk(b'IEND', b'')23open('restored-rgb.png', 'wb').write(png)

还原后得到的是一张完整的 A4 扫描文档,表格、文字都清晰可读。

这也解释了另一个现象:

  • 扫描仪自带软件的"另存为 JPG"300 KB:因为它对原始位图做了 JPEG 压缩;
  • 业务系统落盘的 11 MB:因为它把原始位图直接写成了文件,根本没压缩。

6. 为什么 BMP 正常、JPG 不行

佳能 DR-M160 这类扫描仪通过 TWAIN/ISIS 驱动对外输出时,通常交付的是未压缩位图。至于最终存成 JPG、BMP、TIFF 还是 PDF,是上层软件负责封装和压缩。

如果上层软件直接把扫描缓冲区落盘:

  • 写 .bmp 或者给自己加的 .jpg,文件大小都是 11 MB 左右;
  • 因为 .bmp 格式本身可以无压缩,所以写入头信息后反倒能正常打开;
  • 但 .jpg 必须有 JPEG 压缩数据,直接塞 raw 位图进去,怎么看都是"损坏"。

所以,问题根因不是扫描仪坏了,也不是文件传输过程损坏,而是:

上层软件没有请求/执行 JPEG 压缩,误把原始位图当作 JPEG 保存。


7. 给我们的启示

  1. 排查"损坏图片"别急着修图 先看文件头、文件大小、字节结构。大小与像素数量精确吻合时,优先考虑"格式误判"。
  2. 对 TWAIN/ISIS 扫描接口要显式指定输出格式 如果需要 JPEG,应在扫描参数或保存步骤里把压缩打开;只取原始 buffer 落盘,得到的一定是 raw 位图。
  3. 原始位图是可以救回来的 只要知道 dpi、纸型、色彩模式,就可以反推出图像尺寸,再用 Python/OpenCV/PIL 加个头就能正常使用。
  4. 文件扩展名不可信 扩展名是 .jpg 不代表内容就是 JPEG。业务系统落盘时最好检查一下魔数(magic bytes),别只依赖文件名后缀。

8. 如果临时要救这些数据

假设你手里有一批同样的 11 MB "损坏 JPG",可以用下面这段脚本批量还原为 PNG:

python 复制代码
1import os2from PIL import Image34W, H = 1653, 233856for fname in os.listdir('.'):7    if not fname.lower().endswith('.jpg'):8        continue9    with open(fname, 'rb') as f:10        data = f.read()11    if len(data) != W * H * 3:12        continue13    img = Image.frombytes('RGB', (W, H), data)14    base, _ = os.path.splitext(fname)15    img.save(f'{base}.png', 'PNG')16    print(f'{fname} -> {base}.png')

如果扫描设置不同(比如 300 dpi、A5、灰度),只要替换对应的 W、H 和 'RGB' 即可。


9. 一句话总结

它不是 JPEG 坏了,而是扫描仪把原始位图 交给了你,你却把它当JPEG保存了。文件名会骗人,但字节大小和分辨率不会。

相关推荐
Fox爱分享17 分钟前
都问程序员双十一买什么,我的答案一定是这块专业编程显示器
后端·程序员·ai编程
CCblandy30 分钟前
缓存淘汰实测:一次全表扫描把 LRU 命中率从 46% 打到 38%,W-TinyLFU 守住 50% 却贵 11 倍
后端·python·缓存·性能优化·架构
武子康40 分钟前
DOA 100%、GSC 改善 12 dB,ESP32-P4 远场拾音就能用了吗?
人工智能·后端·agent
愚农搬码1 小时前
企业为什么需要表单引擎,而不是继续手写 Vue 表单?
后端
孟陬2 小时前
技术周刊#158:ChatGPT 智能 UI 底层原理、个人智能体协议正式发布、一条 POST 请求即可锁死 Next.js 服务
人工智能·后端·全栈
小蒜学长2 小时前
基于SpringBoot+Vue的云南地区扶贫助农系统的设计与实现(代码+数据库+LW)
java·vue.js·spring boot·后端·乡村振兴
码剑客2 小时前
Gin+GORM+Redis 项目开发常见性能坑汇总(Go后端避坑指南)
redis·后端·golang·gin
java_nnnn2 小时前
Spring Boot 配置文件详解:从基础语法到 YAML 配置
spring boot·后端·github
SimonKing2 小时前
素笺( local-notes)全面优化,新增功能体验不一样的笔记
java·后端·程序员
何中应2 小时前
【Go-1】变量&常量定义
开发语言·后端·golang