Test Table Slide
| Class1 | Class1 | Class1 | Class2 | Class2 | Class2 | |
|---|---|---|---|---|---|---|
| A merged with B | A merged with B | C | A | B | C | |
| R1 | True | False | False | True | True | |
| R2 | True | False | ||||
| R3 | False | False | ||||
| R3 | True | True | ||||
| R4 | False | False | ||||
| R4 | True | True | False | False | ||
| R4 | True | False | True | False | True | False |
With footnote
从这份输出中至少能提炼出 docling 表格导出的四个行为特征:
- **标题独立成标题行**:幻灯片标题 `Test Table Slide` 被识别为标题文本,导出为 `#` 标题,而不是表格内文本;
- **合并单元格按“复制填满”策略输出**:观察第二行 `| A merged with B | A merged with B | C | ...`,这是一个横向合并两个单元格的标题单元。为保持 Markdown 表格的矩形栅格结构,合并后的文本在跨越的每一列上都被重复写出;竖向合并同样如此——`R3` 连续出现两行、`R4` 连续出现三行,正是行方向合并单元格在扁平的 Markdown 网格中被重复填充的结果;
- **空单元格保留占位**:表格中大量 `| |` 空段是合并结构中被覆盖的栅格位,被如实保留为空单元格;
- **表格与文本分离**:表下方的 `With footnote` 是独立段落,说明该文本不属于表格体,而是独立于表格、紧随其后的正文元素。
### 2. 第二张幻灯片:图形文本与列表
紧接着是第二张幻灯片的内容:
```markdown
# Second slide title
A rectangle shape with this text inside.
Let’s introduce a list
- With foo
- Bar
- And baz things
这里体现了 docling 对幻灯片“形状内文本”的抽取:A rectangle shape with this text inside. 原本是绘制在矩形形状(shape)文本框里的文字,转换后被还原为普通段落;随后 Let’s introduce a list 是引导语段落,紧跟的是无序列表演示。
3. 列表与段落如何在连续 Markdown 中并存
基准文件的剩余部分展示了一个更常见也更“麻烦”的幻灯片排版场景——同一页面上多组不同样式的列表、带引导文案的列表交错出现:
1. List item4
2. List item5
3. List item6
- I1
- I2
- I3
- I4
Some info:
- Item A
- Item B
Maybe a list?
1. List1
2. List2
3. List3
- l1
- l2
- l3
这些内容在源幻灯片中分属不同文本框与不同段落组。注意两点输出细节:
- 编号列表与符号列表被区分:凡是源文档使用了自动编号(
buAutoNum)的段落组,导出为有序列表;使用字符符号(buChar)或无符号列表标记的段落组,导出为无序列表。即便多个列表紧挨着排布,docling 也按各自独立的 list group 处理,不会合并成一个列表; - Markdown 输出中不显式标注幻灯片分界:与结构化文档(JSON/itxt,见下节)不同,纯文本 Markdown 导出是“顺排”的——从基准输出中只能通过标题与内容变化推测幻灯片的切换,没有类似
---的分页符。
二、对照“结构化视图”:JSON 与 itxt 揭示的真实层级
仅看 Markdown 会漏掉一个重要信息——这些内容到底如何被组织成文档树。同目录下的两份基准给出了答案。
legacy_sample.ppt.json 是 DoclingDocument 的完整序列化结果,其 schema_name 为 DoclingDocument。从 JSON 的 body 与 groups 节点可以看出,该 .ppt 被组织为 3 个 chapter 分组的幻灯片:slide-0(含标题文本、表格、脚注段落)、slide-1、以及第三个含多个列表的 group。每个文本、表格都被挂在对应 slide-N 分组之下,表格在第 0 张幻灯片,多组列表落在后续幻灯片——这解释了为什么 Markdown 中表格与列表会处于不同位置。
legacy_sample.ppt.itxt 则用缩进文本直观展示了树形层级:
item-0 at level 0: unspecified: group _root_
item-1 at level 1: chapter: group slide-0
item-2 at level 2: title: Test Table Slide
item-3 at level 2: table with [9x7]
item-4 at level 2: paragraph: With footnote
item-5 at level 1: chapter: group slide-1
item-6 at level 2: title: Second slide title
...
item-9 at level 2: list: group list
item-10 at level 3: list_item: With foo
从这里可以确认几个与 Markdown 输出严格对应的关键事实:
- 表格规模为 9 行 × 7 列(
table with [9x7]),与 Markdown 中表头 2 行 + 数据 7 行的 9 行一致; - 幻灯片标题在文档树中的 label 是
title,列表被识别为list: group list,其下每个条目是list_item;段落 label 为paragraph; - 每一张幻灯片是一个
chapter分组,演示文件被表示为“章—节”式的文档层级,这与 docling 对演示文稿的通用建模方式一致(itxt 基准文件、JSON 基准文件)。
三、为什么 .ppt 能转:后端先做“旧到新”的格式桥接
要理解这份基准输出的产生过程,需要看 docling 的 PowerPoint 后端实现 docling/backend/mspowerpoint_backend.py。该后端的类文档字符串明确写道:
Legacy
.pptfiles (binary PowerPoint 97-2003 format) are first converted to.pptxvia LibreOffice before parsing.
在构造函数的初始化逻辑里可以看到这一桥接的直接代码(docling/backend/mspowerpoint_backend.py#L148-L149):
if in_doc.format == InputFormat.PPT:
path_or_stream = convert_to_modern_format(path_or_stream, "ppt", "pptx")
也就是说:docling 自身并不直接解析二进制 .ppt,而是先调用 LibreOffice 将其无头转换为 .pptx,再交给 python-pptx(Presentation)加载解析。真正的 LibreOffice 调用封装在 docling/backend/docx/drawingml/utils.py 的 convert_to_modern_format(source, source_suffix, target_suffix, timeout_s=120) 函数中,其实现要点包括:
- 同时接受文件路径与内存字节流(
BytesIO);传入流时,会先按源扩展名命名写入临时文件,以便 LibreOffice 探测格式; - 以
--headless --convert-to <target_suffix> --outdir <tmp>调用 LibreOffice 子进程; - 为规避多进程/并发转换时 LibreOffice 对用户配置目录的独占锁冲突,转换会使用
-env:UserInstallation=<uri>指向一个一次性临时 profile(见_isolated_libreoffice_profile); - 转换产物以
BytesIO返回,供上层直接解析。
该后端支持的格式集合是 {InputFormat.PPTX, InputFormat.PPT}(supported_formats(),见 docling/backend/mspowerpoint_backend.py#L201-L202),转换后通过 _walk_linear 将每一张幻灯片上的标题、段落、表格、列表、图片等形状抽取为 DoclingDocument 结构。因此,你在 legacy_sample.ppt.md 中看到的表格与列表质量,本质上取决于“LibreOffice 重排出的 .pptx 能否保真还原原版版式”。
四、这份基准是如何被验证的:端到端回归测试
legacy_sample.ppt.md 并非孤立的展示样例,它是自动化回归测试的“答案”。打开端到端测试 tests/test_backend_legacy_msoffice.py,可以看到 .doc、.xls、.ppt 三种旧版二进制 Office 格式共享同一套验证逻辑:
_CASES: list[tuple[InputFormat, str]] = [
(InputFormat.DOC, "tests/data/doc/sources"),
(InputFormat.XLS, "tests/data/xls/sources"),
(InputFormat.PPT, "tests/data/ppt/sources"),
]
该测试的关键行为包括:
- 整个测试文件在 LibreOffice 不可用时被跳过(
pytest.mark.skipif(get_libreoffice_cmd() is None, ...)),印证了格式桥接对 LibreOffice 的硬依赖; - 使用
DocumentConverter(allowed_formats=[fmt])转换sources目录下的每一个样本,然后与groundtruth目录下同名文件比较三类输出:doc.export_to_markdown(compact_tables=True)与.md基准比对;doc._export_to_indented_text(max_text_len=70, explicit_tables=False)与.itxt基准比对;verify_document(doc, ...)与.json基准做带模糊容差的比对;
- 由于 LibreOffice 在不同平台上渲染出的图片尺寸略有差异,bbox 类比对使用了模糊容差(fuzzy tolerance);
- 测试文件头部引用了
test_data_gen_flag的GEN_TEST_DATA——当该标志开启时,比对会进入“重新生成基准”模式,允许维护者用当前版本重新固化输出。
这意味着任何 PR 只要改变了 .ppt 的转换逻辑,就必须让上述三个基准文件保持一致,否则测试失败。所以 legacy_sample.ppt.md 的稳定,直接守护了旧版 PowerPoint 转换功能不被回归破坏。
五、动手复现:如何亲自把 .ppt 导出成这份 Markdown
你可以用 docling 在本地重现与基准完全一致的输出。环境前提是安装 LibreOffice 且保证 libreoffice/soffice 在 PATH 中,否则后端会抛出 “LibreOffice is required to convert a .ppt file to .pptx” 的 RuntimeError。
from pathlib import Path
from docling.document_converter import DocumentConverter
from docling.datamodel.base_models import InputFormat
# 只允许 PPT 输入,强制走 LibreOffice 桥接链路
converter = DocumentConverter(allowed_formats=[InputFormat.PPT])
result = converter.convert(Path("legacy_sample.ppt"))
doc = result.document
# 与 tests/data/ppt/groundtruth/legacy_sample.ppt.md 的生成方式一致
print(doc.export_to_markdown(compact_tables=True))
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00