diff --git a/lib/image/card_image.dart b/lib/image/card_image.dart index 14b1faf..40459a8 100644 --- a/lib/image/card_image.dart +++ b/lib/image/card_image.dart @@ -3,6 +3,13 @@ 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,按源图纵横比自动选择; @@ -20,6 +27,14 @@ class CardImage { /// 宽 ≥ 高 = 横向卡面 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) { diff --git a/lib/print/pavo/pavo_backend.dart b/lib/print/pavo/pavo_backend.dart index 3c3fd14..9b51f89 100644 --- a/lib/print/pavo/pavo_backend.dart +++ b/lib/print/pavo/pavo_backend.dart @@ -113,17 +113,22 @@ Future pavoPrintSync({ } final printer = printers.first.toNativeUtf16(); try { - final frontImg = CardImage.fromDataUrl(frontColor); + // Windows Pavo 直打方向校准(2026-09-23 实测):纵向位图 + byOrientation=1 会被 + // SDK 旋转 90° 且锚定错位;统一预旋转成 1014×642 横向位图后按 byOrientation=2 打印。 + // 旋转方向常量见 card_image.dart 的 pavoPortraitRotateClockwise(测试卡校准)。 + final frontImg = CardImage.fromDataUrl(frontColor).toLandscape(); final frontBgr = frontImg.toBgr24(); - final Uint8List? frontKGray = - frontK != null && frontK.isNotEmpty ? CardImage.fromDataUrl(frontK).toGray8() : null; + final Uint8List? frontKGray = frontK != null && frontK.isNotEmpty + ? CardImage.fromDataUrl(frontK).toLandscape().toGray8() + : null; final Uint8List? backBgr; final Uint8List? backKGray; if (backColor != null && backColor.isNotEmpty) { - final backImg = CardImage.fromDataUrl(backColor); + final backImg = CardImage.fromDataUrl(backColor).toLandscape(); backBgr = backImg.toBgr24(); - backKGray = - backK != null && backK.isNotEmpty ? CardImage.fromDataUrl(backK).toGray8() : null; + backKGray = backK != null && backK.isNotEmpty + ? CardImage.fromDataUrl(backK).toLandscape().toGray8() + : null; } else { backBgr = null; backKGray = null; @@ -136,7 +141,7 @@ Future pavoPrintSync({ frontK: frontKGray, backBgr: backBgr, backK: backKGray, - isLandscape: frontImg.isLandscape, + isLandscape: true, copies: copies, ); } finally { diff --git a/windows/installer.iss b/windows/installer.iss index de3cb02..be703e5 100644 --- a/windows/installer.iss +++ b/windows/installer.iss @@ -55,3 +55,24 @@ Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: [Run] Filename: "{app}\{#AppExeName}"; Description: "立即运行 {#AppName}"; Flags: nowait postinstall skipifsilent + +; 覆盖安装/卸载前结束正在运行的实例,避免 exe/dll 被占用导致文件替换失败 +[Code] +procedure TaskkillApp(); +var + ResultCode: Integer; +begin + Exec(ExpandConstant('{cmd}'), '/C taskkill /F /IM "{#AppExeName}"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; + +function PrepareToInstall(var NeedsRestart: Boolean): String; +begin + TaskkillApp(); + Result := ''; +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +begin + if CurUninstallStep = usUninstall then + TaskkillApp(); +end;