项目踩坑指南

从浏览器打印到原生 EPL:集成 Zebra GT800系列 患者条码打印完整踩坑记录

本文来源于我与我的天才同事(ChatGPT 5.6 sol)接近3小时的来回拉扯

目录

一、背景

这次需求看起来很简单:增加一个“打印条码”按钮,通过终端电脑连接的 Zebra 打印机打印患者 ID。

真正实施后,问题却横跨了多个层面:

  • B/S 系统访问终端本地 USB 打印机

  • 浏览器打印预览与物理标签不一致

  • Windows 打印驱动、USB 端口与打印队列

  • Zebra Browser Print 的设备发现与站点授权

  • EPL 坐标、旋转、标签长度和打印机持久设置

  • 生产环境静态资源缓存

第一版:浏览器生成 SVG,调用系统打印窗口

最初采用了纯浏览器方案:

  1. 前端自己编码 Code 128;

  2. 生成 SVG;

  3. 创建隐藏 iframe;

  4. 使用 window.print()

  5. 在 Windows 系统打印窗口中选择 GT800。

页面尺寸通过 CSS 指定:

@page {
  size: 40mm 75mm;
  margin: 0;
}

遇到的问题

打印预览看起来正常,实际输出却持续出现:

  • 横向、纵向方向相反;

  • 条码只有一部分;

  • 只打印出 ID,没有条码;

  • 条码跨越两张标签;

  • ID 在预览下方,实际却出现在上方;

  • 浏览器默认边距和驱动边距不一致;

  • 调整横向位置后,实际却表现为纵向移动;

  • 浏览器打印对旋转 SVG 的裁切与预期不同。

根本原因

浏览器打印链路过长:

HTML/SVG
→ 浏览器排版
→ 打印预览
→ Windows 页面模型
→ Zebra 驱动
→ 打印机坐标

每一层都可能处理:

  • 页面尺寸

  • DPI

  • 缩放

  • 方向

  • 页边距

  • 可打印区域

  • SVG 旋转边界

热敏标签打印机并不是普通 A4 打印机。对于需要精确控制坐标和走纸的标签,浏览器页面打印并不可靠。

第二版:改用原生 EPL

确认浏览器打印不可控后,开始直接向打印机发送 EPL。

现场验证可用的基本结构是:

N
q354
D10
B300,161,1,1,4,8,180,N,"PATIENT_ID"
A100,286,1,3,1,1,N,"PATIENT_ID"
P1

其中:

指令作用N清空图像缓冲区q354设置打印宽度D10设置打印浓度B...打印 Code 128A...打印可读 IDP1打印一张

B 指令的基本结构是:

B x,y,rotation,type,narrow,wide,height,humanReadable,"data"

例如:

B300,150,1,1,4,8,180,N,"PATIENT_ID"

表示:

  • x=300

  • y=150

  • 旋转 90°

  • Code 128 自动模式

  • 窄条 4 dots

  • 宽条 8 dots

  • 条码高度 180 dots

  • 不使用条码自带文字

ID 通过独立 A 指令输出,这比条码自带文字更容易控制位置。

EPL 指令的详细含义可参考 Zebra EPL Programming Guide

为了发送 RAW EPL,曾经开发本地 PowerShell 服务

B/S 系统无法直接调用 Windows WritePrinter,因此中间尝试过自建本地打印服务:

浏览器
→ http://127.0.0.1:17820/print
→ PowerShell本地服务
→ Windows RAW打印队列
→ GT800

服务只监听回环地址,并限制允许访问的 RTIS Origin。

PowerShell 安装踩坑

第一次执行安装命令时写成:

./powershell -ExecutionPolicy Bypass ...

PowerShell 将 ./powershell 当成本地文件,自然无法识别。正确写法是:

powershell -ExecutionPolicy Bypass -File .\install.ps1

随后又遇到脚本中文乱码:

字符串缺少终止符

根因是 Windows PowerShell 5.1 对 UTF-8 无 BOM 脚本的识别存在历史兼容问题。中文提示乱码后破坏了引号,最终表现为语法错误。

为什么最终放弃

虽然本地服务能够稳定发送 RAW EPL,但内网终端安全软件将自建常驻 PowerShell 服务识别为风险程序并禁止运行。

从安全软件视角看,它具备多个高风险特征:

  • 隐藏窗口启动;

  • 开机自启;

  • 本机监听端口;

  • 接受浏览器请求;

  • 调用打印机底层接口。

这套方案虽然技术上可行,但在医院受控终端环境中运维成本太高,因此被废弃。

第三版:切换 Zebra Browser Print

最终改用 Zebra 官方 Browser Print。

终端负责:

  • 安装 Zebra 驱动;

  • 安装 Browser Print;

  • 添加 GT800;

  • 将 GT800 设为 Browser Print 默认打印机;

  • 授权 RTIS 网站访问打印机。

前端随系统发布官方 JavaScript SDK:

BrowserPrint-3.1.250.min.js

基本调用方式:

BrowserPrint.getDefaultDevice('printer', success, error)

发送 EPL:

printer.send(epl, success, error)

Browser Print 在 Windows 后台监听 9100/9101,网页通过本机接口访问设备。首次访问时,用户需要允许当前 RTIS 地址加入 Accepted HostsBrowser Print 用户指南

默认打印机与 Windows 默认打印机不同

Browser Print 自己维护默认设备,不等于 Windows 系统默认打印机。

实际代码采用:

  1. 先读取 Browser Print 默认打印机;

  2. 如果默认打印机不是 GT800,则枚举本地打印机;

  3. 只接受精确名称或同时包含 GT800EPL 的设备;

  4. 不允许误发到 PDF、OneNote 或办公打印机。

100×30 mm 标签的走纸与坐标问题

现场标签为:

100 mm × 30 mm
300 dpi
间隙标签

300 dpi 约等于 12 dots/mm。

1. 错误尝试:写死 Q1181,24

一度根据 100 mm 标签长度推算:

Q1181,24

其中:

  • 1181 约等于 100 mm;

  • 24 约等于 2 mm 间隙。

结果条码直接跨越两张标签。

这个问题说明:不能只根据纸张标称尺寸猜测 EPL 的走纸方向、标签节距和当前打印机状态。

2. 使用 FEED 判断传感器是否正常

短按一次 FEED:

  • 如果正好走一张并停在标签缝隙,说明 Gap/Web 传感器和校准正确;

  • 如果走两张或停在标签中间,才需要重新校准。

现场短按 FEED 能准确停在缝隙,因此传感器没有问题。

最终决定:

  • 不在每次 EPL 中写死 Q

  • 使用打印机已经校准的标签长度;

  • 只控制参考点、方向、宽度和内容坐标。

Zebra 官方也建议在 EPL 跨标签或起始位置异常时,同时检查介质传感器和 Q 设置。Zebra 打印质量故障说明

条码跨页不一定是条码太长

条码跨页时,曾将模块宽度从:

4/8

缩小为:

2/4

条码长度明显缩短,但仍然靠近标签缝隙。

这说明问题不只是条码宽度,更可能是坐标原点错误。

于是将条码和 ID 的纵坐标都暂时设置为 0:

B300,0,1,1,2,4,180,N,"..."
A100,0,1,3,1,1,N,"..."

结果发现:Y=0 居然出现在后续标签附近,而不是第一张标签头部。

真正的坐标根因:打印机保留了旧的 R/Z 设置

EPL 中部分设置会保存在打印机中。仅发送:

N

只是清空图像缓冲区,不会重置:

  • 参考点 R

  • 打印方向 Z

  • 标签宽度 q

  • 标签长度 Q

之前的驱动或打印任务可能已经写入了其他参考点或 ZB 方向。因此应用传入的 Y=0,并不等于物理标签头部。

最终在每个任务里显式写入:

R0,0
ZT

含义:

  • R0,0:将水平、垂直参考点恢复为 0;

  • ZT:从图像缓冲区顶部开始打印。

再次用 Y=0 试打后,原点终于回到了第一张标签头部。

这是整个标签定位问题中最关键的一步。

最终 EPL

经过现场多轮试打,最终指令为:

N
R0,0
q354
ZT
D10
B300,150,1,1,4,8,180,N,"PATIENT_ID"
A100,296,1,3,1,1,N,"PATIENT_ID"
P1

最终参数:

参数最终值参考点R0,0打印方向ZT打印宽度q354浓度D10条码类型Code 128 Auto条码旋转90°条码模块宽度4/8条码高度180 dots条码坐标B300,150ID 字体EPL 字体 3ID 坐标A100,296打印数量P1

ID 最终按照与条码相同的偏移量同步调整,避免条码位置正确而可读文字仍停留在旧位置。

静态资源部署造成的“明明改了但结果没变”

在一次排查中,源码已经删除了错误的:

Q1181,24

但现场打印仍然跨页。

后来直接检查服务器实际加载的带哈希 JS 文件,发现生产地址仍在加载旧版本:

static/index-旧哈希.js

旧文件中仍然存在 Q1181,24

重新构建后,新文件虽然已经正确,但服务器没有完整替换 dist,或浏览器仍然缓存旧入口文件。

解决方式:

  1. 完整替换整个 dist

  2. 不能只替换某一个带哈希的 JS;

  3. 确认服务器 index.html 引用了新哈希文件;

  4. 关闭页面重新打开,或强制刷新;

  5. 必要时直接下载服务器 JS,搜索 EPL 字符串确认真实版本。

这次经历说明:现场结果与源码不一致时,必须检查“浏览器实际执行的代码”,不能只看本地仓库。

Browser Print 常见错误

1. 未检测到 Zebra Browser Print

提示:

未检测到 Zebra Browser Print

检查:

  • Browser Print 是否安装;

  • 后台进程是否运行;

  • 9100/9101 是否监听;

  • 当前 RTIS Origin 是否加入 Accepted Hosts;

  • 网站是否曾被加入 Blocked Hosts。

2. Printer is not accepting job

提示:

Failed to write to device:
javax.print.PrintException:
Printer is not accepting job

这表示 Browser Print 已经找到 Windows 打印队列,但 Java 打印层认为队列不接受新任务。

Java 的 PrinterIsAcceptingJobs 与打印机是否处于 IDLE 是两个不同属性。即使队列看起来空闲,只要它返回 NOT_ACCEPTING_JOBS,Java 仍会拒绝任务。Oracle Java PrintService 文档

3. Unable to establish connection

提示:

Unable to establish connection to
ZDesigner GT800-300dpi EPL

现场后来确认,内网终端上:

  • USB001 实际连接的是另一台打印机;

  • GT800 后来改到了 USB002

  • Browser Print 或 Windows Spooler 仍可能缓存旧端口状态。

处理顺序:

  1. 在 GT800 打印机属性中确认实际端口;

  2. 从同一个 GT800 队列打印 Windows 测试页;

  3. 完全退出 Browser Print;

  4. 重启 Windows Spooler或终端电脑;

  5. 等 Windows 重新识别 USB 打印机;

  6. 普通用户启动 Browser Print;

  7. 在 Browser Print Settings 中重新选择 GT800;

  8. 再从 RTIS 打印。

修改 USB 端口后不一定必须重启电脑,但现场环境下重启通常最稳妥,因为它可以同时刷新:

  • USB 枚举

  • Windows Spooler

  • 驱动端口绑定

  • Browser Print Java PrintService 缓存

4. 管理员启动并不一定有效

Browser Print 已经在后台运行时,再选择“以管理员身份运行”,新实例可能因为 9100/9101 已被旧实例占用而无法真正启动。

正确做法不是重复启动,而是:

  1. 从托盘彻底退出旧实例;

  2. 确认 Zebra 的 javaw.exe 已结束;

  3. 再启动 Browser Print。

一般没有必要使用管理员权限。浏览器和 Browser Print 最好运行在同一个普通终端用户会话中。

最容易发生的误判

误判一:本地电脑正常,所以内网终端也正常

本地开发电脑能打印,只能证明:

  • 本地驱动正确;

  • 本地 USB 正确;

  • 本地 Browser Print 正确;

  • 本地打印机状态正确。

它不能证明内网终端的:

  • USB 端口

  • 打印队列

  • Browser Print 默认设备

  • Accepted Hosts

  • 安全软件策略

  • Windows 用户会话

也正确。

打印问题必须在实际发生故障的终端上检查。

误判二:Windows 测试页成功就等于 Browser Print 一定成功

测试页主要验证:

Windows Spooler → 驱动 → USB → 打印机

Browser Print 还多了一层:

Browser Print内置Java
→ javax.print
→ Windows打印队列

因此测试页成功是必要条件,但不是充分条件。

误判三:跨页就是边距问题

跨页可能来自:

  • Q 标签长度错误;

  • 打印方向是 ZB

  • 参考点 R 被历史任务修改;

  • 条码模块宽度过大;

  • 内容纵坐标靠近标签后沿;

  • 浏览器仍加载旧版 JS;

  • 传感器校准错误。

不能一看到跨页就只调整 CSS margin。

误判四:坐标 0 一定是物理标签头部

如果打印机保留了旧的 RZ 设置,应用中的 (0,0) 只是相对于旧图像缓冲区的原点。

正确做法是每个任务都显式声明坐标系:

R0,0
ZT

最终经验

这次集成最重要的经验不是某一条 EPL 指令,而是分层定位问题。

建议始终按以下顺序排查:

1. 业务条码值是否正确
2. 浏览器是否获得正确条码
3. Browser Print是否运行
4. 网站是否得到访问授权
5. Browser Print选择了哪一个设备
6. Windows测试页是否成功
7. 打印队列绑定的USB端口是否正确
8. FEED能否准确识别标签间隙
9. EPL是否显式设置参考点和方向
10. 浏览器实际加载的是否为最新前端包

浏览器适合业务交互,但不适合承担热敏标签的精确排版。最终稳定方案应当是:

后端确定业务数据
+ 浏览器协调用户操作
+ 官方本地桥接组件
+ 打印机原生指令语言

这比在浏览器打印预览里不断调整 CSS、页面尺寸和边距可靠得多。

本文为个人实践复盘,不构成医疗建议。涉及的患者、医院、厂商、系统名称、数据与时间均已脱敏或调整。