flutter:小票标签打印【跨平台解决方案】

引言

使用 flutter 进行 pos 软件开发,诸多的业务场景都涉及到了 小票标签 打印。基于通用化,跨平台的设计要求,小编整理了这篇文章。

核心设计思想

跨平台通用化方案我们主要解决以下三点:


  • 票据样式: 拒绝硬编码,直接使用 flutter-widget 进行样式开发,更加直观灵活

  • 打印指令集: 不嵌入厂商的打印SDK,适配一码多用,无后续接入开发消耗

  • 传输方式: 指令集传输方式可扩展,底层代码无需变动

整体方案流程

票据样式使用 flutter-widget 进行开发,打印策略使用光栅位图的统一标准进行指令集中转,所有的数据转换在 dart 层完成,网络打印机使用 dart-socket 进行传输,usb 传输封装各平台的 write 方法。

整个工具库做统一数据中转,各平台无需考虑数据层的处理,只处理数据传输。

步骤一: 将 widget 转换打印图层

使用 flutter-widget 开发票据样式,数据填充 widget 后,加入队列转 Uint8List 图像数据

功能实现可直接使用我们的开源库 print_image_generate_tool,提供能力将 widget 视图转换成 Uint8List 数据,内部维护队列,按加入顺序生成返回图像数据。

使用方式:

首先,初始化打印图层
  1. 在页面根节点下将打印图层 PrintImageGenerateWidget 初始化
MaterialApp(
    onGenerateTitle: (context) => '打印测试',
    home: Scaffold(
    body: PrintImageGenerateWidget(
       contentBuilder: (context) {
       return const HomePage();
       },
       onPictureGenerated: _onPictureGenerated,  //用于接收 widget 转 Uint8List
     ),
   ),
)

//打印图层生成成功
  Future<void> _onPictureGenerated(PicGenerateResult data) async {
    //widget生成的图像的字节结果
    final imageBytes = data.data;
    //打印票据类型(标签、小票)
    final printTypeEnum = printTask.printTypeEnum;
    //... 打印逻辑下面补充
  }
  
  1. 将 widget 生成图层数据,注意: 传入的 tempWidget 必须实现或继承父类 ATempWidget
///生成打印的模板 Widget 需要继承这个类
mixin ATempWidget {
  //生成图片的缩放倍数
  double get pixelRatio => 1;

  //需要生成的票据像素宽度
  int get pixelPagerWidth;

  //需要生成的票据像素高度
  int get pixelPagerHeight => -1;
}
示例1:创建一个 宽45mm,高70mm 的标签模板:
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

// ignore: depend_on_referenced_packages
import 'package:print_image_generate_tool/print_image_generate_tool.dart';

// 标签样式 demo
class LabelTemp extends StatelessWidget with ATempWidget {
  final String data;

  const LabelTemp(this.data, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: Text(data),
    );
  }

  @override
  int get pixelPagerWidth => 360; //1mm对应8像素,45mm 对应像素宽度为 360

  @override
  int get pixelPagerHeight => 560; //1mm对应8像素,70mm 对应像素高度为 560
}

业务方触发打印任务时,使用以下代码将票据模板转成图层数据

// 生成打印图层任务,指定任务类型为标签
    PictureGeneratorProvider.instance.addPicGeneratorTask(
      PicGenerateTask<PrinterInfo>(
        tempWidget: LabelTemp('标签内容') as ATempWidget,
        printTypeEnum: PrintTypeEnum.label, //标识是标签
        params: printerInfo,
      ),
    );
示例2:创建一个 宽80mm 的小票模板:
// 小票样式 demo
class ReceiptTemp extends StatelessWidget with ATempWidget {
  final String data;

  const ReceiptTemp(this.data, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: Text(data),
    );
  }

  @override
  int get pixelPagerWidth => 550;
  
  // 小票不限制高度,pixelPagerHeight = -1 就行,无需重写
}

业务方触发打印任务时,使用以下代码将票据模板转成图层数据

// 生成打印图层任务,指定任务类型为小票
    PictureGeneratorProvider.instance.addPicGeneratorTask(
      PicGenerateTask<PrinterInfo>(
        tempWidget: ReceiptTemp('小票内容') as ATempWidget,
        printTypeEnum: PrintTypeEnum.receipt,
        params: printerInfo,
      ),
    );

步骤二:打印图层转打印指令集

将 widget 转换打印图层后,我们需要将 Uint8List 数据转换为打印机可识别的数据类型,ESC 对应小票机,TSC 对应标签机。

使用 flutter_printer_plus 开源库进行功能实现:

// 转 TSC 字节,imageBytes 类型为 Uint8List
var printData = await PrinterCommandTool.generatePrintCmd(
        imgData: imageBytes,
        printType: PrintTypeEnum.label,
      );
// 转 ESC 字节,imageBytes 类型为 Uint8List
var printData = await PrinterCommandTool.generatePrintCmd(
        imgData: imageBytes,
        printType: PrintTypeEnum.receipt,
      );

步骤三:目标设备发送数据

可以自行扩展实现 write 方法,也可以使用 flutter_printer_plus 内提供的方式。目前已提供的实现如下:

  • USB 传输 (USB打印机)

flutter_printer_plus 提供获取当前已连接的打印机列表,列表内每一个元素类型为 usbDevice。

// usb 打印
        final conn = UsbConn(usbDevice);
        conn.writeMultiBytes(printData, 1024 * 3);
  • IP 传输(网口打印机)

example 内提供获取局域网内可用打印机样例

// IP 打印
        final conn = NetConn(ip);
        conn.writeMultiBytes(printData);

结合以上三个步骤,我们在步骤一内接收 Uint8List 后可以这么写:

//打印图层生成成功
Future<void> _onPictureGenerated(PicGenerateResult data) async {
  final imageBytes = data.data;
  final printTask = data.taskItem;

  //获取指定的目标打印机
  final printerInfo = printTask.params as PrinterInfo;
  //打印票据类型(标签、小票)
  final printTypeEnum = printTask.printTypeEnum;

  if (imageBytes != null) {
    // Uint8List 转小票或标签指令集
    var printData = await PrinterCommandTool.generatePrintCmd(
      imgData: imageBytes,
      printType: printTypeEnum,
    );
    // 发送打印指令
    if (printerInfo.isUsbPrinter) {
      // usb 打印
      final conn = UsbConn(printerInfo.usbDevice!);
      conn.writeMultiBytes(printData, 1024 * 3);
    } else if (printerInfo.isNetPrinter) {
      // 网络 打印
      final conn = NetConn(printerInfo.ip!);
      conn.writeMultiBytes(printData);
    }
  }
}

完整流程级具体实现逻辑可参考 example 示例

建议使用者将上层进行封装(维护队列),打印图层生成成功后先将图像保存本地,等待上一个打印任务结束后再从队列中获取本地图片进行下一个打印任务,避免造成内存抖动。

附上 demo 打印的小票样式

小票样式

疑难点记录

1. 打印的票据,一半正常,一半显示乱码

原因:数据太大导致打印机内存溢出输出乱码。
处理:对图片进行分割处理,分成n个小段进行打印 长图切割实现方式参考

2. 打印机打印票据,出票缓慢,声音卡顿

原因:打印的位图宽度太大。
处理:将位图宽度进行缩小,问题解决。以 80mm 宽度小票为例,通常 1mm 等于 8个像素,因为宽度适配在实际打印中有偏差,不宜设置为完全吻合因此,适合小票打印机打印的图片像素尺寸应为:558px、372px。

开源工具库

print_image_generate_tool:提供 widget 转图像数据(Uint8List)能力;

flutter_printer_plus:提供图像数据(Uint8List) 转 TSC 、ESC能力,提供 ip、usb 打印支持;

esc_utils:小票打印机数据转换工具,提供图像数据(Uint8List) 转 ESC 能力;

tsc_utils:标签打印机数据转换工具,提供图像数据(Uint8List) 转 TSC 能力;

android_usb_printer:flutter android 端插件,提供 usb 打印机 搜索、写入 等能力;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容