11
This commit is contained in:
1 parent
7897e77c52
commit
1cd36f9754
3 files changed
+103
-23
No files matched your search
@@ -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 形式),缩放到卡面画布并铺白底。
|
||||
|
||||
@@ -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: '右半下应为蓝(源右下转置)');
|
||||
});
|
||||
}
|
||||
+60
-21
@@ -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<void> main(List<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user