You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ionic使用esc-pos-encoder-ionic蓝牙热敏打印图片及字体调整异常

Ionic对接ESC/POS蓝牙热敏打印机异常修复方案

现存问题

  • 调用图片打印接口时打印机无图片输出
  • 自定义ESC/POS指令调整字体宽高、字体类型不生效,所有文本始终以默认字体输出
  • 多型号打印机测试问题稳定复现,可排除硬件兼容性问题

问题根因

  1. 图片打印失效原因
    • 图片未等加载完成就传入编码器:代码中创建Image对象设置src后立刻调用编码方法,此时图片资源未完成解码,编码器无法获取像素数据,生成不了有效图片打印指令
    • 部分场景下图片尺寸超出打印机可打印宽度、图片位深不符合要求:常规58mm热敏打印机最大打印宽度为384像素,80mm机型为576像素,非1bit单色位图编码器无法正常识别
  2. 字体格式失效原因
    • 自定义指令集数据类型错误: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指令,内置方法自动生成的格式指令会覆盖自定义配置
  • 蓝牙打印单次写入数据量不要超过打印机缓冲区大小,大图片可拆分成分片写入避免
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 11:24:40