1#![allow(clippy::too_many_lines)]
2mod ofd_reader;
19mod parser;
20mod read_options;
21
22pub mod bad_ofd_exception;
24pub mod content_extractor;
25pub mod delta_tool;
26pub mod dl_ofd_reader;
27pub mod error_path_exception;
28pub mod extractor;
29pub mod keyword;
30pub mod model;
31pub mod ofd_reader_facade;
32pub mod page_info;
33pub mod resource_locator;
34pub mod resource_manage;
35pub mod seal_ofd_reader;
36pub mod tools;
37pub mod zip_util;
38
39pub use bad_ofd_exception::BadOfdException;
41pub use content_extractor::ContentExtractor;
42#[allow(deprecated)]
43pub use dl_ofd_reader::DlOfdReader;
44pub use error_path_exception::ErrorPathException;
45pub use keyword::{KeywordExtractor, KeywordPosition, KeywordResource, TextCodeEntry};
46#[allow(deprecated)]
47pub use model::{
48 AnnotionEntity, OfdDocumentVo, OfdPageVo, SealDataVo, StampAnnotVo, TemplatePageEntity,
49 TemplateZOrder,
50};
51pub use ofd_reader::OfdReader;
52pub use ofd_reader_facade::OfdReaderFacade;
53pub use page_info::PageInfo;
54pub use read_options::ReadOptions;
55pub use resource_locator::ResourceLocator;
56pub use resource_manage::ResourceManage;
57#[allow(deprecated)]
58pub use seal_ofd_reader::SealOfdReader;
59
60pub type OFDReader = OfdReader;
67
68pub type BadOFDException = BadOfdException;
72
73#[allow(deprecated)]
77pub type DLOFDReader = DlOfdReader;
78
79#[allow(deprecated)]
83pub type SealOFDReader = SealOfdReader;
84
85#[allow(deprecated)]
89pub type OFDDocumentVo = OfdDocumentVo;
90
91#[allow(deprecated)]
95pub type OFDPageVo = OfdPageVo;
96
97#[allow(deprecated)]
102pub type NameSpaceModifier = tools::namespace_modifier::NamespaceModifier;
103
104pub type NameSpaceCleaner = tools::namespace_cleaner::NamespaceCleaner;
109
110pub use zip_util as ZipUtil;
114
115pub use delta_tool as DeltaTool;
119
120pub use tools::image_utils as ImageUtils;
124
125pub use tools::namespace_modifier::OFD_NAMESPACE;
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use easyofd_core::{ImageObject, PathObject, TextObject};
134 use easyofd_writer::OfdWriter;
135
136 use easyofd_core::OfdPage;
137
138 fn roundtrip(pages: Vec<OfdPage>) -> Vec<u8> {
139 let mut writer = OfdWriter::new();
140 for page in pages {
141 writer.add_page(page);
142 }
143 writer.build().unwrap()
144 }
145
146 #[test]
147 fn test_empty_document() {
148 let bytes = OfdWriter::new().build().unwrap();
149 let reader = OfdReader::from_bytes(&bytes).unwrap();
150 assert_eq!(reader.page_count(), 0);
151 }
152
153 #[test]
154 fn test_single_text_page() {
155 let mut page = OfdPage::new(210.0, 297.0);
156 page.add_text(TextObject::new(20.0, 30.0, "Hello OFD Reader!"));
157 let bytes = roundtrip(vec![page]);
158
159 let reader = OfdReader::from_bytes(&bytes).unwrap();
160 assert_eq!(reader.page_count(), 1);
161 assert_eq!(reader.pages()[0].content.len(), 1);
162
163 let text = reader.extract_text();
164 assert_eq!(text.len(), 1);
165 assert!(text[0].contains("Hello OFD Reader!"));
166 }
167
168 #[test]
169 fn test_multiple_pages() {
170 let mut pages = Vec::new();
171 for i in 1..=3 {
172 let mut page = OfdPage::new(210.0, 297.0);
173 page.add_text(TextObject::new(10.0, 20.0, format!("Page {i} text")));
174 pages.push(page);
175 }
176 let bytes = roundtrip(pages);
177
178 let reader = OfdReader::from_bytes(&bytes).unwrap();
179 assert_eq!(reader.page_count(), 3);
180 let text = reader.extract_text();
181 assert_eq!(text.len(), 3);
182 assert!(text[0].contains("Page 1"));
183 assert!(text[2].contains("Page 3"));
184 }
185
186 #[test]
187 fn test_extract_all_text() {
188 let mut p1 = OfdPage::new(210.0, 297.0);
189 p1.add_text(TextObject::new(10.0, 20.0, "First"));
190 let mut p2 = OfdPage::new(210.0, 297.0);
191 p2.add_text(TextObject::new(10.0, 20.0, "Second"));
192 let bytes = roundtrip(vec![p1, p2]);
193
194 let reader = OfdReader::from_bytes(&bytes).unwrap();
195 let all = reader.extract_all_text();
196 assert!(all.contains("First"));
197 assert!(all.contains("Second"));
198 assert!(all.contains("---"));
199 }
200
201 #[test]
202 fn test_text_and_image() {
203 let mut page = OfdPage::new(210.0, 297.0);
204 page.add_text(TextObject::new(20.0, 30.0, "Invoice"));
205 page.add_image(ImageObject::jpeg(150.0, 30.0, 30.0, 30.0, vec![0xFF, 0xD8]));
206 let bytes = roundtrip(vec![page]);
207
208 let reader = OfdReader::from_bytes(&bytes).unwrap();
209 assert_eq!(reader.pages()[0].content.len(), 2);
210 let easyofd_core::ContentObject::Image(image) = &reader.pages()[0].content[1] else {
211 panic!("expected image");
212 };
213 assert_eq!(image.data, vec![0xFF, 0xD8]);
214 }
215
216 #[test]
217 fn test_visit_selected_pages_without_collecting() {
218 let mut pages = Vec::new();
219 for number in 1..=4 {
220 let mut page = OfdPage::new(210.0, 297.0);
221 page.add_text(TextObject::new(10.0, 10.0, format!("page {number}")));
222 pages.push(page);
223 }
224 let bytes = roundtrip(pages);
225 let path = std::env::temp_dir().join("easyofd_visit_pages.ofd");
226 std::fs::write(&path, bytes).unwrap();
227 let mut visited = Vec::new();
228 let count = OfdReader::visit_path(
229 &path,
230 ReadOptions {
231 first_page: Some(2),
232 last_page: Some(3),
233 ..ReadOptions::default()
234 },
235 |number, page| {
236 visited.push((number, ofd_reader::page_text(&page)));
237 Ok(())
238 },
239 )
240 .unwrap();
241 assert_eq!(count, 2);
242 assert_eq!(visited[0], (2, "page 2".to_string()));
243 assert_eq!(visited[1], (3, "page 3".to_string()));
244 let _ = std::fs::remove_file(path);
245 }
246
247 #[test]
248 fn test_path_roundtrip_is_not_silently_lost() {
249 let mut page = OfdPage::new(210.0, 297.0);
250 page.add_path(PathObject::hline(10.0, 20.0, 50.0));
251 let bytes = roundtrip(vec![page]);
252 let reader = OfdReader::from_bytes(&bytes).unwrap();
253 assert!(matches!(
254 reader.pages()[0].content[0],
255 easyofd_core::ContentObject::Path(_)
256 ));
257 }
258
259 #[test]
260 fn test_from_file() {
261 let dir = std::env::temp_dir().join("easyofd_reader");
262 std::fs::create_dir_all(&dir).unwrap();
263 let path = dir.join("test.ofd");
264
265 let mut page = OfdPage::new(210.0, 297.0);
266 page.add_text(TextObject::new(10.0, 20.0, "File test"));
267 let mut w = OfdWriter::new();
268 w.add_page(page);
269 w.build_to_file(&path).unwrap();
270
271 let reader = OfdReader::open(&path).unwrap();
272 assert_eq!(reader.page_count(), 1);
273 assert!(reader.extract_all_text().contains("File test"));
274 let _ = std::fs::remove_file(&path);
275 }
276
277 #[test]
278 fn test_invalid_data() {
279 assert!(OfdReader::from_bytes(b"not a zip file").is_err());
280 }
281
282 #[test]
283 fn test_styled_text() {
284 let mut page = OfdPage::new(210.0, 297.0);
285 page.add_text(
286 TextObject::new(10.0, 20.0, "Styled")
287 .font("SimHei")
288 .size(18.0)
289 .bold(),
290 );
291 let bytes = roundtrip(vec![page]);
292 let reader = OfdReader::from_bytes(&bytes).unwrap();
293 assert_eq!(reader.page_count(), 1);
294 assert!(reader.extract_all_text().contains("Styled"));
295 }
296
297 fn build_zip_with_document_res(document_res_xml: &str) -> Vec<u8> {
302 use std::io::Write;
303
304 let mut buf = std::io::Cursor::new(Vec::new());
305 {
306 let mut zip = zip::ZipWriter::new(&mut buf);
307 let options = zip::write::SimpleFileOptions::default()
308 .compression_method(zip::CompressionMethod::Stored);
309
310 zip.start_file("OFD.xml", options).unwrap();
312 zip.write_all(
313 br#"<?xml version="1.0" encoding="UTF-8"?>
314<ofd:OFD xmlns:ofd="http://www.ofdspec.org/2016">
315 <ofd:DocBody><ofd:DocRoot>Doc_0/Document.xml</ofd:DocRoot></ofd:DocBody>
316</ofd:OFD>"#,
317 )
318 .unwrap();
319
320 zip.start_file("Doc_0/Document.xml", options).unwrap();
322 zip.write_all(
323 br#"<?xml version="1.0" encoding="UTF-8"?>
324<ofd:Document xmlns:ofd="http://www.ofdspec.org/2016">
325 <ofd:CommonData>
326 <ofd:PageArea><ofd:PhysicalBox>0 0 210 297</ofd:PhysicalBox></ofd:PageArea>
327 <ofd:DocumentRes>DocumentRes.xml</ofd:DocumentRes>
328 </ofd:CommonData>
329 <ofd:Pages><ofd:Page BaseLoc="Pages/Page_0/Content.xml"/></ofd:Pages>
330</ofd:Document>"#,
331 )
332 .unwrap();
333
334 zip.start_file("Doc_0/Pages/Page_0/Content.xml", options)
336 .unwrap();
337 zip.write_all(
338 br#"<?xml version="1.0" encoding="UTF-8"?>
339<ofd:Page xmlns:ofd="http://www.ofdspec.org/2016">
340 <ofd:Content/>
341</ofd:Page>"#,
342 )
343 .unwrap();
344
345 zip.start_file("Doc_0/DocumentRes.xml", options).unwrap();
347 zip.write_all(document_res_xml.as_bytes()).unwrap();
348
349 zip.finish().unwrap();
350 }
351 buf.into_inner()
352 }
353
354 fn parse_resources_from_zip(bytes: &[u8]) -> std::collections::HashMap<String, String> {
357 use parser::parse_document_resources;
358
359 let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
360 let resources =
361 parse_document_resources(&mut archive, "Doc_0", Some("DocumentRes.xml")).unwrap();
362 resources
363 .into_iter()
364 .map(|(id, entry)| (id, entry.location))
365 .collect()
366 }
367
368 #[test]
373 fn parse_document_resources_respects_base_loc() {
374 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
375<ofd:Res xmlns:ofd="http://www.ofdspec.org/2016" BaseLoc="Res">
376 <ofd:MultiMedias>
377 <ofd:MultiMedia ID="6" Type="Image" Format="PNG">
378 <ofd:MediaFile>qrcode.png</ofd:MediaFile>
379 </ofd:MultiMedia>
380 </ofd:MultiMedias>
381</ofd:Res>"#;
382 let bytes = build_zip_with_document_res(xml);
383 let resources = parse_resources_from_zip(&bytes);
384
385 assert_eq!(
386 resources.get("6").map(String::as_str),
387 Some("Res/qrcode.png"),
388 "MediaFile path must include BaseLoc prefix"
389 );
390 }
391
392 #[test]
395 fn parse_document_resources_no_base_loc_uses_default() {
396 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
397<ofd:Res xmlns:ofd="http://www.ofdspec.org/2016">
398 <ofd:MultiMedias>
399 <ofd:MultiMedia ID="10" Type="Image" Format="JPEG">
400 <ofd:MediaFile>photo.jpg</ofd:MediaFile>
401 </ofd:MultiMedia>
402 </ofd:MultiMedias>
403</ofd:Res>"#;
404 let bytes = build_zip_with_document_res(xml);
405 let resources = parse_resources_from_zip(&bytes);
406
407 assert_eq!(
408 resources.get("10").map(String::as_str),
409 Some("photo.jpg"),
410 "without BaseLoc, raw MediaFile path is kept"
411 );
412 }
413
414 #[test]
418 fn parse_document_resources_nested_res() {
419 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
420<ofd:Res xmlns:ofd="http://www.ofdspec.org/2016" BaseLoc="Outer">
421 <ofd:MultiMedias>
422 <ofd:MultiMedia ID="1" Type="Image">
423 <ofd:MediaFile>outer.png</ofd:MediaFile>
424 </ofd:MultiMedia>
425 </ofd:MultiMedias>
426 <ofd:Res BaseLoc="Inner">
427 <ofd:MultiMedias>
428 <ofd:MultiMedia ID="2" Type="Image">
429 <ofd:MediaFile>inner.png</ofd:MediaFile>
430 </ofd:MultiMedia>
431 </ofd:MultiMedias>
432 </ofd:Res>
433 <ofd:MultiMedias>
434 <ofd:MultiMedia ID="3" Type="Image">
435 <ofd:MediaFile>back_outer.png</ofd:MediaFile>
436 </ofd:MultiMedia>
437 </ofd:MultiMedias>
438</ofd:Res>"#;
439 let bytes = build_zip_with_document_res(xml);
440 let resources = parse_resources_from_zip(&bytes);
441
442 assert_eq!(
443 resources.get("1").map(String::as_str),
444 Some("Outer/outer.png"),
445 "outer resource uses outer BaseLoc"
446 );
447 assert_eq!(
448 resources.get("2").map(String::as_str),
449 Some("Inner/inner.png"),
450 "inner resource uses inner BaseLoc"
451 );
452 assert_eq!(
453 resources.get("3").map(String::as_str),
454 Some("Outer/back_outer.png"),
455 "resource after inner Res closes uses outer BaseLoc again"
456 );
457 }
458
459 #[test]
462 fn test_ofd_reader_alias() {
463 let bytes = OfdWriter::new().build().unwrap();
464 let reader = OFDReader::from_bytes(&bytes).unwrap();
466 assert_eq!(reader.page_count(), 0);
467 }
468
469 #[test]
470 fn test_bad_ofd_exception_alias() {
471 let e = BadOFDException::corrupted("test alias");
472 assert!(e.to_string().contains("test alias"));
473 }
474
475 #[test]
476 #[allow(deprecated)]
477 fn test_dl_ofd_reader_alias() {
478 let bytes = OfdWriter::new().build().unwrap();
479 let reader = DLOFDReader::from_bytes(&bytes).unwrap();
480 assert_eq!(reader.page_count(), 0);
481 }
482
483 #[test]
484 #[allow(deprecated)]
485 fn test_seal_ofd_reader_alias() {
486 let bytes = OfdWriter::new().build().unwrap();
487 let reader = SealOFDReader::from_bytes(&bytes).unwrap();
488 assert_eq!(reader.page_count(), 0);
489 }
490
491 #[test]
492 #[allow(deprecated)]
493 fn test_ofd_document_vo_alias() {
494 let vo = OFDDocumentVo::new("Doc_0", 210.0, 297.0, vec![]);
495 assert_eq!(vo.doc_path, "Doc_0");
496 }
497
498 #[test]
499 fn test_namespace_modifier_alias() {
500 #[allow(deprecated)]
501 let modifier = NameSpaceModifier::new();
502 assert_eq!(modifier.expected_namespace(), OFD_NAMESPACE);
503 }
504
505 #[test]
506 fn test_namespace_cleaner_alias() {
507 let xml = "<ofd:OFD><ofd:DocBody/></ofd:OFD>";
508 let result = NameSpaceCleaner::remove_ofd_prefix(xml);
509 assert!(!result.contains("ofd:"));
510 }
511
512 #[test]
513 fn test_zip_util_module_alias() {
514 let data = {
516 let mut buf = std::io::Cursor::new(Vec::new());
517 {
518 let mut zip = zip::ZipWriter::new(&mut buf);
519 let options = zip::write::SimpleFileOptions::default()
520 .compression_method(zip::CompressionMethod::Stored);
521 zip.start_file("test.txt", options).unwrap();
522 std::io::Write::write_all(&mut zip, b"hello").unwrap();
523 zip.finish().unwrap();
524 }
525 buf.into_inner()
526 };
527 let reader = std::io::Cursor::new(&data);
528 let mut archive = zip::ZipArchive::new(reader).unwrap();
529 let entries = ZipUtil::list_entries(&mut archive).unwrap();
530 assert_eq!(entries.len(), 1);
531 }
532
533 #[test]
534 fn test_delta_tool_module_alias() {
535 let delta = vec!["1.0".into(), "2.0".into()];
536 let result = DeltaTool::get_delta(Some(&delta), 2);
537 assert_eq!(result.len(), 2);
538 }
539
540 #[test]
541 fn test_image_utils_module_alias() {
542 assert_eq!(
543 ImageUtils::detect_format(&[0x89, 0x50, 0x4E, 0x47]),
544 ImageUtils::ImageFormat::Png
545 );
546 }
547
548 #[test]
549 fn test_ofd_namespace_constant() {
550 assert_eq!(OFD_NAMESPACE, "http://www.ofdspec.org/2016");
551 }
552}