use std::fs::File;
use std::io::{Cursor, Read, Seek};
use easyofd_core::{ContentObject, OfdError, OfdMetadata, OfdPage, OfdResult};
use easyofd_package::validate_archive;
use crate::parser::{
doc_path, parse_document_entry, parse_document_resources, parse_ofd_entry, parse_page_entry,
};
use crate::read_options::ReadOptions;
pub struct OfdReader {
pages: Vec<OfdPage>,
metadata: OfdMetadata,
raw_entries: Vec<(String, Vec<u8>)>,
raw_ofd_xml: Option<String>,
raw_document_xml: Option<String>,
}
impl OfdReader {
pub fn open(path: impl AsRef<std::path::Path>) -> OfdResult<Self> {
Self::open_with_options(path, ReadOptions::default())
}
pub fn open_with_options(
path: impl AsRef<std::path::Path>,
options: ReadOptions,
) -> OfdResult<Self> {
let file = File::open(path)?;
Self::from_seek(file, options)
}
pub fn from_bytes(data: &[u8]) -> OfdResult<Self> {
Self::from_seek(Cursor::new(data), ReadOptions::default())
}
pub fn from_seek<R: Read + Seek>(source: R, options: ReadOptions) -> OfdResult<Self> {
let mut pages = Vec::new();
let mut raw_entries = Vec::new();
let (metadata, raw_ofd_xml, raw_document_xml) = visit_archive(
source,
options,
|_, page| {
pages.push(page);
Ok(())
},
&mut raw_entries,
)?;
Ok(Self {
pages,
metadata,
raw_entries,
raw_ofd_xml,
raw_document_xml,
})
}
pub fn visit_path(
path: impl AsRef<std::path::Path>,
options: ReadOptions,
mut visitor: impl FnMut(usize, OfdPage) -> OfdResult<()>,
) -> OfdResult<usize> {
let mut count = 0usize;
let mut raw_entries = Vec::new();
let _ = visit_archive(
File::open(path)?,
options,
|page_number, page| {
count += 1;
visitor(page_number, page)
},
&mut raw_entries,
)?;
Ok(count)
}
#[must_use]
pub fn page_count(&self) -> usize {
self.pages.len()
}
#[must_use]
pub fn pages(&self) -> &[OfdPage] {
&self.pages
}
#[must_use]
pub fn metadata(&self) -> &OfdMetadata {
&self.metadata
}
#[must_use]
pub fn raw_entries(&self) -> &[(String, Vec<u8>)] {
&self.raw_entries
}
#[must_use]
pub fn raw_ofd_xml(&self) -> Option<&str> {
self.raw_ofd_xml.as_deref()
}
#[must_use]
pub fn raw_document_xml(&self) -> Option<&str> {
self.raw_document_xml.as_deref()
}
#[must_use]
pub fn extract_text(&self) -> Vec<String> {
self.pages.iter().map(page_text).collect()
}
#[must_use]
pub fn extract_all_text(&self) -> String {
self.extract_text().join("\n---\n")
}
}
fn visit_archive<R: Read + Seek>(
source: R,
options: ReadOptions,
mut visitor: impl FnMut(usize, OfdPage) -> OfdResult<()>,
raw_entries: &mut Vec<(String, Vec<u8>)>,
) -> OfdResult<(OfdMetadata, Option<String>, Option<String>)> {
let mut archive = zip::ZipArchive::new(source).map_err(|e| OfdError::Zip(e.to_string()))?;
validate_archive(&mut archive, options.package_limits)?;
let ofd_entry = parse_ofd_entry(&mut archive)?;
let doc_dir = &ofd_entry.doc_dir;
let document_entry = parse_document_entry(&mut archive, doc_dir, &ofd_entry.document_file)?;
let page_refs = &document_entry.pages;
let resources = parse_document_resources(
&mut archive,
doc_dir,
document_entry.document_res.as_deref(),
)?;
for i in 0..archive.len() {
let mut file = archive
.by_index(i)
.map_err(|e| OfdError::Zip(e.to_string()))?;
let name = file.name().to_string();
if !writer_regenerates(&name, doc_dir, &ofd_entry.document_file) {
let capacity = usize::try_from(file.size()).unwrap_or(usize::MAX);
let mut data = Vec::with_capacity(capacity);
std::io::Read::read_to_end(&mut file, &mut data).map_err(OfdError::Io)?;
raw_entries.push((name, data));
}
}
for (index, page_loc) in page_refs.iter().enumerate() {
let page_number = index + 1;
if options.first_page.is_some_and(|first| page_number < first)
|| options.last_page.is_some_and(|last| page_number > last)
{
continue;
}
let page_path = doc_path(doc_dir, page_loc);
let page = parse_page_entry(&mut archive, &page_path, doc_dir, &resources)?;
visitor(page_number, page)?;
}
let creation_date_raw = ofd_entry.creation_date.clone();
let mod_date_raw = ofd_entry.mod_date.clone();
let mod_date = ofd_entry.mod_date.as_deref().and_then(parse_ofd_date);
let creation_date = ofd_entry.creation_date.as_deref().and_then(parse_ofd_date);
let metadata = OfdMetadata {
doc_id: ofd_entry.doc_id,
title: ofd_entry.title,
author: ofd_entry.author,
creator: ofd_entry.creator,
creator_version: ofd_entry.creator_version,
mod_date,
mod_date_raw,
creation_date,
creation_date_raw,
max_unit_id: ofd_entry.max_unit_id,
bookmarks: document_entry.bookmarks,
outlines: document_entry.outlines,
custom_datas: ofd_entry.custom_datas,
doc_usage: ofd_entry.doc_usage,
keywords: ofd_entry.keywords,
subject: ofd_entry.subject,
application_box: document_entry.application_box,
content_box: document_entry.content_box,
clip_box: document_entry.clip_box,
bleed_box: document_entry.bleed_box,
trim_box: document_entry.trim_box,
signatures_path: ofd_entry.signatures_path,
template_pages: document_entry.template_pages,
annotations_path: document_entry.annotations_path,
attachments_path: document_entry.attachments_path,
custom_tags_path: document_entry.custom_tags_path,
page_area_present: document_entry.page_area_present,
doc_dir: ofd_entry.doc_dir.clone(),
document_file: ofd_entry.document_file,
document_res: document_entry.document_res,
document_res_element_present: document_entry.document_res_element_present,
permissions: document_entry.permissions,
public_res_present: {
let target = format!("{doc_dir}/PublicRes.xml");
archive.by_name(&target).is_ok()
},
public_res_element_present: document_entry.public_res_element_present,
..OfdMetadata::default()
};
Ok((
metadata,
Some(ofd_entry.raw_xml),
Some(document_entry.raw_xml),
))
}
fn writer_regenerates(name: &str, doc_dir: &str, document_file: &str) -> bool {
if name == "OFD.xml"
|| name == format!("{doc_dir}/{document_file}")
|| name.ends_with("/PublicRes.xml")
|| (name.contains(&format!("/{doc_dir}/Pages/Page_"))
&& name.ends_with("/Content.xml"))
{
return true;
}
let prefix = format!("{doc_dir}/Res/Image_");
if let Some(rest) = name.strip_prefix(&prefix) {
return !rest.is_empty()
&& rest
.rsplit_once('.')
.is_some_and(|(idx, _)| idx.chars().all(|c| c.is_ascii_digit()));
}
false
}
fn parse_ofd_date(s: &str) -> Option<chrono::NaiveDateTime> {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
return Some(dt.naive_utc());
}
for fmt in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] {
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, fmt) {
return Some(dt);
}
}
if let Ok(d) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
return d.and_hms_opt(0, 0, 0);
}
let digits: String = s
.strip_prefix("D:")
.or_else(|| s.strip_prefix('D'))
.unwrap_or(s)
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
chrono::NaiveDateTime::parse_from_str(&digits[..digits.len().min(14)], "%Y%m%d%H%M%S").ok()
}
pub(crate) fn page_text(page: &OfdPage) -> String {
page.content
.iter()
.filter_map(|obj| {
if let ContentObject::Text(t) = obj {
Some(t.text.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n")
}