本来我以为,前端的工作内容只需要跟浏览器打交道,最多再做一做客户端和后端;万万没想到,如标题所示,我居然需要解决打印机的问题。

奇怪的需求

我们有一个画面单的平台,关于这个平台,我之前已经写过几篇文章,这里就不多提了。

一天,面单的产品突然把我拉到了一个群里,因为我们的业务 PM(BPM)接到了一个紧急需求:在我们开始跟某个卖家合作时,他们提出:我们在打印面单的时候,必须支持通过一个叫 Zebra 的打印机来打印,否则就不跟我们合作。由于那是一个大卖家,因此这似乎是一个无法拒绝的需求。虽然我们完全没听说过这种打印机,还是硬着头皮上了。

经过调查发现,Zebra 打印机与一般的打印机不太一样,它无法通过系统的打印框来打印,甚至不是走的 COM 或 USB 接口,而是监听了一个 TCP 端口来与电脑通讯。发送给打印机的也不是一般的格式,而是一个叫 ZPL 的私有格式,看起来像是纯文本(例子来自英文维基):

^XA
^LH30,60
^FO20,10
^ADN,90,50
^FDWikipedia^FS
^XZ

第一眼看上去并看不懂。但通过谷歌,我们搜到了 ZPL 的各类教程,以及一个简易的预览工具:Online ZPL Viewer

开始调研

在调研之初就遇到了一道选择题:是直接生成 ZPL 格式,还是通过某些中间格式来转换呢?这两种方式似乎都可行:

  • 直接生成:面单平台生成的面单原始格式是 JSON,这可以很方便的存储和导入导出。我们写了一个转换器,当需要渲染面单时,可以调用转换器将 JSON 转换为 HTML,然后再调用 Headless Chrome 将 HTML 转换为 PDF 返回给第三方。既然有这个转换器,ZPL 又是纯文本,语法还是确定的,稍微学一学 ZPL 再写一个转换器似乎是个最好的办法。
  • 转换生成:我们团队没人接触过 ZPL,甚至我周围都找不到 Zebra 打印机的影子。如何知道预览工具与实际打印出来的差距有多大?如果有一个很成熟的库可以帮忙转换,无论是我们的支持速度,还是转换效果,都会好很多。

经过与同事们的讨论,如果直接生成的话,我们有学习成本,也有不可预期的兼容问题,因此决定转换生成。

技术方案与实现

搜遍了全网,唯一一个可用的库是 kylemacfarlane/zplgrf,是一个 Python 库。面单平台本来已经基本从 Python 切换为 Golang 了,为了这个库,还是得再起一个 Python 服务。了解了一下这个库,它可以将 PDF 转换为一个叫 GRF 的格式,再将其转换为 ZPL,只需要很简单的代码就可以实现:

from zplgrf import GRF
import sys

with open(sys.argv[1], 'rb') as pdf:
    data = pdf.read()
    # 203 dpi 就是 8 dpmm,一个比较低的精度
    pages = GRF.from_pdf(data, 'DEMO', dpi=203)
for grf in pages:
    print(grf.to_zpl())

我随便从测试环境搞了一张面单,导出成了 PDF 文件,它长这样:

转换后的 ZPL,虽然里面看起来一堆 Base64,但预览出来除了糊一点以外,似乎没什么问题:

ZPL 的分辨率有很多选项,但高分辨率意味着转换速度慢。我们跟 BPM 合作,得知了目标用户持有的打印机型号,多次对 UAT 环境的几张真实面单采用 8 dpmm、12 dpmm、24 dpmm 的分辨率做转换并记录转换时间。经过权衡,我们决定还是采用 8 dpmm 的分辨率。

经过一段时间与卖家中心团队的联调与测试,功能顺利上了 UAT。

卷纸问题的修复

本来以为万事大吉,结果业务报来了一个问题:对于某一个特定型号的打印机,在打印结束后,打印机会多吐一点点纸,然后把它卷回去。所有卖家的那个型号的打印机都能稳定复现这个问题,但他们打印其它平台的 ZPL 面单就没有问题。这可能会让我们的用户感到不满,因此我们必须解决。

一个棘手的问题是,我们这边完全没有 Zebra 打印机,现买的话也太慢了。我只好硬着头皮看生成的文件,并且让 BPM 提供了一个其它平台的 ZPL,我要知道究竟是什么东西导致的。

经过对比,我发现我们生成的 ZPL 文件有一些奇怪的指令。参考了 ZPL 指令集文档,发现有个叫 ^MM 的指令可以控制打印完成时打印机的行为,如果把它设置为 R,可以阻止带有卷纸控件的打印机的卷纸行为。我搜了一下那个型号的打印机(谷歌的结果排前几名的居然是我们的竞争对手 Lazada 与 Tokopedia),确实有卷纸控件,这应该就是解决方法了。

在上面的代码中,GRF.from_pdf 函数可以接受一个参数 print_mode,将其修改为 R 之后,卷纸问题解决了,但业务反馈说出现了错位问题……后来我找到了 Zebra 打印机的官方驱动文档,发现打印机的默认行为应当是 T,于是设置 print_mode='T',问题解决。

由于我们与业务有一些时差,我们所做的所有尝试,业务都需要第二天才能验证,因此解决的时间稍微长了一些。

文字模糊问题的迅速修复

终于把需求上线了。但刚上线没多久,大量卖家就反馈文字不清晰,很多文字的部分是缺失的:

之前的经验 立即告诉我,是纯黑白与分辨率导致的问题:这个字号的文字在 8 dpmm 的分辨率下,一部分“笔画”由于太细,会被直接转换为白色。解决方法也很简单:要么提升分辨率(花费更多时间且需要重新发版),要么加粗一下面单的文字试一试。

我们先让业务团队帮忙修改面单,将文字加粗。问题居然就解决了,效果还不错:

随即,业务发来了感谢信,许多卖家感谢我们如此迅速的解决问题。

但这毕竟只是个临时的解决方法,问题的根本原因还是分辨率太低(打印机只支持纯黑白的问题我们无法解决)。为了避免以后再出现类似的问题,我们需要提供一个高分辨率的选项。经过估算,如果从 8 dpmm 提升到 12 dpmm,由于面积扩大到两倍多,转换时间应当也是两倍多。我们的后端也会与卖家中心的后端进行进一步的测试,以评估这个修改的影响范围。

从这个需求中学到的

  1. 理论上来说,问题都是可以解决的,需要有一定的搜索技巧和探索精神。
  2. 学好英文很重要,我查的全部资料,都是没有中文的。
  3. 在必要的情况下,可能需要深入阅读官方的驱动文档或指令集文档。

总的来说,也是一次不错的经历,最后问题也解决了。之后再遇到类似的需求,我应该会比这次更加从容。