Files
2026-09-23 18:11:35 +08:00

151 lines
5.4 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:isolate';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import '../../image/card_image.dart';
import '../../server/protocol.dart';
import '../../utils/file_log.dart';
import '../backend.dart';
import '../cups_backend.dart' show noopLog;
import 'pavo_ffi.dart';
import 'pavo_printer.dart';
/// Windows 打印后端:HiTi PavoApi 直打(BGR/K 分层 BITMAP)。
///
/// 所有 ffi 调用(含像素转换的 CPU 密集计算)都放进 `Isolate.run`:
/// PavoApi 的同步调用最长可阻塞数秒(发送 2MB 像素)、打印轮询最长 3 分钟,
/// 放独立 isolate 保证主 isolate 的 HTTP 服务(任务状态轮询)不被卡住。
///
/// **重要**:传给 Isolate.run 的闭包/函数不得捕获任何实例状态(this 链会经
/// onLog→AgentLog→StreamController→UI 订阅链一路拽到 Timer 等 unsendable 对象,
/// isolate 消息序列化直接抛 "Illegal argument in isolate message")——
/// 因此 isolate 入口全部是顶层函数,只以纯数据(字符串)进出。
class PavoPrintBackend implements PrintBackend {
PavoPrintBackend({void Function(String line)? onLog}) : _log = onLog ?? noopLog;
final void Function(String line) _log;
@override
String get name => 'pavo';
@override
Future<PrinterStatus> getStatus() {
// 日志文件路径以纯数据传入 isolate(isolate 内 stdout 在 GUI 模式下丢失)
final logPath = FileLog.currentPath;
return Isolate.run(() => pavoStatusSync(logPath));
}
@override
Future<void> printOneCard(PrintRequest request) {
_log('Pavo 直打任务开始(份数 ${request.copies},后端在子 isolate 执行)');
final front = request.front!;
final back = request.back;
// 先解出纯数据再进闭包:闭包只捕获字符串/整数(最小捕获面,杜绝实例链)
final frontColor = front.color;
final frontK = front.k;
final backColor = back?.color;
final backK = back?.k;
final copies = request.copies;
final logPath = FileLog.currentPath;
return Isolate.run(() => pavoPrintSync(
frontColor: frontColor,
frontK: frontK,
backColor: backColor,
backK: backK,
copies: copies,
logPath: logPath,
));
}
}
/// isolate 入口:查询打印机状态(顶层函数,入参均为纯数据)。
PrinterStatus pavoStatusSync(String? logPath) {
try {
final api = PavoApi();
final printers = PavoPrinter.enumUsbPrinters(api, logPath: logPath);
if (printers.isEmpty) {
return PrinterStatus.offline('打印机未连接');
}
final printer = printers.first.toNativeUtf16();
try {
final status = PavoPrinter.getStatus(api, printer);
final (ribbonType, remainCount) = PavoPrinter.getRibbonInfo(api, printer);
final dto = PrinterStatus(
online: true,
model: PavoPrinter.getDeviceInfoText(api, printer, PavoApi.devinfoModelName),
firmware: PavoPrinter.getDeviceInfoText(api, printer, PavoApi.devinfoFirmwareVersion),
connection: 'USB',
ribbon: RibbonInfo(
type: PavoPrinter.ribbonTypeName(ribbonType),
// 设备返回剩余张数,按常见 100 张装 YMCKO 色带折算百分比
remain: ribbonType == PavoApi.ribbonMissing ? -1 : (remainCount > 100 ? 100 : remainCount),
remainCount: remainCount,
),
status: status,
statusText: PavoPrinter.statusTextOf(status),
);
if (status == PavoApi.dsCardLow || status == PavoApi.dsCardOut) {
dto.cardLow = true;
}
return dto;
} finally {
calloc.free(printer);
}
} catch (e) {
return PrinterStatus.offline('状态读取失败:$e');
}
}
/// isolate 入口:转换图层并直打(顶层函数,参数全部为可发送的纯数据)。
Future<void> pavoPrintSync({
required String frontColor,
String? frontK,
String? backColor,
String? backK,
required int copies,
String? logPath,
}) async {
final api = PavoApi();
final printers = PavoPrinter.enumUsbPrinters(api, logPath: logPath);
if (printers.isEmpty) {
throw StateError('未找到 USB 连接的 HiTi 证卡打印机');
}
final printer = printers.first.toNativeUtf16();
try {
// Windows Pavo 直打方向校准(2026-09-23 实测):纵向位图 + byOrientation=1 会被
// SDK 旋转 90° 且锚定错位;统一预旋转成 1014×642 横向位图后按 byOrientation=2 打印。
// 旋转方向常量见 card_image.dart 的 pavoPortraitRotateClockwise(测试卡校准)。
final frontImg = CardImage.fromDataUrl(frontColor).toLandscape();
final frontBgr = frontImg.toBgr24();
final Uint8List? frontKGray = frontK != null && frontK.isNotEmpty
? CardImage.fromDataUrl(frontK).toLandscape().toGray8()
: null;
final Uint8List? backBgr;
final Uint8List? backKGray;
if (backColor != null && backColor.isNotEmpty) {
final backImg = CardImage.fromDataUrl(backColor).toLandscape();
backBgr = backImg.toBgr24();
backKGray = backK != null && backK.isNotEmpty
? CardImage.fromDataUrl(backK).toLandscape().toGray8()
: null;
} else {
backBgr = null;
backKGray = null;
}
await PavoPrinter.printOneCard(
api,
printer,
frontBgr: frontBgr,
frontK: frontKGray,
backBgr: backBgr,
backK: backKGray,
isLandscape: true,
copies: copies,
);
} finally {
calloc.free(printer);
}
}