109 lines
4.2 KiB
Dart
109 lines
4.2 KiB
Dart
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);
|
||
final transposed = pavoPortraitRotateClockwise
|
||
? flipHorizontal(rotated)
|
||
: flipVertical(rotated);
|
||
return CardImage._(transposed, transposed.width, transposed.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;
|
||
}
|
||
}
|