柯尼卡美能达 KONICA MINOLTA PagePro 6180e,2017 年上市、2025 年初停产, 柯尼卡美能达官方的支持列表里从来没有过 macOS。 我用了一周时间,从零把它做成了 macOS 上的一等公民:系统打印队列、Word/Excel/WPS 直接打印、600dpi、照片、多页、横向。
一、缘起:不是驱动不支持,是没人做
打印机是柯尼卡 PagePro 6180e,通过打印服务器(Xerox Phasor,USB 转网口)接到局域网。 Windows 上有官方驱动,macOS 上没有 —— 于是每次打印都得开虚拟机挂共享,打印一份表格要折腾三分钟。
一开始想找现成的:OpenPrinting 有 6180e 的 PPD,但那是给 PostScript/PCL 打印机用的,
而这台机器只认一种私有格式:GWS8(DRIVERTYPE=GDI + COMPRESS=JBIG)。
Windows 官方驱动 GWSG8pr.dll 能生成它,macOS 上没有任何东西能生成。
所以只能自己做。路线是:抓 Windows 的真实输出,字节级反推格式,然后自己实现生成器。
二、逆向方法:让 Windows 打印到我的 Mac
打印机通过网络打印服务器共享给 Windows。我在 Mac 上写了个监听 9100 端口的抓取器, 然后把 Windows 的打印端口临时指向这台 Mac,打印一份文档,抓到的就是官方驱动的真实字节流。
# Mac 上监听 RAW 9100
python3 pp6180-capture9100.py /tmp/cap
# Windows 上把打印机端口改成 <Mac的IP>:9100,打印任意文档
# 抓到的二进制流就是 GWS8 样本
拿到样本后,strings 一下就能看到 PJL 命令序列:
\x1b%-12345X@PJL
@PJL SET DRIVERTYPE=GDI
@PJL SET COMPRESS=JBIG
@PJL SET MEDIATYPE=PAPER
@PJL SET PAGESTATUS=START
@PJL SET ORIENTATION=PORTRAIT
@PJL SET RESOLUTION=600
@PJL SET PAPERWIDTH=4961
@PJL SET PAPERLENGTH=7016
@PJL SET PAPER=A4
@PJL SET IMAGELEN=32768
@PJL SET IMAGELEN=11152
@PJL SET DATATYPE=11
@PJL SET DOTCOUNT=771672
@PJL SET PAGESTATUS=END
@PJL EOJ
\x1b%-12345X
至此格式基本明朗:PJL 头 + 一段 JBIG 位图数据 + PJL 尾。
三、最终的数据链路
macOS 上没有现成的 GWS8 过滤器,只能自己写一个 CUPS filter:
PDF ──sips(CoreGraphics)──> 未压缩 TIFF(按页面物理尺寸 × 600dpi)
──tiff2pbm(C)──────> 1bit PBM(真灰度 + alpha白底合成 + FS误差扩散)
──pbmtojbg────────> 单块 JBIG
──GWS8封装(PJL)───> 打印服务器:9100 ──USB──> 6180e
最终交付物:
| 文件 | 作用 |
|---|---|
rastogws8 |
CUPS 过滤器(Python),装到 /usr/libexec/cups/filter/ |
tiff2pbm.c |
TIFF→1bit PBM,C 实现(纯 Python 跑一张 A4 要几十秒) |
pbmtojbg |
JBIG 编码器(JBIG-Kit 2.1 自编译) |
KM6180e-GWS8.ppd |
PPD,*cupsFilter: "application/pdf 50 rastogws8" |
install-gws8-driver.sh |
一键安装/更新,附带 7 步能力核验 |
# 用法
lp -d PagePro_6180e -o media=A4 文件.pdf
四、九个坑(这才是真正的成本)
格式搞对只花了半天,后面三分之二的时间全花在"格式对了但打印不对"上。 以下每一条都是实测踩出来的,不是推断。
① JBIG 必须是「单块」
JBIG-Kit 默认会降采样成多层 + stripe=128,一页被切成 55 块(每块结尾一个 FF 02)。
柯尼卡这套嵌入式 GDI 解释器只解单块,多块流全糊成噪声,打出来是全黑。
修法:pbmtojbg -q -s <页高>(整页一块),并把 JBIG 头 [16] 从 0x08 改成 0x00。
改完后与 Windows 官方驱动的输出前 16 字节完全一致。
② CUPS 过滤器看不到 /opt/homebrew
CUPS 以 root + Seatbelt 沙箱运行过滤器,PATH 里没有 Homebrew, ImageMagick 直接报「找不到 magick」→ Filter failed。
修法:过滤器只用 macOS 自带的 /usr/bin/sips + Python 标准库,依赖全在 CUPS 可见路径。
③ 过滤器必须先处理 -i / -m
CUPS 探测过滤器时会调 filter -i 询问支持的 mime。不响应 → 探测时卡住 → 判定 Filter failed。
④ 透明背景 = 整页全黑
macOS / WPS 把 .xlsx 渲染成页面背景透明的 PDF,sips 转出的 TIFF 里
透明区 RGB 全是 0(黑)+ alpha=0(实测 hasAlpha: yes, samplesPerPixel: 4)。
直接取 RGB 判黑白 → 整页 97.57% 全黑。
同一个 TIFF 的对照实验:
| 处理方式 | 黑点率 | 结果 |
|---|---|---|
| 取 B 通道 + 忽略 alpha | 97.57% | 整页全黑 ❌ |
| 真灰度 + alpha 白底合成 | 5.13% | 正常 ✅ |
白底合成要分两种 alpha 存储方式(TIFF 的 ExtraSamples 字段):
- 1 associated(RGB 已预乘):out = rgb + (255 - a)
- 2 unassociated(RGB 未预乘):out = rgb*a/255 + (255 - a)
顺带一个坑:
ExtraSamples(tag 338)的值是内联在字段里的,不是偏移量。 按偏移量读会拿到 19712 这种垃圾值,alpha 合成走错分支。
⑤ 有序抖动救不了照片,误差扩散才行
1bit 设备上,Bayer 8×8 的动态范围只有 ±16/255。真实彩色现场照片里 草地亮度 93、红土 104,都低于阈值 128 → 整片压成纯黑。
改用 Floyd–Steinberg 误差扩散(把量化误差扩散到邻域,中灰自动呈 50% 网点)后, 草地纹理、衣服褶皱、地面细节全部保留。
| 模式 | 真实 600dpi 彩色照片效果 |
|---|---|
bayer |
草地/黑衣塌成纯白纯灰块,层次全丢 ❌ |
fs(默认) |
中间调保留,接近真实灰度照片 ✅ |
纯文字表格两种都干净(纯白背景误差为 0,不产生噪点)。
⑥ exit 1 = 亲手停用整个队列
CUPS 过滤器的退出码语义是「数据有没有写出去」,不是「处理成功没有」:
| 过滤器行为 | CUPS 判定 | 后果 |
|---|---|---|
exit 1 |
写设备失败 → Filter failed | 整个队列被标记 stopped,之后所有作业全卡死 |
| 输出 0 字节 | Broken pipe | 同上 |
| 输出一份合法 GWS8 | 写成功 | 队列健康,只是这页内容不对 |
真实翻车记录:我为了拦「页宽超过 A3」加了个尺寸检查,结果 Excel 是横向 A4
(842pt 宽 → 600dpi 换算 7017 点),比我设的上限 7016 多 1 个点 → exit 1 → 队列直接被停用。
教训:拦截阈值必须按纸张物理尺寸算;拦截之前先想清楚「拦截后队列会怎样」。
⑦ 横向页面:旋转位图,而不是指望打印机转纸
6180e 的 A4 只能长边进纸,打印机不会自己把纸转 90°。 实测:把 7017×4959 的横向位图直接送过去 → 打印机把它缩到 A4 竖向宽度 → 表格完整但整体小 1.41 倍。
正确做法(也是 Windows GDI 在不支持横向的机型上的行为): 把内容旋转 270° 塞进纵向纸面 → 表格长边正好对应 297mm 纸面高度 → 1:1 满幅输出, 用户把纸顺时针转 90° 看就是标准横向排版。
用 sips -r 实现,无损且保留 alpha 通道(已验证)。
⑧ 拆多页 PDF:/Kids 与 /Type /Pages 的顺序不固定
sips 渲染 PDF 只输出第一页,所以多页必须自己拆。
拆法是等长改写 PDF 的页面树:
/Kids [3 0 R 5 0 R 7 0 R] → /Kids [5 0 R ] ← 其余用空格填充
/Count 3 → /Count 1
关键:所有替换保持字节长度不变,于是 xref 表里每个对象的偏移量依然正确 —— 文件不用重建,也不用重算 xref。PDF 语法允许数组内出现任意空白。
但坑在于不同生成器的字段顺序不一样:
textutil / cgtexttopdf: << /Type /Pages /MediaBox [...] /Count 4 /Kids [...] >>
WPS / Quartz PDFContext: <</Kids [6 0 R 19 0 R 21 0 R] /Count 3 /Type /Pages>>
我最初按前者的顺序写,测什么都好;换真实 WPS 文档立刻翻车。
修法:以 /Type /Pages 为锚点,向前找到字典起点 <<,在整个字典范围内找 /Kids 和 /Count。
(另外 /Count 在 PDF 规范里是可选的,缺失时要能用 Kids 引用数兜底。)
教训:测试样本必须用真实业务文件。自造的样本可能刚好绕开 bug。
⑨ 多页打印的三条硬约束(全部实测确定)
6180e 的多页不是"把数据拼起来"那么简单:
① 整个 job 只能有一个 UEL 块
每页一个独立 UEL 块(UEL…EOJ…UEL)→ 打印机只印第一页。
它把整个 TCP 流当成一个 job,只处理第一个 UEL 块里的第一个 PAGESTATUS 对。
必须改成:单个 UEL 块 + 多个 PAGESTATUS=START/END 对,每页重复一整组设置。
② 每页的位图数据必须固定 2 块
Windows 样本里 43920 字节的 JBIG 被写成两条 IMAGELEN(32768 + 11152),
中间只隔一条 PJL 命令(25 字节)。不是两个 BIE 层,而是同一段位图按 32KB 分块送。
打印机对「一页 = 固定块数」有预期,块数忽多忽少会多吐一张空白页。 用对照实验定位:
| 发出的数据 | 打印机表现 |
|---|---|
| 页1(36951→2块) + 页3(45263→2块) | ✅ 2 张纸,内容正确 |
| 页1(2块) + 页2(21653→1块) + 页3(2块) | ❌ 4 张纸,第3张空白(页序没错、内容也对) |
③ 位图像素必须向下取整
A4 高 842pt → 842 × 600 / 72 = 7016.67。用 round() 得 7017,
比 A4 幅面(600dpi = 7016 行)多 1 行 → 溢出成一张几乎空白的纸。
Windows 官方驱动用的正是 4961×7016。
五、一个省时间的技巧:绕过 CUPS 直接测协议
改协议格式时,每次都要 sudo 装一遍过滤器再打印,迭代太慢。
其实可以直接把数据发给打印服务器的 9100 端口:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("<打印服务器IP>", 9100))
s.sendall(gws8_bytes)
这样不用装就能实测各种格式变体,一次只改一个变量,每次实验只花 1~2 张纸。 上面那条「块数」结论,就是这样用对照实验一次性锁定的。
对照实验的设计原则:让每个样本只在一个维度上不同。 比如「只发块数相同的两页,跳过可疑的那页」,一次就能定位元凶。
六、使用
# 打印 PDF / 预览打开的任何文档
lp -d PagePro_6180e -o media=A4 文件.pdf
# Word:双击用「预览」打开 → Cmd+P(别直接 lp 打 .docx)
# Excel/WPS:正常打印,横向会自动旋转 270°,顺时针转纸 90° 查看
# 可调参数
-o dither=bayer # 换有序抖动(纯文字更锐利)
-o threshold=96 # 印得更黑(调高更淡)
-o rot=90 # 转纸方向改为逆时针
-o landscape=keep # 横向不旋转,沿用旧行为
安装脚本自带 7 步能力核验,第 7 步会造一个 2 页 PDF 跑一遍过滤器, 确认拆页正常、每页块数正确 —— 防止"以为装上了其实没装":
✓ 多页拆页自检通过(kids_first 结构,2 页 → 2 段 GWS8,每页 2 块)
✓ 多页拆页自检通过(kids_after 结构,2 页 → 2 段 GWS8,每页 2 块)
七、一点感想
这件事本身的技术难度不算高,难的是在没有文档、没有样本、没有报错信息的情况下, 靠"打印出来看"做二分定位。打印机不会告诉你哪里错了,你只能从纸张上反推。
回头看,真正省时间的不是某个技巧,而是这几条:
- 先建立可观测性:把过滤器的诊断信息(stderr)打开,
LogLevel=warn会把一切吞掉 - 对照实验一次只变一个维度
- 用真实业务文件当测试样本
- 每次实验都留证据(对比图、数据统计),而不是凭印象判断
最后一点尤其重要 —— 前后对比图比任何描述都有说服力, 那张「Bayer vs 误差扩散」的照片对照图,直接决定了抖动算法的选型。
成果:一台官方从未支持过 macOS 的打印机,现在在 Mac 上是系统级的打印队列, 600dpi、照片清晰、多页正常、横向满幅。打印一份表格,从三分钟变成十秒。
