diff --git a/lib/image/card_image.dart b/lib/image/card_image.dart index 40459a8..925017c 100644 --- a/lib/image/card_image.dart +++ b/lib/image/card_image.dart @@ -27,12 +27,15 @@ class CardImage { /// 宽 ≥ 高 = 横向卡面 bool get isLandscape => width >= height; - /// 纵向卡面预旋转为横向位图(Windows Pavo 直打用,见 [pavoPortraitRotateClockwise])。 + /// 纵向卡面转置为横向位图(Windows Pavo 直打用,见 [pavoPortraitRotateClockwise])。 /// 已是横向的原样返回。返回新实例,原实例不变(正/背面各自转换)。 CardImage toLandscape() { if (isLandscape) return this; final rotated = copyRotate(_canvas, angle: pavoPortraitRotateClockwise ? 90 : -90); - return CardImage._(rotated, rotated.width, rotated.height); + final transposed = pavoPortraitRotateClockwise + ? flipHorizontal(rotated) + : flipVertical(rotated); + return CardImage._(transposed, transposed.width, transposed.height); } /// 解析 data:image/png;base64,…(或其他 data URL 形式),缩放到卡面画布并铺白底。 diff --git a/test/card_image_landscape_test.dart b/test/card_image_landscape_test.dart new file mode 100644 index 0000000..9109702 --- /dev/null +++ b/test/card_image_landscape_test.dart @@ -0,0 +1,38 @@ +// toLandscape 转置变换的像素级验证:纵向四象限色块 → 转置后对角象限互换。 +// 转置语义:P(x, y) -> out(y, x),即左上/右下不动,右上↔左下互换。 +import 'dart:convert'; + +import 'package:desktop_plugin/image/card_image.dart'; +import 'package:image/image.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('toLandscape 把纵向卡面转置为横向(对角象限互换)', () { + // 构造 642×1014 纵向源图:四象限填不同颜色(fromDataUrl 尺寸相同不触发缩放) + final src = Image(width: 642, height: 1014); + fillRect(src, x1: 0, y1: 0, x2: 641, y2: 506, color: ColorRgb8(255, 0, 0)); // 上:红 + fillRect(src, x1: 0, y1: 507, x2: 320, y2: 1013, color: ColorRgb8(0, 255, 0)); // 左下:绿 + fillRect(src, x1: 321, y1: 507, x2: 641, y2: 1013, color: ColorRgb8(0, 0, 255)); // 右下:蓝 + final dataUrl = 'data:image/png;base64,${base64Encode(encodePng(src))}'; + + final card = CardImage.fromDataUrl(dataUrl); + expect(card.isLandscape, isFalse, reason: '源图应为纵向'); + + final out = card.toLandscape(); + expect(out.isLandscape, isTrue, reason: '转置后应为横向 1014×642'); + expect(out.width, 1014); + expect(out.height, 642); + + // 像素断言:转置后 out(x', y') = src(y', x')(以中心附近取样避免象限边界) + final bgr = out.toBgr24(); + // 转置后左上(原上半红区)≈ 红 + (int, int, int) px(int x, int y) { + final o = (y * out.width + x) * 3; + return (bgr[o + 2], bgr[o + 1], bgr[o]); // BGR 转回 RGB + } + expect(px(300, 100), (255, 0, 0), reason: '左上应保持红(源上半)'); + // 转置把源的"右上"(x大,y小)映到"左下":源上半仍红,源左下绿映到右半 + expect(px(800, 300), (0, 255, 0), reason: '右半上应为绿(源左下转置)'); + expect(px(800, 500), (0, 0, 255), reason: '右半下应为蓝(源右下转置)'); + }); +} diff --git a/tool/make_test_card.dart b/tool/make_test_card.dart index 08143a4..28b53b4 100644 --- a/tool/make_test_card.dart +++ b/tool/make_test_card.dart @@ -1,38 +1,77 @@ -// 生成 1014×642 测试卡 PNG(真实打印验收用),默认输出 build/test_card.png -// dart run tool/make_test_card.dart [输出路径] +// 方向校准卡(Windows Pavo 直打校准用):642×1014 纵向,四角 ▲■●◆ + 中央大写 F + 左右箭头。 +// 打一张即可从 8 种变换(4 旋转 × 2 镜像)中唯一定出打印机的实际映射。 +// +// 用法(在项目根目录): +// dart run tool/make_test_card.dart # 只生成 build/calibration_card.png +// dart run tool/make_test_card.dart --print # 生成并直接 POST 到本机助手打印一张 +// +// 放卡方式与正式打印一致:打印面朝上、文字对着自己。 +import 'dart:convert'; import 'dart:io'; import 'package:image/image.dart'; Future main(List args) async { - final img = Image(width: 1014, height: 642); + final doPrint = args.contains('--print'); + final black = ColorRgb8(0, 0, 0); + final img = Image(width: 642, height: 1014); fill(img, color: ColorRgb8(255, 255, 255)); - // 边框 - drawRect(img, x1: 8, y1: 8, x2: 1005, y2: 633, color: ColorRgb8(0, 120, 215)); + // 边框(四边留 6px) + drawRect(img, x1: 6, y1: 6, x2: 635, y2: 1007, color: ColorRgb8(0, 120, 215)); - // CMYK 色块:验证四色分色与套印 - fillRect(img, x1: 60, y1: 60, x2: 300, y2: 300, color: ColorRgb8(0, 174, 239)); - fillRect(img, x1: 320, y1: 60, x2: 560, y2: 300, color: ColorRgb8(236, 0, 140)); - fillRect(img, x1: 580, y1: 60, x2: 820, y2: 300, color: ColorRgb8(255, 221, 0)); - fillRect(img, x1: 840, y1: 60, x2: 960, y2: 300, color: ColorRgb8(0, 0, 0)); + // 四角标记(各不相同,一印即辨) + // 左上:实心三角 ▲ + fillPolygon(img, vertices: [Point(40, 40), Point(150, 40), Point(40, 150)], color: black); + drawString(img, 'LT', font: arial14, x: 44, y: 158, color: black); + // 右上:实心方块 ■ + fillRect(img, x1: 492, y1: 40, x2: 602, y2: 150, color: black); + drawString(img, 'RT', font: arial14, x: 500, y: 158, color: black); + // 左下:实心圆 ● + fillCircle(img, x: 95, y: 930, radius: 55, color: black); + drawString(img, 'LB', font: arial14, x: 44, y: 856, color: black); + // 右下:实心菱形 ◆ + fillPolygon(img, vertices: [Point(547, 860), Point(602, 930), Point(547, 1000), Point(492, 930)], color: black); + drawString(img, 'RB', font: arial14, x: 560, y: 1004, color: black); - drawString(img, 'DESKTOP-PLUGIN TEST CARD', font: arial24, x: 60, y: 360, color: ColorRgb8(20, 20, 20)); - drawString( - img, - 'flutter print verify ${DateTime.now().toIso8601String().substring(0, 19)}', - font: arial14, - x: 60, - y: 420, - color: ColorRgb8(90, 90, 90), - ); - drawString(img, '127.0.0.1:38200 /api/card-printer/print', font: arial14, x: 60, y: 450, color: ColorRgb8(90, 90, 90)); + // 中部超大 F(旋转/镜像一票判别) + // arial 字体最大 48 号,642 宽卡面已足够醒目 + drawString(img, 'F', font: arial48, x: 300, y: 520, color: black); + + // 左右语义标注 + 箭头线 + drawString(img, '<-LEFT', font: arial24, x: 60, y: 430, color: black); + drawString(img, 'RIGHT->', font: arial24, x: 440, y: 430, color: black); + drawLine(img, x1: 180, y1: 340, x2: 440, y2: 340, color: black, thickness: 8); + fillPolygon(img, vertices: [Point(440, 320), Point(490, 340), Point(440, 360)], color: black); + drawString(img, 'CALIB 0923', font: arial14, x: 180, y: 312, color: black); final png = encodePng(img); - final out = args.isNotEmpty ? args.first : 'build/test_card.png'; + final out = 'build/calibration_card.png'; final file = File(out); await file.parent.create(recursive: true); await file.writeAsBytes(png); // ignore: avoid_print print('written $out (${png.length} bytes)'); + + if (doPrint) { + final b64 = base64Encode(png); + final body = jsonEncode({ + 'copies': 1, + 'front': {'color': 'data:image/png;base64,$b64'}, + }); + final client = HttpClient(); + try { + final req = await client.postUrl(Uri.parse('http://127.0.0.1:38200/api/card-printer/print')); + req.headers.contentType = ContentType.json; + req.write(body); + final res = await req.close(); + final text = await res.transform(utf8.decoder).join(); + // ignore: avoid_print + print('print -> ${res.statusCode} $text'); + // ignore: avoid_print + print('任务已提交;等打印机出卡后查看方向(四角标记:LT三角 RT方块 LB圆 RB菱形,中央大F)'); + } finally { + client.close(); + } + } }