typlite/writer/docx/
image_processor.rs

1//! Image processing functionality for DOCX conversion
2
3use base64::Engine;
4use docx_rs::*;
5use std::io::Cursor;
6
7use crate::Result;
8
9/// Image processor for DOCX documents
10pub struct DocxImageProcessor;
11
12impl DocxImageProcessor {
13    /// Create a new image processor
14    pub fn new() -> Self {
15        Self
16    }
17
18    /// Convert SVG data to PNG format
19    pub fn convert_svg_to_png(&self, svg_data: &[u8]) -> Result<Vec<u8>> {
20        // Check if data is valid SVG
21        let svg_str = match std::str::from_utf8(svg_data) {
22            Ok(s) => s,
23            Err(_) => return Err("Unable to parse input data as UTF-8 string".into()),
24        };
25
26        let dpi = 300.0;
27        let scale_factor = dpi / 96.0;
28
29        let opt = resvg::usvg::Options {
30            dpi,
31            ..resvg::usvg::Options::default()
32        };
33
34        // Parse SVG
35        let rtree = match resvg::usvg::Tree::from_str(svg_str, &opt) {
36            Ok(tree) => tree,
37            Err(e) => return Err(format!("SVG parsing error: {e:?}").into()),
38        };
39
40        let size = rtree.size().to_int_size();
41        let width = (size.width() as f32 * scale_factor) as u32;
42        let height = (size.height() as f32 * scale_factor) as u32;
43
44        // Create pixel buffer
45        let mut pixmap = match resvg::tiny_skia::Pixmap::new(width, height) {
46            Some(pixmap) => pixmap,
47            None => return Err("Unable to create pixel buffer".into()),
48        };
49
50        // Render SVG to pixel buffer
51        resvg::render(
52            &rtree,
53            resvg::tiny_skia::Transform::from_scale(scale_factor, scale_factor),
54            &mut pixmap.as_mut(),
55        );
56
57        // Encode as PNG
58        pixmap
59            .encode_png()
60            .map_err(|e| format!("PNG encoding error: {e:?}").into())
61    }
62
63    /// Process image data and add to document
64    pub fn process_image_data(
65        &self,
66        docx: Docx,
67        data: &[u8],
68        alt_text: Option<&str>,
69        scale: Option<f32>,
70    ) -> Docx {
71        // Add image format validation
72        match image::guess_format(data) {
73            Ok(..) => {
74                // Process image data
75
76                // For other formats, try to convert to PNG
77                let pic = match image::load_from_memory(data) {
78                    Ok(img) => {
79                        let (w, h) =
80                            Self::image_dim(::image::GenericImageView::dimensions(&img), scale);
81                        let mut buffer = Vec::new();
82                        if img
83                            .write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png)
84                            .is_ok()
85                        {
86                            Pic::new_with_dimensions(buffer, w, h)
87                        } else {
88                            // If conversion fails, return original document (without image)
89                            let err_para = Paragraph::new().add_run(Run::new().add_text(
90                                        "[Image processing error: Unable to convert to supported format]".to_string(),
91                                    ));
92                            return docx.add_paragraph(err_para);
93                        }
94                    }
95                    Err(_) => {
96                        // If unable to load image, return original document (without image)
97                        let err_para = Paragraph::new().add_run(Run::new().add_text(
98                            "[Image processing error: Unable to load image]".to_string(),
99                        ));
100                        return docx.add_paragraph(err_para);
101                    }
102                };
103
104                let img_para = Paragraph::new().add_run(Run::new().add_image(pic));
105                let doc_with_img = docx.add_paragraph(img_para);
106
107                if let Some(alt) = alt_text {
108                    if !alt.is_empty() {
109                        let caption_para = Paragraph::new()
110                            .style("Caption")
111                            .add_run(Run::new().add_text(alt));
112                        doc_with_img.add_paragraph(caption_para)
113                    } else {
114                        doc_with_img
115                    }
116                } else {
117                    doc_with_img
118                }
119            }
120            Err(_) => {
121                // If unable to determine image format, return original document (without image)
122                let err_para =
123                    Paragraph::new()
124                        .add_run(Run::new().add_text(
125                            "[Image processing error: Unknown image format]".to_string(),
126                        ));
127                docx.add_paragraph(err_para)
128            }
129        }
130    }
131
132    /// Process inline image and add to Run
133    pub fn process_inline_image(&self, mut run: Run, data: &[u8]) -> Result<Run> {
134        match image::guess_format(data) {
135            Ok(..) => {
136                // Try to convert to PNG
137                let pic = match image::load_from_memory(data) {
138                    Ok(img) => {
139                        let (w, h) = ::image::GenericImageView::dimensions(&img);
140                        let mut buffer = Vec::new();
141                        if img
142                            .write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png)
143                            .is_ok()
144                        {
145                            Pic::new_with_dimensions(buffer, w, h)
146                        } else {
147                            run = run.add_text("[Image conversion error]");
148                            return Ok(run);
149                        }
150                    }
151                    Err(_) => {
152                        run = run.add_text("[Image loading error]");
153                        return Ok(run);
154                    }
155                };
156
157                run = run.add_image(pic);
158                Ok(run)
159            }
160            Err(_) => {
161                run = run.add_text("[Unknown image format]");
162                Ok(run)
163            }
164        }
165    }
166
167    /// Process data URL inline image
168    pub fn process_data_url_image(&self, run: Run, src: &str, is_typst_block: bool) -> Result<Run> {
169        if let Some(data_start) = src.find("base64,") {
170            let base64_data = &src[data_start + 7..];
171            if let Ok(img_data) = base64::engine::general_purpose::STANDARD.decode(base64_data) {
172                // If it's a typst-block (SVG data), special handling is needed
173                if is_typst_block {
174                    // Use resvg to convert SVG to PNG
175                    if let Ok(png_data) = self.convert_svg_to_png(&img_data) {
176                        let mut new_run = run;
177                        new_run = self.process_inline_image(new_run, &png_data)?;
178                        return Ok(new_run);
179                    } else {
180                        return Ok(run.add_text("[SVG conversion failed]"));
181                    }
182                } else {
183                    // Normal image processing
184                    let mut new_run = run;
185                    new_run = self.process_inline_image(new_run, &img_data)?;
186                    return Ok(new_run);
187                }
188            }
189        }
190        Ok(run.add_text("[Invalid data URL]"))
191    }
192
193    /// Calculate image dimensions for DOCX
194    pub fn image_dim((w, h): (u32, u32), scale_factor: Option<f32>) -> (u32, u32) {
195        let actual_scale = scale_factor.unwrap_or(1.0);
196
197        let max_width = 5486400;
198        let scaled_w = (w as f32 * actual_scale) as u32;
199        let scaled_h = (h as f32 * actual_scale) as u32;
200
201        if scaled_w > max_width {
202            let ratio = scaled_h as f32 / scaled_w as f32;
203            let new_width = max_width;
204            let new_height = (max_width as f32 * ratio) as u32;
205            (new_width, new_height)
206        } else {
207            (scaled_w, scaled_h)
208        }
209    }
210}