Skip to main content

ofd_rs/writer/
builder.rs

1//! High-level OFD writer / builder.
2
3use 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
15/// Library name used as default Creator in DocInfo.
16const LIB_NAME: &str = "ofd-rs";
17
18/// Library version used as default CreatorVersion in DocInfo.
19const LIB_VERSION: &str = env!("CARGO_PKG_VERSION");
20
21/// Source for a single image page to be embedded in the OFD.
22///
23/// Each `ImageSource` becomes one page in the output document.
24/// The image fills the entire page boundary (no margins).
25pub struct ImageSource {
26    /// Raw image bytes.
27    pub bytes: Vec<u8>,
28    /// Detected or specified image format.
29    pub format: ImageFormat,
30    /// Page size in millimeters.
31    pub page_size: PageSize,
32}
33
34impl ImageSource {
35    /// Create from image bytes with explicit format and mm dimensions.
36    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    /// Create from JPEG bytes with known pixel dimensions and DPI.
45    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    /// Create from JPEG bytes with page size in mm.
54    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    /// Create from PNG bytes with page size in mm.
63    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    /// Auto-detect format and dimensions, calculate page size from DPI.
72    ///
73    /// Returns `None` if format or dimensions cannot be detected.
74    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    /// Auto-detect format and dimensions using pixels-per-mm ratio.
85    ///
86    /// Uses ofdrw's conversion: `mm = pixels / ppm`.
87    /// See [`PPM_DEFAULT`] for the standard 5 px/mm ratio.
88    /// Returns `None` if format or dimensions cannot be detected.
89    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    /// Auto-detect format and dimensions using ofdrw's default ratio (5 px/mm).
100    ///
101    /// Equivalent to `auto_detect_ppm(bytes, PPM_DEFAULT)`.
102    /// Returns `None` if format or dimensions cannot be detected.
103    pub fn auto_detect_default(bytes: Vec<u8>) -> Option<Self> {
104        Self::auto_detect_ppm(bytes, PPM_DEFAULT)
105    }
106
107    /// Auto-detect format, use explicit mm dimensions for page size.
108    ///
109    /// Returns `None` if format cannot be detected from file header.
110    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
120/// OFD document writer.
121///
122/// Generates an OFD file (ZIP archive) containing image pages.
123///
124/// # Examples
125///
126/// ```no_run
127/// use ofd_rs::{OfdWriter, ImageSource};
128///
129/// let jpeg_bytes = std::fs::read("photo.jpg").unwrap();
130/// let ofd = OfdWriter::from_images(vec![
131///     ImageSource::jpeg(jpeg_bytes, 2480, 3508, 300.0),
132/// ]).build().unwrap();
133/// std::fs::write("output.ofd", ofd).unwrap();
134/// ```
135pub struct OfdWriter {
136    doc_info: DocInfo,
137    pages: Vec<ImageSource>,
138}
139
140impl OfdWriter {
141    /// Create a new writer with default metadata (matching ofdrw conventions).
142    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    /// Quick constructor: one image per page.
156    pub fn from_images(images: Vec<ImageSource>) -> Self {
157        let mut writer = Self::new();
158        writer.pages = images;
159        writer
160    }
161
162    /// Set document metadata.
163    pub fn set_doc_info(&mut self, info: DocInfo) -> &mut Self {
164        self.doc_info = info;
165        self
166    }
167
168    /// Add a page with an image.
169    pub fn add_image_page(&mut self, image: ImageSource) -> &mut Self {
170        self.pages.push(image);
171        self
172    }
173
174    /// Build the OFD file and return the ZIP bytes.
175    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        // First page's size as default page area
187        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            // Media resource
213            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            // Layer + ImageObject
222            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            // Per-page area override only if different from default
234            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        // Generate XML
253        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        // Build ZIP
258        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/// Errors that can occur during OFD generation.
292#[derive(Debug)]
293pub enum OfdError {
294    /// No pages were added to the writer.
295    NoPages,
296    /// ZIP packaging error.
297    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
311// --- helpers ---
312
313fn 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    // Use system time to get current date in yyyy-MM-dd format.
332    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    // Simple date calculation (no external crate needed)
338    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    // Algorithm from http://howardhinnant.github.io/date_algorithms.html
345    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}