typlite/writer/docx/
image_processor.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
//! Image processing functionality for DOCX conversion

use base64::Engine;
use docx_rs::*;
use std::io::Cursor;

use crate::Result;

/// Image processor for DOCX documents
pub struct DocxImageProcessor;

impl DocxImageProcessor {
    /// Create a new image processor
    pub fn new() -> Self {
        Self
    }

    /// Convert SVG data to PNG format
    pub fn convert_svg_to_png(&self, svg_data: &[u8]) -> Result<Vec<u8>> {
        // Check if data is valid SVG
        let svg_str = match std::str::from_utf8(svg_data) {
            Ok(s) => s,
            Err(_) => return Err("Unable to parse input data as UTF-8 string".into()),
        };

        let dpi = 300.0;
        let scale_factor = dpi / 96.0;

        let opt = resvg::usvg::Options {
            dpi,
            ..resvg::usvg::Options::default()
        };

        // Parse SVG
        let rtree = match resvg::usvg::Tree::from_str(svg_str, &opt) {
            Ok(tree) => tree,
            Err(e) => return Err(format!("SVG parsing error: {:?}", e).into()),
        };

        let size = rtree.size().to_int_size();
        let width = (size.width() as f32 * scale_factor) as u32;
        let height = (size.height() as f32 * scale_factor) as u32;

        // Create pixel buffer
        let mut pixmap = match resvg::tiny_skia::Pixmap::new(width, height) {
            Some(pixmap) => pixmap,
            None => return Err("Unable to create pixel buffer".into()),
        };

        // Render SVG to pixel buffer
        resvg::render(
            &rtree,
            resvg::tiny_skia::Transform::from_scale(scale_factor, scale_factor),
            &mut pixmap.as_mut(),
        );

        // Encode as PNG
        pixmap
            .encode_png()
            .map_err(|e| format!("PNG encoding error: {:?}", e).into())
    }

    /// Process image data and add to document
    pub fn process_image_data(
        &self,
        docx: Docx,
        data: &[u8],
        alt_text: Option<&str>,
        scale: Option<f32>,
    ) -> Docx {
        // Add image format validation
        match image::guess_format(data) {
            Ok(..) => {
                // Process image data

                // For other formats, try to convert to PNG
                let pic = match image::load_from_memory(data) {
                    Ok(img) => {
                        let (w, h) =
                            Self::image_dim(::image::GenericImageView::dimensions(&img), scale);
                        let mut buffer = Vec::new();
                        if img
                            .write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png)
                            .is_ok()
                        {
                            Pic::new_with_dimensions(buffer, w, h)
                        } else {
                            // If conversion fails, return original document (without image)
                            let err_para = Paragraph::new().add_run(Run::new().add_text(
                                        "[Image processing error: Unable to convert to supported format]".to_string(),
                                    ));
                            return docx.add_paragraph(err_para);
                        }
                    }
                    Err(_) => {
                        // If unable to load image, return original document (without image)
                        let err_para = Paragraph::new().add_run(Run::new().add_text(
                            "[Image processing error: Unable to load image]".to_string(),
                        ));
                        return docx.add_paragraph(err_para);
                    }
                };

                let img_para = Paragraph::new().add_run(Run::new().add_image(pic));
                let doc_with_img = docx.add_paragraph(img_para);

                if let Some(alt) = alt_text {
                    if !alt.is_empty() {
                        let caption_para = Paragraph::new()
                            .style("Caption")
                            .add_run(Run::new().add_text(alt));
                        doc_with_img.add_paragraph(caption_para)
                    } else {
                        doc_with_img
                    }
                } else {
                    doc_with_img
                }
            }
            Err(_) => {
                // If unable to determine image format, return original document (without image)
                let err_para =
                    Paragraph::new()
                        .add_run(Run::new().add_text(
                            "[Image processing error: Unknown image format]".to_string(),
                        ));
                docx.add_paragraph(err_para)
            }
        }
    }

    /// Process inline image and add to Run
    pub fn process_inline_image(&self, mut run: Run, data: &[u8]) -> Result<Run> {
        match image::guess_format(data) {
            Ok(..) => {
                // Try to convert to PNG
                let pic = match image::load_from_memory(data) {
                    Ok(img) => {
                        let (w, h) = ::image::GenericImageView::dimensions(&img);
                        let mut buffer = Vec::new();
                        if img
                            .write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png)
                            .is_ok()
                        {
                            Pic::new_with_dimensions(buffer, w, h)
                        } else {
                            run = run.add_text("[Image conversion error]");
                            return Ok(run);
                        }
                    }
                    Err(_) => {
                        run = run.add_text("[Image loading error]");
                        return Ok(run);
                    }
                };

                run = run.add_image(pic);
                Ok(run)
            }
            Err(_) => {
                run = run.add_text("[Unknown image format]");
                Ok(run)
            }
        }
    }

    /// Process data URL inline image
    pub fn process_data_url_image(&self, run: Run, src: &str, is_typst_block: bool) -> Result<Run> {
        if let Some(data_start) = src.find("base64,") {
            let base64_data = &src[data_start + 7..];
            if let Ok(img_data) = base64::engine::general_purpose::STANDARD.decode(base64_data) {
                // If it's a typst-block (SVG data), special handling is needed
                if is_typst_block {
                    // Use resvg to convert SVG to PNG
                    if let Ok(png_data) = self.convert_svg_to_png(&img_data) {
                        let mut new_run = run;
                        new_run = self.process_inline_image(new_run, &png_data)?;
                        return Ok(new_run);
                    } else {
                        return Ok(run.add_text("[SVG conversion failed]"));
                    }
                } else {
                    // Normal image processing
                    let mut new_run = run;
                    new_run = self.process_inline_image(new_run, &img_data)?;
                    return Ok(new_run);
                }
            }
        }
        Ok(run.add_text("[Invalid data URL]"))
    }

    /// Calculate image dimensions for DOCX
    pub fn image_dim((w, h): (u32, u32), scale_factor: Option<f32>) -> (u32, u32) {
        let actual_scale = scale_factor.unwrap_or(1.0);

        let max_width = 5486400;
        let scaled_w = (w as f32 * actual_scale) as u32;
        let scaled_h = (h as f32 * actual_scale) as u32;

        if scaled_w > max_width {
            let ratio = scaled_h as f32 / scaled_w as f32;
            let new_width = max_width;
            let new_height = (max_width as f32 * ratio) as u32;
            (new_width, new_height)
        } else {
            (scaled_w, scaled_h)
        }
    }
}