前言
最近有看到标签打印机的需求,之前做过斑马证卡打印机的联调,想着应该不难,实际少我还是太年轻了。
佳博的标签打印机虽然有很多官方例子和文档,但是没有一个能把Java最佳实践写清楚的,大多都是C或者其他。其实也对,用Java来对打印机二次开发多少是有点强人所难,但是别人有的,我们也要有!
准备工作
先准备以下内容:
- 佳博的标签打印机 本文用的是 佳博 GP3120TUC
- 下载动态链接库(Dynamic Link Library)文件
- 打印机驱动正确安装
1.打印机驱动
请认准官方链接:
官方还有很多SDK可以参考和下载。
SDK里面,可以找到JAVA有关的例子,前提条件是dll文件已经配置正确了可以使用。
下载之后正确的安装驱动,这里就不展开了。
2.找到动态链接库文件dll
官方SDK例子里面有dll文件,一个名字叫TSCLIB.dll的文件,这很重要,这关系到Java能否和打印机通信。
注:最新的sdk可以从TSC官网下载,官方SDK不是最新的。
3.正确安装打印机驱动
以windows为例,能够达到插上打印机USB接口并且接通了电源之后,windows的打印机列表里面存在打印机就行了,如果不放心可以使用 windows打印机列表里的打印测试页尝试打印。
这里有个小插曲,我从犄角旮旯的地方找到了一个打印机的web demo。里面有个使用文档,说需要使用zading-2.8.exe 重新安装打印机驱动。 如果有人也看到了这个demo请注意,按照文档使用之后可以通过js库调用打印机,但是无法再让windows或者java通过驱动文件找到打印机了,此时打印机变成了一个[usb printer]。这里的差距我暂时还不知道,而且我仿佛也没有什么办法把他变回原状(尝试过删除驱动重新安装….貌似都没有效果。)有懂的大哥可以留言交流一下。
二次开发
关于打印
打印我的建议和习惯是把要打印的内容输出成位图(bitmap),然后发送给打印机进行打印。这样可以避免字符问题和排版不一致,并且大多数现代打印机都可以支持图片输入(例如斑马的证卡打印机,佳博的标签打印机),位图唯一的缺点是需要计算宽高,需要渲染一张和打印纸相同大小的图片,并且边距可能会发生些许变化,但是相比字符集问题和排版一点一点调整我选择位图。位图的排版可以通过生成图片来看效果,不用一直尝试打印看效果。
Java依赖
通过maven管理依赖可以引入以下内容:
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>3.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>1.0-alpha1</version>
</dependency>
第一个jna是java读取dll并执行相关函数的库。JNA
第二个commons-imaging稍后做解释。
生成位图
生成位图Java还是可以做的,java.awt包下的类可以绘制图片。下面给一个例子绘制一个表格的图片。
//图片总宽
private static final int width = 550;
//图片总高度
private static final int height = 400;
/**
* 表格左右边距
*/
public static final int padding = 20;
/**
* 表格起始位置
*/
public static final int margin_top = 50;
/**
* 表格距离底部距离
*/
public static final int margin_bottom = 10;
public static BufferedImage createBitmapWithTable(String[][] content) {
// 创建一个空的BufferedImage
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 创建Graphics2D对象
Graphics2D graphics = image.createGraphics();
// 设置背景颜色为黑色
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, width, height);
// 设置画笔颜色为白色
graphics.setColor(Color.black);
graphics.setStroke(new java.awt.BasicStroke(2));
// 设置字体
Font font = new Font("宋体", Font.BOLD, 18);
graphics.setFont(font);
FontMetrics metrics = graphics.getFontMetrics(font);
// 表格行数和列数
int rows = 4;
int cols = 4;
// 行高和列宽
int rowHeight = (height - 100 - margin_bottom - margin_top) / 4;
int colWidth = (width - 2 * padding) / cols;
// 绘制表格
// 画横线
//起始位置
int currentRow = 0;
for (int i = 0; i <= rows; i++) {
if (i == 3){
currentRow += rowHeight + 100;
}else if (i == 0){
//起始位置
currentRow = margin_top;
} else {
currentRow += rowHeight;
}
graphics.drawLine(padding, currentRow, width - padding, currentRow);
}
//画竖线
int currentCol = 0;
for (int i = 0; i <= cols; i++) {
//设置竖线长度
int distance = height - margin_bottom;
//单元格合并 竖线少画几个
if (i > 1 && i < 4){
distance = rowHeight * 2 + margin_top;
}
if (i == 0) {
currentCol = padding;
}else if (i == 1 || i == 3){
//第一列和第三列短一点
currentCol += (colWidth - 20);
}else {
// 第二列和第四列宽一点
currentCol += colWidth + 20;
}
graphics.drawLine(currentCol, margin_top, currentCol, distance);
}
//内容起始行
int currentContentRow = 0;
for (int row = 0; row < content.length; row++) {
if (row == 0){
currentContentRow = margin_top;
}else if (row == 3){
currentContentRow += rowHeight + 100;
}else {
currentContentRow += rowHeight;
}
//内容起始列
int currentContentCol = 0;
for (int col = 0; col < content[row].length; col++) {
//一行一行来
if (col == 0){
currentContentCol = padding;
}else if (col == 1 || col == 3){
currentContentCol += (colWidth - 20);
}else {
currentContentCol += colWidth + 20;
}
String text = content[row][col];
// 左边距
int x = currentContentCol + 10;
// 上边距
int y = currentContentRow + metrics.getHeight();
// 绘制多行文本 这里需要考虑如果太长超出单元格,需要自己换行
// 额外行数
int extraLines = 0;
if (text.contains("\n")) {
String[] lines = text.split("\n");
for (int i = 0; i < lines.length; i++) {
String textItem = autoWrapText(lines[i],font, colWidth * 3);
int yPosition = y + (i + extraLines) * metrics.getHeight();
if (textItem.contains("\n")){
String[] itemArray = textItem.split("\n");
for (int j = 0; j < itemArray.length; j++) {
String item = itemArray[j];
yPosition = yPosition + j * metrics.getHeight();
graphics.drawString(item, x, yPosition);
}
extraLines += itemArray.length - 1;
}else {
graphics.drawString(textItem, x, yPosition);
}
}
} else {
graphics.drawString(text, x, y);
}
}
}
// 释放资源
graphics.dispose();
return image;
}
/**
* 文本自动换行
* @param text 文本内容
* @param font 字体
* @param maxWidth 最大宽度(超出换行)
*/
public static String autoWrapText(String text, Font font, int maxWidth) {
StringBuilder sb = new StringBuilder();
int lineWidth = 0;
// 创建一个临时的 BufferedImage 用于获取 Graphics 对象
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
g.setFont(font);
FontMetrics metrics = g.getFontMetrics(font);
String[] words = text.split(""); // Split by each character
for (String word : words) {
int wordWidth = metrics.stringWidth(word);
if (lineWidth + wordWidth > maxWidth) {
sb.append("\r\n");
lineWidth = 0;
}
sb.append(word);
lineWidth += wordWidth;
}
// 释放 Graphics 资源
g.dispose();
return sb.toString();
}
生成的图片效果如下:

BufferedImage生成完成后,我们可以把它写到文件里面,或者输出到浏览器。这里我选择写到磁盘:
//选一个好地方存,这里忽略
String pngPath = "xxxx.png";
//选一个好地方存,这里忽略
String pcxPath = "xxxx.pcx";
BufferedImage image = createBitmapWithTable(content);
File outputfile = new File(pngPath);
ImageIO.write(image, "bmp", outputfile);
logger.info("图片输出到:" + outputfile.getAbsolutePath());
重点(踩坑点一)
接下来遇到了打印机的第一个坑点
首先问一个问题,你了解什么是位图么?我最开始也是一知半解,位图就是通过比特位描述的图片?来看一下微软的解释,微软的介绍,位图类型可以看到,我们常见的jpeg、png、gif也是位图,这也是为什么我们java代码保存文件通过png来存储。(当然也可以用bmp格式文件保存BufferedImage)
第二个问题,什么是pcx图片?在佳博打印机的官方demo中,有一个打印图片的例子。他上传了一张pcx格式的图片到打印机,然后通过命令让打印机打印出来了。这是一个很小众的图片格式,我之前确实是没有见过。
如果你也不知道pcx图片是什么,让我们看一下简介:
PCX 格式是一种受到广泛支持的位图格式。PCX 不存储灰度或颜色更正表格。对于连续色调图像,其游长压缩方案的效率可能较低。由于 PCX 的不断成熟和发展,PCX 文件可使用各种调色板技术。结果,一些阅读程序无法处理所有可能的 PCX 执行。
这里有百度百科的传送门:pcx图片百度百科介绍
实在搞不懂也没关系,我们现在可以输出png这样的高级位图,打印机需要的是技术更旧的pcx位图,那么,我们需要将png转换为pcx。在最初我们在maven里声明的依赖在这里发挥了作用。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>1.0-alpha1</version>
</dependency>
ps:感谢apache基金会,哭了
pps:在maven中央仓库看,这个包今年也有新版本推出,已经来到了alpha5版本大家有兴趣可以试一下其他版本。
这个包里提供了图片格式转换工具,可以将位图直接转换为pcx图片。
所以我们的代码续写为这样:
//选一个好地方存,这里忽略
String pngPath = "xxxx.png";
//选一个好地方存,这里忽略
String pcxPath = "xxxx.pcx";
BufferedImage image = createBitmapWithTable(content);
File outputfile = new File(pngPath);
//这一行可以省略,png图片用不着了,但是因为pcx图片我打不开,所以还是输出png图片看效果
ImageIO.write(image, "bmp", outputfile);
logger.info("图片输出到:" + outputfile.getAbsolutePath());
// 创建 PCX 文件
File pcxFile = new File(pcxPath);
// 转换并保存为 PCX 格式
Imaging.writeImage(image, pcxFile, ImageFormats.PCX, null);
logger.info("PNG 图像已成功转换并保存为 PCX 格式: " + pcxPath);
return pcxPath;
打印
打印有一个关键的部分,需要Java操作dll文件,所以我们声明一个interface:
import com.catmai.detect.util.config.GprintConfig;
import com.sun.jna.Library;
import com.sun.jna.Native;
public interface TSCLIB extends Library {
//这里的GprintConfig.getDillPath() 就是dll文件的完整路径,我给放到yml文件里了。
//如果不放这里就是:C://xxx/TSCLIB.dll
TSCLIB INSTANCE = (TSCLIB) Native.load(
GprintConfig.getDillPath(),
TSCLIB.class
);
int about ();
int openport (String pirnterName);
int closeport ();
int sendcommand (String printerCommand);
int sendBinaryData (byte[] printerCommand, int CommandLength);
int setup (String width,String height,String speed,String density,String sensor,String vertical,String offset);
//下载pcx文件到打印机
int downloadpcx (String filename,String image_name);
int barcode (String x,String y,String type,String height,String readable,String rotation,String narrow,String wide,String code);
int printerfont (String x,String y,String fonttype,String rotation,String xmul,String ymul,String text);
int clearbuffer ();
int printlabel (String set, String copy);
int windowsfont (int x, int y, int fontheight, int rotation, int fontstyle, int fontunderline, String szFaceName, String content);
int windowsfontUnicode(int x, int y, int fontheight, int rotation, int fontstyle, int fontunderline, String szFaceName, byte[] content);
int windowsfontUnicodeLengh(int x, int y, int fontheight, int rotation, int fontstyle, int fontunderline, String szFaceName, byte[] content, int length);
byte usbportqueryprinter();
}
这里所有的方法都是TSCLIB里有的我们才能用,关系到jna的实现和原理,具体可以看github:JNA
以下是简介:
JNA 全称 Java Native Access,是一个建立在经典的 JNI 技术之上的 Java 开源框架。JNA 提供一组 Java 工具类用于在运行期动态访问系统本地库(native library:如 Window 的 dll)而不需要编写任何 Native/JNI 代码。开发人员只要在一个 java 接口中描述目标 native library 的函数与结构,JNA 将自动实现 Java 接口到native function 的映射。
有了工具之后,我们就可以再封装一层,抽象成传入pcx图片地址,直接调用打印机打印。
/**
* 打印
* @param outputPathPcx
*/
public static void print(String outputPathPcx){
//TSCLIB.INSTANCE.about();
log.info("=====准备打印=====");
TSCLIB.INSTANCE.openport("GP-3120TUC");
log.info("=====开启端口=====");
TSCLIB.INSTANCE.setup("70","50","4","8","0","3.0","0");
log.info("=====初始化完毕=====");
TSCLIB.INSTANCE.clearbuffer();
log.info("=====清空缓存区=====");
TSCLIB.INSTANCE.downloadpcx(outputPathPcx, "IMG.PCX");
log.info("=====发送图片=====");
TSCLIB.INSTANCE.sendcommand("PUTPCX 1,1,\"IMG.PCX\"");
log.info("=====设置打印脚本=====");
TSCLIB.INSTANCE.printlabel("1", "1");
log.info("=====开始打印=====");
TSCLIB.INSTANCE.closeport();
log.info("=====关闭端口=====");
}
需要注意的是,这里的GP-3120TUC,需要和windows打印机列表里的名字一样,点击windows设备管理,找到打印机,确保自己正确安装了打印机的驱动,这样可以在列表中看到打印机名称,如果你要改成其他的,去打印机属性里面更改,代码里同步也得更改掉。
TSCLIB.INSTANCE.openport("GP-3120TUC");
//TSC的说明是这样的:pirnterName
int openport (String pirnterName);
小插曲(踩坑点二)
我在打印过程中一直在弹出DLL Version的一个弹窗,效果如下:

每次调用打印的时候,都会弹出这样一个弹窗,不点击确认的话,打印过程会卡住。为此我找了很多资料并不知道是什么情况,甚至还给TSC官方写了邮件。在此先感谢TSC官方的耐心,他回复了我的邮件,最终我们确认了,about()函数的作用就是弹出DLL Version。诶,我还以为他的返回值是about的信息,没想到是直接弹出。在此记录一下,以防有其他人遇到相同的问题。
TSCLIB.INSTANCE.about();

总结
记录了一下使用Java对TSC系列打印机二次开发的过程,其中有一些小问题出现给出了解决方法。开发过程中需要用到的依赖也都汇总了一下,方便查找。Java操作一些硬件设备的例子网上都比较少,遇到问题很难找到相应的解决方案。大部分硬件制造商都不会关注到Java端的二次开发,他们也很少会推出相应的SDK,这都可以理解,毕竟Java来操作这些设备终究还是少数。但是回到开头,别人有的我们Java也一定要有!
如果文中出现错误,或者你有更好的观点或者想法,欢迎到评论区讨论~