- lib/print/pavo/:PavoApiW_x64.dll 的 dart:ffi 绑定(结构体布局对齐 C#,W 后缀导出名回退兼容) - pavo_printer.dart:枚举(UTF-8/UTF-16 自动解码)/状态/色带信息/单卡直打/忙碌轮询(3 分钟超时)/LayerHolder 非托管内存管理 - pavo_backend.dart:全部 ffi 调用与像素转换跑在 Isolate.run,主 isolate HTTP 服务零阻塞 - lib/image/card_image.dart:PNG data URL → 纵横比选卡面 → 手动 alpha 铺白底 → BGR24/Gray8 紧凑像素(image 包 compositeImage 对全透明像素非 src-over 语义,改为逐像素混合对齐 C#) - windows/ 目录:从 Flutter SDK windows.tmpl 手动实例化(mac 上 flutter create 拒绝生成的绕过),窗口标题"饭卡打印助手" - windows/vendor/:收录 SDK 的 PavoApiW_x64.dll/PavoCardSDK_x64.dll/HeatTB3.dll/heatthcd.bin,CMake POST_BUILD 自动拷到 exe 旁 - backend.dart 工厂接通 Windows 分支;analyze 零问题、单测 12 个全过
91 lines
3.2 KiB
Dart
91 lines
3.2 KiB
Dart
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;
|
||
}
|
||
}
|