# 需求:EasyScreenshot OCR 结果导出表格(导出格式可选)

状态: 进行中(2026-10-31)
归属: 产品功能(easy_screenshot)
来源: boss 口述(2026-10-31,原话「Easy Screenshot 新增 OCR 识别导出表格,就是 OCR 识别完毕以后,导出格式可选导出表格」)
日期: 2026-10-31
建议: 做。V1 已按 Windows 词框落地(表格列由 OCR 词框坐标聚类得出),mac 一期退化单列,二期补 Vision 词框。

## 1. 现状(改动前实测)

| 环节 | 改动前行为 | 位置 |
| --- | --- | --- |
| 入口 | overlay 工具栏「OCR」按钮 | `src/renderer/overlay/controller.ts:1016 ocrText()` |
| 命令 | 识别成功**直接写剪贴板**,返回 `QrScanResult {found,text}` | `src-tauri/src/snip_commands.rs:297 snip_ocr_text` |
| 引擎 | Windows=`Windows.Media.Ocr`(`result.Text()`,行间 `\n`、词间单空格,**列信息已丢失**);mac=Apple Vision(`mac_glue.m` 只回字符串) | `src-tauri/src/ocr.rs`、`native/mac_glue.m` |
| 出口 | 仅 toast「已复制 N 字」+ 剪贴板;无结果面板、无导出、无格式选择 | `controller.ts:1028` |
| 付费墙 | OCR 归会员,命令壳强制拦截(`ensure_allowed("ocr")`) | `snip_commands.rs:298` |

结论:识别结果的**结构**(行/词/词框)在命令层就丢了,前端只拿到一整串文本,无法还原表格列。

## 2. 方案(V1)

### 2.1 Rust:识别结果带词框
- `ocr.rs` 新增 `OcrWord{text,x,y,w,h}`、`OcrLine{text,words}`(serde camelCase),`recognize_png_with_lines()` 一次引擎调用同时产出「整串文本 + 行/词框」,`recognize_png_lines()` 为词框专用门面;**Windows 取 `OcrResult.Lines()→Words()→BoundingRect()`(原图像素坐标)**;`recognize_png()` 语义逐字节不变(兼容测试/CLI)。
- mac/其他平台:`recognize_png_lines()` 返回空 Vec → 前端自动走退化路径;**不动 `mac_glue.m` / FFI**(本机编不到 mac,改了无法验证)。
- `snip_ocr_text` 返回值加 `lines` 字段,`found`/`text` 语义与剪贴板行为不变;OCR 结果单独定义结构调整,不复用二维码的 `QrScanResult`。

### 2.2 Rust:导出命令
- 新命令 `snip_ocr_export(app, format, content) -> {saved, path}`:format 只认 `csv`/`txt`,其余 `saved:false` 不 panic;默认名 `EasyScreenshot-OCR-yyyyMMdd-HHmmss.<ext>`;对话框 filter 按格式;**CSV 写 UTF-8 BOM**(Excel 中文不乱码),txt 不写;**必须在 `spawn_blocking` 弹对话框**(2026-09-18 主线程死锁根因纪律),父窗取 overlay 防被全屏置顶压住;用户取消 `saved:false`。
- 新命令 `snip_copy_text(text)`(结果浮窗「复制全文」;与色值复制分名,日志不串味)。

### 2.3 TS:列聚类 + CSV(纯函数,可单测)
`src/shared/ocr-table.ts`:
1. **行内并格**:同一格里多个词只隔一个空格(词间距 ≤ 容差),列与列之间隔的是列宽;不先并格会把 "New York" 拆成两列。
2. **跨行聚类**:按格**左边缘 x** 聚类,容差 = 词高中位数 × 0.6。用左边缘而不是中心:同列里宽窄不同的格(表头 `Amount` 64px、数据 `300` 28px)中心差 18px 已超容差,会被误切出第四列;左对齐表格的对齐轴就是左边缘。
3. **退化**:无词框(mac 一期)、或整图只聚出 1 列但原行含分隔符 → 按「≥2 空格 / 制表符 / 全角空格」切列;行数恒等于输入行数(空行不丢);行长度不齐补 `""`。
4. `toCsv(rows)`:RFC4180(含 `,` `"` 换行的字段加引号、内部 `"` 双写、行尾 `\n`);`buildOcrRows(lines) -> {rows, columnCount}` 为 UI 门面。

### 2.4 overlay UI:识别结果浮窗
- `controller.ts ocrText()` 识别成功后弹**原生 `<dialog>` 深色玻璃浮窗**(复用会员墙弹窗写法与深色 token):标题「识别结果」+ 文本预览(`<pre>`,可滚,超长截断到前 200 行)+「导出格式」下拉(**表格 (.csv) 默认 / 纯文本 (.txt)**)+ 主按钮「导出」+ 次按钮「复制全文」+ 关闭;Esc 关;按键不外溢;不结束截图会话。
- 导出调 `window.snipApi.ocrExport(format, 内容)`;成功 toast 带保存路径,失败 toast 人话,取消静默;选过的格式记本次会话(**不新增设置项**,后续要配置化再走设置页)。
- bridge/types/i18n:命令挂 `window.snipApi`,五语词典(含 de/ja/es,机检闸要求 key/占位符全对齐)。

## 3. 入口与层级(按 UI-STANDARD-DESKTOP §11 决策树)

- **放哪**:不新增常驻入口。导出是「OCR 结果」的次级动作,跟着结果走 —— 就放在**识别结果浮窗**里(§11.1 判定 2「高频但场景依赖」:只有刚识别完那一屏才有意义,离开场景再找它反而是负担)。
- **初始层级**:浮动层(modal dialog),与会员墙升级引导 / 同步历史弹窗同级;不进设置页(设置页只放开关与偏好,§11.1 判定 3 不适用),不进工具条(工具条 icon 已 9 个,§11.2 上限)。
- **进一级位依据**:无。OCR 入口本身已在工具条一级位(既有),本次不加第二个主入口(§11.1「同一功能只允许一个主入口」)。
- **视觉权重**:浮窗内「导出」= primary(一屏一个,`#1f5fbf`);「复制全文」「关闭」= secondary/ghost;格式下拉 = 表单项。禁用 emoji,走深色 token(overlay 属画布类深色玻璃豁免面)。
- **默认值**:导出格式默认 **表格 (.csv)** —— 老板提的诉求就是表格,开箱即用(§11.4)。

## 4. 验收标准

1. Windows 截图含三列表格 → OCR → 浮窗选择「表格 (.csv)」→ 导出文件用 Excel 打开:中文不乱码(UTF-8 BOM)、**三列对齐**、空洞格为空串;
2. 选择「纯文本 (.txt)」→ 文件无 BOM、内容为识别原文;
3. 取消保存对话框 → 无文件、无错误提示(静默);
4. mac:结果浮窗可用,导出 CSV 为单列(按分隔符切列),无崩溃;
5. 免费用户点 OCR 仍被升级引导拦截(付费墙口径未改);
6. 机检:`npm run typecheck` exit 0、`npx vitest run` 全绿(含 `tests/unit/ocr-table.test.ts` 13 条)、`cargo test --lib` 全绿、`cargo clippy --all-targets` 无 error。

## 5. 分期

- **V1(本次,Windows 真数据)**:词框列聚类 + 结果浮窗 + CSV/TXT 导出;mac 一期退化单列(Apple Vision 只回整串文本)。
- **二期**:mac Vision 词框(`VNRecognizedText.characterBoxes` 或 word 级 boundary,需改 `mac_glue.m` + 真机验证);右对齐数字列(按右边缘二次聚类);合并单元格/跨页表格;格式偏好进设置页(若要常驻记忆)。

## 6. 红线

- 不动付费墙判定口径(OCR 仍归会员,拦截形态不变);
- 不动 `mac_glue.m` / mac FFI(无法本机验证的改动不落);
- 不加第三方依赖(前端无新包、`Cargo.toml` 依赖段零改动);
- 不新增原生 `alert/confirm/prompt`;文案进 `src/locales/*.json` 五语;
- 不改设置页布局(格式选择只活在本次会话的浮窗里)。
