Metadata-Version: 2.4
Name: dy_docs_tools
Version: 0.1.2
Summary: High-performance document conversion tools powered by Rust + PyO3.
Author: PC
Requires-Python: >=3.7
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# dy_docs_tools

`docs_tools` 是一个基于 Rust + PyO3 的文档转换工具包，当前支持：

- `PDF -> 图片`
- `PDF -> PDF`（先转为受控图片，再重建回 PDF）
- `图片 -> PDF`
- `DOCX -> PDF`
- `DOCX -> 图片`

## 1. 环境准备

### Rust 工具链

在 Windows PowerShell 中安装 Rust：

```powershell
winget install Rustlang.Rustup
```

安装完成后重新打开终端，并执行：

```powershell
cargo --version
rustc --version
```

### Python 构建工具

```powershell
python -m pip install --upgrade pip
python -m pip install maturin
```

### 外部转换器

- `pdftoppm`（来自 Poppler），用于 `PDF -> 图片`
- `soffice`（来自 LibreOffice），用于 `DOCX -> PDF`
- 如果 `pdftoppm` 不在 `PATH` 中，库会在 Windows 和 Linux 的常见安装目录中自动查找

Windows 示例安装命令：

```powershell
winget install oschwartz10612.Poppler
winget install TheDocumentFoundation.LibreOffice
```

确认命令可用：

```powershell
pdftoppm -v
soffice --version
```

## 2. 构建与安装

在项目根目录执行：

```powershell
maturin develop
```

如果更倾向于构建 wheel：

```powershell
maturin build --release
python -m pip install .\target\wheels\*.whl
```

Linux 发布基线说明：

- 已发布的 Linux wheel 覆盖 `manylinux_2_24`（`glibc >= 2.24`）
- 额外提供 `manylinux_2_36`（`glibc >= 2.36`）版本
- GitHub Actions 发布流程默认基于 `manylinux_2_24` 构建
- `manylinux_2_36` wheel 在 Debian 12 容器中通过 `maturin + auditwheel` 构建
- 如果要复现 Linux 发布环境，优先使用 `cibuildwheel` 或与 `manylinux_2_24` 一致的容器，不要直接在更新的宿主机系统上构建发布轮子

### 快速验证

```powershell
python scripts\verify.py
```

如果包导入失败，脚本会自动尝试加载 `target/wheels` 中最新生成的 wheel。

## 3. Python 接口

```python
from docs_tools import (
    close_converter_service,
    pdf_to_pdf,
    pdf_to_image,
    image_to_pdf,
    docx_to_pdf,
    docx_to_image,
    get_converter_service,
)

# PDF -> 图片
images = pdf_to_image(
    "a.pdf",
    "out",
    dpi=300,
    fmt="png",
    prefix="page",
    max_area=36_000_000,
    max_size_mb=10,
)

# PDF -> PDF（默认 fmt 为 jpg）
pdf_out = pdf_to_pdf(
    "a.pdf",
    "a_rebuilt.pdf",
    dpi=300,
    fmt="jpg",
    max_area=36_000_000,
    max_size_mb=10,
)

# 图片 -> PDF
pdf_path = image_to_pdf(["1.png", "2.png"], "merged.pdf", max_area=36_000_000, max_size_mb=10)
pdf_bytes = image_to_pdf(["1.png", "2.png"], return_bytes=True)

# DOCX -> PDF（默认 fmt 为 jpg）
pdf_path = docx_to_pdf("a.docx", "out/a.pdf", dpi=300, fmt="jpg", max_area=36_000_000, max_size_mb=10)

# DOCX -> PDF，并启用受限重编码（默认关闭）
pdf_path_limited = docx_to_pdf(
    "a.docx",
    "out/a_limited.pdf",
    apply_constraints=True,
    dpi=300,
    fmt="jpg",
    max_area=36_000_000,
    max_size_mb=10,
)
pdf_bytes2 = docx_to_pdf("a.docx", return_bytes=True)

# DOCX -> 图片
images = docx_to_image("a.docx", "out", dpi=200, fmt="jpg", max_area=36_000_000, max_size_mb=10)
image_bytes_list = docx_to_image("a.docx", dpi=200, fmt="jpg", return_bytes=True)

# 持久化工作线程池，用于提升批量吞吐
service = get_converter_service(workers=4)
try:

    # 异步提交并获取句柄
    h1 = service.submit_to_images("a.pdf", "out/a", 200, "png", "page", max_area=36_000_000, max_size_mb=10)
    h2 = service.submit_to_pdf("a.docx", "out/a_from_service.pdf")
    h3 = service.submit_to_pdf(
        "a.docx",
        "out/auto_from_docx.pdf",
        apply_constraints=True,
        dpi=300,
        fmt="jpg",
        max_area=36_000_000,
        max_size_mb=10,
    )

    if not h1.done():
        print("h1 仍在运行")

    images = h1.result()             # 阻塞直到完成
    pdf_path = h2.result(10_000)     # 超时单位为毫秒
    pdf_path2 = h3.result(10_000)
finally:
    close_converter_service()
```

图片输出限制：

- 所有转换接口都支持：`pdf_to_image`、`docx_to_image`、`image_to_pdf`、`docx_to_pdf`、`pdf_to_pdf`，以及对应的池化版本
- 参数为 `max_area`（像素面积）和 `max_size_mb`（MB）
- 默认值分别为 `36_000_000` 和 `10`
- 当图片超过限制时，会自动缩放或压缩，直到满足约束

返回模式：

- 路径模式：默认返回输出路径或路径列表
- 字节模式：传入 `return_bytes=True`

调试日志：

- 设置 `DOCS_TOOLS_DEBUG=1`，可在 `stderr` 中输出更详细的处理日志，方便分析快捷路径、栅格化、约束收敛和 PDF 重建过程

## 4. 优化与压测结论

### 4.1 当前版本已落地的优化

- 基准脚本和压测脚本支持 `--in-flight` 滑窗提交，避免一次性把任务全部压入队列
- 对 Poppler 和 LibreOffice 外部进程做全局并发限制，减少过度竞争
- 优先按可信扩展名判断输入类型，减少内容嗅探开销
- PDF 重建时减少重复图片解码和重复读取尺寸信息
- `return_bytes` / `data_url` 路径改为内存构建 PDF，去掉临时文件往返
- 保留扫描件快路径和并行图片转码路径
- `simple-scan` JPEG 默认质量保持为 `90`

### 4.2 测试数据集信息

- 数据集目录：`C:\Users\PC\pros\python\tests`
- 文件总数：`10`
- 总大小：`27,515,936` 字节（约 `26.24 MiB`）
- 本次 README 中的最新结果基于 wheel：`target\wheels\docs_tools-0.1.0-cp37-abi3-win_amd64.whl`

| 类型 | 数量 | 总字节 | 占比 |
| --- | ---: | ---: | ---: |
| `.pdf` | `4` | `21,061,866` | `76.54%` |
| `.jpg` | `5` | `5,627,337` | `20.45%` |
| `[无扩展名]` | `1` | `826,733` | `3.00%` |

- 最大文件：`505344.pdf`
- 文件大小：`19,430,977` 字节（约 `18.53 MiB`）
- 页数：`6`
- 占整个测试集字节量：`70.62%`
- 结论：`505344.pdf` 是这个目录下最主要的性能瓶颈文件

### 4.3 最新连续压测结果（`return_mode=path`，`dpi=300`，`page_dpi=300`）

- 结果目录：`target\stress_loop_monitor_page300_latest_path`
- 汇总文件：`target\stress_loop_monitor_page300_latest_path\summary.json`
- 明细文件：`target\stress_loop_monitor_page300_latest_path\details.json`
- 压测参数：`workers=4`、`in_flight=4`、`fmt=jpg`、`rounds=10`

| 指标 | 数值 |
| --- | ---: |
| 总轮数 | `10` |
| 每轮文件数 | `10` |
| 总任务数 | `100` |
| 成功 / 失败 | `100 / 0` |
| 总耗时 | `47.171s` |
| 平均每轮耗时 | `4.717s` |
| 单轮最短耗时 | `4.200s` |
| 单轮最长耗时 | `4.985s` |
| 平均吞吐 | `2.120 files/s` |
| Python 自身 RSS 起止 | `21.81 MB -> 24.01 MB` |
| Python 自身峰值 RSS | `63.109 MB` |
| 根进程峰值 RSS | `59.934 MB` |
| 进程树峰值 RSS | `729.738 MB` |

- 结论：连续 10 轮压测全部成功，Python 自身常驻内存变化较小，峰值主要来自外部转换子进程

### 4.4 大文件 `505344.pdf` 重点指标

- 输入文件：`C:\Users\PC\pros\python\tests\505344.pdf`
- 10 轮平均耗时：`4.696371s`
- P50 耗时：`4.778077s`
- 最快 / 最慢：`4.175831s / 4.971845s`
- 输出大小：`3,432,157` 字节（约 `3.27 MiB`）
- 相对源文件减小：`82.34%`
- 占平均轮耗时：`99.57%`
- 样例输出：`target\stress_loop_monitor_page300_latest_path\outputs\505344.round10.pdf`

补充说明：

- 同目录下其余较慢 PDF 中，`505976.pdf` 的 10 轮平均耗时为 `2.483662s`
- 另一份扫描票据类 PDF 的 10 轮平均耗时为 `2.481054s`
- 在这组测试数据中，`505344.pdf` 明显决定了整轮尾延迟

### 4.5 `dpi=150` 补充压测结果

- 结果目录：`target\stress_loop_monitor_page150_latest_path`
- 汇总文件：`target\stress_loop_monitor_page150_latest_path\summary.json`
- 明细文件：`target\stress_loop_monitor_page150_latest_path\details.json`
- 压测参数：`workers=4`、`in_flight=4`、`fmt=jpg`、`rounds=1`、`page_dpi=300`

| 指标 | 数值 |
| --- | ---: |
| 单轮总耗时 | `4.302403s` |
| 吞吐 | `2.323 files/s` |
| Python 自身峰值 RSS | `59.492 MB` |
| 进程树峰值 RSS | `611.133 MB` |
| `505344.pdf` 单轮耗时 | `4.278890s` |
| `505344.pdf` 输出大小 | `3,432,157` 字节 |
| `505344.pdf` 样例输出 | `target\stress_loop_monitor_page150_latest_path\outputs\505344.round01.pdf` |

- 结论：把 `dpi` 从 `300` 调到 `150` 后，这个大文件的耗时和输出大小都没有出现明显下降，说明当前瓶颈并不主要由栅格 DPI 本身决定

### 4.6 `505344.pdf` 的分辨率、压缩与清晰度说明

- 源页面宽度：`2355.91 pt`（约 `32.72 inch`）
- 输出页面宽度：`564.96 pt`（约 `7.85 inch`）
- 源页图宽度：`2354 px`
- 输出页图宽度：`2354 px`
- 源有效 DPI：约 `71.94`
- 输出有效 DPI：`300`

关键结论：

- 当前快路径没有把 `505344.pdf` 的页图像素放大
- `300 dpi` 来自物理页面尺寸缩小，不是把页图重采样到更高像素
- 如果沿用源 PDF 的 `MediaBox`，有效 DPI 会继续接近 `72`，而不是 `300`

JPEG 与画质说明：

- `simple-scan` 的默认 JPEG 质量是 `90`
- 本次 `505344.pdf` 测试没有触发 `max_size_mb=10` 的二次压缩阈值，因此没有额外降低 JPEG 质量
- 抽取页图总字节约为 `18.09 MB -> 3.27 MB`
- 平均 `PSNR`：`43.13 dB`
- 平均绝对像素差：`1.07 / 255`
- 综合判断：这是轻微有损压缩，文本和票据类内容的可读性变化较小

### 4.7 复现命令

下面这组命令可直接复现当前 README 中的核心性能数据：

```powershell
# 构建最新 wheel
python -m maturin build --release -o target\wheels

# 单次目录基准：顺序提交
python scripts\benchmark_submit_to_pdf.py `
  --input-dir C:\Users\PC\pros\python\tests `
  --output-dir target\benchmark_seq_page300 `
  --dpi 300 --page-dpi 300 --fmt jpg `
  --workers 4 --in-flight 1 `
  --csv target\benchmark_seq_page300\results.csv

# 单次目录基准：并发滑窗提交
python scripts\benchmark_submit_to_pdf.py `
  --input-dir C:\Users\PC\pros\python\tests `
  --output-dir target\benchmark_conc_page300 `
  --dpi 300 --page-dpi 300 --fmt jpg `
  --workers 4 --in-flight 4 `
  --csv target\benchmark_conc_page300\results.csv

# 连续 10 轮压测（README 当前主结果）
python scripts\stress_submit_to_pdf_monitor.py `
  --input-dir C:\Users\PC\pros\python\tests `
  --output-dir target\stress_loop_monitor_page300_latest_path `
  --rounds 10 `
  --dpi 300 --page-dpi 300 --fmt jpg `
  --workers 4 --in-flight 4

# dpi=150 补充对比
python scripts\stress_submit_to_pdf_monitor.py `
  --input-dir C:\Users\PC\pros\python\tests `
  --output-dir target\stress_loop_monitor_page150_latest_path `
  --rounds 1 `
  --dpi 150 --page-dpi 300 --fmt jpg `
  --workers 4 --in-flight 4
```

## 5. CI 与发布

- CI 工作流：`.github/workflows/ci.yml`
  - 构建 Linux 的 abi3 wheel
  - Linux wheel 默认基于 `manylinux_2_24`（`glibc >= 2.24`）
  - 额外构建 `manylinux_2_36`（`glibc >= 2.36`）
  - 同时构建源码分发包 `sdist`
- 发布工作流：`.github/workflows/publish.yml`
  - 通过 `v0.1.0` 这类 git tag 触发
  - 将所有构建产物上传到 PyPI
  - Linux wheel 基线固定为 `manylinux_2_24`
  - 同时上传 `manylinux_2_36` 版本

推荐的 PyPI 配置：

- 优先使用 GitHub 仓库 / 工作流对应的 Trusted Publishing（OIDC）
- 如果使用 API Token 方式，需要在发布步骤中切换为 token 认证，并在仓库密钥中配置 `PYPI_API_TOKEN`

