1use serde::{Deserialize, Serialize};
4
5#[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#[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 #[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 #[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 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
123pub 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); 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}