Metadata-Version: 2.4
Name: atlisp-pdf2dwg
Version: 0.1.2
Summary: Convert PDF drawings and documents to DXF (optionally DWG) using local OCR, native PDF vectors and raster image vectorization.
Author: vitalgg
License-Expression: MIT
Project-URL: Homepage, https://gitee.com/atlisp/pdf2dwg
Project-URL: Repository, https://gitee.com/atlisp/pdf2dwg
Project-URL: Issues, https://gitee.com/atlisp/pdf2dwg/issues
Keywords: pdf,dxf,dwg,cad,ocr,vectorize,ezdxf
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Manufacturing
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pymupdf>=1.24
Requires-Dist: ezdxf>=1.3
Requires-Dist: numpy>=1.24
Requires-Dist: Pillow>=9.0
Requires-Dist: opencv-python-headless>=4.8
Provides-Extra: rapid
Requires-Dist: rapidocr>=3.0; extra == "rapid"
Requires-Dist: onnxruntime>=1.17; extra == "rapid"
Provides-Extra: surya
Requires-Dist: surya-ocr>=0.6; extra == "surya"
Provides-Extra: potrace
Requires-Dist: potracer>=0.0.4; extra == "potrace"
Provides-Extra: all
Requires-Dist: pdf2dwg[potrace,rapid]; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=4; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Dynamic: license-file

# atlisp-pdf2dwg

把 PDF 图纸/文档转换为可编辑的 **DXF**（可选 **DWG**）。

流程：**OCR 图文混排 → 原生 PDF 矢量提取 + 位图矢量化 → ezdxf 组装 DXF**。

- **OCR 图文混排**：默认为本地 RapidOCR（PP-OCR，逐行坐标、CPU、快）；可选 surya VLM
  （整页布局 + 文本，精度高但重）。
- **原生矢量**：若 PDF 仍是矢量（含被转曲的文字），直接提取线条，比追踪位图更清晰、更小；
  文字块内的字形轮廓会被剔除并替换为真实文字实体。
- **位图矢量化**：PDF 内嵌图片先按墨色感知的方式量化颜色，再用 **potrace** 拟合平滑贝塞尔
  曲线（回退到 OpenCV 轮廓追踪），随后采样+简化多边形，按原始变换/旋转映射回图纸并裁剪到页面。
  相同图片（页眉 logo、二维码等）跨页只追踪一次，并可多进程并行，质量与速度兼顾。
- **默认输出线条（LWPOLYLINE），不用 HATCH**。这是实测决定的：653 个 HATCH 的图经 LibreDWG
  转 DWG 会报 653 条 `Skip HATCH common handles`（填充大面积丢失），同样内容用多边形则一条都没有。
  代价是平铺多段线依赖绘制顺序、洞的正确性不如 HATCH，9 页实测召回 0.613 → 0.516。
  若 DXF 是最终交付、且更在意洞的正确性，可加 `--vector-mode rings` 改用 HATCH。
- **页面堆叠**：多页 PDF 默认纵向堆叠到模型空间，页间留间距；可用 `--flat` 关闭。

## 安装

```bash
pip install atlisp-pdf2dwg     # 核心（矢量 + OpenCV 位图，不含 OCR）
pip install "atlisp-pdf2dwg[potrace]"  # + potrace 曲线追踪（推荐，图片更精细）
pip install "atlisp-pdf2dwg[rapid]"    # + RapidOCR（推荐的本地 OCR）
pip install "atlisp-pdf2dwg[all]"      # rapid + potrace
pip install "atlisp-pdf2dwg[surya]"    # + surya VLM（可选，重）
```

需要 DWG 输出时另装 [GNU LibreDWG](https://www.gnu.org/software/libredwg/) 的 `dxf2dwg`，
或设置环境变量 `PDF2DWG_DXF2DWG=/path/to/dxf2dwg`。

## 命令行

```bash
# 默认：RapidOCR + 矢量 + 位图，输出 DXF
pdf2dwg convert 图纸.pdf -o 图纸.dxf

# 指定页码、DPI、关闭位图矢量化
pdf2dwg convert 图纸.pdf --pages 1-5,8 --dpi 300 --no-images

# 只提取矢量+位图（不 OCR，文字保留为轮廓）
pdf2dwg convert 图纸.pdf --no-ocr

# 输出 DWG（需 dxf2dwg）
pdf2dwg convert 图纸.pdf --format dwg

# 复用/缓存 OCR 结果
pdf2dwg convert 图纸.pdf --ocr-cache .ocr_cache

# 图片追踪质量/性能
pdf2dwg convert 图纸.pdf --trace potrace --image-max-dim 1600 --image-colors 8 --jobs 8
pdf2dwg convert 图纸.pdf --trace opencv        # 不装 potrace 时的回退
pdf2dwg convert 图纸.pdf --image-min-area 8 --image-max-polys 20000   # 压体积

# 扫描线稿：笔画中心线追踪（自带 4000 光栅与线宽）
pdf2dwg convert 图纸.pdf --image-line-art on
pdf2dwg convert 图纸.pdf --image-line-art on --no-line-width    # 粗细交给 CAD 线宽
pdf2dwg convert 图纸.pdf --no-text-color                        # 文字不取真彩色

# 其它子命令
pdf2dwg ocr 图纸.pdf -o ocr_json --engine rapid
pdf2dwg render 图纸.dxf -o preview.png --scale 1.5
pdf2dwg to-dwg 图纸.dxf
pdf2dwg info                 # 查看引擎与 dxf2dwg 路径
```

## Python API

```python
from pdf2dwg import convert_pdf

convert_pdf(
    "图纸.pdf",
    "图纸.dxf",
    page_spec="1-10",      # 1-based；或 pages=[0,1,2]
    dpi=200,
    ocr="rapid",           # 或 "surya" / None
    images=True,           # 位图矢量化
    vectors=True,          # 原生 PDF 矢量
    stack=True,            # 多页纵向堆叠
    ocr_cache=".ocr_cache",
    image_opts={"line_art": True},   # 扫描线稿：笔画中心线追踪
    text_color=True,                 # 文字真彩色（默认开）
)
```

分步使用：

```python
import pymupdf
from pdf2dwg import get_engine, build_dxf, render_page

doc = pymupdf.open("图纸.pdf")
engine = get_engine("rapid")
pages_ocr = {i: engine.run_image(render_page(doc, i, 200), i + 1, 200)
             for i in range(doc.page_count)}
build_dxf("图纸.pdf", "图纸.dxf", pages_ocr=pages_ocr)
```

## 输出图层

| 图层 | 内容 |
|------|------|
| `TEXT` / `TEXT-TITLE` | OCR 文字（正文 / 标题），样式 `HZ`（默认 `simsun.ttc`），带真彩色 |
| `GRAPHICS` / `GRAPHICS-FILL` | PDF 原生矢量（描边 / 填充），真彩色 |
| `IMAGES` | 位图矢量化结果（真彩色）；线稿模式下带 `const_width` 笔画粗细 |
| `PAGE` | 页面边框与页码 |

## 说明

- 坐标单位与 PDF 一致（points，1 pt = 1/72 inch）。
- 输出为 **R2004** DXF：更早的版本无法写出真彩色（420 组码），颜色会全部丢失。
- **线稿可切换为笔画中心线追踪**：`--image-line-art on` 跳过"填充区域"模型，直接追踪笔画的
  中轴线，适合扫描线稿。9 页实测（`text_mode="none"`，`--image-max-dim 4000`）：

  | 模式 | ink_iou | ink_recall | ink_colour_acc | 实体数 |
  |------|---------|-----------|----------------|--------|
  | `legacy`（默认，区域填充） | 0.267 | 0.613 | 86.57% | 716 |
  | `rings`（HATCH 带洞） | 0.284 | 0.516 | 90.19% | 525 |
  | **line-art** | **0.337** | **0.701** | 89.03% | 2,855 |

  代价是实体数约 4 倍。照片会明显变糟，且它会把文字也描一遍（OCR 实体已经画过），所以
  **默认关闭**；`--auto-tune` 会把它当候选实测后再决定要不要用。**测量时请用
  `text_mode="none"`**——用 `"strokes"` 评分会让线稿因重复画文字而虚高。
- 默认 `--vector-mode legacy`（平铺 LWPOLYLINE）。`rings` 用 HATCH 带洞，IoU 略高、彩色更准，
  但**召回更低**（上表 0.516 对 0.613：HATCH 逐环奇偶填充更准，平铺多段线依赖绘制顺序），
  而且 **LibreDWG 无法承载 HATCH**：653 个 HATCH 的图会报 653 条 `Skip HATCH common handles`，
  多边形一条都没有。所以只有需要 DWG 输出、且更在意洞的正确性时，才值得切到 rings。
- 中文文字样式默认字体 `simsun.ttc`，可在 `build_dxf(..., text_font=...)` 中修改；
  若 CAD 中字体缺失，替换为本地已安装的中文字体即可。
- **线稿带笔画粗细**：DXF 组码 43（`const_width`）。取源图墨迹掩膜的距离变换，在每段折线
  中点处读出局部线宽。页内线宽变化可达 3 倍（p10 1.38pt ~ p90 4.13pt），所以这是真实信息而非常数。
  在实体数完全不变的前提下，9 页实测（`--image-max-dim 2400`）IoU 0.281 → 0.323、
  召回 0.611 → 0.674、彩色一致率 88.37% → 89.19%。`--no-line-width` 可关闭。区域填充不给宽度：闭合环已围住自己的面积，
  再加宽度会重复计算。
- **文字也带真彩色**：从原页面在每个 OCR 文字框内采样墨色写入实体，所以蓝色标题不会变黑。
  每个框按**自身纸张底色**取阈值（取比纸色暗 60% 的像素中位数），因此灰度扫描件和有色纸都能用。
  9 页实测彩色一致率 86.19% → 88.53%，`rgb_mae` 24.43 → 21.52。`--no-text-color` 可关闭。
- 位图矢量化对线条图效果最佳；照片类图片会产生较多多边形，可用
  `--image-min-area` / `--image-max-polys` / `--no-images` 控制。
- **灰阶抗锯齿归并**（默认开启，`merge_neutral`）：~200 DPI 扫描件里的 0.25 pt 线只有一个
  抗锯齿像素，一条黑线会散成 (121,121,121)…(216,216,216) 一整条灰阶，每一级都变成独立的
  填充，最浅那级在白纸上基本看不见。现在把**严格中性灰**（max-min ≤ 12）归并到最深的一档，
  并合并标签消除重复环。只处理中性灰是有意为之：封面的米色 (200,197,185) 通道差为 15、
  蓝色 logo 根本不是中性色，都会被保留。实测墨迹召回 70.1% → 82.0%，实体数 −19.8%。
  用 `vectorize_image(..., merge_neutral=False)` 可关闭。
- 合并后的中性灰掩膜还可再做 **3×3 形态学闭运算**（`neutral_close`，**默认 0，关闭**）。
  合并会交错灰阶各级、掩膜边界呈阶梯状，闭运算确实能收拾掉冗余环：9 页实测召回
  82.0% → 84.2%、实体数 −9.4%。但它会让笔画变粗，而**变粗的笔画既吃掉更多墨迹、又不再细长**，
  实际发布的线条模式因此反而变差（召回 72.9% → 68.3%），只换来约 2% 的体积。所以默认关闭；
  要用请同时重测两种模式。
- `--image-max-dim` 是体积与保真的主要开关，**两种模式的合适值相差很大**：
  - **区域填充默认 600**。绝大多数内嵌图实际放置尺寸远小于其像素尺寸，600 已足够；
    调到 1000 换不到 0.005 IoU 却让实体数涨 22~30%。只有当某页把扫描图**放大显示**时
    才值得提高。
  - **线稿默认 4000**（`cli.LINE_ART_MAX_DIM`），有两个独立的实测理由：其一，骨架化会删掉
    窄于约 2px 的笔画；其二，**降采样后的光栅会报出偏大的线宽**——用真实裁图配对测量，
    未降采样时线宽中位误差约 0%，一旦真降采样则 753px 的图在 600 时偏大 25%、400 时偏大 88%
    （距离变换不再跨越真实笔画，这个误差无法靠缩放系数补回）。4000 能让参考图纸全部 214 张
    内嵌图都不降采样。
- 质量度量工具。**`ink_iou` / `pct_bad_px` 是灰度的，对颜色完全失明**——把整张图压成
  单一墨色也能拿高 IoU。所以务必同时看 `ink_colour_acc` 与 `rgb_mae`：

  同理，`ink_recall` 看的是"源图墨迹被复现了多少"，IoU 上升时覆盖率可能反而在下降。

  ```bash
  python tools/compare_modes.py 图纸.pdf --pages 1-5 --dpi 100   # legacy / rings / line-art
  python tools/skeleton_trace.py 图纸.pdf --page 20              # 逐图预览线稿追踪
  python tools/auto_tune.py    图纸.pdf --pages 1-61              # 逐文档评估追踪参数
  ```

- `--auto-tune N`（实验性）会抽样 N 页评估追踪参数并采纳胜出者。它只在超过保守阈值时
  才改变配置；在参考图纸上它的建议在**未抽样页上反而更差**（颜色 −1.2 点、实体 +4.6%，
  仅换 +0.005 IoU），因此默认值被保留。建议先用 `tools/auto_tune.py` 诊断再决定。

## 开发

```bash
pip install -e ".[dev,rapid]"
bash tools/check.sh            # ruff + pytest（就是 CI 跑的那套）
bash tools/check.sh --full     # 再加 build + twine check，发版前用
```

单独跑：`python -m pytest`（82 个测试，无需网络/样本）、`python -m ruff check src tests tools`
（硬闸门，`pyproject.toml` 只选真 bug 规则 `E9,F63,F7,F82,F401,F841`；更宽的规则集有约 136 条
历史风格问题，属建议性质）、`python -m build`。

## 许可

MIT
