1use std::fs::File;
4use std::io::{Cursor, Read, Seek};
5
6use easyofd_core::{ContentObject, OfdError, OfdMetadata, OfdPage, OfdResult};
7use easyofd_package::validate_archive;
8
9use crate::parser::{
10 doc_path, parse_document_entry, parse_document_resources, parse_ofd_entry, parse_page_entry,
11};
12use crate::read_options::ReadOptions;
13
14pub struct OfdReader {
16 pages: Vec<OfdPage>,
17 metadata: OfdMetadata,
18 raw_entries: Vec<(String, Vec<u8>)>,
21 raw_ofd_xml: Option<String>,
23 raw_document_xml: Option<String>,
25}
26
27impl OfdReader {
28 pub fn open(path: impl AsRef<std::path::Path>) -> OfdResult<Self> {
34 Self::open_with_options(path, ReadOptions::default())
35 }
36
37 pub fn open_with_options(
43 path: impl AsRef<std::path::Path>,
44 options: ReadOptions,
45 ) -> OfdResult<Self> {
46 let file = File::open(path)?;
47 Self::from_seek(file, options)
48 }
49
50 pub fn from_bytes(data: &[u8]) -> OfdResult<Self> {
56 Self::from_seek(Cursor::new(data), ReadOptions::default())
57 }
58
59 pub fn from_seek<R: Read + Seek>(source: R, options: ReadOptions) -> OfdResult<Self> {
65 let mut pages = Vec::new();
66 let mut raw_entries = Vec::new();
67 let (metadata, raw_ofd_xml, raw_document_xml) = visit_archive(
68 source,
69 options,
70 |_, page| {
71 pages.push(page);
72 Ok(())
73 },
74 &mut raw_entries,
75 )?;
76 Ok(Self {
77 pages,
78 metadata,
79 raw_entries,
80 raw_ofd_xml,
81 raw_document_xml,
82 })
83 }
84
85 pub fn visit_path(
93 path: impl AsRef<std::path::Path>,
94 options: ReadOptions,
95 mut visitor: impl FnMut(usize, OfdPage) -> OfdResult<()>,
96 ) -> OfdResult<usize> {
97 let mut count = 0usize;
98 let mut raw_entries = Vec::new();
99 let _ = visit_archive(
100 File::open(path)?,
101 options,
102 |page_number, page| {
103 count += 1;
104 visitor(page_number, page)
105 },
106 &mut raw_entries,
107 )?;
108 Ok(count)
109 }
110
111 #[must_use]
113 pub fn page_count(&self) -> usize {
114 self.pages.len()
115 }
116
117 #[must_use]
119 pub fn pages(&self) -> &[OfdPage] {
120 &self.pages
121 }
122
123 #[must_use]
125 pub fn metadata(&self) -> &OfdMetadata {
126 &self.metadata
127 }
128
129 #[must_use]
135 pub fn raw_entries(&self) -> &[(String, Vec<u8>)] {
136 &self.raw_entries
137 }
138
139 #[must_use]
145 pub fn raw_ofd_xml(&self) -> Option<&str> {
146 self.raw_ofd_xml.as_deref()
147 }
148
149 #[must_use]
155 pub fn raw_document_xml(&self) -> Option<&str> {
156 self.raw_document_xml.as_deref()
157 }
158
159 #[must_use]
161 pub fn extract_text(&self) -> Vec<String> {
162 self.pages.iter().map(page_text).collect()
163 }
164
165 #[must_use]
167 pub fn extract_all_text(&self) -> String {
168 self.extract_text().join("\n---\n")
169 }
170}
171
172fn visit_archive<R: Read + Seek>(
173 source: R,
174 options: ReadOptions,
175 mut visitor: impl FnMut(usize, OfdPage) -> OfdResult<()>,
176 raw_entries: &mut Vec<(String, Vec<u8>)>,
177) -> OfdResult<(OfdMetadata, Option<String>, Option<String>)> {
178 let mut archive = zip::ZipArchive::new(source).map_err(|e| OfdError::Zip(e.to_string()))?;
179 validate_archive(&mut archive, options.package_limits)?;
180
181 let ofd_entry = parse_ofd_entry(&mut archive)?;
182 let doc_dir = &ofd_entry.doc_dir;
183 let document_entry = parse_document_entry(&mut archive, doc_dir, &ofd_entry.document_file)?;
184 let page_refs = &document_entry.pages;
185 let resources = parse_document_resources(
186 &mut archive,
187 doc_dir,
188 document_entry.document_res.as_deref(),
189 )?;
190
191 for i in 0..archive.len() {
197 let mut file = archive
198 .by_index(i)
199 .map_err(|e| OfdError::Zip(e.to_string()))?;
200 let name = file.name().to_string();
201 if !writer_regenerates(&name, doc_dir, &ofd_entry.document_file) {
202 let capacity = usize::try_from(file.size()).unwrap_or(usize::MAX);
204 let mut data = Vec::with_capacity(capacity);
205 std::io::Read::read_to_end(&mut file, &mut data).map_err(OfdError::Io)?;
206 raw_entries.push((name, data));
207 }
208 }
209
210 for (index, page_loc) in page_refs.iter().enumerate() {
211 let page_number = index + 1;
212 if options.first_page.is_some_and(|first| page_number < first)
213 || options.last_page.is_some_and(|last| page_number > last)
214 {
215 continue;
216 }
217 let page_path = doc_path(doc_dir, page_loc);
218 let page = parse_page_entry(&mut archive, &page_path, doc_dir, &resources)?;
219 visitor(page_number, page)?;
220 }
221 let creation_date_raw = ofd_entry.creation_date.clone();
226 let mod_date_raw = ofd_entry.mod_date.clone();
227 let mod_date = ofd_entry.mod_date.as_deref().and_then(parse_ofd_date);
228 let creation_date = ofd_entry.creation_date.as_deref().and_then(parse_ofd_date);
229
230 let metadata = OfdMetadata {
231 doc_id: ofd_entry.doc_id,
232 title: ofd_entry.title,
233 author: ofd_entry.author,
234 creator: ofd_entry.creator,
235 creator_version: ofd_entry.creator_version,
236 mod_date,
237 mod_date_raw,
238 creation_date,
239 creation_date_raw,
240 max_unit_id: ofd_entry.max_unit_id,
241 bookmarks: document_entry.bookmarks,
242 outlines: document_entry.outlines,
243 custom_datas: ofd_entry.custom_datas,
244 doc_usage: ofd_entry.doc_usage,
245 keywords: ofd_entry.keywords,
246 subject: ofd_entry.subject,
247 application_box: document_entry.application_box,
248 content_box: document_entry.content_box,
249 clip_box: document_entry.clip_box,
250 bleed_box: document_entry.bleed_box,
251 trim_box: document_entry.trim_box,
252 signatures_path: ofd_entry.signatures_path,
253 template_pages: document_entry.template_pages,
254 annotations_path: document_entry.annotations_path,
255 attachments_path: document_entry.attachments_path,
256 custom_tags_path: document_entry.custom_tags_path,
257 page_area_present: document_entry.page_area_present,
258 doc_dir: ofd_entry.doc_dir.clone(),
259 document_file: ofd_entry.document_file,
260 document_res: document_entry.document_res,
261 document_res_element_present: document_entry.document_res_element_present,
262 permissions: document_entry.permissions,
263 public_res_present: {
264 let target = format!("{doc_dir}/PublicRes.xml");
265 archive.by_name(&target).is_ok()
266 },
267 public_res_element_present: document_entry.public_res_element_present,
268 ..OfdMetadata::default()
269 };
270
271 Ok((
272 metadata,
273 Some(ofd_entry.raw_xml),
274 Some(document_entry.raw_xml),
275 ))
276}
277
278fn writer_regenerates(name: &str, doc_dir: &str, document_file: &str) -> bool {
288 if name == "OFD.xml"
289 || name == format!("{doc_dir}/{document_file}")
290 || name.ends_with("/PublicRes.xml")
291 || (name.contains(&format!("/{doc_dir}/Pages/Page_"))
294 && name.ends_with("/Content.xml"))
295 {
296 return true;
297 }
298 let prefix = format!("{doc_dir}/Res/Image_");
300 if let Some(rest) = name.strip_prefix(&prefix) {
301 return !rest.is_empty()
302 && rest
303 .rsplit_once('.')
304 .is_some_and(|(idx, _)| idx.chars().all(|c| c.is_ascii_digit()));
305 }
306 false
307}
308
309fn parse_ofd_date(s: &str) -> Option<chrono::NaiveDateTime> {
316 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
317 return Some(dt.naive_utc());
318 }
319 for fmt in ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] {
320 if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, fmt) {
321 return Some(dt);
322 }
323 }
324 if let Ok(d) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
325 return d.and_hms_opt(0, 0, 0);
326 }
327 let digits: String = s
330 .strip_prefix("D:")
331 .or_else(|| s.strip_prefix('D'))
332 .unwrap_or(s)
333 .chars()
334 .take_while(|c| c.is_ascii_digit())
335 .collect();
336 chrono::NaiveDateTime::parse_from_str(&digits[..digits.len().min(14)], "%Y%m%d%H%M%S").ok()
337}
338
339pub(crate) fn page_text(page: &OfdPage) -> String {
341 page.content
342 .iter()
343 .filter_map(|obj| {
344 if let ContentObject::Text(t) = obj {
345 Some(t.text.as_str())
346 } else {
347 None
348 }
349 })
350 .collect::<Vec<_>>()
351 .join("\n")
352}