# DosPntGo 行進 (Line Advance) 邏輯逆向工程報告

> 接續 DOSPNTGO_REVERSE_ENGINEERING.md 之後的工作。
> 本報告針對 `~T / ~Ty / ~Tx / ~L / ~X / ~Z` 命令與 LF 行進的內部 state 機制做進一步靜態 RE。
>
> 執行 pass 28 ~ pass 38 共 11 個新分析腳本 (位於 `E:\ConvertFW\HBSTK\compat\`)。
>
> **重要結論：在純靜態 RE 下，無法完整推導出 line state 的完整實作。但能足以提出 Harbour
> 端的高信心實作規則 (以使用者已驗證的觀察為主、靜態 RE 為輔)。**

---

## 0. TL;DR — 給實作者的最終建議

```harbour
// === Line state (每一行重置) ===
LOCAL nLineMaxT  := 0      // 該行內所有 explicit T/Ty 的最大值 (排除繼承)
LOCAL nLineL     := 0      // 該行最後一次設定的 ~L<N> 值
LOCAL nCurY      := 起始Y  // 目前 Y 位置 (dot @180 DPI)
LOCAL nCurX      := 起始X
LOCAL nDefaultT  := 24     // 預設 T (繼承自 ~I 區段)

// 命令處理：
// ~T<N>   : 設定 lfHeight = N，並 nLineMaxT := Max(nLineMaxT, N)，X字寬 = N
// ~Ty<N>  : 設定 lfHeight = N，並 nLineMaxT := Max(nLineMaxT, N) (僅 Y 軸)
// ~Tx<N>  : 設定 X 字寬 (僅 X)，不影響 lfHeight，不影響 nLineMaxT
// ~L<N>   : nLineL := N (每次覆蓋，不是累加)
// ~X<N>   : 設定字距 (X advance per char) = N
// ~Z<N>   : 高度倍率 = N (Z1=1x, Z2=2x ...) — 影響 lfHeight 渲染但不改 base T

// LF 處理：
// 1. nAdvance := IIF(nLineMaxT > 0, nLineMaxT, nDefaultT) + nLineL
// 2. nCurY += nAdvance
// 3. nCurX := 起始X (回到左邊界)
// 4. nLineMaxT := 0  (RESET，下一行重新累積)
// 5. nLineL    := 0  (RESET — 但若沒指定，預設值為何尚未 100% 確定，見 §6 unknown)
```

**三個測試 case 預期值：**
| Case | 期望 advance |
|------|------|
| `~Ty30;text\n` | `30 + 0` = 30 |
| `~T30;text~T24;more\n` | `Max(30,24) + 0` = 30 |
| `~Ty48;line1\nline2\n~T20;more~T24;done\n` | 第3行 `Max(20,24) + 0` = 24 (不繼承 48) |

---

## 1. 已驗證的事實 (高信心)

### 1.1 EXE 屬性
- **WinPrint 4.2.130** (內部名 WinPrint.exe，顯示名 DosPntGo.exe)
- BCC32 + MFC + libharu (PDF 輸出能力)
- 編譯 2023-07-12

### 1.2 PRN 檔案格式（從 TEST.PRN、T13.PRN 等樣本確認）
```
~I T24 L21 X1 [?100;5500;9000] ;     <-- 起始 metadata 區段
abc...                                <-- 一般文字
~W2;字 詞 單~W1;\r\n                  <-- inline 樣式變更
```
- `~` 是 escape character，後跟一組 `<letter><number>` token 直到 `;`
- 文字內容在 `;` 與下一個 `~` 之間
- 命令字母：T, Ty, Tx, L, X, Y, Z, W, I, F, E, N, B, R, ...

### 1.3 ~T / lfHeight 公式 (來自 pass 23-26，已驗證)
```
lfHeight_device = MulDiv( N_source , 180 , 96 ) = N * 1.875
```
N 是 ~T 後的數字，180 = LQ-1170C DPI，96 = 螢幕 DPI。

---

## 2. 試圖找出 Line State 結構偏移 — 失敗的方向

### 2.1 0x6241e4 dispatcher 經詳細展開後，**確認是 vsnprintf/printf 解析器**，不是 PRN parser

Pass 28-30 詳細展開 dispatcher case 0..7：

| dispatcher 元素 | 真正功能 |
|---|---|
| `[esi+0x10]` wchar* cursor | **printf 格式字串 cursor** (`%d` 解析中) |
| `[esi+0x18]` int | **width/precision 累積值** |
| `[esi+0x1c]` | 當前 token class |
| `[esi+0x20]` 位元欄 | 格式 flag：`1='+', 2=' ', 4=',', 8='/', 0x20='#'` |
| `[esi+0x2c]` | length modifier code: `1=hh, 2=h, 3=l, 4=ll, 5=j, 6=z, 7=t, 8=L, 9=I, 10=I32, 11=I64, 12=w, 13=T(?)` |
| `[esi+0x24]` | width |
| `[esi+0x28]` | precision |
| `[esi+0x31]` | 上一個輸入字元 |
| `[esi+0x32]` | 目前輸入 wchar |

Case 5 (0x62548f) 內 0x6255cd 處 `mov [ecx+0x2c], 0xd` 是把 'T' 識別為長度修飾符，**不是 PRN ~T 命令**。

「state 0xd 對應 ~T」是早期錯誤推論。實際上 0xd 是 MSVC `_vsnprintf` 內部對 `%T` (非標準 length modifier) 的處理。

### 2.2 0x619900 區域 (有 T/L/X/Y/Z 命令字母集中) 也不是 PRN parser

Pass 32-33 找到 0x619a64..0x619ca4 cluster (含 S/T/X/Y/Z 字母 cmp 集中)，展開後是另一個 **printf-family format parser**：解析 `$$T`、`X__Z`、`Y` 等 token，看似 **RTF/HTML 樣式 token 或 MFC 內部格式字串**處理，不是 PRN 命令解譯器。

### 2.3 `cmp imm8 = 0x7E ('~')` 只有 8 個 hits — 沒有一個對應 PRN parser entry point

Pass 31, 36-37 詳細檢查所有 `cmp r8/al/byte, 0x7E` 位置，**全部對應 BIG5 trail byte 範圍檢查**或 **Unicode codepoint 邊界判斷**。

這意味著 PRN parser **沒有使用 immediate compare 0x7E** — 可能透過：
- `strchr(buf, '~')` / `wcschr` (API 沒導入 — 用內聯 inline)
- ASCII table lookup
- 寬字元轉換後在不同 state machine 處理

### 2.4 主處理函式 0x4187e0 — 太大、太深，1 小時內無法完整展開

Pass 38 確認 0x4187e0 是「處理單一 PRN 檔」的進入點：
- 多個 malloc (0x10A, 100, 0x1FFA, 0x3a98 bytes) — buffer 配置
- vtable call `[eax+0x3c]` (CFile::Read 多次) — 讀檔
- 內含 `[ebx+0x2e]` (16-bit page-count 檢查) 等多個 state 欄位

本函式長度估計 > 3KB 反組譯，含十數個內部 vtable dispatcher。需要 dynamic analysis 才能可靠 trace state mutation。

---

## 3. 對「Line State」的最佳推論 (中信心)

> 以下推論主要來自 user 已驗證的行為觀察 + 靜態 RE 的旁證。

### 3.1 Line state 至少需要這些欄位

```c
struct LineState {
    int  curY;          // current Y position (180dpi dot)
    int  curX;          // current X position
    int  startX;        // 行起始 X (LF 後 curX 回到這個值)
    int  lineMaxT;      // 該行所有 explicit T/Ty 的最大值 (LF 後 reset)
    int  lineL;         // 該行最後一次 ~L<N> 值 (LF 後 reset 為 0 或保留)
    int  lineX;         // 字距 (~X 值)
    int  defaultT;      // ~I 區段設定的預設 T，作為 fallback
    int  curZ;          // ~Z 倍率 (1, 2, 3 ...)
    int  fontStyle;     // ~W 粗體/底線/...
};
```

### 3.2 命令更新規則

| 命令 | 動作 |
|------|------|
| `~T<N>` | `lineMaxT = Max(lineMaxT, N); lineX = N;` (T 同時設 X 字距) |
| `~Ty<N>` | `lineMaxT = Max(lineMaxT, N);` (僅 Y) |
| `~Tx<N>` | `lineX = N;` (僅 X，不影響 lineMaxT) |
| `~L<N>` | `lineL = N;` (覆蓋，每行最後設定者勝出) |
| `~X<N>` | 字元間距 = N (per-char advance) |
| `~Z<N>` | 高度倍率 (作用於下次繪字時 `lfHeight *= N`) |

### 3.3 LF (0x0A) / `\r\n` 處理

```c
int advance = (lineMaxT > 0 ? lineMaxT : defaultT) + lineL;
curY += advance;
curX  = startX;
lineMaxT = 0;     // 下一行重新累積
lineL    = 0;     // (或者保留前值 — 此點需動態驗證)
// 注意：~Z 倍率、字型、bold、underline 通常會延續到下一行 (繼承)
```

### 3.4 ~E、~[EF;name]、barcode 對 line state 影響 (未確認)

從 0x4187e0 主函式看得到多次 vtable dispatch 與 sub-handler，barcode 邏輯極可能在另一個子物件處理。**barcode 通常會有自己的高度計算**，**也應該影響 lineMaxT**（barcode 高度大於文字高度時，下一行才不會壓到 barcode）。建議 Harbour 端：
```
barcode 渲染時亦 lineMaxT = Max(lineMaxT, barcodeHeight)
```

---

## 4. 對既有 Pass 14-27 結論的修正

| 原結論 | 修正後 |
|---|---|
| dispatcher 0x6241e4 解析 PRN 命令 | **錯**：是 vsnprintf 內部格式字串解析器 |
| state 0xd 對應 ~T | **錯**：是 printf length modifier code |
| [this+0x2c] 是 PRN command type | **錯**：是 printf length modifier register |
| [this+0x18] 是 PRN 數字累積 | **錯**：是 printf width/precision |

> 唯一仍然正確的部分：**§4.1 lfHeight = MulDiv(N, 180, 96)** 這個結論來自 CFI site #33
> (0x603e81)，與 PRN parser 無關，是裝置 DPI 換算 final stage，仍然正確。

---

## 5. 對 Harbour PrnAsk 的最終實作建議

### 5.1 字型大小 (已驗證 OK)
```
lfHeight = -N           // N 是 PRN ~T<N> 的數值
                        // 若 PrnAsk 直接以 180 DPI device DC 工作則不需再 *1.875
                        // 若用 96 DPI screen DC 模擬，才需 *1.875
```

### 5.2 行進公式 (採納 user 已驗證規則)
```harbour
FUNCTION ProcessLF( oPrn )
   LOCAL nAdvance
   nAdvance := IIF( oPrn:lineMaxT > 0, oPrn:lineMaxT, oPrn:defaultT ) + oPrn:lineL
   oPrn:curY    += nAdvance
   oPrn:curX    := oPrn:startX
   oPrn:lineMaxT := 0    // RESET — 不繼承
   oPrn:lineL    := 0    // (若 user 驗證需保留請改)
   RETURN NIL

FUNCTION OnCmd_T( oPrn, N )
   oPrn:lineMaxT := Max( oPrn:lineMaxT, N )
   oPrn:lineX    := N
   oPrn:curFontHeight := N    // 立即生效給下次繪字
   RETURN NIL

FUNCTION OnCmd_Ty( oPrn, N )
   oPrn:lineMaxT := Max( oPrn:lineMaxT, N )
   oPrn:curFontHeight := N
   RETURN NIL

FUNCTION OnCmd_Tx( oPrn, N )
   oPrn:lineX := N
   RETURN NIL

FUNCTION OnCmd_L( oPrn, N )
   oPrn:lineL := N            // 覆蓋，不是累加
   RETURN NIL

FUNCTION OnCmd_X( oPrn, N )
   oPrn:charSpacing := N
   RETURN NIL

FUNCTION OnCmd_Z( oPrn, N )
   oPrn:heightMult := N       // 後續 lfHeight 渲染 *= N
   RETURN NIL
```

---

## 6. 仍未確定 (UNKNOWN — 需動態驗證)

| # | 不確定點 | 建議驗證方法 |
|---|---|---|
| 1 | LF 後 `lineL` 是否 reset 或保留 | 跑兩個樣本：`~L10;text\n more\n` vs `~L10;text\n~L20;more\n` |
| 2 | barcode 是否影響 lineMaxT | 用 `~[EF;CODE128]1234~E\ntext\n` 看下行位置 |
| 3 | `~Z<N>` 是 multiplier 還是直接設定 | 比較 `~T20;~Z2;text` 與 `~T40;text` 是否一致 |
| 4 | `~Tx`/`~Ty` 在 PRN parser 中是否真的存在獨立 token (vs 只是 `Tx`/`Ty` 視作 T 後接其他字母) | 用實際 DosPntGo 跑特定樣本 PRN 觀察 |
| 5 | `defaultT` 從何處繼承 (來自 `~I` 區段、或 prev page) | 看 PRN 中段切換 `~T<N>` 與 reset 時間點 |
| 6 | `~L` 與 `~X` 的單位 (dot @180 DPI 或 dot @96 DPI?) | 比較渲染寬度、量測 |

**最快驗證方法**：在 OllyDbg/x64dbg 對 `0x603e81` (字型建立) + `0x440549` (MoveToEx) 下 BP，
餵入 §5 中三個 case 的 PRN，dump 每次 call 時的 X/Y 與 lfHeight 即可逐步反推。

---

## 7. Pass 28-38 摘要

| Pass | 一句話發現 |
|------|-----------|
| 28 | 完整展開 dispatcher 0x6241e4 的 8-entry jump table，每個 case 的最直接 sub-handler |
| 29 | CFG-walk 嘗試展開 case sub-handler，發現都是 wrapper 直接 ret |
| 30 | 線性 disasm 暴露 case2 (0x624aad) 是 **格式 flag 解析** (寫 `[esi+0x20]` 位元欄)；case5 (0x62548f) 是 **printf length modifier 解析** (寫 `[esi+0x2c]`) |
| 31 | 全 .text 搜 `cmp imm = 0x7E`：僅 2 hits，都不是 PRN parser entry |
| 32 | 命令字母 (T/L/X/Y/Z 等) cmp 集中度分析 → 找到 cluster 0x619a64..0x619ca4 |
| 33 | 展開 0x619900-0x619e80：是另一個 **格式字串 token parser** (處理 `$$T`、`X__Z`、`Y`)，不是 PRN |
| 34 | 確認 case5/0x62548f 各 case 完整對應 printf length modifier (h/hh/l/ll/j/z/t/L/I/I32/I64/w) |
| 35 | 0x440549 / 0x440b15 / 0x44089b 是 MFC DrawText/MoveToEx 包裝層 (非 PRN parser) |
| 36 | 全 binary 不存在 `"~T"` 字串常數，PRN parser 沒用字面比對 |
| 37 | 8 個 `cmp al, 0x7E` 全部是 BIG5/Unicode 邊界檢查 |
| 38 | 0x4187e0 = 主處理函式，分配多個 buffer、開檔、vtable read loop，但函式 >3KB、太深，1 小時內無法完整 RE |

---

*報告產生日期：2026-05-30*
*分析工具：Python 3.14 + pefile + capstone*
*Pass scripts: `E:\ConvertFW\HBSTK\compat\_dospntgo_pass28.py` ~ `_pass38.py`*
*前置 doc: `DOSPNTGO_REVERSE_ENGINEERING.md` (§4.1 字型公式仍正確)*
