feat(client): 标签 TSPL 裸发支持 Windows(winspool WritePrinter)
- 抽出 _findThermalPrinter(macOS: lpstat -e;Windows: Printing.listPrinters) 与 _sendRaw(macOS/Linux: lp -o raw;Windows: winspool WritePrinter RAW) - 新增 _winRawPrint:OpenPrinter/StartDocPrinter(RAW)/WritePrinter 直送裸字节 - 位图渲染与 TSPL 生成跨平台复用;macOS 行为不变 - 依赖新增 win32/ffi(仅 Windows 运行时调用,已 Platform.isWindows 守卫) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,18 @@ import 'dart:io' show Platform, Process, File, Directory;
|
||||
import 'dart:convert' show ascii;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:ffi';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:win32/win32.dart'
|
||||
show
|
||||
OpenPrinter,
|
||||
StartDocPrinter,
|
||||
StartPagePrinter,
|
||||
WritePrinter,
|
||||
EndPagePrinter,
|
||||
EndDocPrinter,
|
||||
ClosePrinter,
|
||||
DOC_INFO_1;
|
||||
import 'package:flutter/foundation.dart' show kIsWeb, debugPrint;
|
||||
import 'package:flutter/services.dart';
|
||||
import '../errors/error_reporter.dart';
|
||||
@@ -39,26 +51,105 @@ const _ink = PdfColor(0.067, 0.067, 0.067); // #111 body text
|
||||
// 得力 DL-888B 等热敏机走系统打印(CUPS 通用驱动)会多走纸/空白,必须发原生 TSPL。
|
||||
// 直接画位图而非走 PDF 光栅化:避免二维码渲染丢失、SIZE 用整数避免多走纸。
|
||||
|
||||
/// 查找热敏标签机的 CUPS 队列名(名字含 DL-888 / 888B / Deli)
|
||||
Future<String?> _findThermalQueue() async {
|
||||
try {
|
||||
final r = await Process.run('lpstat', ['-e']);
|
||||
if (r.exitCode != 0) return null;
|
||||
for (final raw in (r.stdout as String).split('\n')) {
|
||||
final q = raw.trim();
|
||||
if (q.isEmpty) continue;
|
||||
final lo = q.toLowerCase();
|
||||
if (lo.contains('dl-888') || lo.contains('dl_888') ||
|
||||
lo.contains('888b') || lo.contains('deli')) {
|
||||
return q;
|
||||
bool _isThermalName(String n) {
|
||||
final lo = n.toLowerCase();
|
||||
return lo.contains('dl-888') ||
|
||||
lo.contains('dl_888') ||
|
||||
lo.contains('888b') ||
|
||||
lo.contains('deli');
|
||||
}
|
||||
|
||||
/// 查找热敏标签机(名字含 DL-888 / 888B / Deli)。
|
||||
/// macOS: CUPS 队列名(lpstat -e);Windows: 打印机名(Printing.listPrinters)。
|
||||
Future<String?> _findThermalPrinter() async {
|
||||
if (Platform.isMacOS) {
|
||||
try {
|
||||
final r = await Process.run('lpstat', ['-e']);
|
||||
if (r.exitCode == 0) {
|
||||
for (final raw in (r.stdout as String).split('\n')) {
|
||||
final q = raw.trim();
|
||||
if (q.isNotEmpty && _isThermalName(q)) return q;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[label] lpstat 失败: $e');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[label] lpstat 失败: $e');
|
||||
return null;
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
try {
|
||||
for (final p in await Printing.listPrinters()) {
|
||||
if (_isThermalName(p.name)) return p.name;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[label] listPrinters 失败: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 把裸 TSPL 字节发送到打印机:
|
||||
/// macOS/Linux 用 `lp -o raw`;Windows 用 winspool WritePrinter。失败抛异常。
|
||||
Future<void> _sendRaw(String printer, Uint8List tspl) async {
|
||||
if (Platform.isWindows) {
|
||||
if (!_winRawPrint(printer, tspl)) {
|
||||
throw Exception('Windows 裸发失败: $printer');
|
||||
}
|
||||
return;
|
||||
}
|
||||
final f = File(
|
||||
'${Directory.systemTemp.path}/label_${DateTime.now().millisecondsSinceEpoch}.tspl');
|
||||
await f.writeAsBytes(tspl, flush: true);
|
||||
final r = await Process.run('lp', ['-d', printer, '-o', 'raw', f.path]);
|
||||
debugPrint('[label] lp exit=${r.exitCode} out=${r.stdout} err=${r.stderr}');
|
||||
if (r.exitCode != 0) {
|
||||
throw Exception('lp 裸发失败(exit ${r.exitCode}): ${r.stderr}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Windows: 通过 winspool 以 RAW 数据类型把字节直接送进打印队列
|
||||
bool _winRawPrint(String printerName, Uint8List data) {
|
||||
final pName = printerName.toNativeUtf16();
|
||||
final phPrinter = calloc<IntPtr>();
|
||||
final docName = 'jiu label'.toNativeUtf16();
|
||||
final dataType = 'RAW'.toNativeUtf16();
|
||||
final docInfo = calloc<DOC_INFO_1>();
|
||||
final pcWritten = calloc<Uint32>();
|
||||
final pData = calloc<Uint8>(data.length);
|
||||
try {
|
||||
if (OpenPrinter(pName, phPrinter, nullptr) == 0) {
|
||||
debugPrint('[label] OpenPrinter 失败: $printerName');
|
||||
return false;
|
||||
}
|
||||
final h = phPrinter.value;
|
||||
docInfo.ref
|
||||
..pDocName = docName
|
||||
..pOutputFile = nullptr
|
||||
..pDatatype = dataType;
|
||||
if (StartDocPrinter(h, 1, docInfo) == 0) {
|
||||
ClosePrinter(h);
|
||||
return false;
|
||||
}
|
||||
StartPagePrinter(h);
|
||||
pData.asTypedList(data.length).setAll(0, data);
|
||||
final ok = WritePrinter(h, pData.cast(), data.length, pcWritten);
|
||||
EndPagePrinter(h);
|
||||
EndDocPrinter(h);
|
||||
ClosePrinter(h);
|
||||
debugPrint('[label] WritePrinter ok=$ok written=${pcWritten.value}');
|
||||
return ok != 0;
|
||||
} finally {
|
||||
calloc.free(pName);
|
||||
calloc.free(phPrinter);
|
||||
calloc.free(docName);
|
||||
calloc.free(dataType);
|
||||
calloc.free(docInfo);
|
||||
calloc.free(pcWritten);
|
||||
calloc.free(pData);
|
||||
}
|
||||
}
|
||||
|
||||
/// 在 canvas 上画一行文字(黑色,单行不换行,CJK 走系统字体回退)
|
||||
void _drawText(ui.Canvas canvas, String s, double x, double y, double size,
|
||||
double maxW, {bool bold = false, bool center = false}) {
|
||||
@@ -107,10 +198,10 @@ Future<bool> _printFlatLabelThermal({
|
||||
required String date,
|
||||
required Uint8List qrBytes,
|
||||
}) async {
|
||||
if (kIsWeb || !Platform.isMacOS) return false; // 目前仅 macOS(Windows 待加)
|
||||
final queue = await _findThermalQueue();
|
||||
debugPrint('[label] thermal queue = $queue');
|
||||
if (queue == null) return false;
|
||||
if (kIsWeb || !(Platform.isMacOS || Platform.isWindows)) return false;
|
||||
final printer = await _findThermalPrinter();
|
||||
debugPrint('[label] thermal printer = $printer');
|
||||
if (printer == null) return false;
|
||||
|
||||
const w = 320, h = 160; // 40×20mm @203dpi
|
||||
final recorder = ui.PictureRecorder();
|
||||
@@ -182,15 +273,8 @@ Future<bool> _printFlatLabelThermal({
|
||||
out.add(ascii.encode('\r\nPRINT 1,1\r\n'));
|
||||
final tspl = out.toBytes();
|
||||
|
||||
final f = File(
|
||||
'${Directory.systemTemp.path}/label_${DateTime.now().millisecondsSinceEpoch}.tspl');
|
||||
await f.writeAsBytes(tspl, flush: true);
|
||||
debugPrint('[label] flat tspl ${tspl.length}B -> lp -d $queue -o raw ${f.path}');
|
||||
final r = await Process.run('lp', ['-d', queue, '-o', 'raw', f.path]);
|
||||
debugPrint('[label] lp exit=${r.exitCode} out=${r.stdout} err=${r.stderr}');
|
||||
if (r.exitCode != 0) {
|
||||
throw Exception('lp 裸发失败(exit ${r.exitCode}): ${r.stderr}');
|
||||
}
|
||||
debugPrint('[label] flat tspl ${tspl.length}B -> $printer');
|
||||
await _sendRaw(printer, tspl);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user