docx_template/
docx.rs

1use quick_xml::Writer;
2use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
3use reqwest::Client;
4use std::collections::HashMap;
5use std::fs::File;
6use std::io::{Cursor, Read, Write};
7use std::path::Path;
8use std::time::Duration;
9use thiserror::Error;
10use uuid::Uuid;
11use zip::write::SimpleFileOptions;
12use zip::{ZipArchive, ZipWriter};
13
14pub struct DocxTemplate {
15    // 待替换的字符串
16    text_replacements: HashMap<String, String>,
17    // 待替换的图片
18    image_replacements: HashMap<String, Option<DocxImage>>,
19    // 请求对象
20    client: Client,
21}
22
23impl DocxTemplate {
24    pub fn new() -> Self {
25        DocxTemplate {
26            text_replacements: HashMap::new(),
27            image_replacements: HashMap::new(),
28            client: Client::builder()
29                .timeout(Duration::from_secs(10)) // 设置超时
30                .build()
31                .unwrap(),
32        }
33    }
34
35    /// 添加待替换的字符以及对应的值
36    /// @param placeholder 替换的字符串
37    /// @param value 替换的值
38    pub fn add_text_replacement(&mut self, placeholder: &str, value: &str) {
39        self.text_replacements
40            .insert(placeholder.to_string(), value.to_string());
41    }
42
43    /// 添加要替换的图片
44    /// @param placeholder 替换的字符串
45    /// @param image_path 图片路径
46    pub fn add_image_replacement(
47        &mut self,
48        placeholder: &str,
49        image_path: Option<&str>,
50    ) -> Result<(), DocxError> {
51        match image_path {
52            None => {
53                // 插入图片到属性中
54                self.image_replacements
55                    .insert(placeholder.to_string(), None);
56            }
57            Some(data) => {
58                // 插入图片到属性中
59                self.image_replacements
60                    .insert(placeholder.to_string(), Some(DocxImage::new(data)?));
61            }
62        }
63
64        Ok(())
65    }
66
67    /// 添加要替换的图片
68    /// @param placeholder 替换的字符串
69    /// @param image_path 图片路径
70    pub async fn add_image_url_replacement(
71        &mut self,
72        placeholder: &str,
73        image_url: Option<&str>,
74    ) -> Result<(), DocxError> {
75        match image_url {
76            None => {
77                // 插入图片到属性中
78                self.image_replacements
79                    .insert(placeholder.to_string(), None);
80            }
81            Some(url) => {
82                // 发送请求
83                let response = self.client.get(url).send().await?;
84                // 检查状态码
85                if response.status().is_success() {
86                    // 读取字节
87                    let image_data = response.bytes().await?.to_vec();
88                    // 插入图片到属性中
89                    self.image_replacements.insert(
90                        placeholder.to_string(),
91                        Some(DocxImage::new_image_data(url, image_data)?),
92                    );
93                }
94            }
95        }
96
97        Ok(())
98    }
99
100    /// 处理模板
101    /// @param template_path 模板路径
102    /// @param output_path 输出路径
103    pub fn process_template(
104        &self,
105        template_path: &str,
106        output_path: &str,
107    ) -> Result<(), DocxError> {
108        // 1. 打开模板文件
109        let template_file = File::open(template_path)?;
110        let mut archive = ZipArchive::new(template_file)?;
111
112        // 2. 创建输出文件
113        let output_file = File::create(output_path)?;
114        let mut zip_writer = ZipWriter::new(output_file);
115
116        // 3. 遍历ZIP中的文件
117        for i in 0..archive.len() {
118            let mut file = archive.by_index(i)?;
119            // 文件内容
120            let mut contents = Vec::new();
121            // 读取文件内容到数组中
122            file.read_to_end(&mut contents)?;
123            // 匹配文件类型
124            match file.name() {
125                "word/document.xml" => {
126                    // 处理文档主内容,替换模板内容
127                    contents = self.process_document_xml(&contents)?;
128                }
129                "word/_rels/document.xml.rels" => {
130                    // 处理关系文件
131                    contents = self.process_rels_xml(&contents)?;
132                }
133                &_ => {}
134            }
135
136            // 写入新文件
137            let option = SimpleFileOptions::default()
138                .compression_method(file.compression())
139                .unix_permissions(file.unix_mode().unwrap_or(0o644));
140            // 写入内容
141            zip_writer.start_file(file.name(), option)?;
142            zip_writer.write_all(&contents)?;
143        }
144
145        // 4. 添加新的图片文件
146        for (_, replacement) in &self.image_replacements {
147            if let Some(replacement) = replacement {
148                let image_path = format!(
149                    "word/media/image_{}.{}",
150                    replacement.relation_id,
151                    DocxTemplate::get_extension(&replacement.image_path)?
152                );
153                // 写入图片到word压缩文件中
154                zip_writer.start_file(&image_path, SimpleFileOptions::default())?;
155                zip_writer.write_all(&replacement.image_data)?;
156            }
157        }
158        // 将内容写入压缩文件(docx)
159        zip_writer.finish()?;
160        Ok(())
161    }
162
163    fn process_element(&self, _element: &mut BytesStart) -> Result<(), DocxError> {
164        // println!("{:?}", String::from_utf8_lossy(element.name().as_ref()));
165        Ok(())
166    }
167
168    /// 处理文件内容
169    /// @param contents 文件内容数组
170    fn process_document_xml(&self, contents: &[u8]) -> Result<Vec<u8>, DocxError> {
171        // 创建xml写对象
172        let mut xml_writer = Writer::new(Cursor::new(Vec::new()));
173        // 写入xml文件头
174        // xml_writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), Some("yes"))))?;
175        // 读取xml文件的内容
176        let mut reader = quick_xml::Reader::from_reader(&contents[..]);
177        // 缓存数组
178        let mut buf = Vec::new();
179        // 图片对应的字符串占位符
180        let mut current_placeholder = String::new();
181        // 循环读取xml数据
182        loop {
183            // 读取数据
184            match reader.read_event_into(&mut buf)? {
185                Event::Start(e) => {
186                    let mut element = e.to_owned();
187                    self.process_element(&mut element)?;
188                    if e.name().as_ref() == b"w:p" {
189                        current_placeholder.clear();
190                    }
191                    xml_writer.write_event(Event::Start(element))?;
192                }
193                Event::Text(e) => {
194                    // 读取标签的内容
195                    let mut text = e.unescape()?.into_owned();
196                    // 替换文本占位符操作
197                    self.process_text(&mut text);
198                    // 判断图片占位符是否包含在内容
199                    if self.image_replacements.contains_key(&text) {
200                        current_placeholder.push_str(&text);
201                    } else {
202                        xml_writer.write_event(Event::Text(BytesText::new(text.as_str())))?;
203                    }
204                }
205                Event::End(e) => {
206                    // 判断标签是w:p,并且判断当前待替换的图片字符串是否为空
207                    if e.name().as_ref() == b"w:p" && !current_placeholder.is_empty() {
208                        if let Some(Some(docx_image)) =
209                            self.image_replacements.get(&current_placeholder)
210                        {
211                            // 替换占位符为图片
212                            DocxTemplate::create_drawing_element(
213                                &mut xml_writer,
214                                &docx_image.relation_id,
215                                docx_image.width,
216                                docx_image.height,
217                            )?;
218                        } else {
219                            // 保留原始占位符文本
220                            xml_writer.write_event(Event::Text(BytesText::from_escaped(
221                                // current_placeholder.as_str(),
222                                "",
223                            )))?;
224                        }
225                        current_placeholder.clear();
226                    }
227                    xml_writer.write_event(Event::End(e))?;
228                }
229                Event::Eof => break,
230                e => {
231                    // 写入原有信息
232                    xml_writer.write_event(e)?
233                }
234            }
235            buf.clear();
236        }
237        // 返回文件数组
238        Ok(xml_writer.into_inner().into_inner())
239    }
240
241    fn process_rels_xml(&self, xml_data: &[u8]) -> Result<Vec<u8>, DocxError> {
242        // 创建xml写对象
243        let mut writer = Writer::new(Cursor::new(Vec::new()));
244        // 写入xml标签头
245        writer.write_event(Event::Decl(BytesDecl::new(
246            "1.0",
247            Some("UTF-8"),
248            Some("yes"),
249        )))?;
250
251        // 写入XML根元素
252        writer.write_event(Event::Start(
253            BytesStart::new("Relationships").with_attributes([(
254                "xmlns",
255                "http://schemas.openxmlformats.org/package/2006/relationships",
256            )]),
257        ))?;
258
259        // 读取原始数据
260        let mut reader = quick_xml::Reader::from_reader(xml_data);
261        let mut buf = Vec::new();
262
263        loop {
264            // 读取关系文件
265            match reader.read_event_into(&mut buf)? {
266                // 判断关系文件内容是否为关联标签
267                Event::Empty(e) if e.name().as_ref() == b"Relationship" => {
268                    // 写入关系标签内容
269                    writer.write_event(Event::Empty(e))?;
270                }
271                // 文件读取完毕
272                Event::Eof => break,
273                _ => {}
274            }
275            // 清理内容
276            buf.clear();
277        }
278
279        // 添加新的图片关系
280        for (_, value) in &self.image_replacements {
281            if let Some(docx_image) = value {
282                // 获取图片扩展名
283                let extension = DocxTemplate::get_extension(&docx_image.image_path)?;
284                // 创建图片路径
285                let image_path = format!("media/image_{}.{}", docx_image.relation_id, extension);
286                // 创建图片关系标签
287                let relationship = BytesStart::new("Relationship").with_attributes([
288                    ("Id", docx_image.relation_id.as_str()),
289                    (
290                        "Type",
291                        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
292                    ),
293                    ("Target", &image_path),
294                ]);
295                // 写入关系标签数据
296                writer.write_event(Event::Empty(relationship))?;
297            }
298        }
299
300        // 结束根元素
301        writer.write_event(Event::End(BytesEnd::new("Relationships")))?;
302        // 输出关系文件内容
303        Ok(writer.into_inner().into_inner())
304    }
305
306    fn get_extension(image_path: &str) -> Result<&str, DocxError> {
307        Path::new(image_path)
308            .extension()
309            .and_then(|s| s.to_str())
310            .ok_or_else(|| {
311                DocxError::ImageNotFound("Could not determine image extension".to_string())
312            })
313    }
314    // 替换模板属性
315    fn process_text(&self, text: &mut String) {
316        for (placeholder, value) in &self.text_replacements {
317            *text = text.replace(placeholder, value);
318        }
319    }
320
321    fn create_drawing_element<T>(
322        writer: &mut Writer<T>,
323        relation_id: &str,
324        width: u64,
325        height: u64,
326    ) -> Result<(), DocxError>
327    where
328        T: Write,
329    {
330        let drawing = format!(
331            r#"
332        <w:drawing>
333            <wp:inline distT="0" distB="0" distL="0" distR="0">
334                <wp:extent cx="{}" cy="{}"/>
335                <wp:docPr id="1" name="Picture 1" descr="Generated image"/>
336                <wp:cNvGraphicFramePr>
337                    <a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
338                </wp:cNvGraphicFramePr>
339                <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
340                    <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
341                        <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
342                            <pic:nvPicPr>
343                                <pic:cNvPr id="0" name="Picture 1" descr="Generated image"/>
344                                <pic:cNvPicPr><a:picLocks noChangeAspect="1"/></pic:cNvPicPr>
345                            </pic:nvPicPr>
346                            <pic:blipFill>
347                                <a:blip r:embed="{}"/>
348                                <a:stretch>
349                                    <a:fillRect/>
350                                </a:stretch>
351                            </pic:blipFill>
352                            <pic:spPr>
353                                <a:xfrm>
354                                    <a:off x="0" y="0"/>
355                                    <a:ext cx="{}" cy="{}"/>
356                                </a:xfrm>
357                                <a:prstGeom prst="rect">
358                                    <a:avLst/>
359                                </a:prstGeom>
360                            </pic:spPr>
361                        </pic:pic>
362                    </a:graphicData>
363                </a:graphic>
364            </wp:inline>
365        </w:drawing>
366    "#,
367            width, height, relation_id, width, height,
368        );
369
370        let mut reader = quick_xml::Reader::from_str(&drawing);
371        reader.config_mut().trim_text(true);
372        let mut buf = Vec::new();
373
374        loop {
375            match reader.read_event_into(&mut buf)? {
376                Event::Eof => break,
377                e => {
378                    writer.write_event(e)?;
379                }
380            }
381        }
382        Ok(())
383    }
384}
385
386// 添加的图标对象
387struct DocxImage {
388    // 图片路径
389    pub image_path: String,
390    // 图片数据
391    pub image_data: Vec<u8>,
392    // 关联id
393    pub relation_id: String,
394    // 图片高度
395    pub width: u64,
396    // 图片高度
397    pub height: u64,
398}
399
400impl DocxImage {
401    /// 创建图片对象
402    /// @param image_path 图片路径
403    pub fn new(image_path: &str) -> Result<Self, DocxError> {
404        Self::new_size(image_path, 6.09, 5.9)
405    }
406    /// 设置图片大小
407    /// @param image_path 图片路径
408    /// @param width 图片宽度
409    /// @param height 图片高度
410    pub fn new_size(image_path: &str, width: f32, height: f32) -> Result<Self, DocxError> {
411        // 打开文件读取数据到数组中
412        let mut file = File::open(image_path)?;
413        let mut image_data = Vec::new();
414        file.read_to_end(&mut image_data)?;
415        DocxImage::new_image_data_size(image_path, image_data, width, height)
416    }
417
418    /// 设置图片大小
419    /// @param image_url 图片路径
420    /// @param image_data 图片数据
421    pub fn new_image_data(image_url: &str, image_data: Vec<u8>) -> Result<Self, DocxError> {
422        DocxImage::new_image_data_size(image_url, image_data, 6.09, 5.9)
423    }
424
425    /// 设置图片大小
426    /// @param image_url 图片路径
427    /// @param image_data 图片数据
428    /// @param width 图片宽度
429    /// @param height 图片高度
430    pub fn new_image_data_size(
431        image_url: &str,
432        image_data: Vec<u8>,
433        width: f32,
434        height: f32,
435    ) -> Result<Self, DocxError> {
436        Ok(DocxImage {
437            image_path: image_url.to_string(),
438            relation_id: format!("rId{}", Uuid::new_v4().simple()),
439            width: (width * 360000.0) as u64,
440            height: (height * 360000.0) as u64,
441            image_data,
442        })
443    }
444}
445
446#[derive(Error, Debug)]
447pub enum DocxError {
448    #[error("IO error: {0}")]
449    Io(#[from] std::io::Error),
450    #[error("Zip error: {0}")]
451    Zip(#[from] zip::result::ZipError),
452    #[error("XML error: {0}")]
453    Xml(#[from] quick_xml::Error),
454    #[error("UTF-8 error: {0}")]
455    Utf8(#[from] std::string::FromUtf8Error),
456    #[error("Image not found: {0}")]
457    ImageNotFound(String),
458    #[error("Image url not found: {0}")]
459    ImageUrlFound(#[from] reqwest::Error),
460}