1use std::io::{Cursor, Write};
4use zip::write::SimpleFileOptions;
5use zip::ZipWriter;
6
7use crate::model::document::{PageDef, PageSize, PPM_DEFAULT};
8use crate::model::graphic::{GraphicObject, ImageObject};
9use crate::model::ofd::DocInfo;
10use crate::model::resource::{detect_image_dimensions, ImageFormat, MediaDef};
11use crate::types::StBox;
12
13use super::xml_gen;
14
15const LIB_NAME: &str = "ofd-rs";
17
18const LIB_VERSION: &str = env!("CARGO_PKG_VERSION");
20
21pub struct ImageSource {
26 pub bytes: Vec<u8>,
28 pub format: ImageFormat,
30 pub page_size: PageSize,
32}
33
34impl ImageSource {
35 pub fn new(bytes: Vec<u8>, format: ImageFormat, width_mm: f64, height_mm: f64) -> Self {
37 Self {
38 bytes,
39 format,
40 page_size: PageSize::new(width_mm, height_mm),
41 }
42 }
43
44 pub fn jpeg(bytes: Vec<u8>, width_px: u32, height_px: u32, dpi: f64) -> Self {
46 Self {
47 bytes,
48 format: ImageFormat::Jpeg,
49 page_size: PageSize::from_pixels(width_px, height_px, dpi),
50 }
51 }
52
53 pub fn jpeg_mm(bytes: Vec<u8>, width_mm: f64, height_mm: f64) -> Self {
55 Self {
56 bytes,
57 format: ImageFormat::Jpeg,
58 page_size: PageSize::new(width_mm, height_mm),
59 }
60 }
61
62 pub fn png_mm(bytes: Vec<u8>, width_mm: f64, height_mm: f64) -> Self {
64 Self {
65 bytes,
66 format: ImageFormat::Png,
67 page_size: PageSize::new(width_mm, height_mm),
68 }
69 }
70
71 pub fn auto_detect(bytes: Vec<u8>, dpi: f64) -> Option<Self> {
75 let format = ImageFormat::detect(&bytes)?;
76 let (w_px, h_px) = detect_image_dimensions(&bytes)?;
77 Some(Self {
78 bytes,
79 format,
80 page_size: PageSize::from_pixels(w_px, h_px, dpi),
81 })
82 }
83
84 pub fn auto_detect_ppm(bytes: Vec<u8>, ppm: f64) -> Option<Self> {
90 let format = ImageFormat::detect(&bytes)?;
91 let (w_px, h_px) = detect_image_dimensions(&bytes)?;
92 Some(Self {
93 bytes,
94 format,
95 page_size: PageSize::from_pixels_ppm(w_px, h_px, ppm),
96 })
97 }
98
99 pub fn auto_detect_default(bytes: Vec<u8>) -> Option<Self> {
104 Self::auto_detect_ppm(bytes, PPM_DEFAULT)
105 }
106
107 pub fn auto_detect_mm(bytes: Vec<u8>, width_mm: f64, height_mm: f64) -> Option<Self> {
111 let format = ImageFormat::detect(&bytes)?;
112 Some(Self {
113 bytes,
114 format,
115 page_size: PageSize::new(width_mm, height_mm),
116 })
117 }
118}
119
120pub struct OfdWriter {
136 doc_info: DocInfo,
137 pages: Vec<ImageSource>,
138}
139
140impl OfdWriter {
141 pub fn new() -> Self {
143 Self {
144 doc_info: DocInfo {
145 doc_id: uuid::Uuid::new_v4().to_string().replace('-', ""),
146 creator: Some(LIB_NAME.into()),
147 creator_version: Some(LIB_VERSION.into()),
148 creation_date: Some(today()),
149 ..Default::default()
150 },
151 pages: Vec::new(),
152 }
153 }
154
155 pub fn from_images(images: Vec<ImageSource>) -> Self {
157 let mut writer = Self::new();
158 writer.pages = images;
159 writer
160 }
161
162 pub fn set_doc_info(&mut self, info: DocInfo) -> &mut Self {
164 self.doc_info = info;
165 self
166 }
167
168 pub fn add_image_page(&mut self, image: ImageSource) -> &mut Self {
170 self.pages.push(image);
171 self
172 }
173
174 pub fn build(self) -> Result<Vec<u8>, OfdError> {
176 if self.pages.is_empty() {
177 return Err(OfdError::NoPages);
178 }
179
180 let mut id_counter: u32 = 0;
181 let mut next_id = || -> u32 {
182 id_counter += 1;
183 id_counter
184 };
185
186 let default_page_size = &self.pages[0].page_size;
188 let default_page_area = default_page_size.to_box();
189
190 let mut page_defs: Vec<PageDef> = Vec::new();
191 let mut media_defs: Vec<MediaDef> = Vec::new();
192
193 struct PageBuildData {
194 page_dir: String,
195 layer_id: u32,
196 objects: Vec<GraphicObject>,
197 page_area: Option<StBox>,
198 image_file_path: String,
199 image_bytes: Vec<u8>,
200 }
201 let mut page_builds: Vec<PageBuildData> = Vec::new();
202
203 for (i, source) in self.pages.iter().enumerate() {
204 let page_id = next_id();
205 let page_dir = format!("Pages/Page_{}", i);
206
207 page_defs.push(PageDef {
208 page_id,
209 base_loc: format!("{}/Content.xml", page_dir),
210 });
211
212 let media_id = next_id();
214 let file_name = format!("Image_{}.{}", i, source.format.extension());
215 media_defs.push(MediaDef {
216 id: media_id,
217 format: source.format,
218 file_name: file_name.clone(),
219 });
220
221 let layer_id = next_id();
223 let img_obj_id = next_id();
224 let boundary = source.page_size.to_box();
225
226 let objects = vec![GraphicObject::Image(ImageObject {
227 id: img_obj_id,
228 boundary,
229 resource_id: media_id,
230 alpha: None,
231 })];
232
233 let page_area = if differs(&source.page_size, default_page_size) {
235 Some(source.page_size.to_box())
236 } else {
237 None
238 };
239
240 page_builds.push(PageBuildData {
241 page_dir,
242 layer_id,
243 objects,
244 page_area,
245 image_file_path: format!("Doc_0/Res/{}", file_name),
246 image_bytes: source.bytes.clone(),
247 });
248 }
249
250 let max_id = id_counter;
251
252 let ofd_xml = xml_gen::gen_ofd_xml(&self.doc_info);
254 let document_xml = xml_gen::gen_document_xml(&page_defs, max_id, &default_page_area);
255 let doc_res_xml = xml_gen::gen_document_res_xml(&media_defs);
256
257 let buf = Cursor::new(Vec::new());
259 let mut zip = ZipWriter::new(buf);
260 let options = SimpleFileOptions::default()
261 .compression_method(zip::CompressionMethod::Deflated);
262
263 zip_file(&mut zip, "OFD.xml", ofd_xml.as_bytes(), options)?;
264 zip_file(&mut zip, "Doc_0/Document.xml", document_xml.as_bytes(), options)?;
265 zip_file(&mut zip, "Doc_0/DocumentRes.xml", doc_res_xml.as_bytes(), options)?;
266
267 for pb in &page_builds {
268 let content_xml = xml_gen::gen_content_xml(
269 pb.layer_id,
270 &pb.objects,
271 pb.page_area.as_ref(),
272 );
273 let content_path = format!("Doc_0/{}/Content.xml", pb.page_dir);
274 zip_file(&mut zip, &content_path, content_xml.as_bytes(), options)?;
275 zip_file(&mut zip, &pb.image_file_path, &pb.image_bytes, options)?;
276 }
277
278 let result = zip
279 .finish()
280 .map_err(|e| OfdError::Zip(e.to_string()))?;
281 Ok(result.into_inner())
282 }
283}
284
285impl Default for OfdWriter {
286 fn default() -> Self {
287 Self::new()
288 }
289}
290
291#[derive(Debug)]
293pub enum OfdError {
294 NoPages,
296 Zip(String),
298}
299
300impl std::fmt::Display for OfdError {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 match self {
303 Self::NoPages => write!(f, "no pages added to OFD writer"),
304 Self::Zip(msg) => write!(f, "ZIP error: {}", msg),
305 }
306 }
307}
308
309impl std::error::Error for OfdError {}
310
311fn differs(a: &PageSize, b: &PageSize) -> bool {
314 (a.width_mm - b.width_mm).abs() > 0.01 || (a.height_mm - b.height_mm).abs() > 0.01
315}
316
317fn zip_file(
318 zip: &mut ZipWriter<Cursor<Vec<u8>>>,
319 path: &str,
320 data: &[u8],
321 options: SimpleFileOptions,
322) -> Result<(), OfdError> {
323 zip.start_file(path, options)
324 .map_err(|e| OfdError::Zip(e.to_string()))?;
325 zip.write_all(data)
326 .map_err(|e| OfdError::Zip(e.to_string()))?;
327 Ok(())
328}
329
330fn today() -> String {
331 let now = std::time::SystemTime::now();
333 let secs = now
334 .duration_since(std::time::UNIX_EPOCH)
335 .unwrap_or_default()
336 .as_secs();
337 let days = (secs / 86400) as i64;
339 let (y, m, d) = days_to_ymd(days);
340 format!("{:04}-{:02}-{:02}", y, m, d)
341}
342
343fn days_to_ymd(days_since_epoch: i64) -> (i32, u32, u32) {
344 let z = days_since_epoch + 719468;
346 let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
347 let doe = (z - era * 146097) as u32;
348 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
349 let y = (yoe as i64 + era * 400) as i32;
350 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
351 let mp = (5 * doy + 2) / 153;
352 let d = doy - (153 * mp + 2) / 5 + 1;
353 let m = if mp < 10 { mp + 3 } else { mp - 9 };
354 let y = if m <= 2 { y + 1 } else { y };
355 (y, m, d)
356}