From 2ca65e8077004e74c3faf960d2e1ae1f01a75ed2 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 15 Jun 2026 21:10:54 +0800 Subject: [PATCH] chore: release v1.0.52 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 5 + client/lib/core/utils/print_util_stub.dart | 118 +++++++++++++----- .../Flutter/GeneratedPluginRegistrant.swift | 2 + client/pubspec.lock | 8 ++ client/pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 7 files changed, 105 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a705e..edef9d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.52] - 2026-06-15 + +### 改进 +- 热敏标签打印改用打印机内置中文点阵字体(TSS24.BF2/TSS16.BF2),文字清晰度大幅提升,彻底解决 203 DPI 下字体模糊问题 + ## [1.0.51] - 2026-06-15 ### 改进 diff --git a/client/lib/core/utils/print_util_stub.dart b/client/lib/core/utils/print_util_stub.dart index abdd980..6611c15 100644 --- a/client/lib/core/utils/print_util_stub.dart +++ b/client/lib/core/utils/print_util_stub.dart @@ -3,6 +3,7 @@ import 'dart:convert' show ascii; import 'dart:typed_data'; import 'dart:ui' as ui; import 'dart:ffi'; +import 'package:charset_converter/charset_converter.dart'; import 'package:ffi/ffi.dart'; import 'package:win32/win32.dart' show @@ -265,8 +266,20 @@ Future _renderLabelBitmap({ return recorder.endRecording().toImage(sw, sh); // 返回 2× 图(640×320) } -/// 桌面端把「扁平标签」直接画成单色位图,TSPL BITMAP 裸发到热敏机。 -/// 版式(无样式):酒行名 / 酒名(长名缩小) / 度数(系列)+规格 / 日期;右侧二维码 + 扫码溯源。 +/// TSPL TEXT 命令辅助:将文本以 GBK 编码写入 BytesBuilder。 +/// TSS24.BF2/TSS16.BF2 是 TSC 固件内置简体中文点阵字体,需 GBK 编码。 +Future _addTextCmd( + BytesBuilder buf, int x, int y, String font, String text) async { + if (text.isEmpty) return; + final gbk = await CharsetConverter.encode('GBK', text); + buf.add(ascii.encode('TEXT $x,$y,"$font",0,1,1,"')); + buf.add(gbk); + buf.add(ascii.encode('"\r\n')); +} + +/// 桌面端把标签以 TSPL TEXT(内置中文点阵字体)+ QR BITMAP 混合方式裸发到热敏机。 +/// TEXT 走 TSS24.BF2/TSS16.BF2(GBK 编码,203 DPI 下比 TrueType 位图清晰数倍)。 +/// QR 仍走 BITMAP(内置 QRCODE 命令扫描不可靠)。 /// 检测不到热敏机 -> 返回 false(交系统打印);检测到则强制 TSPL,失败抛异常。 /// [printerName] 非空时跳过自动检测直接使用,为空则走 _findThermalPrinter()。 Future _printFlatLabelThermal({ @@ -283,42 +296,79 @@ Future _printFlatLabelThermal({ debugPrint('[label] thermal printer = $printer'); if (printer == null) return false; - // img 是 2× 超采样图(640×320);TSPL 输出仍是 320×160 - const w = 320, h = 160; - final img = await _renderLabelBitmap( - shop: shop, name: name, code: code, degSpec: degSpec, date: date, qrBytes: qrBytes); - final bd = await img.toByteData(format: ui.ImageByteFormat.rawRgba); - final rgba = bd!.buffer.asUint8List(); - final sw = img.width; // 640(2×宽) + // QR BITMAP: 124×124 dots, 右侧 x=192, y=8 + const qrW = 124, qrH = 124; + const qrX = 192, qrY = 8; + const qrWBytes = (qrW + 7) >> 3; // 16 bytes/row - // 打包 TSPL(SIZE 用整数,避免固件不认小数导致多走纸) - const wbytes = (w + 7) >> 3; - final out = BytesBuilder(); - out.add(ascii.encode('SIZE 40 mm,20 mm\r\nGAP 2 mm,0 mm\r\nDIRECTION 1\r\n' - 'REFERENCE 0,0\r\nDENSITY 10\r\nSPEED 2\r\nCLS\r\n' - 'BITMAP 0,0,$wbytes,$h,1,')); - for (int yy = 0; yy < h; yy++) { - final row = Uint8List(wbytes)..fillRange(0, wbytes, 0xFF); // 默认白 bit=1 - for (int xx = 0; xx < w; xx++) { - // 2×2 四像素平均后阈值,消除反锯齿模糊(阈值 160 = 偏灰的边缘算黑) - double lum = 0; - for (int dy = 0; dy < 2; dy++) { - for (int dx = 0; dx < 2; dx++) { - final i = ((yy * 2 + dy) * sw + (xx * 2 + dx)) * 4; - final a = rgba[i + 3]; - lum += a < 128 - ? 255.0 - : 0.299 * rgba[i] + 0.587 * rgba[i + 1] + 0.114 * rgba[i + 2]; - } - } - if (lum / 4 < 128) row[xx >> 3] &= ~(0x80 >> (xx & 7)); // 黑点 -> bit0 + // 将后端 256×256 QR PNG 缩放到 124×124 并转 1-bit + final codec = await ui.instantiateImageCodec(qrBytes, targetWidth: qrW, targetHeight: qrH); + final qrImg = (await codec.getNextFrame()).image; + final qrBd = await qrImg.toByteData(format: ui.ImageByteFormat.rawRgba); + final qrRgba = qrBd!.buffer.asUint8List(); + + final buf = BytesBuilder(); + buf.add(ascii.encode( + 'SIZE 40 mm,20 mm\r\nGAP 2 mm,0 mm\r\nDIRECTION 1\r\n' + 'REFERENCE 0,0\r\nDENSITY 10\r\nSPEED 2\r\nCLS\r\n', + )); + + // QR BITMAP 命令 + buf.add(ascii.encode('BITMAP $qrX,$qrY,$qrWBytes,$qrH,1,')); + for (int yy = 0; yy < qrH; yy++) { + final row = Uint8List(qrWBytes)..fillRange(0, qrWBytes, 0xFF); // 默认白 + for (int xx = 0; xx < qrW; xx++) { + final i = (yy * qrW + xx) * 4; + final a = qrRgba[i + 3]; + final lum = a < 128 + ? 255.0 + : 0.299 * qrRgba[i] + 0.587 * qrRgba[i + 1] + 0.114 * qrRgba[i + 2]; + if (lum < 128) row[xx >> 3] &= ~(0x80 >> (xx & 7)); } - out.add(row); + buf.add(row); } - out.add(ascii.encode('\r\nPRINT 1,1\r\n')); - final tspl = out.toBytes(); + buf.add(ascii.encode('\r\n')); - debugPrint('[label] flat tspl ${tspl.length}B -> $printer'); + // "扫码溯源" 居中于 QR 正下方 + // TSS16 CJK 每字 16 dots; 4字=64; QR 中心 x=192+62=254; 文字起始 x=254-32=222 + await _addTextCmd(buf, 222, qrY + qrH + 2, 'TSS16.BF2', '扫码溯源'); + + // 左侧文字(x=4, 宽度至 x=186) + // TSS24.BF2: 24×24 点阵; TSS16.BF2: 16×16 点阵 + const kFontL = 'TSS16.BF2'; // 小号(编号/度数/日期) + const kFontXL = 'TSS24.BF2'; // 大号(商品名,≤7字时使用) + final nameFont = name.runes.length <= 7 ? kFontXL : kFontL; + final nameH = nameFont == kFontXL ? 24 : 16; + final codeText = code.isNotEmpty ? '编号:$code' : ''; + + // (文本, 字体, 字符高度点数) + final rows = <(String, String, int)>[]; + if (shop.isNotEmpty) rows.add((shop, kFontL, 16)); + if (name.isNotEmpty) rows.add((name, nameFont, nameH)); + if (codeText.isNotEmpty) rows.add((codeText, kFontL, 16)); + if (degSpec.isNotEmpty) rows.add((degSpec, kFontL, 16)); + if (date.isNotEmpty) rows.add((date, kFontL, 16)); + + // 在 y=8..150 区间均匀分布各行 + const kTextTop = 8; + const kTextBot = 150; + const kTextX = 4; + final totalH = rows.fold(0, (s, r) => s + r.$3); + final n = rows.length; + final gapSize = n > 0 + ? ((kTextBot - kTextTop - totalH) / (n + 1)).round().clamp(2, 20) + : 8; + + int yPos = kTextTop + gapSize; + for (final r in rows) { + await _addTextCmd(buf, kTextX, yPos, r.$2, r.$1); + yPos += r.$3 + gapSize; + } + + buf.add(ascii.encode('PRINT 1,1\r\n')); + final tspl = buf.toBytes(); + + debugPrint('[label] TEXT+BITMAP tspl ${tspl.length}B -> $printer'); await _sendRaw(printer, tspl); return true; } diff --git a/client/macos/Flutter/GeneratedPluginRegistrant.swift b/client/macos/Flutter/GeneratedPluginRegistrant.swift index 6ad9ca7..693f7fc 100644 --- a/client/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/client/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import charset_converter import file_picker import file_selector_macos import package_info_plus @@ -13,6 +14,7 @@ import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + CharsetConverterPlugin.register(with: registry.registrar(forPlugin: "CharsetConverterPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/client/pubspec.lock b/client/pubspec.lock index 2319233..6131e19 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + charset_converter: + dependency: "direct main" + description: + name: charset_converter + sha256: af00b5b73b367101c661b8dfaa52abf3e533de7b3e247e3f65c535ebc4df820a + url: "https://pub.dev" + source: hosted + version: "2.4.0" clock: dependency: transitive description: diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 5d5f5a6..71c01cc 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -29,6 +29,8 @@ dependencies: # Windows 热敏机 TSPL 裸发(winspool WritePrinter),仅 Windows 运行时调用 win32: ^5.5.0 ffi: ^2.1.3 + # GBK 编码(TSPL TEXT 内置中文字体需要),支持 macOS/Windows/Linux + charset_converter: ^2.1.0 dev_dependencies: flutter_test: diff --git a/client/windows/flutter/generated_plugin_registrant.cc b/client/windows/flutter/generated_plugin_registrant.cc index 7126a31..fec53a6 100644 --- a/client/windows/flutter/generated_plugin_registrant.cc +++ b/client/windows/flutter/generated_plugin_registrant.cc @@ -6,11 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + CharsetConverterPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CharsetConverterPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); PrintingPluginRegisterWithRegistrar( diff --git a/client/windows/flutter/generated_plugins.cmake b/client/windows/flutter/generated_plugins.cmake index 584fea7..d269f39 100644 --- a/client/windows/flutter/generated_plugins.cmake +++ b/client/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + charset_converter file_selector_windows printing url_launcher_windows