一、背景
这次需求看起来很简单:增加一个“打印条码”按钮,通过终端电脑连接的 Zebra 打印机打印患者 ID。
真正实施后,问题却横跨了多个层面:
B/S 系统访问终端本地 USB 打印机
浏览器打印预览与物理标签不一致
Windows 打印驱动、USB 端口与打印队列
Zebra Browser Print 的设备发现与站点授权
EPL 坐标、旋转、标签长度和打印机持久设置
生产环境静态资源缓存
第一版:浏览器生成 SVG,调用系统打印窗口
最初采用了纯浏览器方案:
前端自己编码 Code 128;
生成 SVG;
创建隐藏 iframe;
使用
window.print();在 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=300y=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 Hosts。Browser Print 用户指南
默认打印机与 Windows 默认打印机不同
Browser Print 自己维护默认设备,不等于 Windows 系统默认打印机。
实际代码采用:
先读取 Browser Print 默认打印机;
如果默认打印机不是 GT800,则枚举本地打印机;
只接受精确名称或同时包含
GT800、EPL的设备;不允许误发到 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,或浏览器仍然缓存旧入口文件。
解决方式:
完整替换整个
dist;不能只替换某一个带哈希的 JS;
确认服务器
index.html引用了新哈希文件;关闭页面重新打开,或强制刷新;
必要时直接下载服务器 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 仍可能缓存旧端口状态。
处理顺序:
在 GT800 打印机属性中确认实际端口;
从同一个 GT800 队列打印 Windows 测试页;
完全退出 Browser Print;
重启 Windows Spooler或终端电脑;
等 Windows 重新识别 USB 打印机;
普通用户启动 Browser Print;
在 Browser Print Settings 中重新选择 GT800;
再从 RTIS 打印。
修改 USB 端口后不一定必须重启电脑,但现场环境下重启通常最稳妥,因为它可以同时刷新:
USB 枚举
Windows Spooler
驱动端口绑定
Browser Print Java PrintService 缓存
4. 管理员启动并不一定有效
Browser Print 已经在后台运行时,再选择“以管理员身份运行”,新实例可能因为 9100/9101 已被旧实例占用而无法真正启动。
正确做法不是重复启动,而是:
从托盘彻底退出旧实例;
确认 Zebra 的
javaw.exe已结束;再启动 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 一定是物理标签头部
如果打印机保留了旧的 R 或 Z 设置,应用中的 (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、页面尺寸和边距可靠得多。