funinkina/Ricoh-SP200-Linux
GitHub: funinkina/Ricoh-SP200-Linux
通过对 Windows 驱动的 USB 流量进行逆向工程,为无官方 Linux 支持的 Ricoh SP 200 打印机实现了完整的 CUPS 过滤器,填补了 foo2zjs 等开源驱动方案的空白。
Stars: 0 | Forks: 0
# Ricoh SP 200 Linux CUPS 驱动
这是一个为 Ricoh SP 200 黑白激光打印机编写的原生 Linux CUPS 过滤器,通过对官方 Windows 驱动的 USB 数据抓包进行逆向工程开发而来。
该打印机没有官方的 Linux 驱动程序,并且在 foo2zjs、OpenPrinting、HPLIP 和 Gutenprint 中均未提供支持。本驱动从头实现了完整的打印协议。
## 文件
| 文件 | 用途 |
|---|---|
| `rastertoricohjbig.c` | CUPS 过滤器源代码 |
| `ricoh-sp200.ppd` | PPD 打印机描述文件 |
## 依赖
**Arch Linux**
```
sudo pacman -S cups ghostscript jbigkit
```
**Debian / Ubuntu**
```
sudo apt install libcups2-dev libcupsimage2-dev libjbig-dev jbigkit-bin gcc ghostscript
```
**Fedora / RHEL**
```
sudo dnf install cups-devel cups-libs jbigkit-devel jbigkit-libs gcc ghostscript
```
**macOS**
```
brew install cups jbigkit
brew services start cups # run Homebrew CUPS on port 631
```
## 构建与安装
### 使用 make(推荐)
```
# 构建并安装 filter + PPD
sudo make install
# 向 CUPS 注册打印机(打印机必须通过 USB 连接)
sudo make register
# 测试打印
echo "Hello from Linux" | lpr -P Ricoh_SP_200_DDST
# 卸载全部内容
sudo make uninstall
```
### 手动步骤 — Linux
```
# 编译
gcc -O2 -o rastertoricohjbig rastertoricohjbig.c \
$(cups-config --libs) -lcupsimage -ljbig
# 安装 filter 和 PPD
sudo install -m 755 rastertoricohjbig /usr/lib/cups/filter/rastertoricohjbig
sudo install -m 644 ricoh-sp200.ppd /usr/share/ppd/cupsfilters/
# 注册打印机(打印机必须通过 USB 连接)
sudo lpadmin -p Ricoh_SP_200_DDST \
-v "$(lpinfo -v | grep -i ricoh | awk '{print $2}' | head -1)" \
-P /usr/share/ppd/cupsfilters/ricoh-sp200.ppd \
-E
# 测试打印
echo "Hello from Linux" | lpr -P Ricoh_SP_200_DDST
```
### 手动步骤 — macOS
```
BREW=$(brew --prefix)
# 编译(Homebrew 提供 cups 和 jbigkit 头文件/库)
gcc -O2 -I$BREW/include -o rastertoricohjbig rastertoricohjbig.c \
-L$BREW/lib -lcups -lcupsimage -ljbig
# 安装 filter 和 PPD
sudo install -m 755 rastertoricohjbig $BREW/libexec/cups/filter/rastertoricohjbig
sudo install -m 644 ricoh-sp200.ppd /Library/Printers/PPDs/Contents/Resources/
# 注册打印机(打印机必须通过 USB 连接)
sudo lpadmin -p Ricoh_SP_200_DDST \
-v "$(lpinfo -v | grep -i ricoh | awk '{print $2}' | head -1)" \
-P /Library/Printers/PPDs/Contents/Resources/ricoh-sp200.ppd \
-E
# 测试打印
echo "Hello from macOS" | lpr -P Ricoh_SP_200_DDST
```
## 逆向工程过程
### 1. 抓取 USB 流量
打印机通过 USB 透传给 Windows 虚拟机。在 Linux 主机上,使用 `usbmon` + Wireshark 在打印测试文档时抓取了所有的 USB 批量传输。
```
# 使用的 Wireshark filter:
usb.transfer_type == 0x03 && usb.endpoint_address.direction == OUT
```
单页打印任务产生了两个 `URB_BULK out` 数据包(约 65 KB 和约 59 KB)。双页打印任务产生了三个数据包,这揭示了 Windows 驱动程序会将 JBIG 流按照需要分割成多个 USB 批量传输(每个不超过 65 508 字节)。
### 2. 识别协议
数据包 1 的前几个字节解码为 ASCII:
```
ESC%-12345X@PJL
@PJL SET COMPRESS=JBIG
@PJL SET PAPERWIDTH=4961
@PJL SET IMAGELEN=65556
```
这立即识别出了协议:包装 **JBIG1 (ITU-T T.82)** 压缩光栅数据的 **PJL (Printer Job Language)**。最初关于打印机使用 HBPL2(像其他 foo2zjs Ricoh 打印机那样)的假设是错误的——在数据流中用 `grep` 搜索 `HBPL` 一无所获。
### 3. 解码 JBIG BIE 头
紧随 PJL 头之后的 20 个字节最初被假定为一个 Ricoh 专有头。结果证明它们是一个标准的 **JBIG1 BIE (Bi-level Image Entity) 头**:
| 偏移量 | 值 | 字段 | 含义 |
|---|---|---|---|
| 0–3 | `00 00 01 00` | DL, D, P, reserved | Layer 0, 0 diffs, 1 plane |
| 4–7 | `00 00 13 61` | Xd | **4961 px** = A4 宽度 @ 600 dpi |
| 8–11 | `00 00 1b 68` | Yd | **7016 px** = A4 高度 @ 600 dpi |
| 12–15 | `00 00 00 80` | L0 | 128 行/条 |
| 16–17 | `00 00` | Mx, Dmax | 0 |
| 18 | `03` | order | 直接存储在 BIE 字节 18 中 |
| 19 | `48` | options | 直接存储在 BIE 字节 19 中 |
### 4. 发现多页协议
第二次抓取了打印双页文档的过程。从拼接的 USB 流中提取所有 `@PJL` 令牌揭示了完整的页面生命周期:
```
[page 1 JBIG data — first chunk]
@PJL SET IMAGELEN=13451 ← second chunk of page 1 (driver splits large pages)
[13451 bytes — continuation of page 1 JBIG stream]
@PJL SET DOTCOUNT=1477935 ← black pixel count for page 1
@PJL SET PAGESTATUS=END ← eject page 1
@PJL SET PAGESTATUS=START ← begin page 2
@PJL SET COPIES=1
@PJL SET MEDIASOURCE=TRAY1
@PJL SET MEDIATYPE=PLAINRECYCLE
@PJL SET PAPER=A4 ← page 2 media settings (repeated every page)
@PJL SET PAPERWIDTH=4961
@PJL SET PAPERLENGTH=7016
@PJL SET RESOLUTION=600
@PJL SET IMAGELEN=65556 ← page 2 first chunk
[65556 bytes — page 2 JBIG data, first chunk]
@PJL SET IMAGELEN=10550 ← page 2 second chunk
[10550 bytes — page 2 JBIG continuation]
@PJL SET DOTCOUNT=1266291
@PJL SET PAGESTATUS=END ← eject page 2
@PJL EOJ
ESC%-12345X
```
主要发现:
- **`PAGESTATUS=END` 是页面弹出触发器。** 如果没有它,固件会将后续所有的 `IMAGELEN` 视为仍处于打开状态的同一页面的另一个数据块,并且永远不会弹出。
- **每个页面可以跨越多个 `IMAGELEN` 数据块。** 驱动程序将任何超过一次 USB 批量传输大小的 JBIG 流拆分为连续的 `@PJL SET IMAGELEN` 块,它们都属于同一页。Linux 过滤器生成的 JBIG 流较小(无需分块),但固件能够同时处理这两种情况。
- **除第一页外,每一页在首次 `IMAGELEN` 之前都有其专属的 `PAGESTATUS=START` + 完整介质头**(`COPIES`、`MEDIASOURCE`、`MEDIATYPE`、`PAPER`、`PAPERWIDTH`、`PAPERLENGTH`、`RESOLUTION`)。
- **`DOTCOUNT`** 报告页面的总黑色像素数,供打印机用于碳粉寿命估算。
### 5. 完整的协议结构
```
── Job header (once) ──────────────────────────────────────────────────────────
ESC%-12345X@PJL\r\n
@PJL SET TIMESTAMP=YYYY/MM/DD HH:MM:SS\r\n
@PJL SET FILENAME=...\r\n
@PJL SET COMPRESS=JBIG\r\n
@PJL SET USERNAME=...\r\n
@PJL SET COVER=OFF\r\n
@PJL SET HOLD=OFF\r\n
@PJL SET PAGESTATUS=START\r\n ← covers page 1
@PJL SET COPIES=N\r\n
@PJL SET MEDIASOURCE=TRAY1\r\n
@PJL SET MEDIATYPE=PLAINRECYCLE\r\n
── Per-page block (repeat for every page) ─────────────────────────────────────
[pages 2+ only:]
@PJL SET PAGESTATUS=START\r\n
@PJL SET COPIES=N\r\n
@PJL SET MEDIASOURCE=TRAY1\r\n
@PJL SET MEDIATYPE=PLAINRECYCLE\r\n
@PJL SET PAPER=\r\n
@PJL SET PAPERWIDTH=\r\n
@PJL SET PAPERLENGTH=\r\n
@PJL SET RESOLUTION=600\r\n
@PJL SET IMAGELEN=\r\n
@PJL SET DOTCOUNT=\r\n
@PJL SET PAGESTATUS=END\r\n ← triggers paper ejection
── End of job (once) ──────────────────────────────────────────────────────────
@PJL EOJ\r\n
ESC%-12345X\r\n
```
### 6. 构建 CUPS 过滤器
该过滤器(`rastertoricohjbig.c`)使用了:
- **libcups / libcupsimage** — 从标准输入读取 CUPS 光栅流
- **libjbig** (jbigkit 2.1) — 将每一页编码为 JBIG1 BIE
- 编码后的 BIE 被缓存在内存中,测量其大小,然后在前面加上 `@PJL SET IMAGELEN=N` 输出到标准输出
PPD 声明了 `*cupsBitsPerColor: 1` 和 `*cupsColorSpace: 3`,因此 CUPS 会直接传递 1 位打包的 K 通道位图——在正常路径下无需进行转换。
### 7. 开发过程中发现的 Bug
**Bug 1 — UEL 后缺少裸 `@PJL\r\n`**
Windows 驱动程序在第一个 SET 命令之前会发送 `ESC%-12345X@PJL\r\n`。如果没有这个裸 `@PJL\r\n` 行,打印机将静默丢弃整个打印任务。
**Bug 2 — 缺少 `@PJL SET PAPERLENGTH`**
打印机要求同时提供 `PAPERWIDTH` 和 `PAPERLENGTH`。如果没有 `PAPERLENGTH`,打印机会初始化其引擎(发出噪音 + LED 闪烁),但永远不会吸纸。
**Bug 3 — 错误的 JBIG BIE 选项字节**
jbigkit 2.1 将 `jbg_enc_options()` 的 `options` 参数**直接**存储到 BIE 字节 19 中,而不进行位转换。Windows 驱动程序生成的字节 19 = `0x48`。传入 `0x08` 会导致打印机接受任务、预热,然后拒绝进纸。传入 `0x48` 解决了此问题。
**Bug 4 — PPD 缺少光栅格式指令**
如果没有 `*cupsBitsPerColor: 1`、`*cupsColorSpace: 3` 及相关的 PPD 键,CUPS 会向过滤器传递 8 位灰度数据。步幅是按 `(w+7)/8`(假设为 1 位)计算的,但实际数据却宽了 8 倍,导致生成了格式错误的 JBIG 流。
**Bug 5 — 多页:仅打印第一页**
通过抓取 Windows 驱动程序的双页任务发现。固件使用 `@PJL SET PAGESTATUS=END` 作为页面弹出触发器——而不是下一个 `IMAGELEN` 的到来。如果每页的 JBIG 数据之后没有 `PAGESTATUS=END`,后续每个 `IMAGELEN` 块都会被视为同一打开页面的另一个数据块。打印机会将所有页面的数据累积为一个巨大的第 1 页,并且从不将其弹出。在同一次抓包中还发现了其他三个遗漏:
- `PAGESTATUS=START` + 完整介质头(`COPIES`、`MEDIASOURCE`、`MEDIATYPE`、`PAPER`、`PAPERWIDTH`、`PAPERLENGTH`、`RESOLUTION`)必须位于除第一页之外的每个页面之前。
- `DOTCOUNT`(黑色像素计数)必须逐页发送,紧接在 `PAGESTATUS=END` 之前。
- 从 CUPS 光栅头获取的 `cupsBytesPerLine` 必须用作读取像素数据时的行步长——手动计算的 `(bpp*w+7)/8` 可能会与 CUPS 的实际行大小产生偏差,导致流错位并使 `cupsRasterReadHeader2` 在处理第 2 页时失败。
## 故障排除
**`lpinfo -v` 未找到打印机**
```
lsusb | grep -i ricoh
```
**过滤器未被调用(任务静默消失)**
```
ls -la /usr/lib/cups/filter/rastertoricohjbig
# 必须是:-rwxr-xr-x root root
sudo chmod 755 /usr/lib/cups/filter/rastertoricohjbig
sudo chown root:root /usr/lib/cups/filter/rastertoricohjbig
```
**CUPS 显示打印机已停止**
```
sudo cupsenable Ricoh_SP_200_DDST
sudo cupsaccept Ricoh_SP_200_DDST
```
**检查 CUPS 错误日志**
```
sudo tail -40 /var/log/cups/error_log
```
**构建失败 — 缺少 jbig.h**
```
# Arch: sudo pacman -S jbigkit
# Debian: sudo apt install libjbig-dev
# Fedora: sudo dnf install jbigkit-devel
# macOS: brew install jbigkit
```
**macOS:过滤器已安装但 CUPS 找不到打印机**
```
# 确保 Homebrew CUPS 正在运行(而不是系统 daemon)
brew services restart cups
lpinfo -v | grep -i ricoh
```
*2026 年 5 月逆向工程。未使用或分发任何专有代码。*
*协议线缆格式文档不受版权限制。*
标签:Awesome, CUPS, DDST, Ghostscript, JBIG, PPD, rastertoricohjbig, Ricoh SP 200, USB协议, 云资产清单, 单色打印机, 客户端加密, 开源驱动, 打印机驱动, 激光打印机, 硬件适配, 系统管理, 编译构建, 逆向工程