引言
使用 flutter 进行 pos 软件开发,诸多的业务场景都涉及到了 小票、标签 打印。基于通用化,跨平台的设计要求,小编整理了这篇文章。
核心设计思想
跨平台通用化方案我们主要解决以下三点:
票据样式: 拒绝硬编码,直接使用 flutter-widget 进行样式开发,更加直观灵活
打印指令集: 不嵌入厂商的打印SDK,适配一码多用,无后续接入开发消耗
传输方式: 指令集传输方式可扩展,底层代码无需变动
整体方案流程
票据样式使用 flutter-widget 进行开发,打印策略使用光栅位图的统一标准进行指令集中转,所有的数据转换在 dart 层完成,网络打印机使用 dart-socket 进行传输,usb 传输封装各平台的 write 方法。
整个工具库做统一数据中转,各平台无需考虑数据层的处理,只处理数据传输。
步骤一: 将 widget 转换打印图层
使用 flutter-widget 开发票据样式,数据填充 widget 后,加入队列转 Uint8List 图像数据
功能实现可直接使用我们的开源库 print_image_generate_tool,提供能力将 widget 视图转换成 Uint8List 数据,内部维护队列,按加入顺序生成返回图像数据。
使用方式:
首先,初始化打印图层
- 在页面根节点下将打印图层 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;
//... 打印逻辑下面补充
}
- 将 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 打印机 搜索、写入 等能力;