Skip to main content

docparser_core/
model.rs

1//! 统一 IR(与 Python `model.py` 语义一致,含 serde 便于 JSON/CLI 往返)。
2
3use serde::{Deserialize, Serialize};
4
5/// 轴对齐矩形,坐标语义由产生方决定(页面 px / pt / 逻辑单位)。
6#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
7pub struct Rect {
8    pub x0: f32,
9    pub y0: f32,
10    pub x1: f32,
11    pub y1: f32,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum BlockKind {
17    Heading,
18    #[default]
19    Paragraph,
20    List,
21    Code,
22    Table,
23    Image,
24    Quote,
25}
26
27/// 结构化表格:rows[row][col] = 单元格文本。
28#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
29pub struct TableData {
30    pub rows: Vec<Vec<String>>,
31}
32
33#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
34pub struct Block {
35    #[serde(rename = "kind")]
36    pub kind: BlockKind,
37    #[serde(default)]
38    pub content: String,
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub bbox: Option<Rect>,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub level: Option<u8>,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub ordered: Option<bool>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub lang: Option<String>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub table: Option<TableData>,
49}
50
51#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
52pub struct Image {
53    #[serde(default, skip_serializing_if = "Vec::is_empty")]
54    pub data: Vec<u8>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub bbox: Option<Rect>,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub dedup_key: Option<String>,
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub ocr_text: Option<String>,
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
64pub struct Page {
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub name: Option<String>,
67    #[serde(default)]
68    pub blocks: Vec<Block>,
69    #[serde(default)]
70    pub text: String,
71    #[serde(default)]
72    pub images: Vec<Image>,
73    /// 页面几何(有物理页概念的格式填写)
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub width: Option<f32>,
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub height: Option<f32>,
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub rotation: Option<i32>,
80}
81
82#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
83pub struct DocMeta {
84    pub file_type: String,
85    pub page_count: usize,
86    pub text_length: usize,
87    pub image_count: usize,
88    pub image_text_length: usize,
89    /// 插件级附加信息:书籍元数据(书名/作者/语言/封面路径)、处理链告警等。
90    /// 空时不进 JSON。与 Python 参考实现的 `meta.extra` 对齐。
91    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
92    pub extra: std::collections::BTreeMap<String, String>,
93}
94
95#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
96pub struct Document {
97    pub meta: DocMeta,
98    #[serde(default)]
99    pub pages: Vec<Page>,
100}
101
102impl Document {
103    /// 回填统计(插件组装完 pages 后调用)。
104    pub fn finalize(mut self) -> Self {
105        self.meta.page_count = self.pages.len();
106        self.meta.text_length = self.pages.iter().map(|p| p.text.chars().count()).sum();
107        self.meta.image_count = self.pages.iter().map(|p| p.images.len()).sum();
108        self.meta.image_text_length = self
109            .pages
110            .iter()
111            .flat_map(|p| &p.images)
112            .map(|im| {
113                im.ocr_text
114                    .as_deref()
115                    .map(|s| s.chars().count())
116                    .unwrap_or(0)
117            })
118            .sum();
119        self
120    }
121}
122
123/// 渲染块序列为页文本(表格展开全部行,逗号分列——与 Python `blocks_to_text` 一致)。
124pub fn blocks_to_text(blocks: &[Block], sep: &str) -> String {
125    let parts: Vec<String> = blocks
126        .iter()
127        .filter_map(|b| match b.kind {
128            BlockKind::Table => b.table.as_ref().map(|t| {
129                t.rows
130                    .iter()
131                    .map(|r| r.join(","))
132                    .collect::<Vec<_>>()
133                    .join("\n")
134            }),
135            _ if !b.content.is_empty() => Some(b.content.clone()),
136            _ => None,
137        })
138        .collect();
139    parts.join(sep)
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn document_finalize_counts() {
148        let doc = Document {
149            pages: vec![
150                Page {
151                    text: "你好".into(),
152                    images: vec![Image {
153                        ocr_text: Some("x".into()),
154                        ..Default::default()
155                    }],
156                    ..Default::default()
157                },
158                Page {
159                    text: "world".into(),
160                    ..Default::default()
161                },
162            ],
163            ..Default::default()
164        }
165        .finalize();
166        assert_eq!(doc.meta.page_count, 2);
167        assert_eq!(doc.meta.text_length, 7); // "你好world"
168        assert_eq!(doc.meta.image_count, 1);
169        assert_eq!(doc.meta.image_text_length, 1);
170    }
171
172    #[test]
173    fn blocks_to_text_expands_tables() {
174        let b = Block {
175            kind: BlockKind::Table,
176            table: Some(TableData {
177                rows: vec![vec!["a".into(), "b".into()], vec!["1".into(), "2".into()]],
178            }),
179            ..Default::default()
180        };
181        let txt = blocks_to_text(&[b], "\n\n");
182        assert_eq!(txt, "a,b\n1,2");
183    }
184
185    #[test]
186    fn roundtrip_json() {
187        let doc = Document {
188            pages: vec![Page {
189                text: "t".into(),
190                blocks: vec![Block {
191                    kind: BlockKind::Paragraph,
192                    content: "hi".into(),
193                    ..Default::default()
194                }],
195                ..Default::default()
196            }],
197            ..Default::default()
198        };
199        let s = serde_json::to_string(&doc).unwrap();
200        let back: Document = serde_json::from_str(&s).unwrap();
201        assert_eq!(back, doc);
202    }
203}