# 开源 PDF 转 Markdown 库（TypeScript，Node 与浏览器）

URL: https://mindmax.me/zh/open-source/pdf-markdown/
Last verified: 2026-11-09

> MindMax 的 PDF 转换引擎以 @mindmax/pdf-markdown 发布，Apache-2.0。它重建什么、明确不做什么，以及靠什么防止自己悄悄变差。

## PDF → Markdown 引擎，开源

[PDF 转 Markdown 那一页](https://mindmax.me/zh/tools/pdf-to-markdown/)上的转换器，不是一个恰好免费的在线服务。它是一个跑在你浏览器里的 TypeScript 库，以 `@mindmax/pdf-markdown` 发布，Apache-2.0 许可；你在 MindMax 里打开 PDF 时跑的也是同一份代码。这一页说它做什么、明确不做什么，以及靠什么防止自己悄悄变差。

| | |
| --- | --- |
| 包 | @mindmax/pdf-markdown v0.1.0 |
| 源码 | github.com/woodpeng/mindmax-pdf-markdown |
| 许可 | Apache-2.0，覆盖引擎而非 MindMax 应用，边界见开源总页 |
| 运行时依赖 | 0 个 |
| 体积 | gzip 25.1 KB |
| 运行环境 | Node 18+ 与任意浏览器；不用 fs，不用 window |
| 守着它的 | 22 份文档、170 个测试、97.3% 语句覆盖 |

## 它重建什么[​](https://mindmax.me/#它重建什么)

从 PDF 里直接复制文字，拿到的是生产方写进文件的顺序，而不是人读的顺序。这个库反过来做：拿 pdf.js 报出来的文字片段和它们的位置，把版面当成结构的来源。

- **阅读顺序。** 双栏论文先读左栏。跨页断开的段落仍然是一段，为分栏而断开的段落会接回去。

- **标题。** 级别来自字号、字重和编号（`3.1`、`第X章`、`Chapter 4`）；PDF 自带书签时用书签校正，没有书签时用 Tagged PDF 的结构树。

- **表格。** 从文字落在哪还原行列——列间距、对齐、重复的列起点——写成 Markdown 表格，而不是被绞成一行行碎句。

- **列表和链接。** 项目符号变成 `-`，编号列表保持编号；PDF 里某个词指向的链接会还原。PDF 没存链接标注的地方，不会凭空造一个。

- **页眉页脚。** 跑动页眉、页脚、页码、水印都去掉。Tagged PDF 会把它们标成 `/Artifact`，优先用这个；绝大多数 PDF 没有标记，退回几何判断。

出来的那串文字同时也是所有锚点的落点。`buildPdfTextMap` 记录了每一页从哪里开始、每个文字片段落在哪里，所以读者在 PDF 里高亮一句话，能按偏移量拿回同一句话——不是再搜一遍文字然后祈祷它只出现一次。

## 它明确不做[​](https://mindmax.me/#它明确不做)

| 不做 | 为什么 |
| --- | --- |
| 识别扫描件 | 扫描件没有文字层。叫它 OCR 的每一步都会是模型调用、账单和另一个产品。这个库的做法是直接告诉你这页是空的。 |
| 还原公式 | PDF 存的是定位好的字形，不是公式。x² 到手里就是 x2。库会检出哪些页发生了这件事、共有几页，而不是假装不上模型也能拿回 LaTeX。 |
| 带出图片 | 插图留在 PDF 里。抽图是另一件事，失败方式也完全不同。 |
| 上传任何东西 | 包里没有任何网络调用。在浏览器里跑，文件不会离开这个标签页。 |

这四件恰好是问得最多的四件，所以四条都正面写出来。一个声称做到其中两件、实际两件都没做到的转换器，比一个说清自己停在哪里的转换器更糟。

## 一页纸怎么变成 Markdown[​](https://mindmax.me/#一页纸怎么变成-markdown)

| 步骤 | 发生什么 |
| --- | --- |
| 1. 采集 | 每页一次 pdf.js 调用，取文字片段、变换矩阵，以及 Tagged PDF 上的 /Artifact 标记。 |
| 2. 去家具 | 先把 Artifact 标记的片段抹空，再按跨页重复去掉页眉、页脚、页码。片段序号保持不变，偏移量才有效。 |
| 3. 重排成行 | 按基线把字形聚成行，按间距和缩进把行聚成段。断行处的连字符词接回去。 |
| 4. 排出页内顺序 | 检出分栏的列间距，逐栏读；通栏的块把页面切成上下段。 |
| 5. 判定块类型 | 字号和编号判标题，符号判列表，几何判表格，字号和分隔线判脚注。 |
| 6. 输出 | Markdown，以及"哪些字符来自 PDF、哪些是拼装时写的"这份映射。这一步不做任何"顺手排得好看点"的重排，因为那会挪动每一个偏移量。 |

## 两种调用方式[​](https://mindmax.me/#两种调用方式)

多数调用方要的是一篇文档；也有人要的是结构化的正文和映射。这两种需求不该合进一个两边都做不好的 API。

`

import { convertPdfToMarkdown } from &#x27;@mindmax/pdf-markdown&#x27;;

// 手上已经有一个打开好的 pdf.js 文档

const result = convertPdfToMarkdown(pdfDocument);

if (result.ok) {

 result.markdown; // .md 文件内容

 result.links; // 还原了多少个链接

 result.textLayerIssues; // 有公式或未映射字形的页，没有则为 null

} else {

 result.reason; // &#x27;scanned&#x27; | &#x27;empty&#x27;

}

`

`

import { buildPdfTextMap } from &#x27;@mindmax/pdf-markdown&#x27;;

// 要的是正文本身，不是文件

const map = buildPdfTextMap(pagesOfTextItems);

map.text; // 所有锚点都指向这串 Markdown 形状的正文

map.pages; // 每页：从哪开始，以及每个片段在其中的区间

`

`CANONICAL_TEXT_VERSION` 存在的理由和这些偏移量一样：你把偏移量存下来，就得知道哪次升级会挪动它们。它只在同样输入下 `map.text` 真的变了时才跳版本——Markdown 渲染可以改而不跳它。

上面两段都没有 import pdf.js。`Pdf*Like` 都是结构化类型，浏览器构建、Node legacy 构建、测试里的假对象都能满足，所以这个包不会往你的 bundle 里塞第二份 pdf.js。

## 为什么语料是重点[​](https://mindmax.me/#为什么语料是重点)

仓库里带着 22 份真实文档，以及每一份转出来的 Markdown 原文。其中 16 份 CC-BY、5 份美国联邦政府作品、1 份中国官方文件——每一份都可以再分发，所以快照能放在仓库里，测试在哪都能跑。

一份 PDF 转坏了，修法不是"调一下启发式再碰运气"。这份文档连同一个描述错在哪的失败断言一起进语料，测试变红，改代码直到变绿，用例永久留下。整个贡献模型就这一条，也是为什么一份附了文档的 bug 报告，比一段很长的文字描述有用得多。

[逐文档结果表](https://mindmax.me/zh/open-source/pdf-markdown/benchmark/)就是这份语料加上数字：每份文档转出来什么、被多少条断言检查、还有哪些缺陷没修。它由测试跑出来的 JSON 直接渲染，没有一个手打、会过期的数。

## 谁在维护[​](https://mindmax.me/#谁在维护)

引擎由 MindMax 团队公开维护，而且 MindMax 自己依赖的就是发布到 npm 的这个包，不是私有副本——应用要修的问题，先在这里发版。许可覆盖这个引擎，不覆盖外面的应用；这条线以及团队在安全、治理、连续性上的承诺，统一写在[开源总页](https://mindmax.me/zh/open-source/)。

## 试用与贡献[​](https://mindmax.me/#试用与贡献)

看输出最快的方式就是[这个转换器本身](https://mindmax.me/zh/tools/pdf-to-markdown/)：它在你的浏览器里跑这个库，不用装、不用注册。只想要一个"丢进 PDF、吐出 Markdown"、周围什么都没有的版本，用 [playground](https://woodpeng.github.io/mindmax-pdf-markdown/)，源码在仓库的 `examples/browser` 目录。

两条贡献规则比其余都重要。改动会让语料里某份文档的输出变化时，得在 diff 里写清理由；修好一份文档不能弄坏另一份——CI 在每个 PR 上跑全部文档。其余的在 [CONTRIBUTING.md](https://github.com/woodpeng/mindmax-pdf-markdown/blob/main/CONTRIBUTING.md)。

只想报告一份转坏的文档，用[语料用例模板](https://github.com/woodpeng/mindmax-pdf-markdown/issues/new?template=corpus_case.yml)，它只问一件让问题可修的事：这份文档，或者你有权分享的其中一页。

- [它重建什么](https://mindmax.me/#它重建什么)
- [它明确不做](https://mindmax.me/#它明确不做)
- [一页纸怎么变成 Markdown](https://mindmax.me/#一页纸怎么变成-markdown)
- [两种调用方式](https://mindmax.me/#两种调用方式)
- [为什么语料是重点](https://mindmax.me/#为什么语料是重点)
- [谁在维护](https://mindmax.me/#谁在维护)
- [试用与贡献](https://mindmax.me/#试用与贡献)
