Files
desktop-plugin/test/card_image_test.dart
dzc 62909609d1 fix(windows): 枚举解码误判 UTF-16 为 UTF-8 导致设备路径碎片化——0x80 全假象
用户 Windows 无驱动实测日志破案:PavoApiW_x64 枚举成功(size=256),缓冲是
UTF-16LE 的裸 USB 设备路径(\\?\usb#vid_0d16&pid_0309#serial#{guid}),
但 printableRatio 把 \0 计为可打印字符,UTF-8 误判通过 → 路径被 \0 拆成
单字符碎片(first='\')→ CheckPrinterStatus/PrintOneCard 全部 0x80——
此前所有"离线"均为碎片名所致,设备与通信从未真正尝试过。

结论反转:**Windows 免驱动成立且必须免驱动**——PavoApi 直连裸 USB 设备
(GUID_DEVINTERFACE_USB_DEVICE);装 HiTi 驱动反而接管 USB 接口导致枚举为空
(用户实测:装驱动前枚举到、装后 size=0、卸载后又枚举到)。

修复:
- decodeAuto 增加 UTF-16LE 特征判定(奇数位字节全零)优先于比例启发;
  新增守护测试(设备路径形态整串不碎片化)
- 向导 Windows 分支反转:不再提供驱动安装(mac only),文案明确
  "SDK 直连无需驱动;已装 HiTi 驱动请卸载(驱动占用 USB 接口导致无法直连)"
2026-09-21 17:49:15 +08:00

113 lines
4.6 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:convert';
import 'dart:typed_data';
import 'package:desktop_plugin/image/card_image.dart';
import 'package:desktop_plugin/print/pavo/pavo_ffi.dart';
import 'package:desktop_plugin/print/pavo/pavo_printer.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart';
String dataUrl(Image img) => 'data:image/png;base64,${base64.encode(encodePng(img))}';
void main() {
group('CardImage 像素层转换', () {
test('横向图缩放到 1014×642,纯红转 BGR 字节序正确', () {
final img = Image(width: 100, height: 50);
fill(img, color: ColorRgb8(255, 0, 0));
final card = CardImage.fromDataUrl(dataUrl(img));
expect(card.isLandscape, isTrue);
expect(card.width, 1014);
expect(card.height, 642);
final bgr = card.toBgr24();
expect(bgr.length, 1014 * 642 * 3);
// 红色像素 → BGR 序 [B=0, G=0, R=255]
expect(bgr[0], 0);
expect(bgr[1], 0);
expect(bgr[2], 255);
// 中间像素一致(无行对齐错位/跨行污染)
final mid = (642 ~/ 2) * 1014 * 3 + (1014 ~/ 2) * 3;
expect(bgr[mid], 0);
expect(bgr[mid + 1], 0);
expect(bgr[mid + 2], 255);
});
test('纵向图缩放到 642×1014,纯蓝转 BGR 正确', () {
final img = Image(width: 50, height: 100);
fill(img, color: ColorRgb8(0, 0, 255));
final card = CardImage.fromDataUrl(dataUrl(img));
expect(card.isLandscape, isFalse);
expect(card.width, 642);
expect(card.height, 1014);
final bgr = card.toBgr24();
// 蓝色像素 → BGR 序 [B=255, G=0, R=0]
expect(bgr[0], 255);
expect(bgr[1], 0);
expect(bgr[2], 0);
});
test('透明像素铺白,避免黑底', () {
// 注意:image v4 的 Image() 默认是 3 通道不透明黑,透明图必须显式 numChannels: 4
final img = Image(width: 100, height: 50, numChannels: 4); // RGBA(0,0,0,0) 全透明
final card = CardImage.fromDataUrl(dataUrl(img));
final bgr = card.toBgr24();
expect(bgr[0], 255);
expect(bgr[1], 255);
expect(bgr[2], 255);
});
test('灰度转换:红 ≈ 76、白 = 255(K 层白底黑字语义)', () {
final red = Image(width: 100, height: 50);
fill(red, color: ColorRgb8(255, 0, 0));
final card = CardImage.fromDataUrl(dataUrl(red));
final gray = card.toGray8();
expect(gray.length, 1014 * 642);
expect(gray[0], 255 * 299 ~/ 1000); // = 76
expect(gray.every((g) => g == 76), isTrue);
final white = Image(width: 100, height: 50);
fill(white, color: ColorRgb8(255, 255, 255));
expect(CardImage.fromDataUrl(dataUrl(white)).toGray8()[0], 255);
});
test('空 data URL 报错', () {
expect(() => CardImage.fromDataUrl(''), throwsArgumentError);
});
});
group('PavoPrinter 纯 Dart 工具', () {
test('decodeAuto 识别 UTF-8 / UTF-16 打印机名', () {
// 纯 ASCII:UTF-8 解码即可正确还原(NUL 计入可打印率,两种编码都可能命中 UTF-8 路径)
final ascii = Uint8List.fromList('HiTi CS-200e'.codeUnits);
expect(PavoPrinter.decodeAuto(ascii).startsWith('HiTi CS-200e'), isTrue);
// 带中文:UTF-8 解码得乱码(可打印率低),走 UTF-16 路径正确还原
final name = '证卡打印机';
final bytes = <int>[];
for (final c in name.codeUnits) {
bytes..add(c & 0xff)..add(c >> 8);
}
expect(PavoPrinter.decodeAuto(Uint8List.fromList(bytes)), name);
});
test('decodeAuto 识别 UTF-16LE 设备路径(Windows 实测形态,整串不碎片化)', () {
// Win11 无驱动实测:枚举返回 UTF-16LE 的 \\?\usb#vid_xxxx&pid_xxxx#... 设备路径。
// 奇数位字节全零特征必须命中 UTF-16 路径,否则整串会被 \0 拆成单字符碎片
// (后续 API 拿 '\' 单字符调用全部 0x80)。
const path = r'\\?\usb#vid_0d16&pid_0309#2ec511089913092#{28d78fad-5a12-11d1-ae5b-0000f803a8c2}';
final bytes = <int>[];
for (final c in path.codeUnits) {
bytes..add(c & 0xff)..add(c >> 8);
}
bytes..add(0)..add(0); // UTF-16 NUL 结尾
final decoded = PavoPrinter.decodeAuto(Uint8List.fromList(bytes));
expect(decoded, contains(r'\\?\usb#vid_0d16&pid_0309'));
expect(decoded.split('\x00').where((s) => s.isNotEmpty).first, path);
});
test('statusTextOf 状态文案', () {
expect(PavoPrinter.statusTextOf(0), '就绪');
expect(PavoPrinter.statusTextOf(PavoApi.dsOutOfRibbon), '色带已用尽,请更换色带');
expect(PavoPrinter.statusTextOf(0x12345678), contains('设备状态异常'));
});
});
}