spirngboot + 佳博标签打印机踩坑记录
文章目录
前言:本项目基于
springboot
项目,后端调用打印机,佳博官方提供的Java
版demo
就可以满意整合需求
问题一:打印中文,乱码问题
- 描述:打印中文文字,打印出来的是乱码
- 解决方案:在每次打印前,设置这个参数
System.setProperty("jna.encoding", "GBK");
当时排查时,以为打印机没有这个楷体或者宋体等字体导致的问题,后来翻阅了大量打印机对接代码,突然看到这么一行,拿过来试一试,直接解决。
问题二:打印内容,左边边距坐标不能从(0,0)开始
- 描述:打印内容时,把x轴的坐标设置为0,但是每次打印距离左边都有一定的距离,而且距离有时候还不一样
- 解决方案:一样的参数,但是有时候打印出来的位置看起来有差异,因为卡纸位置没有固定住,用滚轴上的侧边东西把纸固定住;x轴不能从0开始打印,是打印机设计如此,通过佳博提供的官方电脑应用,可以测试出来,预览时,左边距大概默认是2mm;这个只能从产品设计上去规定,打印的内容左边距不能小于
2mm
,然后拿到参数X坐标减去2mm
既可以满足需求
佳博官网提供的打印应用,通过在线客服可以得到下载地址
问题三:打印图片,只能选择PCX和BMP
- 描述:我们产品需求是打印二维码,把二维码图片保存为
png
,jpg
的方法都烂大街了,但是保存为PCX
很少,保存为bmp
的也有,但是如何把bmp
通过代码转化为打印机期望的位深,以及如何发送到打印机内部,这个没研究明白。 - 解决方案:选择了
PCX
,使用的第三方依赖com.github.jai-imageio
,仓库地址
问题四:保存为PCX图片不能打印
- 描述:有时候我们通过代码转化出来的图片是属于RGB的,但是打印机需要的是
Bitmap
的格式的,在new BufferedImage()
时,图片类型设置为BufferedImage.TYPE_BYTE_BINARY
BufferedImage qrCode = new BufferedImage(qrCodeWidth,qrCodeHeight, BufferedImage.TYPE_BYTE_BINARY);
问题五:保存为PCX图片,但是右边有黑色竖线,这个问题独立于打印机,属于代码问题
- 描述:打印的二维码,有些尺寸打印出来有黑色的竖线,但是有的尺寸打印出来没有
- 解决方案:
- 方案一:后来发现只要打印的尺寸,是8的倍数就可以,通过补偿的方式来实现;余数小于等于4则减去余数;余数大于4则减去余数再加8,这样尺寸会有一些偏差,产品上如果能接受,也是可以的
int size = 139;
int remainder = size % 8;
if (remainder <= 4) {
size = size - remainder;
}
if (remainder > 4) {
size = size - remainder + 8;
}
- 方案二:修改
com.github.jai-imageio
的源码,这个方案是在方案一之前尝试的,但是代码注释很少,写的又是很多计算逻辑,也看的不太懂,代码于2018年就停止维护了,后来尝试了一天就战略放弃了,想到了方案一。当方案一处理好之后,灵光一闪,8???好像在代码里见过;直接上PCXImageWriter
的代码
public class PCXImageWriter extends ImageWriter implements PCXConstants {
private ImageOutputStream ios;
private Rectangle sourceRegion;
private Rectangle destinationRegion;
private int colorPlanes, bytesPerLine;
private Raster inputRaster = null;
private int scaleX, scaleY;
public PCXImageWriter(PCXImageWriterSpi imageWriterSpi) {
super(imageWriterSpi);
}
public void setOutput(Object output) {
super.setOutput(output); // validates output
if (output != null) {
if (!(output instanceof ImageOutputStream))
throw new IllegalArgumentException("output not instance of ImageOutputStream");
ios = (ImageOutputStream) output;
ios.setByteOrder(ByteOrder.LITTLE_ENDIAN);
} else
ios = null;
}
public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) {
if (inData instanceof PCXMetadata)
return inData;
return null;
}
public IIOMetadata convertStreamMetadata(IIOMetadata inData, ImageWriteParam param) {
return null;
}
public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) {
PCXMetadata md = new PCXMetadata();
md.bitsPerPixel = (byte) imageType.getSampleModel().getSampleSize()[0];
return md;
}
public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
return null;
}
public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException {
if (ios == null) {
throw new IllegalStateException("output stream is null");
}
if (image == null) {
throw new IllegalArgumentException("image is null");
}
clearAbortRequest();
processImageStarted(0);
if (param == null)
param = getDefaultWriteParam();
boolean writeRaster = image.hasRaster();
sourceRegion = param.getSourceRegion();
SampleModel sampleModel = null;
ColorModel colorModel = null;
if (writeRaster) {
inputRaster = image.getRaster();
sampleModel = inputRaster.getSampleModel();
colorModel = ImageUtil.createColorModel(null, sampleModel);
if (sourceRegion == null)
sourceRegion = inputRaster.getBounds();
else
sourceRegion = sourceRegion.intersection(inputRaster.getBounds());
} else {
RenderedImage input = image.getRenderedImage();
inputRaster = input.getData();
sampleModel = input.getSampleModel();
colorModel = input.getColorModel();
Rectangle rect = new Rectangle(input.getMinX(), input.getMinY(),
input.getWidth(), input.getHeight());
if (sourceRegion == null)
sourceRegion = rect;
else
sourceRegion = sourceRegion.intersection(rect);
}
if (sourceRegion.isEmpty())
throw new IllegalArgumentException("source region is empty");
IIOMetadata imageMetadata = image.getMetadata();
PCXMetadata pcxImageMetadata = null;
ImageTypeSpecifier imageType = new ImageTypeSpecifier(colorModel, sampleModel);
if (imageMetadata != null) {
// Convert metadata.
pcxImageMetadata = (PCXMetadata) convertImageMetadata(imageMetadata, imageType, param);
} else {
// Use default.
pcxImageMetadata = (PCXMetadata) getDefaultImageMetadata(imageType, param);
}
scaleX = param.getSourceXSubsampling();
scaleY = param.getSourceYSubsampling();
int xOffset = param.getSubsamplingXOffset();
int yOffset = param.getSubsamplingYOffset();
// cache the data type;
int dataType = sampleModel.getDataType();
sourceRegion.translate(xOffset, yOffset);
sourceRegion.width -= xOffset;
sourceRegion.height -= yOffset;
int minX = sourceRegion.x / scaleX;
int minY = sourceRegion.y / scaleY;
int w = (sourceRegion.width + scaleX - 1) / scaleX;
int h = (sourceRegion.height + scaleY - 1) / scaleY;
xOffset = sourceRegion.x % scaleX;
yOffset = sourceRegion.y % scaleY;
destinationRegion = new Rectangle(minX, minY, w, h);
boolean noTransform = destinationRegion.equals(sourceRegion);
// Raw data can only handle bytes, everything greater must be ASCII.
int[] sourceBands = param.getSourceBands();
boolean noSubband = true;
int numBands = sampleModel.getNumBands();
if (sourceBands != null) {
sampleModel = sampleModel.createSubsetSampleModel(sourceBands);
colorModel = null;
noSubband = false;
numBands = sampleModel.getNumBands();
} else {
sourceBands = new int[numBands];
for (int i = 0; i < numBands; i++)
sourceBands[i] = i;
}
ios.writeByte(MANUFACTURER);
ios.writeByte(VERSION_3_0);
ios.writeByte(ENCODING);
int bitsPerPixel = sampleModel.getSampleSize(0);
ios.writeByte(bitsPerPixel);
ios.writeShort(destinationRegion.x); // xmin
ios.writeShort(destinationRegion.y); // ymin
ios.writeShort(destinationRegion.x + destinationRegion.width - 1); // xmax
ios.writeShort(destinationRegion.y + destinationRegion.height - 1); // ymax
ios.writeShort(pcxImageMetadata.hdpi);
ios.writeShort(pcxImageMetadata.vdpi);
byte[] smallpalette = createSmallPalette(colorModel);
ios.write(smallpalette);
ios.writeByte(0); // reserved
colorPlanes = sampleModel.getNumBands();
ios.writeByte(colorPlanes);
// 这里有讲究 我的理解就是把宽按照8划分,这里是8bit = 1byte吗?我也不懂,反正就是知道划分处理就对了
bytesPerLine = destinationRegion.width * bitsPerPixel / 8;
// 这里有按照是否是2的倍数,进行了余数补偿,感觉更有讲究了,具体为啥这样做,我理解就是对宽度进行更精准的处理
bytesPerLine += bytesPerLine % 2;
ios.writeShort(bytesPerLine);
if (colorModel.getColorSpace().getType() == ColorSpace.TYPE_GRAY)
ios.writeShort(PALETTE_GRAYSCALE);
else
ios.writeShort(PALETTE_COLOR);
ios.writeShort(pcxImageMetadata.hsize);
ios.writeShort(pcxImageMetadata.vsize);
for (int i = 0; i < 54; i++)
ios.writeByte(0);
// 在这之前的ios.writeXXX 我就默认理解为图片需要设置一些头部信息之类的,就好像TCP报文有报文头和数据部分组成
// 可能我的理解也不对
// write image data
if (colorPlanes == 1 && bitsPerPixel == 1) {
write1Bit();
} else if (colorPlanes == 1 && bitsPerPixel == 4) {
write4Bit();
} else {
write8Bit();
}
// write 256 color palette if needed
if (colorPlanes == 1 && bitsPerPixel == 8 &&
colorModel.getColorSpace().getType() != ColorSpace.TYPE_GRAY) {
ios.writeByte(12); // Magic number preceding VGA 256 Color Palette Information
ios.write(createLargePalette(colorModel));
}
if (abortRequested()) {
processWriteAborted();
} else {
processImageComplete();
}
}
private void write4Bit() throws IOException {
int[] unpacked = new int[sourceRegion.width];
int[] samples = new int[bytesPerLine];
for (int line = 0; line < sourceRegion.height; line += scaleY) {
inputRaster.getSamples(sourceRegion.x, line + sourceRegion.y, sourceRegion.width, 1, 0, unpacked);
int val = 0, dst = 0;
for (int x = 0, nibble = 0; x < sourceRegion.width; x += scaleX) {
val = val | (unpacked[x] & 0x0F);
if (nibble == 1) {
samples[dst++] = val;
nibble = 0;
val = 0;
} else {
nibble = 1;
val = val << 4;
}
}
int last = samples[0];
int count = 0;
for (int x = 0; x < bytesPerLine; x += scaleX) {
int sample = samples[x];
if (sample != last || count == 63) {
writeRLE(last, count);
count = 1;
last = sample;
} else
count++;
}
if (count >= 1) {
writeRLE(last, count);
}
processImageProgress(100.0F * line / sourceRegion.height);
}
}
private void write1Bit() throws IOException {
int[] unpacked = new int[sourceRegion.width];
int[] samples = new int[bytesPerLine];
for (int line = 0; line < sourceRegion.height; line += scaleY) {
inputRaster.getSamples(sourceRegion.x, line + sourceRegion.y, sourceRegion.width, 1, 0, unpacked);
int val = 0, dst = 0;
for (int x = 0, bit = 1 << 7; x < sourceRegion.width; x += scaleX) {
if (unpacked[x] > 0)
val = val | bit;
if (bit == 1) {
samples[dst++] = val;
bit = 1 << 7;
val = 0;
} else {
bit = bit >> 1;
}
}
int last = samples[0];
int count = 0;
// 上文的根据宽度计算的值,在这里进行了使用处理
for (int x = 0; x < bytesPerLine; x += scaleX) {
int sample = samples[x];
if (sample != last || count == 63) {
writeRLE(last, count);
count = 1;
last = sample;
} else
count++;
}
// 关键的地方在这了,这里进行了for循环之后的单独处理
// 像极了生成图片右边的那个竖线逻辑
if (count >= 1) {
// 原来的
// writeRLE(last, count);
// 修改之后的,盲目修改,也不知道修改的对不对,反正生成图片没毛病
writeRLE(samples[0], count);
}
processImageProgress(100.0F * line / sourceRegion.height);
}
}
private void write8Bit() throws IOException {
int[][] samples = new int[colorPlanes][bytesPerLine];
for (int line = 0; line < sourceRegion.height; line += scaleY) {
for (int band = 0; band < colorPlanes; band++) {
inputRaster.getSamples(sourceRegion.x, line + sourceRegion.y, sourceRegion.width, 1, band, samples[band]);
}
int last = samples[0][0];
int count = 0;
for (int band = 0; band < colorPlanes; band++) {
for (int x = 0; x < bytesPerLine; x += scaleX) {
int sample = samples[band][x];
if (sample != last || count == 63) {
writeRLE(last, count);
count = 1;
last = sample;
} else
count++;
}
}
if (count >= 1) {
writeRLE(last, count);
}
processImageProgress(100.0F * line / sourceRegion.height);
}
}
private void writeRLE(int val, int count) throws IOException {
if (count == 1 && (val & 0xC0) != 0xC0) {
ios.writeByte(val);
} else {
ios.writeByte(0xC0 | count);
ios.writeByte(val);
}
}
private byte[] createSmallPalette(ColorModel cm) {
byte[] palette = new byte[16 * 3];
if (!(cm instanceof IndexColorModel))
return palette;
IndexColorModel icm = (IndexColorModel) cm;
if (icm.getMapSize() > 16)
return palette;
for (int i = 0, offset = 0; i < icm.getMapSize(); i++) {
palette[offset++] = (byte) icm.getRed(i);
palette[offset++] = (byte) icm.getGreen(i);
palette[offset++] = (byte) icm.getBlue(i);
}
return palette;
}
private byte[] createLargePalette(ColorModel cm) {
byte[] palette = new byte[256 * 3];
if (!(cm instanceof IndexColorModel))
return palette;
IndexColorModel icm = (IndexColorModel) cm;
for (int i = 0, offset = 0; i < icm.getMapSize(); i++) {
palette[offset++] = (byte) icm.getRed(i);
palette[offset++] = (byte) icm.getGreen(i);
palette[offset++] = (byte) icm.getBlue(i);
}
return palette;
}
}
问题六:修改源码后,如何整合到自己的项目中
- 描述:修改完后,但是如何整合到项目中呢?我本来想是直接把源码重新打包成
jar
,放在lib
目录下,然后在maven
目录下引用,但是感觉不够优雅 - 解决方案:
-
看到源码里是通过插件方式进行拓展
JDK
的ImageIO
,那么我是不是也可以的呢?说干就干,直接copy
源码的PCX
文件到项目中,所有文件加上前缀Custom
,然后resources
目录下,加上文件如下图,内容是类的全路径:com.xxxxx.xxxxx.imageio.pcx.CustomPCXImageWriterSpi
;这里的实现原理建议参考本人另外一篇拙作设计模式之职责链模式——百看不如一练
-
然后注意修改文件类型,避免与源码冲突
-
使用如下
ImageIO.write(bufferedImage, "cpcx", new File("D:\\xxxxx\\xxxx\\" + "im_141_png_new" + ".pcx"));
-