docparser_core/plugin.rs
1//! 能力抽象(与 Python `capability.py` 对齐):
2//! 解析能力(格式绑定)→ RawDoc;处理能力(全局共享,就地增强)。
3
4use std::fmt;
5
6use std::collections::BTreeMap;
7
8use crate::error::Result;
9use crate::model::{MetaValue, Rect, TableData};
10
11/// 解析产出的原始文本项:text + 可选 bbox。
12#[derive(Debug, Clone, Default)]
13pub struct RawTextItem {
14 pub text: String,
15 pub bbox: Option<Rect>,
16 /// 标题层级(1–6):Some 表示这一项是标题(ODF `text:outline-level` 等),
17 /// None 表示普通段落。插件 `assemble` 据此产出 `BlockKind::Heading`。
18 pub level: Option<u8>,
19 /// 列表项标记:`Some(ordered)` 表示这一项是列表(docx `w:numPr` 编号段落),
20 /// `ordered` 来自 numbering 定义(`bullet` / `none` 为无序)。插件 `assemble`
21 /// 据此产出 `BlockKind::List`。标题优先于列表(两个字段都有值时按标题处理)。
22 pub list: Option<bool>,
23 /// 表格项:`Some(rows)` 表示这一项是表格(对齐 Python item kind `table`)。
24 /// `text` 同时保留展开文本(行内 `,` 连接、行间 `\n`),页文本层用它;
25 /// 插件 `assemble` 据此产出 `BlockKind::Table`。表格优先于标题/列表。
26 pub table: Option<TableData>,
27 /// 图表项(pptx `c:chart` 的图表内文本):`assemble` 产出 `BlockKind::Chart`。
28 /// 优先级:表格 > 图表 > 标题 > 列表 > 段落。
29 pub chart: bool,
30 /// 图片项(对齐 Python item kind `image`):`Some` 时这一项**只**产
31 /// `BlockKind::Image`(content = `ocr_text` 或空串),`text` 恒为空、不进页文本。
32 /// 之所以把图片塞进 item 列表而不是只放 `RawPage.images`:Docx/Pptx 的图片块
33 /// 在文档里是**有位置**的(Python 把 `("image", im)` 追加在同段落 item 之后),
34 /// 只保留 `images` 会让块顺序无法还原。
35 pub image: Option<RawImage>,
36 /// 块级元信息(xlsx `merges` / `truncated` 等):`assemble` 原样搬到 `Block.meta`,
37 /// 与 Python 在 page.meta 上挂的同类信息对齐。
38 pub meta: BTreeMap<String, MetaValue>,
39}
40
41/// 解析产出的原始图片(可携带 OCR 结果)。
42#[derive(Debug, Clone, Default)]
43pub struct RawImage {
44 pub data: Vec<u8>,
45 pub bbox: Option<Rect>,
46 pub ext: String,
47 /// 内嵌位图像素尺寸(宽, 高);装饰图过滤 < 50px 用(对齐 Python `px_size`)
48 pub px: Option<(u32, u32)>,
49 /// 嵌入图 OCR 文本(未识别为 None;与 Python `meta["ocr_text"]` 对齐)
50 pub ocr_text: Option<String>,
51}
52
53/// 页面上的一块「区域」内容(docx 页眉 / 页脚 / 脚注 / 尾注):
54/// 与正文分离,自带类型;默认既不进块也不进页文本。
55#[derive(Debug, Clone, Default)]
56pub struct RawRegion {
57 /// 区域类型:`header` / `footer` / `footnote` / `endnote`
58 /// (与 Python `page.meta["regions"]` 的键、以及 `Block.meta["region"]` 的取值一致)
59 pub kind: String,
60 /// 区域内的一段文本(同一区域可能有多段,各自成块)
61 pub text: String,
62}
63
64/// 单元格批注(xlsx):与正文分离,默认既不进块也不进页文本。
65#[derive(Debug, Clone, Default)]
66pub struct RawComment {
67 /// 单元格引用(`A1` / `B3`):进 `Block.meta["cell"]`,便于下游定位回写
68 pub cell: String,
69 /// 批注正文(已 trim)
70 pub text: String,
71}
72
73#[derive(Debug, Clone, Default)]
74pub struct RawPage {
75 pub name: Option<String>,
76 pub items: Vec<RawTextItem>,
77 pub images: Vec<RawImage>,
78 /// 附属文本(pptx 备注页等):不进块,只追加到页文本末尾
79 /// (对齐 Python `page.meta["notes"]` 的默认行为)
80 pub notes: Option<String>,
81 /// 区域内容(docx 页眉/页脚/脚注/尾注/批注):`assemble` 在 `include_regions`
82 /// 打开时按 `BlockKind::Note` + `meta["region"] = kind` 产出,**不进页文本**
83 /// (对齐 Python `DocxPlugin.assemble`;`kind = "comment"` 是 Rust 侧新增,
84 /// 批注作者/日期不进文本)。
85 pub regions: Vec<RawRegion>,
86 /// 单元格批注(xlsx):`assemble` 在 `include_regions` 打开时按
87 /// `BlockKind::Note` + `meta["region"] = "comment"` 产出,**不进页文本**
88 /// (对齐 Python `XlsxPlugin.assemble`)。
89 pub comments: Vec<RawComment>,
90 pub width: Option<f32>,
91 pub height: Option<f32>,
92 pub rotation: Option<i32>,
93 /// 解析即已得到的表格结构 rows × cells
94 pub tables: Vec<Vec<Vec<String>>>,
95 /// 快路径页文本(`structure_mode` 非语义分支):`Some` 时该页**不出块**,
96 /// 页文本直接取此值(对齐 Python `blocks=[]` + `text="\n".join(texts)`)。
97 /// 仅 `regions`/`comments` 仍随 `include_regions` 产 `Note` 块;`notes` 被忽略
98 /// (Python 非语义分支不看 `meta["notes"]`)。
99 pub flat_text: Option<String>,
100}
101
102/// 解析能力的输出:尚未经过处理能力。
103#[derive(Debug, Clone, Default)]
104pub struct RawDoc {
105 pub pages: Vec<RawPage>,
106 pub meta: std::collections::HashMap<String, String>,
107 pub needs_ocr: bool,
108}
109
110/// 能力实现形态。
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum BackendKind {
113 Bundled,
114 External,
115 Composite,
116}
117
118impl BackendKind {
119 pub fn as_str(&self) -> &'static str {
120 match self {
121 BackendKind::Bundled => "bundled",
122 BackendKind::External => "external",
123 BackendKind::Composite => "composite",
124 }
125 }
126}
127
128impl fmt::Display for BackendKind {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 f.write_str(self.as_str())
131 }
132}
133
134/// 解析能力:格式绑定,把文件读成 RawDoc。
135pub trait ParseCapability: Send + Sync {
136 fn id(&self) -> &str;
137 fn formats(&self) -> &[&str];
138 fn kind(&self) -> BackendKind {
139 BackendKind::Bundled
140 }
141 fn is_available(&self) -> bool {
142 true
143 }
144 fn parse(&self, path: &str) -> Result<RawDoc>;
145}
146
147/// 处理能力:全局共享,就地增强 RawDoc(OCR / layout)。
148pub trait ProcessCapability: Send + Sync {
149 fn id(&self) -> &str;
150 fn is_available(&self) -> bool {
151 true
152 }
153 fn should_apply(&self, _raw: &RawDoc) -> bool {
154 true
155 }
156 fn process(&self, raw: &mut RawDoc) -> Result<()>;
157}
158
159/// 文档插件:某文件类型的完整处理器(组装 RawDoc → IR Document)。
160pub trait FormatPlugin: Send + Sync {
161 fn id(&self) -> &str;
162 fn formats(&self) -> &[&str];
163 fn default_parse_chain(&self) -> Vec<&'static str>;
164 fn default_process_chain(&self) -> Vec<&'static str>;
165 fn assemble(&self, raw: &RawDoc) -> crate::model::Document;
166}