Skip to main content

docparser_core/
model.rs

1//! 统一 IR(与 Python `model.py` 语义一致,含 serde 便于 JSON/CLI 往返)。
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7/// 轴对齐矩形,坐标语义由产生方决定(页面 px / pt / 逻辑单位)。
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
9pub struct Rect {
10    pub x0: f32,
11    pub y0: f32,
12    pub x1: f32,
13    pub y1: f32,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum BlockKind {
19    Heading,
20    #[default]
21    Paragraph,
22    List,
23    Code,
24    Table,
25    Image,
26    Quote,
27    /// 图表(pptx `c:chart` 等):`content` 为图表内文本(标题 + 数据标签)
28    Chart,
29    /// 备注 / 批注 / 脚注等附属文本(对齐 Python `BlockKind.NOTE`):
30    /// `meta["region"]` 标注来源区域(如 pptx 备注页 = `"notes"`)。
31    Note,
32}
33
34/// 结构化表格:rows[row][col] = 单元格文本。
35#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
36pub struct TableData {
37    pub rows: Vec<Vec<String>>,
38    /// 前 N 行为表头(0 = 未识别)。与 Python `TableData.header_rows` 同口径:
39    /// 目前只有 xlsx 冻结窗格(`<pane state="frozen" ySplit>0`)会给出 1。
40    #[serde(default, skip_serializing_if = "is_zero")]
41    pub header_rows: usize,
42}
43
44fn is_zero(n: &usize) -> bool {
45    *n == 0
46}
47
48/// 块级元信息的值(与 Python `Block.meta: Dict[str, Any]` 的**实际取值形态**同形):
49/// 只允许布尔 / 字符串 / 字符串数组三种,序列化后分别是 `true` / `"x"` / `["a"]`。
50///
51/// 刻意不用 `serde_json::Value`:`docparser-core` 的运行时依赖只有 `serde`,
52/// 为了一个 meta 字段把 JSON 值模型拉进内核,会让「裸内核依赖预算」破功。
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54#[serde(untagged)]
55pub enum MetaValue {
56    /// `truncated` 一类开关
57    Bool(bool),
58    /// `region` 一类单值标注
59    Str(String),
60    /// `merges` 一类字符串列表(xlsx 合并区域 `A1:B2`)
61    List(Vec<String>),
62}
63
64impl From<bool> for MetaValue {
65    fn from(v: bool) -> Self {
66        MetaValue::Bool(v)
67    }
68}
69
70impl From<String> for MetaValue {
71    fn from(v: String) -> Self {
72        MetaValue::Str(v)
73    }
74}
75
76impl From<&str> for MetaValue {
77    fn from(v: &str) -> Self {
78        MetaValue::Str(v.to_string())
79    }
80}
81
82impl From<Vec<String>> for MetaValue {
83    fn from(v: Vec<String>) -> Self {
84        MetaValue::List(v)
85    }
86}
87
88#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
89pub struct Block {
90    #[serde(rename = "kind")]
91    pub kind: BlockKind,
92    #[serde(default)]
93    pub content: String,
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub bbox: Option<Rect>,
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub level: Option<u8>,
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub ordered: Option<bool>,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub lang: Option<String>,
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub table: Option<TableData>,
104    /// 块级附加信息(对齐 Python `Block.meta`):目前 xlsx 表格用
105    /// `merges`(合并区域列表)+ `truncated`(是否被预算截断)。
106    /// 空表不序列化,避免给所有格式的 JSON 加噪音。
107    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
108    pub meta: BTreeMap<String, MetaValue>,
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
112pub struct Image {
113    #[serde(default, skip_serializing_if = "Vec::is_empty")]
114    pub data: Vec<u8>,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub bbox: Option<Rect>,
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub dedup_key: Option<String>,
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub ocr_text: Option<String>,
121}
122
123#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
124pub struct Page {
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub name: Option<String>,
127    #[serde(default)]
128    pub blocks: Vec<Block>,
129    #[serde(default)]
130    pub text: String,
131    #[serde(default)]
132    pub images: Vec<Image>,
133    /// 页面几何(有物理页概念的格式填写)
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub width: Option<f32>,
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub height: Option<f32>,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub rotation: Option<i32>,
140}
141
142#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
143pub struct DocMeta {
144    pub file_type: String,
145    pub page_count: usize,
146    pub text_length: usize,
147    pub image_count: usize,
148    pub image_text_length: usize,
149    /// 插件级附加信息:书籍元数据(书名/作者/语言/封面路径)、处理链告警等。
150    /// 空时不进 JSON。与 Python 参考实现的 `meta.extra` 对齐。
151    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
152    pub extra: std::collections::BTreeMap<String, String>,
153}
154
155#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
156pub struct Document {
157    pub meta: DocMeta,
158    #[serde(default)]
159    pub pages: Vec<Page>,
160}
161
162impl Document {
163    /// 回填统计(插件组装完 pages 后调用)。
164    pub fn finalize(mut self) -> Self {
165        self.meta.page_count = self.pages.len();
166        self.meta.text_length = self.pages.iter().map(|p| p.text.chars().count()).sum();
167        self.meta.image_count = self.pages.iter().map(|p| p.images.len()).sum();
168        self.meta.image_text_length = self
169            .pages
170            .iter()
171            .flat_map(|p| &p.images)
172            .map(|im| {
173                im.ocr_text
174                    .as_deref()
175                    .map(|s| s.chars().count())
176                    .unwrap_or(0)
177            })
178            .sum();
179        self
180    }
181}
182
183/// 渲染块序列为页文本(表格展开全部行,逗号分列——与 Python `blocks_to_text` 一致)。
184pub fn blocks_to_text(blocks: &[Block], sep: &str) -> String {
185    let parts: Vec<String> = blocks
186        .iter()
187        .filter_map(|b| match b.kind {
188            BlockKind::Table => b.table.as_ref().map(|t| {
189                t.rows
190                    .iter()
191                    .map(|r| r.join(","))
192                    .collect::<Vec<_>>()
193                    .join("\n")
194            }),
195            _ if !b.content.is_empty() => Some(b.content.clone()),
196            _ => None,
197        })
198        .collect();
199    parts.join(sep)
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn document_finalize_counts() {
208        let doc = Document {
209            pages: vec![
210                Page {
211                    text: "你好".into(),
212                    images: vec![Image {
213                        ocr_text: Some("x".into()),
214                        ..Default::default()
215                    }],
216                    ..Default::default()
217                },
218                Page {
219                    text: "world".into(),
220                    ..Default::default()
221                },
222            ],
223            ..Default::default()
224        }
225        .finalize();
226        assert_eq!(doc.meta.page_count, 2);
227        assert_eq!(doc.meta.text_length, 7); // "你好world"
228        assert_eq!(doc.meta.image_count, 1);
229        assert_eq!(doc.meta.image_text_length, 1);
230    }
231
232    #[test]
233    fn blocks_to_text_expands_tables() {
234        let b = Block {
235            kind: BlockKind::Table,
236            table: Some(TableData {
237                rows: vec![vec!["a".into(), "b".into()], vec!["1".into(), "2".into()]],
238                ..Default::default()
239            }),
240            ..Default::default()
241        };
242        let txt = blocks_to_text(&[b], "\n\n");
243        assert_eq!(txt, "a,b\n1,2");
244    }
245
246    /// `Block.meta` 的 JSON 形态必须与 Python `Block.meta` 一致:
247    /// bool / 字符串 / 字符串数组都是裸值,不加 `{type, value}` 包装。
248    #[test]
249    fn block_meta_serializes_like_python() {
250        let mut meta = BTreeMap::new();
251        meta.insert("merges".to_string(), vec!["A1:B1".to_string()].into());
252        meta.insert("truncated".to_string(), false.into());
253        meta.insert("region".to_string(), "notes".into());
254        let block = Block {
255            kind: BlockKind::Table,
256            meta,
257            ..Default::default()
258        };
259
260        let json = serde_json::to_string(&block).unwrap();
261        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
262        assert_eq!(v["meta"]["merges"], serde_json::json!(["A1:B1"]));
263        assert_eq!(v["meta"]["truncated"], serde_json::json!(false));
264        assert_eq!(v["meta"]["region"], serde_json::json!("notes"));
265
266        let back: Block = serde_json::from_str(&json).unwrap();
267        assert_eq!(back, block);
268    }
269
270    /// 空 meta 不序列化:不能给所有格式的 JSON 平白加一个 `"meta": {}`。
271    #[test]
272    fn empty_meta_is_omitted() {
273        let json = serde_json::to_string(&Block::default()).unwrap();
274        assert!(!json.contains("meta"), "{json}");
275    }
276
277    /// `BlockKind` 的线上取值必须与 Python `BlockKind` 枚举逐字一致
278    /// (Python 侧有同名单测遍历枚举值域,两边必须同时绿)。
279    #[test]
280    fn block_kind_wire_values_match_python() {
281        let cases = [
282            (BlockKind::Heading, "heading"),
283            (BlockKind::Paragraph, "paragraph"),
284            (BlockKind::List, "list"),
285            (BlockKind::Code, "code"),
286            (BlockKind::Table, "table"),
287            (BlockKind::Image, "image"),
288            (BlockKind::Quote, "quote"),
289            (BlockKind::Chart, "chart"),
290            (BlockKind::Note, "note"),
291        ];
292        for (kind, wire) in cases {
293            let json = serde_json::to_string(&kind).unwrap();
294            assert_eq!(json, format!("\"{wire}\""), "{kind:?}");
295            assert_eq!(serde_json::from_str::<BlockKind>(&json).unwrap(), kind);
296        }
297    }
298
299    #[test]
300    fn roundtrip_json() {
301        let doc = Document {
302            pages: vec![Page {
303                text: "t".into(),
304                blocks: vec![Block {
305                    kind: BlockKind::Paragraph,
306                    content: "hi".into(),
307                    ..Default::default()
308                }],
309                ..Default::default()
310            }],
311            ..Default::default()
312        };
313        let s = serde_json::to_string(&doc).unwrap();
314        let back: Document = serde_json::from_str(&s).unwrap();
315        assert_eq!(back, doc);
316    }
317}