@@ -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
@@ -189,7 +190,8 @@ double _fitFont(String s, double maxW, double maxH, bool bold, double cap) {
return size ;
}
/// 用 dart:ui 把一张标签绘制成 320× 160 位图(热敏标签实际尺寸 40×20mm @203dpi)。
/// 用 dart:ui 把一张标签绘制成位图(热敏标签实际尺寸 40×20mm @203dpi)。
/// 内部以 2× 超采样(640×320)绘制,转 TSPL 时再降采样,消除反锯齿模糊。
/// 供 TSPL 裸发和预览渲染共用同一套画布逻辑,确保预览即实际输出。
Future < ui . Image > _renderLabelBitmap ( {
required String shop ,
@@ -199,10 +201,13 @@ Future<ui.Image> _renderLabelBitmap({
required String date ,
required Uint8List qrBytes ,
} ) async {
const w = 320 , h = 160 ; // 40× 20mm @203dpi
const w = 320 , h = 160 ; // 40× 20mm @203dpi(逻辑尺寸)
const scale = 2 ; // 2× 超采样,内部以 640×320 绘制
const sw = w * scale , sh = h * scale ;
final recorder = ui . PictureRecorder ( ) ;
final canvas = ui . Canvas (
recorder , ui . Rect . fromLTWH ( 0 , 0 , w . toDouble ( ) , h . toDouble ( ) ) ) ;
recorder , ui . Rect . fromLTWH ( 0 , 0 , sw . toDouble ( ) , sh . toDouble ( ) ) ) ;
canvas . scale ( scale . toDouble ( ) , scale . toDouble ( ) ) ; // 坐标系放大,后续坐标不变
canvas . drawRect ( ui . Rect . fromLTWH ( 0 , 0 , w . toDouble ( ) , h . toDouble ( ) ) ,
ui . Paint ( ) . . color = const ui . Color ( 0xFFFFFFFF ) ) ;
@@ -236,8 +241,8 @@ Future<ui.Image> _renderLabelBitmap({
final codeText = code . isNotEmpty ? ' 编号: $ code ' : ' ' ;
final rows = < ( String , double , bool ) > [
if ( shop . isNotEmpty ) ( shop , kShopSize , true ) ,
if ( name . isNotEmpty ) ( name , nameSize , true ) ,
if ( shop . isNotEmpty ) ( shop , kShopSize , false ) , // 店名细体,突出商品名
if ( name . isNotEmpty ) ( name , nameSize , true ) , // 商品名粗体
if ( codeText . isNotEmpty ) ( codeText , kSmallSize , false ) ,
if ( degSpec . isNotEmpty ) ( degSpec , kSmallSize , false ) ,
if ( date . isNotEmpty ) ( date , kSmallSize , false ) ,
@@ -258,11 +263,23 @@ Future<ui.Image> _renderLabelBitmap({
yy + = r . $2 * kLineH + gap ;
}
return recorder . endRecording ( ) . toImage ( w , h ) ;
return recorder . endRecording ( ) . toImage ( sw , sh ) ; // 返回 2× 图(640× 320)
}
/// 桌面端把「扁平标签」直接画成单色位图,TSPL BITMAP 裸发到热敏机 。
/// 版式(无样式):酒行名 / 酒名(长名缩小) / 度数(系列)+规格 / 日期;右侧二维码 + 扫码溯源 。
/// TSPL TEXT 命令辅助:将文本以 GBK 编码写入 BytesBuilder 。
/// TSS24.BF2/TSS16.BF2 是 TSC 固件内置简体中文点阵字体,需 GBK 编码 。
Future < void > _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 < bool > _printFlatLabelThermal ( {
@@ -279,34 +296,79 @@ Future<bool> _printFlatLabelThermal({
debugPrint ( ' [label] thermal printer = $ printer ' ) ;
if ( printer = = null ) return false ;
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 ( ) ;
// 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 \n GAP 2 mm,0 mm \r \n DIRECTION 1 \r \n '
' REFERENCE 0,0 \r \n DENSITY 10 \r \n SPEED 2 \r \n CLS \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 + + ) {
final i = ( yy * w + xx ) * 4 ;
final a = rgba [ i + 3 ] ;
// 将后端 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 \n GAP 2 mm,0 mm \r \n DIRECTION 1 \r \n '
' REFERENCE 0,0 \r \n DENSITY 10 \r \n SPEED 2 \r \n CLS \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 * rgba [ i ] + 0.587 * rgba [ i + 1 ] + 0.114 * rgba [ i + 2 ] ;
if ( lum < 128 ) row [ xx > > 3 ] & = ~ ( 0x80 > > ( xx & 7 ) ) ; // 黑点 -> bit0
: 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 \n PRINT 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 ;
}