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