import 'dart:convert'; import 'dart:typed_data'; import 'package:image/image.dart'; /// 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; /// 解析 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; } }