java 实现标签打印图片

这篇文章来讲讲自己在工作中如何实现在标签打印中打印图片。

背景

为什么会有打印图片的需求?在许多 app 内都会有打印小票的功能,例如美团点外卖后商家会打印小票,小票上可以放商家的 logo、自定义图片等

最近在项目中对接快递鸟发货后需要把快递鸟的返回的打印样式给打印出来。由于发货类型和快递公司非常多,也就存在多套打印模版,我们不可能每次对接一家公司就自己写一套模板,于是就迫切需要打印图片这么一个功能。

系统数据传输

打印操作本质是通过 java 输出字节流到打印机上实现打印,所以我们要做的就是用 java 生成打印机支持指令的字节流,传送给打印机进行打印。

而我们都知道计算机系统间传输数据的基本单位是位(bit,也就是 0 或 1),而通常计算机是以字节(1 字节 = 8 位)为单位来展示字符的,如 ASCII 码。这在 java 中体现为其 I/O 流是以字节为单位进行操作,从 OutputStream 中 write 方法也可以看出来。

位图打印

了解以上后,我们再来看看标签打印的指令集。标签打印通常使用的是 TSPL 指令集,其打印图片的指令在官方文档中如下:

该指令是 bitmap,也就是打印位图,输出 0 打印黑点,输出 1 不打印(白点)。

由于 java 不能输出位,我们只能遍历图片每个像素点。每个像素点输出一个位( 0 或 1),再以 8 个位为一组生成字节数组,所以图片的宽度应调整为 8 的倍数。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ImagePrint {
// 回车换行
private static final byte[] CRLF = new byte[]{13, 10};

public byte[] buildImage(BufferedImage image) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 定义宽高
out.write("SIZE 76 mm,130 mm".getBytes());
out.write(CRLF);
// 打印纸间隙
out.write("GAP 2 mm,0".getBytes());
out.write(CRLF);
// 打印方向
out.write("DIRECTION 0".getBytes());
out.write(CRLF);
out.write("SET PEEL OFF".getBytes());
out.write(CRLF);
out.write("CLS".getBytes());
out.write(CRLF);

// 宽度 76 为字节数组长度
// 高度 1040 = 130 * 8
byte[] lineBytes = "BITMAP 0,0,76,1040,0,".getBytes();
out.write(lineBytes);

// byteBinary 方法调整图片为黑白图
// resize 方法将图片放大 8 倍
image = ImageUtil.resize(ImageUtil.byteBinary(image), 76 * 8);
byte[] bytes = ImageUtil.encodeImage(image);
out.write(bytes);
out.write(CRLF);

out.write("PRINT 1".getBytes());
out.write(CRLF);
out.write("END".getBytes());
out.write(CRLF);

return out.toByteArray();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class ImageUtil {

public static byte[] encodeImage(BufferedImage image) throws IOException {
int width = image.getWidth() / 8 * 8;
int height = image.getHeight() / 8 * 8;

byte[] bytes = new byte[width / 8 * height];
int idx = 0;
StringBuilder stringBuilder = new StringBuilder();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int pixel = image.getRGB(x, y);
// 白色
if (pixel == 0xFFFFFFFF) {
stringBuilder.append("1");
} else {
stringBuilder.append("0");
}
if (stringBuilder.length() == 8) {
// 转为字节
bytes[idx++] = (byte) Integer.parseInt(stringBuilder.toString(), 2);
// 重置
stringBuilder = new StringBuilder();
}
}
}

return bytes;
}

/**
* 从给定的 URL 读取图片,并返回 BufferedImage 对象。
*
* @param imageUrl 图片的 URL 地址
* @return 读取到的 BufferedImage 对象
*/
public static BufferedImage loadImage(String imageUrl) throws IOException {
URL url = new URL(imageUrl);
return ImageIO.read(url);
}

public static BufferedImage byteBinary(BufferedImage image) throws IOException {
BufferedImage bwImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_BINARY);

Graphics2D g = bwImage.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();

return bwImage;
}

public static BufferedImage resize(BufferedImage image, int targetWidth) {
// 计算高度以保持宽高比
int targetHeight = (int) (image.getHeight() * ((double) targetWidth / image.getWidth()));

// 创建新的图片,按宽度等比例缩放
BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, image.getType());

Graphics2D g = resizedImage.createGraphics();
g.drawImage(image, 0, 0, targetWidth, targetHeight, null);
g.dispose();

return resizedImage;
}
}