一次菜鸟电子面单蓝牙打印改造总结
前言
菜鸟电子面单平台停止蓝牙打印服务订购,很巧合,公司就使用到了他,一个是PC菜鸟组件,一个是蓝牙打印,后者的停用会对业务上产生一些影响,所以也要对其做一些业务上处理。
菜鸟电子面单公告:
尊敬的开发者:
您好!为进一步落实国家、地方法律、法规关于加强消费者个人信息保护以及规范数据处理行为的具体要求,我公司自2023年6月30日起停止蓝牙打印服务订购,6月30前订购的蓝牙打印服务在有效期内仍可继续使用。另外,您可继续使用装鸟提供的4G云打印服务及PC菜鸟组件有线打印服务,不受影响。
如您对蓝牙打印接口关闭事宜有任何疑问,可加入菜鸟钉钉群(群号:44766762)进行沟通。
感谢您对平台的支持与关注!浙江菜鸟供应链管理有限公司
2023年5月12日
问题分析
为了减少对业务的影响,做了并行的两套方案:
1、直接停止此服务,让用户去PC端打印,并做好提示;
2、自己解析菜鸟模板的打印指令。
如果能完成模板指令的解析,就直接上指令解析,那是非常用完美的,对用户零影响,无缝平滑过去;当然,解析菜鸟模板的指令也不是一件简单的事情,时间也很紧张,只有一周多的时间,所以也准备也第二套方案,不能上指令解析,就让用户去PC端打印,同时友好地提示用户。
一些重要信息的整理
整理系统里的订阅物流公司,目前需要处理以下物流就可以:
中通速递 ZTO
申通快递 STO
韵达快递 YUNDA
圆通速递 YTO
德邦 DBKD
菜鸟电子面单解析分析过程
以http://cloudprint.cainiao.com/template/standard/401为例,分析菜鸟电子面单的解析过程。
整体模板主要以js语法和xml标签为主,所以有可能是先使用js模板引擎解析,再解析xml成指令:
模板 + 数据 -> 通过js模板引擎 -> xml 模板 -> 通过解析 xml 模板成指令
猜测大概的过程可能是这样的,接下来我们就验证整个过程是否可行。
验证js模板引擎
通过查找和验证,发现ejs对以上物流公司的蓝牙模板有效,可以正确地解析成xml。
注意:
这个模板引擎不是能全部兼容模板,但是满足我们的需求了,同时由于时间问题,就没有继续验证其他的js模板引擎。
模板引擎是 ejs,地址 https://ejs.bootcss.com/#install
<meta charset="UTF-8">
<script src="ejs.js"></script>
<script>
// 需要自己按照菜鸟接口文档填入数据
let data = {}
let config = {
"needMiddleLogo":"false",
"horizontalCoordinate":"0",
"verticalCoordinate":"0",
"needBottomLogo":"false",
"orientation":"normal",
"extra":"{watermark,true}",
"needTopLogo":"false"
};
// 需要自己填入模板内容,示例模板 http://cloudprint.cainiao.com/template/standard/401
let template = "";
// 实现菜鸟模板中使用到的js函数
context = {
formatStartTime(format) {
const date = new Date(); // 获取当前时间
// 提取年、月、日、小时、分钟和秒
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
// 替换格式字符串中的对应部分
let formattedTime = format.replace('yyyy', year);
formattedTime = formattedTime.replace('MM', month);
formattedTime = formattedTime.replace('dd', day);
formattedTime = formattedTime.replace('HH', hours);
formattedTime = formattedTime.replace('mm', minutes);
formattedTime = formattedTime.replace('ss', seconds);
return formattedTime;
},
documentCount() {
return "1";
},
documentNumber() {
return "1";
}
};
// 解析
html = ejs.render(template, {"_data": data.data, "_config":config, "_context":context});
console.log(html)
</script>
到此,我们的模板已经解析成xml了,接下来就是解析成CPCL指令。
xml 解析成CPCL指令
打印机打印指令的单位是dot,一般8dot=1毫米,所以猜测模板是以毫米为单位的,就按照这个内容来搞个demo,验证是否可行。
<page
xmlns="http://cloudprint.cainiao.com/print"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://cloudprint.cainiao.com/print http://cloudprint-docs-resource.oss-cn-shanghai.aliyuncs.com/lpml_schema.xsd"
xmlns:editor="http://cloudprint.cainiao.com/schema/editor"
width="100" height="180" splitable="true" />
基础指令解析
public class CpclCommand {
public final static String CHARSET = "GB18030";
/** 指令内容 */
private Vector<Byte> command;
public CpclCommand() {
this.command = new Vector<>();
}
/**
* 增加指令
* @param array 数组
*/
public void addCommand(byte[] array) {
if(array == null) {
return;
}
List<Byte> byteList = Bytes.asList(array);
this.command.addAll(command.size(), byteList);
}
/**
* 增加指令
* @param index 索引
* @param array 数组
*/
public void addCommand(int index, byte[] array) {
if (array == null) {
return;
}
List<Byte> byteList = Bytes.asList(array);
this.command.addAll(index, byteList);
}
/**
* 增加指令
* @param bytes 指令字节
*/
public void addCommand(Vector<Byte> bytes) {
if(CollectionUtils.isEmpty(bytes)) {
return;
}
this.command.addAll(bytes);
}
/**
* 增加指令
* @param command CPCL指令
*/
public void addCommand(CpclCommand command) {
if(command == null) {
return;
}
Vector<Byte> bytes = command.getCommand();
if(!CollectionUtils.isEmpty(bytes)) {
this.command.addAll(bytes);
}
}
/**
* 添加文本指令
* @param str 文本内容
*/
private void addStrToCommand(String str) {
if (StringUtil.isBlank(str)) {
return;
}
byte[] bs = str.getBytes(Charset.forName(CHARSET));
for (int i = 0; i < bs.length; i++) {
this.command.add(Byte.valueOf(bs[i]));
}
}
public Vector<Byte> getCommand() {
return command;
}
public void init(int width, int height, int copies) {
StringJoiner sj = new StringJoiner(" ", "", "\r\n");
sj.add("!").add("0").add("200").add("200")
// 高度减去18个点
.add(String.valueOf(height - 18))
.add(String.valueOf(copies));
addStrToCommand(sj.toString());
// 打印宽度
String s = new StringJoiner(" ", "", "\r\n").add("PAGE-WIDTH").add(String.valueOf(width)).toString();
addStrToCommand(s);
}
public void setCn() {
// 设置中文
addStrToCommand("COUNTRY CHINA\r\n");
}
public void print() {
addStrToCommand("GAP-SENSE\r\n");
addStrToCommand("FORM\r\n");
addStrToCommand("PRINT\r\n");
}
/** 文本指令方向 */
public enum TEXT {
TEXT("T"),
VTEXT("TV"),
TEXT90("T90"),
TEXT180("T180"),
TEXT270("T270");
private String name;
TEXT(String name) {
this.name = name;
}
}
/** 字体大小 */
public enum FONT {
F0("0", 12, 24, 24),
F1("1", 9, 17, 24),
F2("2", 12, 24, 24),
F3("3", 10, 20, 20),
F4("4", 16, 32, 32),
F5("5", 9, 17, 24),
F6("6", 12 ,24, 0),
F7("7", 12 ,24, 24),
F8("8", 12 ,24, 24),
F10("10", 24, 48, 48),
F11("11", 8, 16, 24),
F13("13", 12 ,24, 24),
F20("20", 8, 16, 16),
F24("24", 12 ,24, 24),
F41("41", 8, 12, 0),
F42("42", 12, 20, 0),
F43("43", 16, 24, 0),
F44("44", 24, 32, 0),
F45("45",32,48, 0),
F46("46",14,19, 0),
F47("47",21,27, 0),
F48("48",14,25, 0),
F49("49",28,56, 0),
F55("55",8,16, 16)
;
private String value;
private int width;
private int cnWidth;
private int height;
FONT(String value, int width, int height, int cnWidth) {
this.value = value;
this.width = width;
this.cnWidth = cnWidth;
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public int getCnWidth() {
return cnWidth;
}
}
/**
* 文本指令 {command} {font} {size} {x} {y} {data}
* @param text 指令
* @param font 字体
* @param size 请输入任意数字
* @param x 横向起始位置
* @param y 纵向起始位置
* @param data 要打印的文本
*/
public void text(TEXT text, FONT font, int size, int x, int y, String data) {
if (StringUtil.isBlank(data)) {
data = " ";
}
StringJoiner sj = new StringJoiner(" ", "", "\r\n");
sj.add(text.name).add(font.value).add(String.valueOf(size)).
add(String.valueOf(x)).add(String.valueOf(y)).add(data);
this.addStrToCommand(sj.toString());
}
public void addSimulateBlock(TEXT text, FONT font, int size, int x, int y, String data, int width, int xScal, int yScal, int gap, boolean isCenter) {
// 只支持正方向
if (StringUtil.isNotBlank(data)) {
xScal = xScal <= 0 ? 1 : xScal;
int index = getStrIndex(data, width, font, xScal);
if (index <= 0) {
return;
}
// 换行
int i = data.indexOf("\n");
if (i > -1 && index > i) {
index = i;
data = data.replaceFirst("\n", "");
}
String printSubData = data.substring(0, index).replaceFirst("\n", "");
if (isCenter) {
int strLen = getStrLen(printSubData, font, xScal);
int posX = x + (width - strLen) / 2;
text(text, font, size, posX, y, printSubData);
} else {
text(text, font, size, x, y, printSubData);
}
if (index < data.length()) {
int nextY = 0;
if (gap == 0) {
nextY = y + font.getHeight() * yScal * 3 / 2;
} else {
nextY = y + font.getHeight() * yScal + gap;
}
String nextText = data.substring(index);
addSimulateBlock(text, font, size, x, nextY, nextText, width,xScal, yScal, gap, isCenter);
}
}
}
public int calcSimulateBlockNextY(TEXT text, FONT font, int size, int x, int y, String data, int width, int xScal, int yScal, int gap) {
// 只支持正方向
if (StringUtil.isNotBlank(data)) {
xScal = xScal <= 0 ? 1 : xScal;
int index = getStrIndex(data, width, font, xScal);
if (index <= 0) {
return y;
}
// 换行
int i = data.indexOf("\n");
if (i > -1 && index > i) {
index = i;
data = data.replaceFirst("\n", "");
}
if (index < data.length()) {
int nextY = 0;
if (gap == 0) {
nextY = y + font.getHeight() * yScal * 3 / 2;
} else {
nextY = y + font.getHeight() * yScal + gap;
}
String nextText = data.substring(index);
return calcSimulateBlockNextY(text, font, size, x, nextY, nextText, width,xScal, yScal, gap);
}
return y;
}
return y;
}
/**
* 获取字体宽度,数字和中文宽度不一样
* @param str 字符串
* @param width 可打印宽度
* @param font 字体宽度
* @param xScal 放大宽度
* @return 可打印字符串长度索引
*/
private int getStrIndex(String str, int width, FONT font, int xScal) {
char[] chars = str.toCharArray();
int curWidth = 0;
for (int i = 0; i < chars.length; i++) {
if ((chars[i] + "").getBytes().length == 1) {
curWidth += font.getWidth() * xScal;
} else {
curWidth += font.getCnWidth() * xScal;
}
if (width < curWidth) {
return i;
}
}
return chars.length;
}
private int getStrLen(String str, FONT font, int xScal) {
char[] chars = str.toCharArray();
int curWidth = 0;
for (int i = 0; i < chars.length; i++) {
if ((chars[i] + "").getBytes().length == 1) {
curWidth += font.getWidth() * xScal;
} else {
curWidth += font.getCnWidth() * xScal;
}
}
return curWidth;
}
/**
* 命令可将常驻字体放大指定的放大倍数,使用于TEXT命令的上一行
* @param width 宽度放大倍数,有效放大倍数为 1 到 16
* @param height 高度放大倍数,有效放大倍数为 1 到 16
*/
public void setMag(int width, int height) {
StringJoiner sj = new StringJoiner(" ", "", "\r\n");
sj.add("SETMAG").add(String.valueOf(width)).add(String.valueOf(height));
this.addStrToCommand(sj.toString());
}
/**
* 命令可将常驻字体加粗
* @param bold 是否加粗
*/
public void setBlod(boolean bold) {
StringJoiner sj = new StringJoiner(" ", "", "\r\n");
sj.add("SETBLOD").add(bold ? "1" : "0");
this.addStrToCommand(sj.toString());
}
/**
* 条码类型
* BARCODE(或 B) 横向打印条码
* VBARCODE(或 VB) 纵向打印条码
*/
public enum BARCODE {
BARCODE("B"), VBARCODE("VB");
private String command;
BARCODE(String command) {
this.command = command;
}
}
/**
* 条码类型
*/
public enum CODE_TYPE {
UPC_A("UPCA"), UPC_E("UPCE"),EAN13("EAN13"),EAN8("EAN8"),
CODE39("39"),CODE93("93"), CODE128("128"),CODABAR("CODABAR");
private String type;
CODE_TYPE(String type) {
this.type = type;
}
}
/**
* 条码宽高比例
*/
public enum RATIO {
R0("0", 1.5F), R1("1", 2), R2("2", 2.5F), R3("3", 3), R4("4", 3.5F),
R20("20", 20), R21("21", 21), R22("22", 22), R23("23", 23), R24("24", 24),
R25("25", 25), R26("26", 26), R27("27", 27), R28("28", 28), R29("29", 29),
R30("30", 30);
private String key;
private float value;
RATIO(String key, float value) {
this.key = key;
this.value = value;
}
public float getValue() {
return value;
}
}
public enum EEC {
LEVEL_L("L"), LEVEL_M("M"), LEVEL_Q("Q"), LEVEL_H("H");
private final String value;
EEC(String value) { this.value = value; }
public String getValue() { return this.value; }
}
/**
* 条码
* @param barcode 条码指令
* @param codeType 条码类型
* @param width 条码宽度
* @param ratio 宽高比例
* @param height 条码高度
* @param x 条码X方向
* @param y 条码Y方向
* @param data 条码数据
*/
public void barcode(BARCODE barcode, CODE_TYPE codeType, int width, RATIO ratio, int height, int x, int y, String data) {
StringJoiner sj = new StringJoiner(" ", "", "\r\n");
sj.add(barcode.command).add(codeType.type).add(String.valueOf(width)).add(ratio.key).add(String.valueOf(height))
.add(String.valueOf(x)).add(String.valueOf(y)).add(data);
this.addStrToCommand(sj.toString());
}
/**
* 条码底部文本
* @param font 字体
* @param size 大小
* @param offset 文本距离条码的单位偏移量
*/
public void barcodeText(FONT font, int size, int offset) {
StringJoiner sj = new StringJoiner(" ", "", "\r\n");
sj.add("BT").add(font.value).add(String.valueOf(size)).add(String.valueOf(offset));
this.addStrToCommand(sj.toString());
}
/**
* 关闭条码底部文本
*/
public void barcodeTextOff() {
this.addStrToCommand("BT OFF\r\n");
}
/**
* 二维码
* @param barcode 条码指令方向
* @param x x
* @param y y
* @param mm QR Code 规范编号,1 或 2,推荐为 2
* @param un 模块的单位宽度/单位高度 1-32,默认为 6
* @param eec 纠错等级 H - 极高可靠性级别(H 级) Q - 高可靠性级别(Q 级) M - 标准级别(M 级) L - 高密度级别(L 级)
* @param data 打印数据
*/
public void qrCode(BARCODE barcode, int x, int y, int mm, int un, EEC eec, String data) {
StringJoiner sj = new StringJoiner(" ", "", "\r\n");
sj.add(barcode.command).add("QR").add(String.valueOf(x)).add(String.valueOf(y))
.add("M").add(String.valueOf(mm)).add("U").add(String.valueOf(un));
addStrToCommand(sj.toString());
// 二维码数据
addStrToCommand(eec.value + "A," + data + "\r\n");
// 结束二维码
addStrToCommand("ENDQR\r\n");
}
/**
* 用户可以使用 BOX 命令生成具有指定线条宽度的矩形。
* @param x0 左上角的 X 坐标
* @param y0 左上角的 Y 坐标
* @param x1 右下角的 X 坐标
* @param y1 右下角的 Y 坐标
* @param width 形成矩形框的线条的单位宽度 1
*/
public void box(float x0, float y0, float x1, float y1, float width) {
StringJoiner sj = new StringJoiner(" ", "", "\r\n");
sj.add("BOX").add(String.valueOf(x0)).add(String.valueOf(y0)).
add(String.valueOf(x1)).add(String.valueOf(y1)).add(String.valueOf(width));
addStrToCommand(sj.toString());
}
/**
* 使用 LINE 命令可以绘制任何长度、宽度和角度方向的线条。
* @param x0 左上角的 X 坐标
* @param y0 左上角的 Y 坐标
* @param x1 右下角的 X 坐标
* @param y1 右下角的 Y 坐标
* @param width 形成矩形框的线条的单位宽度 1
*/
public void line(int x0, int y0, int x1, int y1, int width) {
StringJoiner sj = new StringJoiner(" ", "", "\r\n");
sj.add("L").add(String.valueOf(x0)).add(String.valueOf(y0)).
add(String.valueOf(x1)).add(String.valueOf(y1)).add(String.valueOf(width));
addStrToCommand(sj.toString());
}
/**
* 生成图片
* @param x0 x
* @param y0 y
* @param width 图片宽度
* @param height 图片高度
* @param bmp bmp 数据
*/
public void graphics(int x0, int y0, int width, int height, BufferedImage bmp) {
if (bmp != null) {
//生成2进制字符串
int[] rgb = new int[3];
width = bmp.getWidth();
height = bmp.getHeight();
int wModByte = (width % 8) == 0 ? 0 : 8 - (width % 8);
int pixelCount = (width + wModByte) * height;
int wPrintByte = (width + wModByte) / 8;
StringBuilder res = new StringBuilder();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int pixel = bmp.getRGB(x, y);
// 下面三行代码将一个数字转换为RGB数字
rgb[0] = (pixel & 0xff0000) >> 16;
rgb[1] = (pixel & 0xff00) >> 8;
rgb[2] = (pixel & 0xff);
String bitStr = (rgb[0] + rgb[1] + rgb[2]) / 3 >= 200 ? "1" : "0";
res.append(bitStr);
}
for (int k = 0; k < wModByte; k++) {
res.append("1");
}
}
//生成二值图字节流
String binary2 = res.toString();
StringBuilder hexStr = new StringBuilder();
for (int i = 0; i < binary2.length(); i = i + 8) {
String substring = binary2.substring(i, i + 8);
String hex = Long.toHexString(Long.parseLong(substring,2));
if(hex.length() == 1) {
hex = "0" + hex;
} else if(hex.length() > 2) {
hex = hex.substring(hex.length() - 2);
}
hexStr.append(hex);
}
StringJoiner sj = new StringJoiner(" ", "", "\r\n");
sj.add("EG").add(String.valueOf(wPrintByte)).add(String.valueOf(height))
.add(String.valueOf(x0)).add(String.valueOf(y0)).add(hexStr.toString());
addStrToCommand(sj.toString());
}
}
private String toHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for(Byte b : bytes) {
String hex = Integer.toHexString(b.intValue()).toUpperCase();
if(hex.length() == 1) {
hex = "0" + hex;
} else if(hex.length() > 2) {
hex = hex.substring(hex.length() - 2);
}
sb.append(hex);
}
return sb.toString();
}
}
模板解析处理
标签
public enum TAG {
LAYOUT("layout"),
HEADER("header"),
FOOTER("footer"),
TEXT("text"),
IMAGE("image"),
BARCODE("barcode"),
LINE("line"),
BOX("box");
private final String value;
TAG(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
属性
public enum ATTR {
COMMAND("command"),
SRC("src"),
FONT_SIZE("fontSize"),
FONT_WEIGHT("fontWeight"),
WIDTH("width"),
HEIGHT("height"),
TYPE("type"),
/** 定义两张卷标纸间的垂直间距距离 */
GAP("gap"),
/** BARCODE: 0 表示人眼不可识,1 表示人眼可识 */
/** BARCODE: 一维条码种类 */
CODE_TYPE("codeType"),
/** BOX: 方框左上角 X 坐标,单位 dot */
START_X("startX"),
/** BOX: 方框左上角 Y 坐标,单位 dot */
START_Y("startY"),
/** BOX: 方框右下角 X 坐标,单位 dot */
END_X("endX"),
/** BOX: 方框右下角 Y 坐标,单位 dot */
END_Y("endY"),
/** BOX: 方框线宽,单位 dot */
/** TEXT: 字体名称 */
FONT("font"),
/** TEXT: X 方向放大倍率 1-10*/
X_MULTIPLICATION("xMultiplication"),
/** TEXT: Y 方向放大倍率 1-10*/
Y_MULTIPLICATION("yMultiplication"),
/** 打印份数 */
COPIES("copies"),
LEFT("left"),
TOP("top"),
VALUE("value"),
;
private final String value;
ATTR(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
打印指令类型
public abstract class Printer<T> {
/** 解析模板 */
public abstract byte[] wrapTemplate(Element element);
public abstract T printText(Element element);
/** 打印条形码 */
public abstract T printBarcode(Element element);
/** 打印图片 */
public abstract T printImage(Element element);
/** 打印线条 */
public abstract T printLine(Element element);
}
public class CpclPrinter extends Printer<CpclCommand> {
private final static Logger logger = LoggerFactory.getLogger(CpclPrinter.class);
@Override
public byte[] wrapTemplate(Element element) {
if(element == null) {
return null;
}
CpclCommand cpclCommand = new CpclCommand();
// 指令预处理
this.preHandle(cpclCommand, element);
int totalHeight = getIntValue(element.attributeValue(ATTR.HEIGHT.getValue()), 0);
handlerNode(element, cpclCommand, totalHeight);
// 模版解析后指令处理
this.afterHandle(cpclCommand);
Vector<Byte> command = cpclCommand.getCommand();
return ArrayUtils.toPrimitive(command.toArray(new Byte[command.size()]));
}
private void handlerNode(Element element, CpclCommand cpclCommand, int totalHeight) {
TAG parentElemEnum = EnumUtils.getEnum(TAG.class, element.getName().toUpperCase());
Iterator iterator = element.elementIterator();
while (iterator.hasNext()) {
Element elem = (Element) iterator.next();
String elemName = elem.getName();
TAG elemEnum = EnumUtils.getEnum(TAG.class, elemName.toUpperCase());
if ((TAG.HEADER.equals(parentElemEnum) || TAG.FOOTER.equals(parentElemEnum)) && TAG.LAYOUT.equals(elemEnum)) {
// 把高度添加下下面layout中
int parentHeight = getIntValue(element.attributeValue(ATTR.HEIGHT.getValue()), 0);
elem.addAttribute("top", String.valueOf((totalHeight - parentHeight) / 8));
}
if (TAG.LAYOUT.equals(parentElemEnum) && TAG.LAYOUT.equals(elemEnum)) {
// 把高度添加下下面layout中
int parentHeight = getIntValue(element.attributeValue(ATTR.HEIGHT.getValue()), 0);
int parentTop = getIntValue(element.attributeValue(ATTR.TOP.getValue()), 0);
int height = getIntValue(elem.attributeValue(ATTR.HEIGHT.getValue()), 0);
int top = getIntValue(elem.attributeValue(ATTR.TOP.getValue()), 0);
elem.addAttribute("height", String.valueOf((parentHeight + height) / 8));
elem.addAttribute("top", String.valueOf((parentTop + top) / 8));
}
if (TAG.HEADER.equals(elemEnum) || TAG.FOOTER.equals(elemEnum) || TAG.LAYOUT.equals(elemEnum)) {
handlerNode(elem, cpclCommand, totalHeight);
} else {
handlerTag(cpclCommand, elem);
}
}
}
private void handlerTag(CpclCommand cpclCommand, Element elem) {
String elemName = elem.getName();
TAG elemEnum = EnumUtils.getEnum(TAG.class, elemName.toUpperCase());
if(elemEnum == null) {
return;
}
switch (elemEnum) {
case TEXT:
cpclCommand.addCommand(printText(elem));
break;
case IMAGE:
cpclCommand.addCommand(printImage(elem));
break;
case BARCODE:
cpclCommand.addCommand(printBarcode(elem));
break;
case LINE:
cpclCommand.addCommand(printLine(elem));
break;
case BOX:
cpclCommand.addCommand(printBox(elem));
break;
default:
break;
}
}
private void preHandle(CpclCommand cpclCommand, Element element) {
int width = getIntValue(element.attributeValue(ATTR.WIDTH.getValue()), 0);
int height = getIntValue(element.attributeValue(ATTR.HEIGHT.getValue()), 0);
int copies = getIntValue(element.attributeValue(ATTR.COPIES.getValue()), 1);
cpclCommand.init(width, height, copies);
}
private void afterHandle(CpclCommand cpclCommand) {
cpclCommand.print();
}
public CpclCommand printText(Element element) {
Element parent = element.getParent();
String style = element.attributeValue("style");
String[] split = style.split(";");
CpclCommand cpclCommand = new CpclCommand();
CpclCommand.TEXT textCommand = getValue(EnumUtils.getEnum(CpclCommand.TEXT.class, element.attributeValue(ATTR.COMMAND.getValue())), CpclCommand.TEXT.TEXT);
int x = getIntValue(parent.attributeValue(ATTR.LEFT.getValue()), 0);
int y = getIntValue(parent.attributeValue(ATTR.TOP.getValue()), 0);
String data = element.getText().trim();
int size = getIntValue(element.attributeValue(ATTR.FONT_SIZE.getValue()), 0);
int gap = getIntValue(element.attributeValue(ATTR.GAP.getValue()), 0);
int width = getIntValue(parent.attributeValue(ATTR.WIDTH.getValue()), 0);
int height = getIntValue(parent.attributeValue(ATTR.HEIGHT.getValue()), 0);
int blod = getIntValue(element.attributeValue(ATTR.FONT_WEIGHT.getValue()), 0);
// 放大
int xMultiplication = getIntValue(element.attributeValue(ATTR.X_MULTIPLICATION.getValue()), 1);
int yMultiplication = getIntValue(element.attributeValue(ATTR.Y_MULTIPLICATION.getValue()), 1);
CpclCommand.FONT font = getValue(EnumUtils.getEnum(CpclCommand.FONT.class, element.attributeValue(ATTR.FONT.getValue())), CpclCommand.FONT.F24);
boolean isCenter = false;
for (int i = 0; i < split.length; i++) {
String[] split1 = split[i].split(":");
String key = split1[0];
String value = split1[1];
// 字体加粗
if (key.contains("fontWeight")) {
if ("bold".equals(value)) {
blod = 1;
}
}
// 字体大小
else if (key.contains("fontSize")) {
if ("auto".equals(value)) {
int nextY = cpclCommand.calcSimulateBlockNextY(textCommand, font, size, x, y, data, width, 2, 2, gap);
if (nextY > y + height) {
xMultiplication = 1;
yMultiplication = 1;
} else {
xMultiplication = 2;
yMultiplication = 2;
}
} else if (Integer.parseInt(value) > 14) {
xMultiplication = 2;
yMultiplication = 2;
} else if (Integer.parseInt(value) <= 6) {
font = CpclCommand.FONT.F55;
} else {
xMultiplication = 1;
yMultiplication = 1;
}
}
// 居中
else if (key.contains("align")) {
if ("center".equals(value)) {
isCenter = true;
}
}
}
// 加粗
if (blod == 1) {
cpclCommand.setBlod(true);
}
// 放大
if (xMultiplication > 1 && yMultiplication > 1) {
cpclCommand.setMag(xMultiplication, yMultiplication);
}
cpclCommand.addSimulateBlock(textCommand, font, size, x, y, data, width, xMultiplication, yMultiplication, gap, isCenter);
// 重置字体样式
if (xMultiplication > 1 && yMultiplication > 1) {
cpclCommand.setMag(1, 1);
}
if (blod == 1) {
cpclCommand.setBlod(false);
}
return cpclCommand;
}
@Override
public CpclCommand printBarcode(Element element) {
Element parent = element.getParent();
CpclCommand cpclCommand = new CpclCommand();
CpclCommand.BARCODE barcode = getValue(EnumUtils.getEnum(CpclCommand.BARCODE.class, element.attributeValue(ATTR.COMMAND.getValue())), CpclCommand.BARCODE.BARCODE);
CpclCommand.CODE_TYPE codeType = getValue(EnumUtils.getEnum(CpclCommand.CODE_TYPE.class, element.attributeValue(ATTR.CODE_TYPE.getValue())), CpclCommand.CODE_TYPE.CODE128);
String type = getValue(element.attributeValue(ATTR.TYPE.getValue()), "");
int x = getIntValue(parent.attributeValue(ATTR.LEFT.getValue()), 0);
int y = getIntValue(parent.attributeValue(ATTR.TOP.getValue()), 0);
int width = getIntValue(parent.attributeValue(ATTR.WIDTH.getValue()), 0);
int height = getIntValue(parent.attributeValue(ATTR.HEIGHT.getValue()), 0);
CpclCommand.RATIO ratio = CpclCommand.RATIO.R2;
String data = StringUtil.valueOf(element.getText()).trim();
if (StringUtil.isBlank(data)) {
data = StringUtil.valueOf(element.attributeValue(ATTR.VALUE.getValue())).trim();
}
if (StringUtil.isBlank(data)) {
return cpclCommand;
}
if ("qrcode".equals(type)) {
cpclCommand.qrCode(barcode, x, y, 2, 3, CpclCommand.EEC.LEVEL_M, data);
} else {
int cellWidth = (int) Math.floor(width / (ratio.getValue() * data.length() * 6));
int posX = x + (int) ((width - ratio.getValue() * data.length() * 6 * cellWidth) * 0.3);
cpclCommand.barcode(barcode, codeType, cellWidth, ratio, height, posX, y, data);
}
return cpclCommand;
}
@Override
public CpclCommand printImage(Element element) {
Element parent = element.getParent();
CpclCommand cpclCommand = new CpclCommand();
int x = getIntValue(parent.attributeValue(ATTR.LEFT.getValue()), 0);
int y = getIntValue(parent.attributeValue(ATTR.TOP.getValue()), 0);
int width = 0;
int height = 0;
String src = element.attributeValue(ATTR.SRC.getValue());
BufferedImage image = null;
try {
if(src.startsWith("data:image/png;base64,")) {
byte[] bytes = Base64.getDecoder().decode(src.substring("data:image/png;base64,".length()));
ByteArrayInputStream is = new ByteArrayInputStream(bytes);
image = ImageIO.read(is);
is.close();
} else {
image = ImgUtils.getImageFromUrl(src);
}
if(image == null) {
return cpclCommand;
}
// 未设定宽度值则根据高度同比例进行压缩
height = getIntValue(parent.attributeValue(ATTR.HEIGHT.getValue()), image.getHeight());
width = getIntValue(parent.attributeValue(ATTR.WIDTH.getValue()), image.getWidth());
// 压缩图片大小
BufferedImage bitMap = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY);
bitMap.getGraphics().drawImage(image, 0, 0, width, height, null);
bitMap.getGraphics().dispose();
cpclCommand.graphics(x, y, width, height, bitMap);
bitMap.flush();
image.flush();
} catch (MalformedURLException e) {
logger.error("图片URL解析异常!", e);
} catch (IOException e) {
logger.error("图片读取或下载异常!", e);
} finally {
if (image != null) {
image.getGraphics().dispose();
}
}
return cpclCommand;
}
@Override
public CpclCommand printLine(Element element) {
CpclCommand cpclCommand = new CpclCommand();
int x0 = getIntValue(element.attributeValue(ATTR.START_X.getValue()), 0);
int y0 = getIntValue(element.attributeValue(ATTR.START_Y.getValue()), 0);
int x1 = getIntValue(element.attributeValue(ATTR.END_X.getValue()), 0);
int y1 = getIntValue(element.attributeValue(ATTR.END_Y.getValue()), 0);
int width = getIntValue(element.attributeValue(ATTR.WIDTH.getValue()), 1);
cpclCommand.line(x0, y0, x1, y1, width);
return cpclCommand;
}
private int getIntValue(String str, int defaultValue) {
return StringUtil.isNotBlank(str) ? (int)(Float.parseFloat(str) * 8) : defaultValue;
}
private <T> T getValue(T value, T defaultValue) {
return value != null ? value : defaultValue;
}
private static BufferedImage getImageFromUrl(String url) throws IOException {
BufferedImage bufferedImage = null;
URL imgUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection)imgUrl.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.connect();
if (HttpURLConnection.HTTP_OK == conn.getResponseCode()) {
InputStream inputStream = conn.getInputStream();
bufferedImage = ImageIO.read(inputStream);
} else {
logger.error("获取网络图片失败,scr={}, code={}, msg={} ", url, conn.getResponseCode(), conn.getResponseMessage());
}
conn.disconnect();
return bufferedImage;
}
/**
* 合并字节数组
* @param bt1
* @param bt2
* @return
*/
public static byte[] byteMerger(byte[] bt1, byte[] bt2) {
byte[] bt3 = new byte[bt1.length + bt2.length];
System.arraycopy(bt1, 0, bt3, 0, bt1.length);
System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
return bt3;
}
}
测试
SAXReader saxreader = new SAXReader();
Document dom = null;
try {
dom = saxreader.read(new ByteArrayInputStream(DemoConstnts.template_74.getBytes("UTF-8")));
} catch (Exception e) {
e.printStackTrace();
}
Element rootEle = dom.getRootElement();
CpclPrinter cpclPrinter = new CpclPrinter();
byte[] cmd = cpclPrinter.wrapTemplate(rootEle);
System.out.println(new String(cmd, "GB18030"));
其他问题
java 调用 js 问题
由于模板引擎是js语言,xml解析成CPCL指令是使用java写的,所以需要在java应用中调用这套js逻辑。
注意:这里也可以把java解析CPCL改造成js的写法,这样就直接起个nodejs项目就可以。
上面java调用js可能会性能问题,还有其他不可预估问题,所以这里的是单独起个应用,不能因为这个问题而影响到其他功能的正常运行。
调用 node 应用
public class EjsRenderUtils {
public static String ejsRender( String data) {
ProcessBuilder processBuilder = new ProcessBuilder();
Random rand = new Random();
String tempFileName = System.currentTimeMillis() + "_" + rand.nextInt(1000);
String dataTempFileName = "temp/" + tempFileName + ".json";
try {
// 清理一小时前的临时文件
File tempDir = new File("temp/");
if (!tempDir.exists()) {
tempDir.mkdirs();
}
long oneHourAgo = System.currentTimeMillis() - (1000 * 60 * 60);
if (tempDir.listFiles() != null) {
for (File file : tempDir.listFiles()) {
if (file.lastModified() < oneHourAgo) {
// Delete the file if it's older than one hour
file.delete();
}
}
}
// 将参数写入临时文件
File dataTempFile = new File(dataTempFileName);
Files.write(dataTempFile.toPath(), data.getBytes(), StandardOpenOption.CREATE);
} catch(IOException e) {
e.printStackTrace();
}
processBuilder.command("node", "script/ejsRender.js", tempFileName);
StringBuilder output = new StringBuilder();
try {
Process process = processBuilder.start();
BufferedReader reader =
new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
int exitCode = process.waitFor();
System.out.println("\nExited with error code : " + exitCode);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Return the output from the script
return output.toString().trim();
}
}
最后整合起来就是
// 获取模板内容
String xmlTemplate = EjsRenderUtils.ejsRender(...);
// 把xml模板转换成document
Document templateDom = null;
try {
SAXReader saxreader = new SAXReader();
templateDom = saxreader.read(new ByteArrayInputStream(xmlTemplate.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
logger.error("SAXReader.read转换错误, {}", e.getMessage());
logger.error("xml解析后的结果: {}", xmlTemplate);
}
// 解析指令
Element rootEle = templateDom.getRootElement();
CpclPrinter cpclPrinter = new CpclPrinter();
byte[] cmd = cpclPrinter.wrapTemplate(rootEle);
整体内容结束!