#![allow(clippy::too_many_lines)]
mod ofd_reader;
mod parser;
mod read_options;
pub mod bad_ofd_exception;
pub mod content_extractor;
pub mod delta_tool;
pub mod dl_ofd_reader;
pub mod error_path_exception;
pub mod extractor;
pub mod keyword;
pub mod model;
pub mod ofd_reader_facade;
pub mod page_info;
pub mod resource_locator;
pub mod resource_manage;
pub mod seal_ofd_reader;
pub mod tools;
pub mod zip_util;
pub use bad_ofd_exception::BadOfdException;
pub use content_extractor::ContentExtractor;
#[allow(deprecated)]
pub use dl_ofd_reader::DlOfdReader;
pub use error_path_exception::ErrorPathException;
pub use keyword::{KeywordExtractor, KeywordPosition, KeywordResource, TextCodeEntry};
#[allow(deprecated)]
pub use model::{
AnnotionEntity, OfdDocumentVo, OfdPageVo, SealDataVo, StampAnnotVo, TemplatePageEntity,
TemplateZOrder,
};
pub use ofd_reader::OfdReader;
pub use ofd_reader_facade::OfdReaderFacade;
pub use page_info::PageInfo;
pub use read_options::ReadOptions;
pub use resource_locator::ResourceLocator;
pub use resource_manage::ResourceManage;
#[allow(deprecated)]
pub use seal_ofd_reader::SealOfdReader;
pub type OFDReader = OfdReader;
pub type BadOFDException = BadOfdException;
#[allow(deprecated)]
pub type DLOFDReader = DlOfdReader;
#[allow(deprecated)]
pub type SealOFDReader = SealOfdReader;
#[allow(deprecated)]
pub type OFDDocumentVo = OfdDocumentVo;
#[allow(deprecated)]
pub type OFDPageVo = OfdPageVo;
#[allow(deprecated)]
pub type NameSpaceModifier = tools::namespace_modifier::NamespaceModifier;
pub type NameSpaceCleaner = tools::namespace_cleaner::NamespaceCleaner;
pub use zip_util as ZipUtil;
pub use delta_tool as DeltaTool;
pub use tools::image_utils as ImageUtils;
pub use tools::namespace_modifier::OFD_NAMESPACE;
#[cfg(test)]
mod tests {
use super::*;
use easyofd_core::{ImageObject, PathObject, TextObject};
use easyofd_writer::OfdWriter;
use easyofd_core::OfdPage;
fn roundtrip(pages: Vec<OfdPage>) -> Vec<u8> {
let mut writer = OfdWriter::new();
for page in pages {
writer.add_page(page);
}
writer.build().unwrap()
}
#[test]
fn test_empty_document() {
let bytes = OfdWriter::new().build().unwrap();
let reader = OfdReader::from_bytes(&bytes).unwrap();
assert_eq!(reader.page_count(), 0);
}
#[test]
fn test_single_text_page() {
let mut page = OfdPage::new(210.0, 297.0);
page.add_text(TextObject::new(20.0, 30.0, "Hello OFD Reader!"));
let bytes = roundtrip(vec![page]);
let reader = OfdReader::from_bytes(&bytes).unwrap();
assert_eq!(reader.page_count(), 1);
assert_eq!(reader.pages()[0].content.len(), 1);
let text = reader.extract_text();
assert_eq!(text.len(), 1);
assert!(text[0].contains("Hello OFD Reader!"));
}
#[test]
fn test_multiple_pages() {
let mut pages = Vec::new();
for i in 1..=3 {
let mut page = OfdPage::new(210.0, 297.0);
page.add_text(TextObject::new(10.0, 20.0, format!("Page {i} text")));
pages.push(page);
}
let bytes = roundtrip(pages);
let reader = OfdReader::from_bytes(&bytes).unwrap();
assert_eq!(reader.page_count(), 3);
let text = reader.extract_text();
assert_eq!(text.len(), 3);
assert!(text[0].contains("Page 1"));
assert!(text[2].contains("Page 3"));
}
#[test]
fn test_extract_all_text() {
let mut p1 = OfdPage::new(210.0, 297.0);
p1.add_text(TextObject::new(10.0, 20.0, "First"));
let mut p2 = OfdPage::new(210.0, 297.0);
p2.add_text(TextObject::new(10.0, 20.0, "Second"));
let bytes = roundtrip(vec![p1, p2]);
let reader = OfdReader::from_bytes(&bytes).unwrap();
let all = reader.extract_all_text();
assert!(all.contains("First"));
assert!(all.contains("Second"));
assert!(all.contains("---"));
}
#[test]
fn test_text_and_image() {
let mut page = OfdPage::new(210.0, 297.0);
page.add_text(TextObject::new(20.0, 30.0, "Invoice"));
page.add_image(ImageObject::jpeg(150.0, 30.0, 30.0, 30.0, vec![0xFF, 0xD8]));
let bytes = roundtrip(vec![page]);
let reader = OfdReader::from_bytes(&bytes).unwrap();
assert_eq!(reader.pages()[0].content.len(), 2);
let easyofd_core::ContentObject::Image(image) = &reader.pages()[0].content[1] else {
panic!("expected image");
};
assert_eq!(image.data, vec![0xFF, 0xD8]);
}
#[test]
fn test_visit_selected_pages_without_collecting() {
let mut pages = Vec::new();
for number in 1..=4 {
let mut page = OfdPage::new(210.0, 297.0);
page.add_text(TextObject::new(10.0, 10.0, format!("page {number}")));
pages.push(page);
}
let bytes = roundtrip(pages);
let path = std::env::temp_dir().join("easyofd_visit_pages.ofd");
std::fs::write(&path, bytes).unwrap();
let mut visited = Vec::new();
let count = OfdReader::visit_path(
&path,
ReadOptions {
first_page: Some(2),
last_page: Some(3),
..ReadOptions::default()
},
|number, page| {
visited.push((number, ofd_reader::page_text(&page)));
Ok(())
},
)
.unwrap();
assert_eq!(count, 2);
assert_eq!(visited[0], (2, "page 2".to_string()));
assert_eq!(visited[1], (3, "page 3".to_string()));
let _ = std::fs::remove_file(path);
}
#[test]
fn test_path_roundtrip_is_not_silently_lost() {
let mut page = OfdPage::new(210.0, 297.0);
page.add_path(PathObject::hline(10.0, 20.0, 50.0));
let bytes = roundtrip(vec![page]);
let reader = OfdReader::from_bytes(&bytes).unwrap();
assert!(matches!(
reader.pages()[0].content[0],
easyofd_core::ContentObject::Path(_)
));
}
#[test]
fn test_from_file() {
let dir = std::env::temp_dir().join("easyofd_reader");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("test.ofd");
let mut page = OfdPage::new(210.0, 297.0);
page.add_text(TextObject::new(10.0, 20.0, "File test"));
let mut w = OfdWriter::new();
w.add_page(page);
w.build_to_file(&path).unwrap();
let reader = OfdReader::open(&path).unwrap();
assert_eq!(reader.page_count(), 1);
assert!(reader.extract_all_text().contains("File test"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_invalid_data() {
assert!(OfdReader::from_bytes(b"not a zip file").is_err());
}
#[test]
fn test_styled_text() {
let mut page = OfdPage::new(210.0, 297.0);
page.add_text(
TextObject::new(10.0, 20.0, "Styled")
.font("SimHei")
.size(18.0)
.bold(),
);
let bytes = roundtrip(vec![page]);
let reader = OfdReader::from_bytes(&bytes).unwrap();
assert_eq!(reader.page_count(), 1);
assert!(reader.extract_all_text().contains("Styled"));
}
fn build_zip_with_document_res(document_res_xml: &str) -> Vec<u8> {
use std::io::Write;
let mut buf = std::io::Cursor::new(Vec::new());
{
let mut zip = zip::ZipWriter::new(&mut buf);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
zip.start_file("OFD.xml", options).unwrap();
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<ofd:OFD xmlns:ofd="http://www.ofdspec.org/2016">
<ofd:DocBody><ofd:DocRoot>Doc_0/Document.xml</ofd:DocRoot></ofd:DocBody>
</ofd:OFD>"#,
)
.unwrap();
zip.start_file("Doc_0/Document.xml", options).unwrap();
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<ofd:Document xmlns:ofd="http://www.ofdspec.org/2016">
<ofd:CommonData>
<ofd:PageArea><ofd:PhysicalBox>0 0 210 297</ofd:PhysicalBox></ofd:PageArea>
<ofd:DocumentRes>DocumentRes.xml</ofd:DocumentRes>
</ofd:CommonData>
<ofd:Pages><ofd:Page BaseLoc="Pages/Page_0/Content.xml"/></ofd:Pages>
</ofd:Document>"#,
)
.unwrap();
zip.start_file("Doc_0/Pages/Page_0/Content.xml", options)
.unwrap();
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<ofd:Page xmlns:ofd="http://www.ofdspec.org/2016">
<ofd:Content/>
</ofd:Page>"#,
)
.unwrap();
zip.start_file("Doc_0/DocumentRes.xml", options).unwrap();
zip.write_all(document_res_xml.as_bytes()).unwrap();
zip.finish().unwrap();
}
buf.into_inner()
}
fn parse_resources_from_zip(bytes: &[u8]) -> std::collections::HashMap<String, String> {
use parser::parse_document_resources;
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
let resources =
parse_document_resources(&mut archive, "Doc_0", Some("DocumentRes.xml")).unwrap();
resources
.into_iter()
.map(|(id, entry)| (id, entry.location))
.collect()
}
#[test]
fn parse_document_resources_respects_base_loc() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<ofd:Res xmlns:ofd="http://www.ofdspec.org/2016" BaseLoc="Res">
<ofd:MultiMedias>
<ofd:MultiMedia ID="6" Type="Image" Format="PNG">
<ofd:MediaFile>qrcode.png</ofd:MediaFile>
</ofd:MultiMedia>
</ofd:MultiMedias>
</ofd:Res>"#;
let bytes = build_zip_with_document_res(xml);
let resources = parse_resources_from_zip(&bytes);
assert_eq!(
resources.get("6").map(String::as_str),
Some("Res/qrcode.png"),
"MediaFile path must include BaseLoc prefix"
);
}
#[test]
fn parse_document_resources_no_base_loc_uses_default() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<ofd:Res xmlns:ofd="http://www.ofdspec.org/2016">
<ofd:MultiMedias>
<ofd:MultiMedia ID="10" Type="Image" Format="JPEG">
<ofd:MediaFile>photo.jpg</ofd:MediaFile>
</ofd:MultiMedia>
</ofd:MultiMedias>
</ofd:Res>"#;
let bytes = build_zip_with_document_res(xml);
let resources = parse_resources_from_zip(&bytes);
assert_eq!(
resources.get("10").map(String::as_str),
Some("photo.jpg"),
"without BaseLoc, raw MediaFile path is kept"
);
}
#[test]
fn parse_document_resources_nested_res() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<ofd:Res xmlns:ofd="http://www.ofdspec.org/2016" BaseLoc="Outer">
<ofd:MultiMedias>
<ofd:MultiMedia ID="1" Type="Image">
<ofd:MediaFile>outer.png</ofd:MediaFile>
</ofd:MultiMedia>
</ofd:MultiMedias>
<ofd:Res BaseLoc="Inner">
<ofd:MultiMedias>
<ofd:MultiMedia ID="2" Type="Image">
<ofd:MediaFile>inner.png</ofd:MediaFile>
</ofd:MultiMedia>
</ofd:MultiMedias>
</ofd:Res>
<ofd:MultiMedias>
<ofd:MultiMedia ID="3" Type="Image">
<ofd:MediaFile>back_outer.png</ofd:MediaFile>
</ofd:MultiMedia>
</ofd:MultiMedias>
</ofd:Res>"#;
let bytes = build_zip_with_document_res(xml);
let resources = parse_resources_from_zip(&bytes);
assert_eq!(
resources.get("1").map(String::as_str),
Some("Outer/outer.png"),
"outer resource uses outer BaseLoc"
);
assert_eq!(
resources.get("2").map(String::as_str),
Some("Inner/inner.png"),
"inner resource uses inner BaseLoc"
);
assert_eq!(
resources.get("3").map(String::as_str),
Some("Outer/back_outer.png"),
"resource after inner Res closes uses outer BaseLoc again"
);
}
#[test]
fn test_ofd_reader_alias() {
let bytes = OfdWriter::new().build().unwrap();
let reader = OFDReader::from_bytes(&bytes).unwrap();
assert_eq!(reader.page_count(), 0);
}
#[test]
fn test_bad_ofd_exception_alias() {
let e = BadOFDException::corrupted("test alias");
assert!(e.to_string().contains("test alias"));
}
#[test]
#[allow(deprecated)]
fn test_dl_ofd_reader_alias() {
let bytes = OfdWriter::new().build().unwrap();
let reader = DLOFDReader::from_bytes(&bytes).unwrap();
assert_eq!(reader.page_count(), 0);
}
#[test]
#[allow(deprecated)]
fn test_seal_ofd_reader_alias() {
let bytes = OfdWriter::new().build().unwrap();
let reader = SealOFDReader::from_bytes(&bytes).unwrap();
assert_eq!(reader.page_count(), 0);
}
#[test]
#[allow(deprecated)]
fn test_ofd_document_vo_alias() {
let vo = OFDDocumentVo::new("Doc_0", 210.0, 297.0, vec![]);
assert_eq!(vo.doc_path, "Doc_0");
}
#[test]
fn test_namespace_modifier_alias() {
#[allow(deprecated)]
let modifier = NameSpaceModifier::new();
assert_eq!(modifier.expected_namespace(), OFD_NAMESPACE);
}
#[test]
fn test_namespace_cleaner_alias() {
let xml = "<ofd:OFD><ofd:DocBody/></ofd:OFD>";
let result = NameSpaceCleaner::remove_ofd_prefix(xml);
assert!(!result.contains("ofd:"));
}
#[test]
fn test_zip_util_module_alias() {
let data = {
let mut buf = std::io::Cursor::new(Vec::new());
{
let mut zip = zip::ZipWriter::new(&mut buf);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
zip.start_file("test.txt", options).unwrap();
std::io::Write::write_all(&mut zip, b"hello").unwrap();
zip.finish().unwrap();
}
buf.into_inner()
};
let reader = std::io::Cursor::new(&data);
let mut archive = zip::ZipArchive::new(reader).unwrap();
let entries = ZipUtil::list_entries(&mut archive).unwrap();
assert_eq!(entries.len(), 1);
}
#[test]
fn test_delta_tool_module_alias() {
let delta = vec!["1.0".into(), "2.0".into()];
let result = DeltaTool::get_delta(Some(&delta), 2);
assert_eq!(result.len(), 2);
}
#[test]
fn test_image_utils_module_alias() {
assert_eq!(
ImageUtils::detect_format(&[0x89, 0x50, 0x4E, 0x47]),
ImageUtils::ImageFormat::Png
);
}
#[test]
fn test_ofd_namespace_constant() {
assert_eq!(OFD_NAMESPACE, "http://www.ofdspec.org/2016");
}
}