JLP352 hiprint 打印方案调研与开发记录
日期: 2026-10-08
设备: 济强 ULT113B(常称 JLP352)
连接方式: Android PDA → Bluetooth SPP
标签: 80 × 50 mm,200 dpi
目标与约束
保留 hiprint 模板编辑和布局;模板数据、打印数据按接口返回的 JSON 使用。目标是缩短打印等待时间,同时不影响复杂元素的兼容性。当前原型在 hiprint-test.html。
当前打印链路
验证页包含两条路径:
- 位图路径: hiprint JSON 模板 + 打印数据 → hiprint HTML → PNG → 黑白光栅 → CPCL
EG→ SPP。 - 原生 CPCL 实验路径: 同一 hiprint 模板和数据 → 遍历模板 JSON → 文本转
TEXT,线框转LINE/BOX,Logo 单独转局部EG→ SPP。
打印按钮已先后通过两个模拟异步接口获取打印数据和模板,再调用 getHtml(printData)。目前模拟接口仍是页面内的 Promise 和定时器,不是真实后端 API。
调研与开发过程
| 阶段 | 结果 |
|---|---|
| 初始验证 | CPCL 文字和小位图可打印;整页 PNG 任务曾卡住,显示准备发送约 124 KB。 |
| SPP 调整 | 蓝牙查询移出界面同步路径,改为分块异步写入。把块增至 8 KB 本身没有明显提速。 |
| EG 数据裁剪 | 按模板有墨点的区域裁剪空白行和左右空白列。实测载荷由约 124,402 字节降至 35,727 字节,SPP 耗时由 27.6 秒降至 9.6 秒。 |
| CG 压缩尝试 | PackBits 十六进制候选打印成条纹,判定该编码与设备 CG 格式不兼容,未用于正常路径。后来测试原始二进制 CG,小图可打印出黑方块,但完整模板和性能仍未确认。 |
| hiprint 模板假数据 | 按用户提供的 80×50 mm 模板结构和假数据构造测试模板,并模拟异步获取模板和数据。 |
| 原生 CPCL 实验 | 初版能打印,但字体、行距和规格文本与 hiprint 预览不一致。之后尝试调整字体与坐标换算,真机打印仍未达到预期。 |
SPP 耗时是从开始连接/发送到 flush 的测量,不等于机械出纸时间。位图路径提速效果有真机数据;原生 CPCL 路径的等待时间尚未得到可靠测量。
当前结论
- 已验证有效的优化是裁剪位图空白区域;8 KB 分块和
SPEED 3没有解决主要传输耗时。 - 当前普通 EG 位图路径是已知可工作的回退方式,复杂模板兼容性较好,但整页栅格数据仍需传输。
- 原生 CPCL 有望减少文字标签的传输量,但目前字体、坐标、文本换行和设备兼容性尚未对齐,不能视为可替代方案。
- CG 仍是实验方向:曾使用的 PackBits 格式不兼容,原始二进制小图可打印,但缺少对完整标签格式及速度收益的验证。
推荐的公共打印入口
后续可把打印逻辑封装为 PDA 公共模块,页面仅传模板、数据和模式:
printService.print({
templateJson,
data,
printerName: 'ULT113B',
mode: 'auto'
});
建议 auto 模式对已验证的文本、线条、矩形、条码使用原生 CPCL;图片及尚未支持的元素回退到局部或整页位图。模板预览继续由 hiprint 渲染。该公共模块目前尚未从测试页抽取实现。
内网依赖处理
CDN 在内网不可用,部署前应将实际需要的依赖固定版本并放入前端资源包。基础项是 vue-plugin-hiprint 及其所需的 jQuery/CSS;html2canvas 用于位图渲染,canvg 用于 SVG 转换,JsBarcode / bwip-js 用于对应条码元素。nzh、颜色选择器、Socket.IO 和 jsPDF 是否保留,取决于实际模板功能和打印方式;应先逐项验证,再移除未使用依赖。
下一步验证标准
- 用原生 CPCL 按钮重新打印同一模板,核对品名、型号、规格、数量、批次号、日期及加工厂商的字体、行距和位置。
- 确认规格文本是否换行完整,Logo 与线框是否与 hiprint 预览对应,内容是否全部位于 80×50 mm 标签范围内。
- 分别记录位图路径与原生 CPCL 路径的 SPP 耗时和实际出纸耗时。
- 原生路径通过中文、溢出和复杂元素测试后,再决定是否抽取为公共
printService;未通过时继续保留 EG 回退。
主代码块
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>hiprint 图片预览测试</title>
<link rel="stylesheet" type="text/css" media="all" href="https://cdn.jsdelivr.net/npm/vue-plugin-hiprint@0.0.60/dist/print-lock.css">
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 16px; color: #252a31; background: #f3f5f7; font-family: Arial, sans-serif; }
button { min-height: 44px; margin: 0 8px 8px 0; padding: 0 16px; border: 0; border-radius: 4px; color: #fff; background: #176b5b; font-size: 15px; }
button:disabled { opacity: .55; }
.status { margin: 12px 0; color: #5e6872; font-size: 13px; }
.preview { overflow: auto; min-height: 100px; padding: 12px; background: #fff; border: 1px solid #d9dee3; }
.preview img { display: block; max-width: 100%; height: auto; margin-top: 12px; border: 1px solid #d9dee3; }
.render-source { position: fixed; left: -10000px; top: 0; }
.note { margin-top: 12px; color: #7b4a00; font-size: 12px; line-height: 1.5; }
</style>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bwip-js@4.5.1/dist/bwip-js.js"></script>
<script src="https://cdn.jsdelivr.net/npm/nzh@1.0.14/dist/nzh.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@claviska/jquery-minicolors@2.3.6/jquery.minicolors.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.5.1/dist/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/canvg@3.0.10/lib/umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-plugin-hiprint@0.0.60/dist/vue-plugin-hiprint.js"></script>
<script>
var hiprintPlugin = window['vue-plugin-hiprint'];
if (hiprintPlugin && typeof hiprintPlugin.disAutoConnect === 'function') hiprintPlugin.disAutoConnect();
</script>
</head>
<body>
<button id="textPrintButton" type="button">测试 CPCL 文字打印</button>
<button id="smallPrintButton" type="button">测试小位图</button>
<button id="renderButton" type="button">生成 PNG 并打印位图</button>
<button id="nativeRenderButton" type="button">hiprint 模板原生 CPCL 实验打印</button>
<div id="status" class="status">等待生成</div>
<div id="preview" class="preview"></div>
<div class="note">80 × 50 mm / 200 dpi</div>
<div id="renderSource" class="render-source"></div>
<script>
var mockApiPrintData = {
BOENum: '',
BatchNo: '5XDAZAHZIKR52L',
ExpirationDate: '2027-04-08 00:00:00',
GrossWeight: '',
LoadTypeDesc: '',
LoadTypeId: '01.01.051.02.061',
LoadTypeName: 'TS805抛光片',
LoadTypeNameM: 'TS805抛光片',
Model: '1',
PONum: '4300462819',
Producer: '2',
ProductionDate: '2026-10-08 00:00:00',
QRCode: '||20Pcs||2026/10/8 0:00:00|2027/4/8 0:00:00|5XDAZAHZIKR52L',
QuantityInfo: '20Pcs',
Remark: '',
SNNum: '',
ShelfLife: '6',
Specification: '141.52±0.1*80.44±0.1*0.54±0.02mm,R2±0.1,C0.15±0.05,红色',
SpecificationM: '141.52±0.1*80.44±0.1*0.54±0.02mm,R2±0.1,C0.15±0.05,红色',
SupplierCode: '',
SupplierId: null,
WWCGDDCode: 'CGDD2608284772'
};
function fetchMockPrintData() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(Object.assign({}, mockApiPrintData));
}, 400);
});
}
var sppUuid = '00001101-0000-1000-8000-00805F9B34FB';
var printerName = 'ULT113B';
var labelWidthDots = 630;
var labelHeightDots = 394;
var printPocVersion = 'POC-20261008-R15';
function mmToPt(value) {
return value * 72 / 25.4;
}
function addTextMm(panel, options) {
['left', 'top', 'width', 'height'].forEach(function(key) {
options[key] = mmToPt(options[key]);
});
panel.addPrintText({ options: options });
}
function createMockTemplateResponse() {
function textElement(title, field, left, top, width, fontSize) {
return {
options: {
left: left,
top: top,
width: width,
height: 9.75,
fontSize: fontSize || 9,
textAlign: 'left',
textContentVerticalAlign: 'middle',
hideTitle: !!field,
title: title,
field: field,
testData: field || null
},
printElementType: { title: '文本', type: 'text' }
};
}
function rule(type, left, top, width, height, borderWidth) {
return {
options: { left: left, top: top, width: width, height: height, borderWidth: borderWidth || '0.75' },
printElementType: { title: type === 'rect' ? '矩形' : '横线', type: type }
};
}
var printElements = [
{
options: { left: 55.5, top: 13.5, width: 9, height: 123 },
printElementType: { title: '竖线', type: 'vline' }
},
rule('rect', 4.5, 13.5, 217.5, 123),
textElement('品名', null, 10.5, 15, 43.5),
textElement('文本', 'LoadTypeNameM', 60, 15, 157.5, 8.25),
rule('hline', 4.5, 27, 217.5, 9),
textElement('型号', null, 10.5, 31.5, 43.5),
textElement('TTT', 'Model', 60, 31.5, 157.5, 8.25),
rule('hline', 4.5, 43.5, 217.5, 9),
textElement('规格', null, 9, 48, 43.5),
textElement('文本', 'SpecificationM', 60, 48, 159, 8.25),
rule('hline', 4.5, 61.5, 217.5, 9),
textElement('数量', null, 9, 66, 43.5),
textElement('文本', 'QuantityInfo', 60, 66, 159, 8.25),
rule('hline', 4.5, 79.5, 217.5, 9),
textElement('批次号', null, 9, 84, 43.5),
textElement('文本', 'BatchNo', 60, 84, 159, 8.25),
rule('hline', 4.5, 97.5, 217.5, 9),
textElement('生产日期', null, 9, 102, 43.5),
textElement('生产日期', 'ProductionDate', 60, 102, 157.5, 8.25),
rule('hline', 4.5, 115.5, 217.5, 9),
textElement('加工厂商', null, 9, 120, 43.5),
textElement('加工厂商', 'Producer', 60, 120, 159, 8.25)
];
var templateJson = {
panels: [{
index: 0,
name: 1,
height: 50,
width: 80,
panelPageRule: 'none',
paperHeader: 2,
paperFooter: 210,
paperNumberDisabled: true,
printElements: printElements,
paperNumberLeft: 124.5,
paperNumberTop: 119,
paperNumberContinue: true,
watermarkOptions: { content: null, rotate: 0, timestamp: false, format: null },
panelLayoutOptions: {}
}]
};
return templateJson;
}
function fetchMockPrintTemplate() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(createMockTemplateResponse());
}, 500);
});
}
function createMockTemplate(hiprint, templateData) {
return new hiprint.PrintTemplate({ template: templateData });
}
function setStatus(message) {
document.getElementById('status').textContent = message;
console.log('[print-poc] ' + message);
}
function requestBluetoothPermission(callback) {
if (!window.plus || !plus.android) {
setStatus('当前页面没有 Android Native.js 环境,请从 PDA APK 打开。');
return;
}
var osVersion = String(plus.os && plus.os.version || '');
var versionMatch = osVersion.match(/\d+/);
var androidMajorVersion = versionMatch ? Number(versionMatch[0]) : 0;
if (androidMajorVersion > 0 && androidMajorVersion < 12) {
callback();
return;
}
if (!plus.android.requestPermissions) {
setStatus('当前 App-Plus Runtime 不支持 requestPermissions;请升级 HBuilderX Runtime。');
return;
}
var permission = 'android.permission.BLUETOOTH_CONNECT';
setStatus('正在请求 Android ' + (osVersion || '12+') + ' 附近设备权限…');
try {
plus.android.requestPermissions([permission], function(result) {
var granted = result && result.granted ? result.granted : [];
var deniedPresent = result && result.deniedPresent ? result.deniedPresent : [];
var deniedAlways = result && result.deniedAlways ? result.deniedAlways : [];
if (granted.indexOf(permission) >= 0) callback();
else if (deniedAlways.indexOf(permission) >= 0) setStatus('附近设备权限被永久拒绝,请到系统应用权限设置中手动开启。');
else if (deniedPresent.indexOf(permission) >= 0) setStatus('附近设备权限未授权,允许权限后再试。');
else setStatus('系统未返回附近设备权限授权结果:' + JSON.stringify(result || {}));
}, function(error) {
setStatus('requestPermissions 接口失败:' + (error.code || '') + ' ' + (error.message || error));
});
} catch (error) {
setStatus('调用 requestPermissions 异常:' + (error.code || '') + ' ' + (error.message || error));
}
}
function findBondedPrinter() {
var BluetoothAdapter = plus.android.importClass('android.bluetooth.BluetoothAdapter');
var adapter = BluetoothAdapter.getDefaultAdapter();
if (!adapter) throw new Error('设备没有可用的蓝牙适配器');
var bonded = adapter.getBondedDevices();
plus.android.importClass(bonded);
var iterator = plus.android.invoke(bonded, 'iterator');
var names = [];
while (plus.android.invoke(iterator, 'hasNext')) {
var device = plus.android.invoke(iterator, 'next');
plus.android.importClass(device);
var name = plus.android.invoke(device, 'getName');
names.push(name);
if (name === printerName) return device;
}
throw new Error('未找到已配对设备 ' + printerName + ';当前已配对设备:' + names.join(', '));
}
function asciiBytes(value) {
var javaString = plus.android.newObject('java.lang.String', value);
return plus.android.invoke(javaString, 'getBytes', 'US-ASCII');
}
function gbkBytes(value) {
var javaString = plus.android.newObject('java.lang.String', value);
return plus.android.invoke(javaString, 'getBytes', 'GBK');
}
function runOnMainThread(callback) {
setTimeout(callback, 0);
}
var sppBusy = false;
var sppSocket = null;
var sppOutput = null;
var sppDeviceAddress = '';
function closeSppConnection() {
try { if (sppOutput) plus.android.invoke(sppOutput, 'close'); } catch (error) {}
try { if (sppSocket) plus.android.invoke(sppSocket, 'close'); } catch (error) {}
sppOutput = null;
sppSocket = null;
sppDeviceAddress = '';
}
function writeSppChunk(payload, jobName, offset, startedAt, encoding) {
try {
if (offset >= payload.length) {
plus.android.invoke(sppOutput, 'flush');
var elapsedSeconds = ((Date.now() - startedAt) / 1000).toFixed(1);
setStatus(jobName + ' 已发送;SPP 耗时 ' + elapsedSeconds + ' 秒,请检查打印结果。');
sppBusy = false;
setPrintButtonsDisabled(false);
return;
}
var chunk = payload.slice(offset, offset + 8192);
plus.android.invoke(sppOutput, 'write', encoding === 'GBK' ? gbkBytes(chunk) : asciiBytes(chunk));
var sent = offset + chunk.length;
console.log('[print-poc] write ' + sent + ' / ' + payload.length);
if (sent === payload.length || sent % 16384 === 0) {
var progressUnit = encoding === 'GBK' ? '字符' : '字节';
setStatus('SPP 已写入 ' + sent + ' / ' + payload.length + ' ' + progressUnit);
}
setTimeout(function() { writeSppChunk(payload, jobName, sent, startedAt, encoding); }, 0);
} catch (error) {
closeSppConnection();
sppBusy = false;
setPrintButtonsDisabled(false);
setStatus('SPP 连接/发送失败:' + (error && error.message ? error.message : error));
}
}
function sendSpp(payload, jobName, encoding) {
requestBluetoothPermission(function() {
if (sppBusy) {
setStatus('已有 SPP 打印任务正在执行,请稍候。');
return;
}
sppBusy = true;
var sppStartedAt = Date.now();
setPrintButtonsDisabled(true);
var payloadUnit = encoding === 'GBK' ? '字符' : '字节';
setStatus(printPocVersion + ':' + jobName + ',准备发送 ' + payload.length + ' ' + payloadUnit);
setTimeout(function() {
try {
var worker = plus.android.implements('java.lang.Runnable', {
run: function() {
try {
console.log('[print-poc] worker entered: ' + jobName);
runOnMainThread(function() { setStatus(printPocVersion + ':SPP 工作线程已启动,查找已配对打印机…'); });
var device = findBondedPrinter();
var deviceAddress = plus.android.invoke(device, 'getAddress');
var connected = sppSocket && sppDeviceAddress === deviceAddress;
if (connected) {
try { connected = !!plus.android.invoke(sppSocket, 'isConnected'); } catch (error) { connected = false; }
}
if (!connected) {
closeSppConnection();
runOnMainThread(function() { setStatus(printPocVersion + ':正在通过 SPP 连接 ' + printerName + '…'); });
var UUID = plus.android.importClass('java.util.UUID');
sppSocket = device.createRfcommSocketToServiceRecord(UUID.fromString(sppUuid));
plus.android.invoke(sppSocket, 'connect');
sppOutput = plus.android.invoke(sppSocket, 'getOutputStream');
sppDeviceAddress = deviceAddress;
} else {
runOnMainThread(function() { setStatus('复用现有 SPP 连接,准备发送 ' + payload.length + ' ' + payloadUnit + '…'); });
}
runOnMainThread(function() { setStatus('SPP 已就绪,开始写入 ' + payload.length + ' ' + payloadUnit + '…'); });
runOnMainThread(function() { writeSppChunk(payload, jobName, 0, sppStartedAt, encoding); });
} catch (error) {
var errorText = error && error.message ? error.message : String(error);
runOnMainThread(function() {
closeSppConnection();
sppBusy = false;
setPrintButtonsDisabled(false);
setStatus('SPP 连接/发送失败:' + errorText);
});
}
}
});
plus.android.invoke(plus.android.newObject('java.lang.Thread', worker), 'start');
} catch (error) {
sppBusy = false;
setPrintButtonsDisabled(false);
setStatus('SPP 初始化失败:' + (error.message || error));
}
}, 0);
});
}
window.addEventListener('beforeunload', closeSppConnection);
function setPrintButtonsDisabled(disabled) {
['textPrintButton', 'smallPrintButton', 'renderButton', 'nativeRenderButton'].forEach(function(id) {
document.getElementById(id).disabled = disabled;
});
}
function createCpclImagePayload(canvas) {
var raster = canvasToCpclRaster(canvas);
var bytesPerRow = Math.ceil(canvas.width / 8);
var rowsPerCommand = 16;
var commands = [
'! 0 200 200 ' + labelHeightDots + ' 1\r\n',
'PW ' + labelWidthDots + '\r\n',
'SPEED 3\r\n'
];
for (var top = 0; top < canvas.height; top += rowsPerCommand) {
var bandBottom = Math.min(top + rowsPerCommand, canvas.height);
var firstRow = -1;
var lastRow = -1;
var firstByte = bytesPerRow;
var lastByte = -1;
for (var row = top; row < bandBottom; row++) {
var rowHasInk = false;
for (var byteIndex = 0; byteIndex < bytesPerRow; byteIndex++) {
if (raster[row * bytesPerRow + byteIndex] !== 0) {
rowHasInk = true;
firstByte = Math.min(firstByte, byteIndex);
lastByte = Math.max(lastByte, byteIndex);
}
}
if (rowHasInk) {
if (firstRow < 0) firstRow = row;
lastRow = row;
}
}
if (firstRow < 0) continue;
var bandHeight = lastRow - firstRow + 1;
var bandWidth = lastByte - firstByte + 1;
var hex = '';
for (var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
for (var column = firstByte; column <= lastByte; column++) {
var index = rowIndex * bytesPerRow + column;
hex += ('0' + raster[index].toString(16)).slice(-2);
}
}
commands.push('EG ' + bandWidth + ' ' + bandHeight + ' ' + (firstByte * 8) + ' ' + firstRow + ' ' + hex + '\r\n');
}
commands.push('FORM\r\nPRINT\r\n');
return commands.join('');
}
function templatePxToDots(value) {
return Math.round(Number(value || 0) * 200 / 96);
}
function wrapCpclText(text, maxWidthDots, asciiWidth, wideWidth) {
var lines = [];
var line = '';
var lineWidth = 0;
Array.from(text).forEach(function(character) {
var characterWidth = character.charCodeAt(0) > 255 ? wideWidth : asciiWidth;
if (line && lineWidth + characterWidth > maxWidthDots) {
lines.push(line);
line = '';
lineWidth = 0;
}
line += character;
lineWidth += characterWidth;
});
if (line) lines.push(line);
return lines;
}
function createNativeTextCommand(element, printData) {
var options = element.options || {};
var value = options.field ? printData[options.field] : options.title;
if (value === null || value === undefined || value === '') return '';
var text = String(value).replace(/[\r\n]+/g, ' ');
var magnification = 0;
var asciiWidth = 8;
var wideWidth = 16;
var lines = wrapCpclText(text, templatePxToDots(options.width), asciiWidth, wideWidth);
var lineHeight = 10;
return lines.map(function(line, index) {
return 'TEXT 0 ' + magnification + ' ' + templatePxToDots(options.left) + ' ' + (templatePxToDots(options.top) + index * lineHeight) + ' ' + line + '\r\n';
}).join('');
}
function createNativeRuleCommand(element) {
var options = element.options || {};
var type = element.printElementType && element.printElementType.type;
var x = templatePxToDots(options.left);
var y = templatePxToDots(options.top);
var width = templatePxToDots(options.width);
var height = templatePxToDots(options.height);
var thickness = Math.max(1, templatePxToDots(parseFloat(options.borderWidth) || 0.75));
if (type === 'rect') return 'BOX ' + x + ' ' + y + ' ' + (x + width) + ' ' + (y + height) + ' ' + thickness + '\r\n';
if (type === 'vline') return 'LINE ' + x + ' ' + y + ' ' + x + ' ' + (y + height) + ' ' + thickness + '\r\n';
if (type === 'hline') return 'LINE ' + x + ' ' + y + ' ' + (x + width) + ' ' + y + ' ' + thickness + '\r\n';
return '';
}
function createNativeImageCommand(element) {
return new Promise(function(resolve, reject) {
var options = element.options || {};
if (!options.src) {
resolve('');
return;
}
var image = new Image();
image.onload = function() {
try {
var width = Math.max(1, templatePxToDots(options.width));
var height = Math.max(1, templatePxToDots(options.height));
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
var raster = canvasToCpclRaster(canvas);
var bytesPerRow = Math.ceil(width / 8);
var hex = '';
for (var index = 0; index < raster.length; index++) hex += ('0' + raster[index].toString(16)).slice(-2);
resolve('EG ' + bytesPerRow + ' ' + height + ' ' + templatePxToDots(options.left) + ' ' + templatePxToDots(options.top) + ' ' + hex + '\r\n');
} catch (error) {
reject(error);
}
};
image.onerror = function() { reject(new Error('模板图片加载失败:' + (element.name || 'image'))); };
image.src = options.src;
});
}
function createNativeCpclPayload(templateData, printData) {
var panel = templateData && templateData.panels && templateData.panels[0];
if (!panel) return Promise.reject(new Error('模拟模板没有 panels[0]'));
var commands = (panel.printElements || []).map(function(element) {
var type = element.printElementType && element.printElementType.type;
if (type === 'text') return createNativeTextCommand(element, printData);
if (type === 'rect' || type === 'hline' || type === 'vline') return createNativeRuleCommand(element);
if (type === 'image') return createNativeImageCommand(element);
return '';
});
return Promise.all(commands).then(function(elementCommands) {
return '! 0 200 200 ' + labelHeightDots + ' 1\r\n' +
'PW ' + labelWidthDots + '\r\n' +
'SPEED 3\r\n' +
elementCommands.join('') +
'FORM\r\nPRINT\r\n';
});
}
function createCpclTextPayload() {
var header = '! 0 200 200 ' + labelHeightDots + ' 1\r\n' +
'PW ' + labelWidthDots + '\r\n' +
'SPEED 3\r\n' +
'TEXT 4 0 24 24 KION CPCL SPP TEST\r\n' +
'TEXT 4 0 24 72 DEVICE ULT113B\r\n' +
'TEXT 4 0 24 120 LABEL 80x50mm 200dpi\r\n';
return header + 'FORM\r\nPRINT\r\n';
}
function canvasToCpclRaster(canvas) {
var image = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
var bytesPerRow = Math.ceil(canvas.width / 8);
var raster = new Uint8Array(bytesPerRow * canvas.height);
for (var y = 0; y < canvas.height; y++) {
for (var x = 0; x < canvas.width; x++) {
var offset = (y * canvas.width + x) * 4;
var luminance = image.data[offset] * 0.299 + image.data[offset + 1] * 0.587 + image.data[offset + 2] * 0.114;
if (luminance < 160) raster[y * bytesPerRow + (x >> 3)] |= 0x80 >> (x & 7);
}
}
return raster;
}
document.getElementById('textPrintButton').addEventListener('click', function() {
try {
sendSpp(createCpclTextPayload(), 'CPCL 文字测试任务');
} catch (error) {
setStatus('CPCL 文字指令生成失败:' + (error.message || error));
}
});
document.getElementById('smallPrintButton').addEventListener('click', function() {
try {
var canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
var context = canvas.getContext('2d');
context.fillStyle = '#ffffff';
context.fillRect(0, 0, 64, 64);
context.fillStyle = '#000000';
context.fillRect(8, 8, 48, 48);
sendSpp(createCpclImagePayload(canvas), '64x64 小位图测试');
} catch (error) {
setStatus('小位图测试失败:' + (error.message || error));
}
});
function renderTemplateAndPrint(useNativeCommands) {
var status = document.getElementById('status');
var preview = document.getElementById('preview');
var source = document.getElementById('renderSource');
var plugin = window['vue-plugin-hiprint'];
var hiprint = window.hiprint || (plugin && plugin.hiprint);
if (!window.jQuery || !window.html2canvas || !hiprint || !hiprint.PrintTemplate) {
status.textContent = '依赖加载失败,请检查设备网络或 CDN 访问。';
return;
}
status.textContent = '正在从模拟接口获取打印数据和模板…';
setPrintButtonsDisabled(true);
preview.innerHTML = '';
source.innerHTML = '';
Promise.all([fetchMockPrintData(), fetchMockPrintTemplate()]).then(function(responses) {
var printData = responses[0];
var templateData = responses[1];
status.textContent = '模拟接口已返回打印数据和模板,正在渲染 hiprint…';
try {
var template = createMockTemplate(hiprint, templateData);
var html = template.getHtml(printData, {});
if (template.svg2canvas) template.svg2canvas(html);
source.appendChild(html[0]);
window.html2canvas(html[0], { scale: 200 / 96, backgroundColor: '#ffffff', useCORS: true }).then(function(renderedCanvas) {
var canvas = document.createElement('canvas');
canvas.width = labelWidthDots;
canvas.height = labelHeightDots;
canvas.getContext('2d').drawImage(renderedCanvas, 0, 0, canvas.width, canvas.height);
var image = document.createElement('img');
image.alt = 'hiprint 模板渲染图片';
image.src = canvas.toDataURL('image/png');
preview.appendChild(image);
if (useNativeCommands) {
status.textContent = '正在将模板元素转换为原生 CPCL…';
createNativeCpclPayload(templateData, printData).then(function(payload) {
status.textContent = '原生 CPCL 生成完成:' + payload.length + ' 个字符,准备 GBK/SPP 发送…';
setPrintButtonsDisabled(false);
sendSpp(payload, 'hiprint 模板原生 CPCL 实验任务', 'GBK');
}).catch(function(error) {
setPrintButtonsDisabled(false);
status.textContent = '原生 CPCL 转换失败:' + (error && error.message ? error.message : error);
});
return;
}
status.textContent = 'PNG 生成成功:' + canvas.width + ' × ' + canvas.height + ' px;正在转换黑白位图…';
var payload = createCpclImagePayload(canvas);
status.textContent = '位图转换完成:' + payload.length + ' 字节,准备 SPP 发送…';
setPrintButtonsDisabled(false);
sendSpp(payload, 'CPCL 位图打印任务');
}).catch(function(error) {
setPrintButtonsDisabled(false);
status.textContent = '图片生成失败:' + (error && error.message ? error.message : error);
});
} catch (error) {
setPrintButtonsDisabled(false);
status.textContent = 'hiprint 渲染失败:' + (error && error.message ? error.message : error);
}
}).catch(function(error) {
setPrintButtonsDisabled(false);
status.textContent = '模拟接口获取打印数据或模板失败:' + (error && error.message ? error.message : error);
});
}
document.getElementById('renderButton').addEventListener('click', function() {
renderTemplateAndPrint(false);
});
document.getElementById('nativeRenderButton').addEventListener('click', function() {
renderTemplateAndPrint(true);
});
</script>
</body>
</html>