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
A rectangle shape with this text inside.
Let’s introduce a list
- With foo
- Bar
- And baz things
- List item4
- List item5
- List item6
- I1
- I2
- I3
- I4
Some info:
- Item A
- Item B
Maybe a list?
- List1
- List2
- List3
- l1
- l2
- l3
逐行观察可得到三个关键结论:每页"标题占位符"被渲染成一级标题 `#`;幻灯片正文中的文本/表格/列表被**线性平铺**;表格的合并单元格以重复文本的方式展开。下面结合源码逐点展开。
## 页与标题:占位符类型决定文本语义
源文件共 3 页(`test_pptx_page_range` 中断言 `page_count == 3`)。在 [tests/data/pptx/groundtruth/powerpoint_sample.pptx.itxt](https://gitcode.com/GitHub_Trending/do/docling/blob/3abb87a7a05ce60264399ab45047304e81bda926/tests/data/pptx/groundtruth/powerpoint_sample.pptx.itxt?utm_source=gitcode_repo_files) 中,每一页都成为文档树里的一个 `chapter` 分组(`group slide-0`/`slide-1`/`slide-2`),结构清晰:
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
这个层级模型恰恰是 JSON/itxt 与 Markdown 的差异点:**Markdown 是"拍平"后的线性视图**,幻灯片的页分组信息并不直接出现在 `.md` 中,而是体现为连续的标题和正文流。
从 XML 侧核对 `ppt/slides/slide1.xml`、`slide2.xml` 可以看到,第 1、2 页都有名为 `Title 1` 的标题占位符(placeholder),所以输出中出现两处 `#` 标题;而第 3 页只含 `TextBox 3`~`TextBox 7` 五个独立文本框、没有标题占位符,因此对应片段**没有**任何 `#` 标题。这与源码中标题判定逻辑吻合:
- 在 [mspowerpoint_backend.py](https://gitcode.com/GitHub_Trending/do/docling/blob/3abb87a7a05ce60264399ab45047304e81bda926/docling/backend/mspowerpoint_backend.py?utm_source=gitcode_repo_files) 的 `_handle_text_elements`(L692 起)与 `_handle_title`(L763 起)中,只有占位符类型为 `PP_PLACEHOLDER.TITLE` / `CENTER_TITLE` 的段落才被标记为 `DocItemLabel.TITLE`(L745-L753);
- 其余普通文本统一落入 `DocItemLabel.PARAGRAPH`;
- 标题在 Markdown 导出中默认渲染为一级标题(`#`),这解释了为什么每出现一个页面标题就会新增一个 `#`。
值得注意的细节是第 1 页的 "With footnote"。它在 XML 中是名为 `Subtitle 2` 的副标题占位符,但 itxt 树里是 `paragraph: With footnote`,在 Markdown 中也只以普通段落呈现,**没有**被当作标题。对照源码:`_handle_title` 中对 `SUBTITLE` 的处理只覆盖"整段即副标题"的分支,而经过正文遍历路径时,由于 `SUBTITLE` 不在 TITLE/CENTER_TITLE 白名单内,默认落到 `PARAGRAPH`——这正是该样本想锁定的边界行为。
## 表格:合并单元格如何在 GFM 中落地
第 1 页的表格是真值文件中最有信息量的部分。从 `ppt/slides/slide1.xml` 的 `<a:tbl>` 看,该表由 `a:tblGrid` 定义 **7 列**、9 行 `<a:tr>`,且大量使用合并:
| 行 | 特征 | XML 属性 |
|---|---|---|
| 表头第 1 行 | `Class1` 横跨 3 列、`Class2` 横跨 3 列 | `gridSpan="3"` |
| 表头第 2 行 | `A merged with B` 横跨 2 列 | `gridSpan="2"` |
| `R3` 数据行 | 纵向合并两行 | `rowSpan="2"` |
| `R4` 数据行 | 纵向合并三行 | `rowSpan="3"` |
后端读取这些原始属性并翻译成结构化 `TableCell`,见 [mspowerpoint_backend.py](https://gitcode.com/GitHub_Trending/do/docling/blob/3abb87a7a05ce60264399ab45047304e81bda926/docling/backend/mspowerpoint_backend.py?utm_source=gitcode_repo_files) 的 `_handle_tables`(L818-L880):
```python
row_span = cell_xml.get("rowSpan") # 纵向合并
col_span = cell_xml.get("gridSpan") # 横向合并
icell = TableCell(
text=cell.text.strip(),
row_span=row_span,
col_span=col_span,
start_row_offset_idx=row_idx,
end_row_offset_idx=row_idx + row_span,
start_col_offset_idx=col_idx,
end_col_offset_idx=col_idx + col_span,
column_header=row_idx == 0, # 首行作为列头
row_header=False,
)
由于 GitHub Flavored Markdown 的管道表格无法表达 rowSpan/gridSpan,导出的 .md 采用"展开填充"策略:横向合并的文本在覆盖的每一列重复出现,纵向合并的行标签在每一行重复出现。对照真值即可验证:
| | Class1 | Class1 | Class1 | Class2 | Class2 | Class2 |:Class1/Class2各自重复 3 次,恰好占满 7 列;| | A merged with B | A merged with B | ...:A merged with B重复 2 次;- 数据区出现连续两行
R3、连续三行R4,正是rowSpan在二维表格中的"视觉展开"。
合并信息并不会丢失——row_span/col_span 以及行列偏移量被完整保留在 TableCell 中,可在 .json 真值(powerpoint_sample.pptx.json)中按结构化方式核对。itxt 真值中的 table with [9x7] 也与 XML 的 9 行 × 7 列完全一致。简言之:Markdown 保布局可读,JSON 保结构无损。
文本与列表:有序 / 无序标记的判定链
第二、三页输出里混排了四种元素:普通段落、"-"无序列表、"1. 2. 3."编号列表。决定它们语义的核心在 mspowerpoint_backend.py 的列表标记解析链:
_is_list_item(L419)先尽量通过段落所属 shape 解析"有效标记",拿不到再退化为检查 XML 中的a:buChar/a:buAutoNum/ 段落层级;_get_effective_list_marker(L486)按四级优先级逐层查找标记定义:段落直接属性a:pPr→ 文本框a:lstStyle→ 版式(layout)占位符 → 母版(master)文本样式p:txStyles;_parse_bullet_from_paragraph_properties(L272)把a:buChar(字符项目符号)、a:buAutoNum(自动编号)、a:buBlip(图片符号)、a:buNone(显式无符号)区分开,段落缩进层级lvl从a:pPr/@lvl(0-8)读取;- 最终在
_handle_text_elements(L692)中,is_a_list=True的连续段落被聚合进doc.add_list_group,其中bullet_type == "Numbered"的项会递增生成"1."、"2."这类枚举标记。
这就是为什么真值里同一页会出现"正文打断列表"的结构:第 3 页中 Some info:(普通段落)隔开了两个列表组,Maybe a list?(普通段落)又把后面的内容切成新的列表组——在源码层面,一旦遍历遇到非列表段落,当前列表组即被关闭,之后新的列表项会另起一个 list 分组(itxt 中 slide-2 下多个独立 list group 正是这一行为的产物)。
视觉阅读顺序:为什么输出顺序不等于创建顺序
PowerPoint 内部按创建顺序/层级存储 shape,这与人的"视觉阅读顺序"常常不一致。后端在 mspowerpoint_backend.py 的 _iter_shapes_by_position(L621-L690)中做了排序:先按 top 从上到下分组,同一行内(顶边差不超过 _SHAPE_ROW_TOLERANCE_EMU = 45720 EMU,即 0.05 英寸)再按 left 从左到右输出;行判定采用"滑动窗口",只和紧邻前一个 shape 比较,因而连续带状的图标+文字组合能归入同一视觉行;坐标缺失的 shape 垫底保留原始相对顺序。
观察第 2 页就能看到它的效果:幻灯片上有标题、"A rectangle shape with this text inside." 的矩形、以及包含 "Let’s introduce a list" 与 3 个 bullet 的内容占位符。若按 XML 创建顺序,内容占位符在矩形之前;但 Markdown 真值输出为:
A rectangle shape with this text inside.
Let’s introduce a list
- With foo
- Bar
- And baz things
即矩形段落先于列表块出现——这正符合"按视觉位置重排"的设计目标(函数 docstring 明确指出:被拆开的文本块需要依据视觉顺序相邻输出)。第 3 页的 5 个独立文本框(编号列表、符号列表、"Some info:"、混合列表、l1-l3)同样按视觉位置从上到下依次成段,构成了真值后三分之一的内容。
本地复现:亲手导出同一份真值 Markdown
要复现本文引用的真值,只需按测试同款参数执行一次转换:
from pathlib import Path
from docling.datamodel.base_models import InputFormat
from docling.document_converter import DocumentConverter
converter = DocumentConverter(allowed_formats=[InputFormat.PPTX])
conv = converter.convert(Path("tests/data/pptx/sources/powerpoint_sample.pptx"))
markdown = conv.document.export_to_markdown(
compact_tables=True, # 测试使用的表格紧凑格式
)
print(markdown)
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 StartedRust0624
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