Files
desktop-plugin/lib/image/card_image.dart
T
2026-09-23 18:11:35 +08:00

106 lines
4.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'dart:typed_data';
import 'package:image/image.dart';
/// 纵向卡面 → 横向位图的预旋转方向(Windows Pavo 直打校准常量)。
///
/// 实测(2026-09-23 Windows CS-200e):纵向缓冲 + byOrientation=1 会被 SDK 旋转 90°
/// 且锚定错位(内容横向贴左、顺时针转卡才可读)。修复=统一预旋转成 1014×642 横向
/// 位图 + byOrientation=2 打印。此常量定旋转方向:打测试卡若差 90°(顺/逆)就取反。
const bool pavoPortraitRotateClockwise = true;
/// PNG data URL 解析与像素层转换(Windows PavoApi 直打用)。
///
/// 卡面为 CR-80 @300dpi:横向 1014×642 / 纵向 642×1014,按源图纵横比自动选择;
/// 透明区域铺白,避免黑底(对齐 C# 版 Hiti/CardImage.cs)。
class CardImage {
CardImage._(this._canvas, this.width, this.height);
static const int landscapeWidth = 1014;
static const int landscapeHeight = 642;
final Image _canvas;
final int width;
final int height;
/// 宽 ≥ 高 = 横向卡面
bool get isLandscape => width >= height;
/// 纵向卡面预旋转为横向位图(Windows Pavo 直打用,见 [pavoPortraitRotateClockwise])。
/// 已是横向的原样返回。返回新实例,原实例不变(正/背面各自转换)。
CardImage toLandscape() {
if (isLandscape) return this;
final rotated = copyRotate(_canvas, angle: pavoPortraitRotateClockwise ? 90 : -90);
return CardImage._(rotated, rotated.width, rotated.height);
}
/// 解析 data:image/png;base64,…(或其他 data URL 形式),缩放到卡面画布并铺白底。
static CardImage fromDataUrl(String dataUrl) {
if (dataUrl.trim().isEmpty) {
throw ArgumentError('卡面图为空');
}
final comma = dataUrl.indexOf(',');
final base64Part = comma >= 0 ? dataUrl.substring(comma + 1) : dataUrl;
final bytes = base64.decode(base64Part.replaceAll(RegExp(r'\s'), ''));
final raw = decodeImage(bytes);
if (raw == null) {
throw ArgumentError('卡面图解码失败(不支持的图片格式)');
}
final landscape = raw.width >= raw.height;
final width = landscape ? landscapeWidth : landscapeHeight;
final height = landscape ? landscapeHeight : landscapeWidth;
final resized = copyResize(raw, width: width, height: height);
final canvas = Image(width: width, height: height);
// 手动 alpha 混合铺白底(与 C# 版逐像素语义一致):
// image 包的 compositeImage 对全透明 src 像素不是 src-over 语义,透明处会漏黑
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
final p = resized.getPixel(x, y);
final a = p.a.toInt() / 255.0;
canvas.setPixelRgba(
x,
y,
(255 * (1 - a) + p.r * a).round().clamp(0, 255),
(255 * (1 - a) + p.g * a).round().clamp(0, 255),
(255 * (1 - a) + p.b * a).round().clamp(0, 255),
255,
);
}
}
return CardImage._(canvas, width, height);
}
/// 转 BGR 24bit 紧凑像素(无行对齐填充,PavoApi BITMAP 用)。
Uint8List toBgr24() {
final packed = Uint8List(width * 3 * height);
for (var y = 0; y < height; y++) {
var offset = y * width * 3;
for (var x = 0; x < width; x++) {
final p = _canvas.getPixel(x, y);
packed[offset] = p.b.toInt() & 0xff;
packed[offset + 1] = p.g.toInt() & 0xff;
packed[offset + 2] = p.r.toInt() & 0xff;
offset += 3;
}
}
return packed;
}
/// 转 8bit 灰度(无行对齐)。树脂 K 层像素语义:0x00 = 转印黑色,
/// 因此前端 K 层 PNG 为白底黑字,灰度值可直接使用。
Uint8List toGray8() {
final gray = Uint8List(width * height);
for (var y = 0; y < height; y++) {
var offset = y * width;
for (var x = 0; x < width; x++) {
final p = _canvas.getPixel(x, y);
gray[offset + x] = (p.r.toInt() * 299 + p.g.toInt() * 587 + p.b.toInt() * 114) ~/ 1000 & 0xff;
}
}
return gray;
}
}