1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7#[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 Chart,
29 Note,
32}
33
34#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
36pub struct TableData {
37 pub rows: Vec<Vec<String>>,
38 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54#[serde(untagged)]
55pub enum MetaValue {
56 Bool(bool),
58 Str(String),
60 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 #[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 #[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 #[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 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
183pub 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); 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 #[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 #[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 #[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}