From 78e6a4c19265a4560e5dd5a6aa19249d326c301f Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 28 Aug 2026 17:31:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(client):=20=E7=83=AD=E6=95=8F=E6=89=93?= =?UTF-8?q?=E5=8D=B0=E4=BB=B7=E7=AD=BE=E4=B8=AD=E6=96=87=E5=AD=97=E4=BD=93?= =?UTF-8?q?=E6=B8=85=E6=99=B0=E5=8C=96(NotoSansSC+4x=E8=B6=85=E9=87=87?= =?UTF-8?q?=E6=A0=B7+=E8=A6=86=E7=9B=96=E7=8E=87=E6=8A=95=E7=A5=A8?= =?UTF-8?q?=E4=BA=8C=E5=80=BC=E5=8C=96,=E5=AD=97=E5=8F=B7=E4=B8=8A?= =?UTF-8?q?=E8=B0=83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Bupi8Kdqkfx2N5acFsHTx5 --- client/lib/core/utils/print_util_stub.dart | 43 +++++++++++++--------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/client/lib/core/utils/print_util_stub.dart b/client/lib/core/utils/print_util_stub.dart index 6a33e87..dda5d97 100644 --- a/client/lib/core/utils/print_util_stub.dart +++ b/client/lib/core/utils/print_util_stub.dart @@ -163,6 +163,7 @@ void _drawText( { if (s.isEmpty) return; final pb = ui.ParagraphBuilder(ui.ParagraphStyle( + fontFamily: 'NotoSansSC', // 打包字体,笔画统一、比系统兜底字更实(热敏二值化后不糊) fontSize: size, fontWeight: bold ? ui.FontWeight.bold : ui.FontWeight.normal, textAlign: center ? ui.TextAlign.center : ui.TextAlign.left, @@ -194,6 +195,7 @@ void _drawBarcode( /// 测量单行文本在指定字号下的宽度 double _measureWidth(String s, double size, bool bold) { final pb = ui.ParagraphBuilder(ui.ParagraphStyle( + fontFamily: 'NotoSansSC', fontSize: size, fontWeight: bold ? ui.FontWeight.bold : ui.FontWeight.normal, maxLines: 1, @@ -239,7 +241,7 @@ List _labelSubFields( } /// 用 dart:ui 把一张标签绘制成位图(热敏标签实际尺寸 40×20mm @203dpi)。 -/// 内部以 2× 超采样(640×320)绘制,转 TSPL 时再降采样,消除反锯齿模糊。 +/// 内部以 4× 超采样(1280×640)绘制,转 TSPL 时按覆盖率投票降采样二值化,边缘锐利不糊。 /// 供 TSPL 裸发和预览渲染共用同一套画布逻辑,确保预览即实际输出。 Future _renderLabelBitmap({ required String shop, @@ -251,7 +253,7 @@ Future _renderLabelBitmap({ bool drawQr = true, // 热敏光栅路径置 false:二维码改用原生 TSPL BITMAP 保证扫码 }) async { const w = 320, h = 160; // 40×20mm @203dpi(逻辑尺寸) - const scale = 2; // 2× 超采样,内部以 640×320 绘制 + const scale = 4; // 4× 超采样,内部以 1280×640 绘制(子像素更密,二值化边缘更锐) const sw = w * scale, sh = h * scale; final recorder = ui.PictureRecorder(); final canvas = @@ -273,7 +275,7 @@ Future _renderLabelBitmap({ const headerH = 24.0; canvas.drawRect( ui.Rect.fromLTWH(0, 0, w.toDouble(), headerH), ui.Paint()..color = cNavy); - _drawText(canvas, shop, 10, 5, 12, w - 20, bold: true, color: cCream); + _drawText(canvas, shop, 10, 4, 14, w - 20, bold: true, color: cCream); // ── 主体两列布局(无页脚:主体直抵纸底,2026-08-27 删掉底部米色尺寸条)──────── const bodyTop = headerH; // 24 @@ -318,9 +320,9 @@ Future _renderLabelBitmap({ const double infoBot = barTop - 4; // 110 const double infoH = infoBot - infoTop; // 80 - const double kSmallSize = 12.0; // 编号+度数 / 规格+日期 + const double kSmallSize = 14.0; // 编号+度数 / 规格+日期(2026-08-28 调大一号 12→14) final double nameSize = - _fitFont(name, leftW, 1000, true, 16.0).clamp(11.0, 16.0); + _fitFont(name, leftW, 1000, true, 18.0).clamp(12.0, 18.0); final rows = <(String, double, bool)>[ if (name.isNotEmpty) (name, nameSize, true), // 品名粗体 for (final s in subs) (s, kSmallSize, false), // 编号+度数 / 规格+日期 @@ -337,7 +339,7 @@ Future _renderLabelBitmap({ yy += r.$2 * kLineH + gap; } - return recorder.endRecording().toImage(sw, sh); // 返回 2× 图(640×320) + return recorder.endRecording().toImage(sw, sh); // 返回 4× 图(1280×640) } /// 桌面端把整张标签「光栅化」成单色位图裸发到热敏机(TSPL BITMAP),令热敏输出与 @@ -363,7 +365,9 @@ Future _printFlatLabelThermal({ const wBytes = (w + 7) >> 3; // 40 bytes/row // ── 文字/抬头整幅光栅:复用预览画布(跳过条码+QR,二者改原生命令)───────── - // 画布内部 2× 超采样(640×320),此处 2×2 盒式降采样 + 亮度 128 阈值二值化,边缘更干净。 + // 画布内部 4× 超采样(1280×640),此处按覆盖率投票降采样二值化: + // 每个墨点看其 4×4=16 个子像素中"是墨"的占比,≥40% 即打黑。 + // 替代旧的"盒式平均+128 阈值"——后者先把边缘抹成灰再一刀切,正是糊字的根因。 final img = await _renderLabelBitmap( shop: shop, name: name, @@ -375,7 +379,9 @@ Future _printFlatLabelThermal({ ); final bd = await img.toByteData(format: ui.ImageByteFormat.rawRgba); final rgba = bd!.buffer.asUint8List(); - final iw = img.width; // 640 + final iw = img.width; // 1280(4× 超采样) + const int ss = 4; // 超采样倍率,须与 _renderLabelBitmap 的 scale 一致 + const int subTotal = ss * ss; // 16 子像素/墨点 final buf = BytesBuilder(); buf.add(ascii.encode( @@ -389,18 +395,19 @@ Future _printFlatLabelThermal({ for (int y = 0; y < h; y++) { final row = Uint8List(wBytes)..fillRange(0, wBytes, 0xFF); for (int x = 0; x < w; x++) { - int lum = 0; - for (int dy = 0; dy < 2; dy++) { - for (int dx = 0; dx < 2; dx++) { - final i = ((y * 2 + dy) * iw + (x * 2 + dx)) * 4; - final a = rgba[i + 3]; - lum += a < 128 - ? 255 - : (0.299 * rgba[i] + 0.587 * rgba[i + 1] + 0.114 * rgba[i + 2]) - .round(); + int ink = 0; // 该墨点内"是墨"的子像素数 + for (int dy = 0; dy < ss; dy++) { + for (int dx = 0; dx < ss; dx++) { + final i = ((y * ss + dy) * iw + (x * ss + dx)) * 4; + if (rgba[i + 3] < 128) continue; // 透明子像素当白 + final lum = + 0.299 * rgba[i] + 0.587 * rgba[i + 1] + 0.114 * rgba[i + 2]; + if (lum < 128) ink++; // 暗且不透明 = 墨 } } - if (lum ~/ 4 < 128) row[x >> 3] &= ~(0x80 >> (x & 7)); + // 覆盖率投票:≥40% 子像素是墨即打黑点。阈值偏低让细笔画不断(热敏易掉墨), + // 且不引入盒式平均的灰边模糊。 + if (ink * 5 >= subTotal * 2) row[x >> 3] &= ~(0x80 >> (x & 7)); } buf.add(row); }