佳博标签打印机最佳实践

佳博标签打印机最佳实践,通过Java调用佳博标签打印机打印内容。

Posted by catmai on July 7, 2024

前言

最近有看到标签打印机的需求,之前做过斑马证卡打印机的联调,想着应该不难,实际少我还是太年轻了。

佳博的标签打印机虽然有很多官方例子和文档,但是没有一个能把Java最佳实践写清楚的,大多都是C或者其他。其实也对,用Java来对打印机二次开发多少是有点强人所难,但是别人有的,我们也要有!

准备工作

先准备以下内容:

  • 佳博的标签打印机 本文用的是 佳博 GP3120TUC
  • 下载动态链接库(Dynamic Link Library)文件
  • 打印机驱动正确安装

1.打印机驱动

请认准官方链接:

佳博驱动

官方还有很多SDK可以参考和下载。

佳博官方SDK DEMO

SDK里面,可以找到JAVA有关的例子,前提条件是dll文件已经配置正确了可以使用。

下载之后正确的安装驱动,这里就不展开了。

2.找到动态链接库文件dll

官方SDK例子里面有dll文件,一个名字叫TSCLIB.dll的文件,这很重要,这关系到Java能否和打印机通信。

注:最新的sdk可以从TSC官网下载,官方SDK不是最新的。

TSC固件下载传送门

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();
}

生成的图片效果如下:

image1

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的一个弹窗,效果如下:

image

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

TSCLIB.INSTANCE.about();

官方回复邮件

总结

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

如果文中出现错误,或者你有更好的观点或者想法,欢迎到评论区讨论~