> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirobody.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 文件处理实现

> C++ 文档转码器：格式、构建开关、Options/Part API，以及 document CLI。

## 概览

文档摄取由 `src/transcode/` 中的**文档转码器**处理 —— [`document.hpp`](https://github.com/thetahealth/mirobody/blob/main/src/transcode/document.hpp) / `document.cpp`。它把一份上传的 PDF 或电子表格分解为一个有序的 **LLM 可消费片段**列表（文本和/或图像），策略为\*\*"文本优先，图像（及 OCR）兜底"\*\*。概念性走查参见[文件处理](/zh/concepts/file-processing)。

<Info>
  文档转码器把上传的文档拆解为文本与图像片段，供具备视觉能力的 LLM 在对话轮次中读取。*(上传接线以当前 HTTP 层为准核实。)*
</Info>

## 支持的格式与构建开关

格式由 magic bytes 嗅探得出（`detect_format`），再路由：

| 格式    | 库                     | CMake 开关              | 默认          | 备注                       |
| ----- | --------------------- | --------------------- | ----------- | ------------------------ |
| PDF   | PDFium                | `MIROBODY_ENABLE_PDF` | **关闭**      | 逐页文本层；扫描页 → 图像           |
| .xlsx | xlnt                  | （找到 xlnt 时自动）         | 存在 xlnt 处开启 | 每个工作表一张 Markdown 表格      |
| .xls  | 内置的 libxls            | `MIROBODY_ENABLE_XLS` | **关闭**      | 旧版 BIFF                  |
| .csv  | 内建（RFC 4180）          | —                     | 始终          | 单张 Markdown 表格           |
| OCR   | Tesseract + Leptonica | `MIROBODY_ENABLE_OCR` | **关闭**      | 需要 `MIROBODY_ENABLE_PDF` |

当某开关关闭时，格式仍会被嗅探，但 `process()` 会抛出 `DocumentError("... not built")`。

```bash theme={null}
cmake -DMIROBODY_ENABLE_PDF=ON -DMIROBODY_ENABLE_OCR=ON -DMIROBODY_ENABLE_XLS=ON ...
```

## API

```cpp theme={null}
namespace mirobody { namespace document {

enum class Format { Unknown, Pdf, Xlsx, Xls, Csv };

struct Part {
    enum class Kind { Text, Image };
    Kind              kind = Kind::Text;
    std::string       text;    // Text: UTF-8 / Markdown
    image::Transcoded image;   // Image: already vision-compliant
    int               page = 0;// 1-based source page / sheet
};

struct Document { Format format; std::vector<Part> parts; };

struct Options {
    image::Limits image_limits = image::QwenLimits;  // Qwen / Gemini / Gpt presets
    int  raster_dpi        = 150;   // density for a rasterized scanned page
    int  pdf_text_threshold = 8;    // < this many chars on a page => treat as a scan
    bool ocr_enabled       = true;  // honored only in a MIROBODY_ENABLE_OCR build
    std::string ocr_lang   = "eng"; // e.g. "eng+chi_sim"
    std::string ocr_datapath;       // dir with <lang>.traineddata; "" => TESSDATA_PREFIX
    std::size_t max_pages  = 0;     // 0 = no cap
};

class Transcoder {
public:
    explicit Transcoder(Options opts = Options());
    Document process(const std::string& input) const;         // throws DocumentError
    static std::string to_markdown(const Document& doc);       // parts -> one Markdown blob
    static Format detect_format(const std::string& input);     // cheap magic-byte sniff
};

}}
```

## 各路径产出什么

* **PDF，文本页** → 一个带提取出的文本层的 `Text` 片段。
* **PDF，扫描页**（文本低于 `pdf_text_threshold`）→ 以 `raster_dpi` 栅格化，编码为 PNG，经 `image::Transcoder` 处理以适配 `image_limits`，作为 `Image` 片段发出。在 `MIROBODY_ENABLE_OCR` 构建中，同一张栅格图还会被 OCR，识别出的文本追加为一个 `Text` 片段。
* **.xlsx / .xls** → 每个工作表一个 `Text` 片段，一张 GitHub 风格的 Markdown 表格（`## <sheet title>` + 表格）。绝不产生图像。
* **.csv** → 单个 `Text` 片段（RFC 4180 解析 → Markdown 表格；剥离 UTF-8 BOM）。

`to_markdown()` 把一个 `Document` 展平：文本片段内联，图像片段作为一行 `![page N image](...)` 占位符。

<Tip>
  `max_pages` 为页 / 工作表设界；触发时会追加一条尾注 "*(remaining … omitted: max\_pages reached)*"，从而不会有内容被悄悄丢弃。
</Tip>

## 线程（PDF）

PDFium 的库初始化/清理是进程全局的且非线程安全，且一份已加载的文档不可被并发访问。PDF 路径把所有 PDFium 工作串行化在一个内部互斥锁之后，并持有那次一次性的全局初始化，因此 `Transcoder` 可从多个线程安全调用 —— 只是在处理一份 PDF 时，PDF 工作并不并行。CSV / xlsx 无此约束。

## 试一试 —— `document` CLI

`cli/document.cpp` 工具（当 `MIROBODY_BUILD_TOOLS` 为 ON 时构建）端到端演练转码器：

```bash theme={null}
document extract <in> [<out.md>] [--target qwen|gemini|gpt] [--dpi N] \
                      [--no-ocr] [--ocr-lang L] [--ocr-data DIR] [--max-pages N]
```

```bash theme={null}
# a scanned PDF, Gemini vision preset, English+Simplified-Chinese OCR
./build/cli/document extract report.pdf out.md --target gemini --ocr-lang eng+chi_sim
```

图像片段（栅格化的扫描页）写在 `<out>` 旁边；文本片段落进 Markdown。`--target` 为栅格化页面挑选视觉预设（默认 `qwen`）。

## 单位

从文档中提取的数值可用单位引擎（`src/fhir/units/`）归一化到 UCUM —— 参见[数据映射](/zh/development/data-mapping#2-——-单位归一化（ucum）)。

## 测试

`tests/transcode/document_test.cpp`、`tests/transcode/file_test.cpp`、`tests/transcode/image_test.cpp`，以及 `tests/fhir/units_test.cpp`（Catch2）。运行转码器子集：

```bash theme={null}
./build/tests/mirobody_tests "[transcode]"
```

<Note>
  把提取出的数值作为带编码的 `Observation` 写回 FHIR 存储（文档 → 指标 → FHIR 流水线）尚在**进行中** —— 转码器与 UCUM 归一化已上线；术语映射与写回尚在计划中（`src/fhir/README.md` 的第 3–4 阶段）。*(verify)*
</Note>

## 下一步

<CardGroup cols={2}>
  <Card title="文件处理（概念）" icon="file" href="/zh/concepts/file-processing">
    流水线一览
  </Card>

  <Card title="数据流" icon="diagram-project" href="/zh/concepts/data-flow">
    上传的定位
  </Card>

  <Card title="数据映射" icon="arrows-rotate" href="/zh/development/data-mapping">
    UCUM 归一化
  </Card>

  <Card title="测试" icon="flask" href="/zh/development/testing">
    运行转码器测试
  </Card>
</CardGroup>
