Ionic使用esc-pos-encoder-ionic蓝牙热敏打印图片及字体调整异常
Ionic对接ESC/POS蓝牙热敏打印机异常修复方案
现存问题
- 调用图片打印接口时打印机无图片输出
- 自定义ESC/POS指令调整字体宽高、字体类型不生效,所有文本始终以默认字体输出
- 多型号打印机测试问题稳定复现,可排除硬件兼容性问题
问题根因
- 图片打印失效原因
- 图片未等加载完成就传入编码器:代码中创建Image对象设置src后立刻调用编码方法,此时图片资源未完成解码,编码器无法获取像素数据,生成不了有效图片打印指令
- 部分场景下图片尺寸超出打印机可打印宽度、图片位深不符合要求:常规58mm热敏打印机最大打印宽度为384像素,80mm机型为576像素,非1bit单色位图编码器无法正常识别
- 字体格式失效原因
- 自定义指令集数据类型错误:
TXT_CUSTOM_SIZE函数返回值包含字符串类型字节,和数字类型指令混写时,raw方法会转码出错误字节,导致指令直接失效 - 宽高指令缺少前置指令头:单独发送
TXT_HEIGHT/TXT_WIDTH的单字节参数,没有配套的ESC !/GS !指令前缀,打印机会将其识别为普通文本数据,不会解析为格式指令 - 内置方法覆盖自定义配置:esc-pos-encoder-ionic自带的
size()、line()方法内部会自动重置文本格式,自定义raw指令发送后调用内置方法,会直接覆盖之前的格式配置 - 缺少初始化步骤:编码前未发送打印机重置指令,缓存内残留的历史配置会干扰新指令执行
- 断开连接时机过早:蓝牙写入完成后立刻断开,缓冲区剩余数据未完全发送到打印机,会导致末尾指令丢失
- 自定义指令集数据类型错误:
修复步骤
1. 修正自定义ESC/POS指令集
统一所有指令为纯数字字节数组,补全缺失的指令参数、修正不完整的指令结构:
// printer-command.ts 修正后内容 export const commands = { LF: [0x0a], ESC: [0x1b], FS: [0x1c], GS: [0x1d], US: [0x1f], FF: [0x0c], DLE: [0x10], DC1: [0x11], DC4: [0x14], EOT: [0x04], NUL: [0x00], HORIZONTAL_LINE: { HR_58MM: '================================', HR2_58MM: '********************************' }, FEED_CONTROL_SEQUENCES: { CTL_LF: [0x0a], CTL_FF: [0x0c], CTL_CR: [0x0d], CTL_HT: [0x09], CTL_VT: [0x0b], }, LINE_SPACING: { LS_DEFAULT: [0x1b,0x32], LS_SET: [0x1b,0x33] }, HARDWARE: { HW_INIT: [0x1b,0x40], HW_SELECT: [0x1b,0x3d,0x01], HW_RESET: [0x1b,0x3f,0x0a,0x00], }, CASH_DRAWER: { CD_KICK_2: [0x1b,0x70,0x00], CD_KICK_5: [0x1b,0x70,0x01], }, MARGINS: { BOTTOM: [0x1b,0x4f], LEFT: [0x1b,0x6c], RIGHT: [0x1b,0x51], }, PAPER: { PAPER_FULL_CUT: [0x1d,0x56,0x00], PAPER_PART_CUT: [0x1d,0x56,0x01], PAPER_CUT_A: [0x1d,0x56,0x41], PAPER_CUT_B: [0x1d,0x56,0x42], }, TEXT_FORMAT: { TXT_NORMAL: [0x1b,0x21,0x00], TXT_2HEIGHT: [0x1b,0x21,0x10], TXT_2WIDTH: [0x1b,0x21,0x20], TXT_4SQUARE: [0x1b,0x21,0x30], // 修正:返回纯数字字节,宽高取值范围强制限制在1-8 TXT_CUSTOM_SIZE: function (width: number, height: number) { const validWidth = Math.min(Math.max(width, 1), 8); const validHeight = Math.min(Math.max(height, 1), 8); const sizeDec = ((validWidth - 1) << 4) | (validHeight - 1); return [0x1d, 0x21, sizeDec] }, // 修正:补全GS ! 指令头,不再单独发送单字节参数 TXT_HEIGHT: { 1: [0x1d,0x21,0x00], 2: [0x1d,0x21,0x01], 3: [0x1d,0x21,0x02], 4: [0x1d,0x21,0x03], 5: [0x1d,0x21,0x04], 6: [0x1d,0x21,0x05], 7: [0x1d,0x21,0x06], 8: [0x1d,0x21,0x07] }, TXT_WIDTH: { 1: [0x1d,0x21,0x00], 2: [0x1d,0x21,0x10], 3: [0x1d,0x21,0x20], 4: [0x1d,0x21,0x30], 5: [0x1d,0x21,0x40], 6: [0x1d,0x21,0x50], 7: [0x1d,0x21,0x60], 8: [0x1d,0x21,0x70] }, TXT_UNDERL_OFF: [0x1b,0x2d,0x00], TXT_UNDERL_ON: [0x1b,0x2d,0x01], TXT_UNDERL2_ON: [0x1b,0x2d,0x02], TXT_BOLD_OFF: [0x1b,0x45,0x00], TXT_BOLD_ON: [0x1b,0x45,0x01], TXT_ITALIC_OFF: [0x1b,0x35,0x00], TXT_ITALIC_ON: [0x1b,0x34,0x01], TXT_FONT_A: [0x1b,0x4d,0x00], TXT_FONT_B: [0x1b,0x4d,0x01], TXT_FONT_C: [0x1b,0x4d,0x02], TXT_ALIGN_LT: [0x1b,0x61,0x00], TXT_ALIGN_CT: [0x1b,0x61,0x01], TXT_ALIGN_RT: [0x1b,0x61,0x02], }, BARCODE_FORMAT: { BARCODE_TXT_OFF: [0x1d,0x48,0x00], BARCODE_TXT_ABV: [0x1d,0x48,0x01], BARCODE_TXT_BLW: [0x1d,0x48,0x02], BARCODE_TXT_BTH: [0x1d,0x48,0x03], BARCODE_FONT_A: [0x1d,0x66,0x00], BARCODE_FONT_B: [0x1d,0x66,0x01], BARCODE_HEIGHT: function(height: number) { return [0x1d,0x68, Math.min(Math.max(height, 1), 255)] }, BARCODE_WIDTH: { 1: [0x1d,0x77,0x02], 2: [0x1d,0x77,0x03], 3: [0x1d,0x77,0x04], 4: [0x1d,0x77,0x05], 5: [0x1d,0x77,0x06], }, BARCODE_HEIGHT_DEFAULT: [0x1d,0x68,0x64], BARCODE_WIDTH_DEFAULT: [0x1d,0x77,0x01], BARCODE_UPC_A: [0x1d,0x6b,0x00], BARCODE_UPC_E: [0x1d,0x6b,0x01], BARCODE_EAN13: [0x1d,0x6b,0x02], BARCODE_EAN8: [0x1d,0x6b,0x03], BARCODE_CODE39: [0x1d,0x6b,0x04], BARCODE_ITF: [0x1d,0x6b,0x05], BARCODE_NW7: [0x1d,0x6b,0x06], BARCODE_CODE93: [0x1d,0x6b,0x48], BARCODE_CODE128: [0x1d,0x6b,0x49], }, }
2. 修正打印业务逻辑
等待图片加载完成后再编码,开头增加打印机初始化,避免混用内置格式方法,写入完成后延迟断开连接:
import { Component, NgZone } from '@angular/core'; import EscPosEncoder from 'esc-pos-encoder-ionic'; import { commands } from '../printer/printer-command'; import { BluetoothSerial } from '@awesome-cordova-plugins/bluetooth-serial/ngx'; @Component({ selector: 'app-tab3', templateUrl: 'tab3.page.html', styleUrls: ['tab3.page.scss'] }) export class Tab3Page { // 58mm打印机设为384,80mm打印机设为576 private readonly PRINTER_MAX_WIDTH = 384; MAC = ''; // 你的打印机MAC地址变量 constructor( private bluetoothSerial: BluetoothSerial, private ngZone: NgZone ) {} async testFont() { // 等待图片加载完成再执行编码 const img = await this.loadImage('assets/header.bmp'); // 按比例缩放图片适配打印机宽度 const imgHeight = Math.round(img.height * (this.PRINTER_MAX_WIDTH / img.width)); const encoder = new EscPosEncoder(); const result = encoder.initialize(); result // 编码开头先发送打印机初始化指令 .raw(commands.HARDWARE.HW_INIT) // 传入加载完成的图片对象,参数为:图片、宽度、高度、抖动算法、亮度阈值 .image(img, this.PRINTER_MAX_WIDTH, imgHeight, 'atkinson', 128) // 图片打印完成后走纸3行 .newline(3) .raw(commands.TEXT_FORMAT.TXT_4SQUARE) .raw(commands.TEXT_FORMAT.TXT_FONT_A) .line('4SQUARE FONT A') .raw(commands.TEXT_FORMAT.TXT_4SQUARE) .raw(commands.TEXT_FORMAT.TXT_FONT_B) .line('4SQUARE FONT B') // 普通便携热敏打印机大多不支持字体C,测试无效可删除该段 .raw(commands.TEXT_FORMAT.TXT_4SQUARE) .raw(commands.TEXT_FORMAT.TXT_FONT_C) .line('4SQUARE FONT C') .raw(commands.TEXT_FORMAT.TXT_NORMAL) .raw(commands.TEXT_FORMAT.TXT_FONT_A) .line('NORMAL FONT A') .raw(commands.TEXT_FORMAT.TXT_NORMAL) .raw(commands.TEXT_FORMAT.TXT_FONT_B) .line('NORMAL FONT B') .raw(commands.TEXT_FORMAT.TXT_NORMAL) .raw(commands.TEXT_FORMAT.TXT_FONT_C) .line('NORMAL FONT C') // 自定义宽高参数仅支持1-8,禁止传入超出范围的值 .raw(commands.TEXT_FORMAT.TXT_CUSTOM_SIZE(3,2)) .line('CUSTOM SIZE TEST') .raw(commands.TEXT_FORMAT.TXT_CUSTOM_SIZE(2,1)) .line('CUSTOM SIZE TEST') .raw(commands.TEXT_FORMAT.TXT_CUSTOM_SIZE(1,1)) .line('CUSTOM SIZE TEST') .raw(commands.TEXT_FORMAT.TXT_HEIGHT[1]) .line('HEIGHT TEST') .raw(commands.TEXT_FORMAT.TXT_HEIGHT[2]) .line('HEIGHT TEST') .raw(commands.TEXT_FORMAT.TXT_HEIGHT[3]) .line('HEIGHT TEST') .raw(commands.TEXT_FORMAT.TXT_HEIGHT[4]) .line('HEIGHT TEST') .raw(commands.TEXT_FORMAT.TXT_HEIGHT[5]) .line('HEIGHT TEST') .raw(commands.TEXT_FORMAT.TXT_WIDTH[1]) .line('WIDTH TEST') .raw(commands.TEXT_FORMAT.TXT_WIDTH[2]) .line('WIDTH TEST') .raw(commands.TEXT_FORMAT.TXT_WIDTH[3]) .line('WIDTH TEST') .raw(commands.TEXT_FORMAT.TXT_WIDTH[4]) .line('WIDTH TEST') .raw(commands.TEXT_FORMAT.TXT_WIDTH[5]) .line('WIDTH TEST') .raw(commands.TEXT_FORMAT.TXT_2WIDTH) .line('2WIDTH TEST') // 统一用raw指令控制格式,不要调用内置size()方法避免覆盖配置 .raw(commands.TEXT_FORMAT.TXT_NORMAL) .raw(commands.TEXT_FORMAT.TXT_FONT_B) .line('small text') .raw(commands.TEXT_FORMAT.TXT_NORMAL) .raw(commands.TEXT_FORMAT.TXT_FONT_A) .line('A line of normal text') // 末尾走纸5行方便撕纸 .newline(5); const resultByte = result.encode(); this.bluetoothSerial.connect(this.MAC).subscribe(() => { this.bluetoothSerial.write(resultByte) .then(() => { this.printNotif() // 延迟200ms断开,等待缓冲区数据全部发送完成 setTimeout(() => { this.bluetoothSerial.disconnect(); }, 200); }) .catch((err) => { this.printError(err) this.bluetoothSerial.disconnect(); }); }) } // 封装图片加载方法,等待资源加载完成再返回 private loadImage(src: string): Promise<HTMLImageElement> { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => resolve(img); img.onerror = (err) => reject(err); img.src = src; }) } private printNotif() { // 自定义打印成功逻辑 } private printError(err: any) { // 自定义打印失败逻辑 } }
额外注意事项
- 打印用图片必须为本地同源资源,不要使用跨域网络图片,否则canvas无法读取像素数据会导致图片编码失败
- 若图片仍无法打印,先将图片转为1bit单色BMP格式,删除透明通道,分辨率控制在打印机最大宽度范围内
- 不要混用编码器内置格式方法和自定义raw指令,内置方法自动生成的格式指令会覆盖自定义配置
- 蓝牙打印单次写入数据量不要超过打印机缓冲区大小,大图片可拆分成分片写入避免
相关产品推荐
相关产品推荐

