docx_handlebars/
template.rs

1use serde_json::Value;
2use std::{io::{Cursor, Read, Write}, sync::{Arc, Mutex}};
3use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions};
4use std::collections::HashMap;
5use crate::{utils::{merge_handlebars_in_xml, register_basic_helpers, remove_table_row_simple, validate_docx_format}, DocxError};
6use crate::imagesize::get_image_dimensions;
7
8use handlebars::{Handlebars, RenderErrorReason, handlebars_helper};
9use uuid::Uuid;
10use base64::{Engine as _, engine::general_purpose};
11
12const REMOVE_TABLE_ROW_KEY: &str = "d53e6de6-fb82-4ca8-95aa-2bc56b6d5791";
13
14pub fn render_template(
15  zip_bytes: Vec<u8>,
16  data: &Value,
17) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
18  // 首先验证输入是否为有效的 DOCX 文件
19  validate_docx_format(&zip_bytes)?;
20  
21  // 创建一个 Cursor 来读取 zip 字节
22  let cursor = Cursor::new(zip_bytes);
23  let mut archive = ZipArchive::new(cursor)?;
24  
25  // 存储解压缩的文件内容
26  let files: Arc<Mutex<HashMap<String, Vec<u8>>>> = Arc::new(Mutex::new(HashMap::new()));
27  
28  // 解压缩所有文件
29  for i in 0..archive.len() {
30    let mut file = archive.by_index(i)?;
31    let file_name = file.name().to_string();
32    
33    // 跳过目录项
34    if file_name.ends_with('/') {
35      continue;
36    }
37    
38    let mut contents = Vec::new();
39    file.read_to_end(&mut contents)?;
40    files.lock().map_err(|e| Box::new(std::io::Error::other(format!("Failed to lock files: {e}"))))?.insert(file_name, contents);
41  }
42  
43  // 处理 document.xml 文件
44  {
45    let file_name = "word/document.xml";
46    let contents = files.lock().map_err(|e| Box::new(std::io::Error::other(format!("Failed to lock files: {e}"))))?.remove(file_name);
47    if let Some(contents) = contents {
48      let xml_content = String::from_utf8(contents.clone())?;
49      
50      let xml_content = merge_handlebars_in_xml(xml_content)?;
51  
52      let mut handlebars = Handlebars::new();
53      
54      handlebars.set_strict_mode(false); // 允许未定义的变量
55      
56      register_basic_helpers(&mut handlebars)?;
57      
58      let files_ref = Arc::clone(&files);
59      // 注册 img helper
60      handlebars.register_helper("img", Box::new(
61        move |
62          h: &handlebars::Helper,
63          _r: &Handlebars,
64          _ctx: &handlebars::Context,
65          _rc: &mut handlebars::RenderContext,
66          out: &mut dyn handlebars::Output
67        | -> Result<(), handlebars::RenderError> {
68          let src = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
69          if src.is_empty() {
70            return Ok(());
71          }
72          let width_param = h.param(1).and_then(|v| v.value().as_u64());
73          let height_param = h.param(2).and_then(|v| v.value().as_u64());
74          
75          let width_param = width_param.and_then(|w| if w > 0 { Some(w) } else { None });
76          let height_param = height_param.and_then(|h| if h > 0 { Some(h) } else { None });
77          
78          let options = h.param(3).map(|v| v.value());
79          
80          // 支持两种方式传递 options:
81          // 1. 直接传对象:image.options
82          // 2. 传 JSON 字符串:'{"anchor":true,"behind_doc":false}'
83          let parsed_json;
84          let options = if let Some(opt) = options {
85            if opt.is_object() {
86              // 已经是对象,直接使用
87              Some(opt)
88            } else if let Some(json_str) = opt.as_str() {
89              // 是字符串,尝试解析为 JSON
90              parsed_json = serde_json::from_str::<serde_json::Value>(json_str).ok();
91              parsed_json.as_ref()
92            } else {
93              None
94            }
95          } else {
96            None
97          };
98          
99          let options_anchor = options.and_then(|o|
100            o.get("anchor")
101             .map(|v| v.as_bool())
102          );
103          // 是否是浮动图片, 默认为 false
104          let options_anchor = options_anchor.unwrap_or_default().unwrap_or_default();
105          // 图片是否覆盖文字
106          let options_behind_doc = options.and_then(|o|
107            o.get("behind_doc")
108              .map(|v| v.as_bool())
109              .unwrap_or_default()
110          ).unwrap_or_default();
111          let behind_doc = if options_behind_doc {
112            "1"
113          } else {
114            "0"
115          };
116          // allowOverlap 是否允许与其他对象重叠
117          let options_allow_overlap = options.and_then(|o|
118            o.get("allow_overlap")
119              .map(|v| v.as_bool())
120              .unwrap_or(Some(true))
121          ).unwrap_or(true);
122          let allow_overlap = if options_allow_overlap {
123            "1"
124          } else {
125            "0"
126          };
127          
128          let options_position_h = options.and_then(|o|
129            o.get("position_h")
130              .and_then(|v| v.as_i64())
131          );
132          
133          let options_position_v = options.and_then(|o|
134            o.get("position_v")
135              .and_then(|v| v.as_i64())
136          );
137          
138          // 生成唯一的关系 ID 和图片 ID
139          let rid = Uuid::new_v4().to_string().replace("-", "");
140          let rid = format!("a{rid}");
141          
142          // 生成唯一的图片内部 ID(用于 docPr 和 cNvPr)
143          // 使用 UUID 的简单哈希作为数字 ID
144          let pic_id = {
145            let uuid = Uuid::new_v4();
146            let uuid_bytes = uuid.as_bytes();
147            let mut id = 0u32;
148            for (i, &byte) in uuid_bytes.iter().take(4).enumerate() {
149              id |= (byte as u32) << (i * 8);
150            }
151            (id % 899999999) + 100000000 // 确保是9位数
152          };
153          
154          // 生成唯一的锚点 ID
155          let anchor_id = Uuid::new_v4().to_string().replace("-", "").to_uppercase();
156          let anchor_id = &anchor_id[..8]; // 取前8位
157          let edit_id = Uuid::new_v4().to_string().replace("-", "").to_uppercase();
158          let edit_id = &edit_id[..8]; // 取前8位
159          
160          let mut files_mut = files_ref.lock().map_err(|e| {
161            RenderErrorReason::Other(e.to_string())
162          })?;
163          
164          {
165            let file_name = "word/_rels/document.xml.rels";
166            if let Some(contents) = files_mut.remove(file_name) {
167              let rels_content = String::from_utf8(contents)?;
168              let new_rels_content = rels_content.replace(
169                "</Relationships>",
170                &format!(
171                  "<Relationship Id=\"{rid}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\" Target=\"media/{rid}.png\"></Relationship></Relationships>",
172                ),
173              );
174              files_mut.insert(file_name.to_string(), new_rels_content.into_bytes());
175            } else {
176              // 如果没有找到关系文件,则创建一个新的
177              let new_rels_content = format!(
178                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"{rid}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\" Target=\"media/{rid}.png\"></Relationship></Relationships>",
179              );
180              files_mut.insert("word/_rels/document.xml.rels".to_string(), new_rels_content.into_bytes());
181            }
182          }
183          
184          {
185            let file_name = format!("word/media/{rid}.png");
186            // image_data 写入文件系统用于调试
187            // std::fs::write(format!("D:/{rid}.base64"), &src)?;
188            // src 是base64 编码的图片数据
189            let image_data = general_purpose::STANDARD.decode(src).map_err(|e| {
190              RenderErrorReason::Other(format!("Failed to decode base64 image: {e}"))
191            })?;
192            
193            // 获取图片的宽高
194            let (orig_w, orig_h) = get_image_dimensions(&image_data).ok_or_else(|| {
195              RenderErrorReason::Other("Failed to get image dimensions".to_string())
196            })?;
197            
198            // 计算目标宽高
199            let (target_w, target_h) = match (width_param, height_param) {
200              (Some(w), Some(h)) => (w as u32, h as u32),
201              (Some(w), None) => {
202                // 只传 width,等比缩放 height
203                let h = (orig_h as f64 * w as f64 / orig_w as f64).round() as u32;
204                (w as u32, h)
205              }
206              (None, Some(h)) => {
207                // 只传 height,等比缩放 width
208                let w = (orig_w as f64 * h as f64 / orig_h as f64).round() as u32;
209                (w, h as u32)
210              }
211              (None, None) => (orig_w, orig_h),
212            };
213            
214            files_mut.insert(file_name, image_data);
215            
216            let mut str = String::new();
217            
218            // 根据图片宽高计算 cx 和 cy
219            let cx = target_w * 9525; // 1px = 9525 EMU
220            let cy = target_h * 9525; // 1px = 9525 EMU
221            
222            // 计算位置偏移(仅对 anchor 模式有效)
223            let position_h = if let Some(h) = options_position_h {
224                h * 9525 // 用户传入像素,转换为 EMU(支持负数)
225            } else {
226                -((target_w as i64 / 2) * 9525) // 默认使用图片宽度一半的负值,实现居中效果
227            };
228            
229            let position_v = if let Some(v) = options_position_v {
230                v * 9525 // 用户传入像素,转换为 EMU(支持负数)
231            } else {
232                -((target_h as i64 / 2) * 9525) // 默认使用图片高度一半的负值,实现居中效果
233            };
234            
235            // let position_h = 0;
236            // let position_v = 0;
237            
238            str.push_str("</w:t></w:r>");
239            str.push_str("<w:r>");
240            str.push_str("<w:drawing>");
241              if !options_anchor {
242                str.push_str(&format!("<wp:inline distT=\"0\" distB=\"0\" distL=\"0\" distR=\"0\" wp14:anchorId=\"{anchor_id}\" wp14:editId=\"{edit_id}\">"));
243              } else {
244                str.push_str(&format!("<wp:anchor distT=\"0\" distB=\"0\" distL=\"114300\" distR=\"114300\" simplePos=\"0\" relativeHeight=\"251658240\" behindDoc=\"{behind_doc}\" locked=\"0\" layoutInCell=\"1\" allowOverlap=\"{allow_overlap}\" wp14:anchorId=\"{anchor_id}\" wp14:editId=\"{edit_id}\">"));
245                str.push_str("<wp:simplePos x=\"0\" y=\"0\" />");
246                str.push_str("<wp:positionH relativeFrom=\"column\">");
247                  str.push_str(&format!("<wp:posOffset>{position_h}</wp:posOffset>"));
248                str.push_str("</wp:positionH>");
249                str.push_str("<wp:positionV relativeFrom=\"paragraph\">");
250                  str.push_str(&format!("<wp:posOffset>{position_v}</wp:posOffset>"));
251                str.push_str("</wp:positionV>");
252              }
253                str.push_str(&format!("<wp:extent cx=\"{cx}\" cy=\"{cy}\" />"));
254                str.push_str("<wp:effectExtent l=\"0\" t=\"0\" r=\"0\" b=\"0\" />");
255                if options_anchor {
256                  str.push_str("<wp:wrapNone />");
257                }
258                str.push_str(&format!("<wp:docPr id=\"{pic_id}\" name=\"{rid}\" />")); // 图片 ID
259                str.push_str("<wp:cNvGraphicFramePr>");
260                  str.push_str("<a:graphicFrameLocks xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" noChangeAspect=\"1\" />");
261                str.push_str("</wp:cNvGraphicFramePr>");
262                str.push_str("<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">");
263                  str.push_str("<a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">");
264                    str.push_str("<pic:pic xmlns:pic=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">");
265                      str.push_str("<pic:nvPicPr>");
266                        str.push_str(&format!("<pic:cNvPr id=\"{pic_id}\" name=\"{rid}\" />")); // 图片 ID
267                        str.push_str("<pic:cNvPicPr />");
268                      str.push_str("</pic:nvPicPr>");
269                      str.push_str("<pic:blipFill>");
270                        str.push_str(&format!("<a:blip r:embed=\"{rid}\">")); // rId4
271                          str.push_str("<a:extLst>");
272                            str.push_str("<a:ext uri=\"{28A0092B-C50C-407E-A947-70E740481C1C}\">");
273                              str.push_str("<a14:useLocalDpi xmlns:a14=\"http://schemas.microsoft.com/office/drawing/2010/main\" val=\"0\" />");
274                            str.push_str("</a:ext>");
275                          str.push_str("</a:extLst>");
276                        str.push_str("</a:blip>");
277                        str.push_str("<a:stretch>");
278                          str.push_str("<a:fillRect />");
279                        str.push_str("</a:stretch>");
280                      str.push_str("</pic:blipFill>");
281                      str.push_str("<pic:spPr>");
282                        str.push_str("<a:xfrm>");
283                          str.push_str("<a:off x=\"0\" y=\"0\" />");
284                          // str.push_str("<a:ext cx=\"2220115\" cy=\"744039\" />");
285                          str.push_str(&format!("<a:ext cx=\"{cx}\" cy=\"{cy}\" />"));
286                        str.push_str("</a:xfrm>");
287                        str.push_str("<a:prstGeom prst=\"rect\">");
288                          str.push_str("<a:avLst />");
289                        str.push_str("</a:prstGeom>");
290                      str.push_str("</pic:spPr>");
291                    str.push_str("</pic:pic>");
292                  str.push_str("</a:graphicData>");
293                str.push_str("</a:graphic>");
294              if !options_anchor {
295                str.push_str("</wp:inline>");
296              } else {
297                str.push_str("</wp:anchor>");
298              }
299            str.push_str("</w:drawing>");
300            str.push_str("</w:r>");
301            str.push_str("<w:r>");
302            str.push_str("<w:t>");
303          
304            out.write(&str)?;
305          }
306          
307          // [Content_Types].xml
308          {
309            let file_name = "[Content_Types].xml";
310            if let Some(contents) = files_mut.remove(file_name) {
311              let content_types_content = String::from_utf8(contents)?;
312              
313              if !content_types_content.contains(" Extension=\"png\" ") {
314                let new_content_types_content = content_types_content.replace(
315                  "</Types>",
316                  "<Default Extension=\"png\" ContentType=\"image/png\" /></Types>",
317                );
318                files_mut.insert(file_name.to_string(), new_content_types_content.into_bytes());
319              } else {
320                // 如果已经存在 png 的默认类型,则不重复添加
321                files_mut.insert(file_name.to_string(), content_types_content.into_bytes());
322              }
323              
324            } else {
325              // 如果没有找到 [Content_Types].xml 文件,则创建一个新的
326              let new_content_types_content = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\"><Default Extension=\"png\" ContentType=\"image/png\" /></Types>".to_string();
327              files_mut.insert(file_name.to_string(), new_content_types_content.into_bytes());
328            }
329          }
330          
331          Ok(())
332        },
333      ));
334      
335      // 附件
336      /*
337      let files_ref = Arc::clone(&files);
338      handlebars.register_helper("att", Box::new(
339        move |
340          h: &handlebars::Helper,
341          _r: &Handlebars,
342          _ctx: &handlebars::Context,
343          _rc: &mut handlebars::RenderContext,
344          out: &mut dyn handlebars::Output
345        | -> Result<(), handlebars::RenderError> {
346          
347          let src = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
348          if src.is_empty() {
349            return Ok(());
350          }
351          
352          let rid = Uuid::new_v4().to_string().replace("-", "");
353          let rid = format!("a{rid}");
354          let mut files_mut = files_ref.lock().map_err(|e| {
355            RenderErrorReason::Other(e.to_string())
356          })?;
357          
358          // word/_rels/document.xml.rels
359          {
360            let file_name = "word/_rels/document.xml.rels";
361            if let Some(contents) = files_mut.remove(file_name) {
362              let rels_content = String::from_utf8(contents)?;
363              let new_rels_content = rels_content.replace(
364                "</Relationships>",
365                &format!(
366                  "<Relationship Id=\"{rid}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\" Target=\"media/{rid}.png\"></Relationship></Relationships>",
367                ),
368              );
369              files_mut.insert(file_name.to_string(), new_rels_content.into_bytes());
370            } else {
371              // 如果没有找到关系文件,则创建一个新的
372              let new_rels_content = format!(
373                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"><Relationship Id=\"{rid}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\" Target=\"media/{rid}.png\"></Relationship></Relationships>",
374              );
375              files_mut.insert("word/_rels/document.xml.rels".to_string(), new_rels_content.into_bytes());
376            }
377          }
378          
379          Ok(())
380        },
381      ));
382      */
383      
384      // 标记删除表格的一行
385      handlebars_helper!(removeTableRow: | | {
386        REMOVE_TABLE_ROW_KEY
387      });
388      handlebars.register_helper("removeTableRow", Box::new(removeTableRow));
389      
390      // std::fs::write("./document0.xml", &xml_content)?;
391      
392      // 渲染模板
393      let xml_content = handlebars.render_template(&xml_content, data)
394        .map_err(|e| {
395          let reason: &RenderErrorReason = e.reason();
396          DocxError::TemplateRenderError(reason.to_string())
397        })?;
398      
399      let xml_content = fix_drawing_with_placeholders(xml_content)?;
400      
401      // std::fs::write("./document.xml", &xml_content)?;
402      
403      files.lock().map_err(|e| Box::new(std::io::Error::other(format!("Failed to lock files: {e}"))))?.insert(file_name.to_string(), xml_content.into_bytes());
404    }
405  }
406  
407  // Extract files from Arc<Mutex<_>>
408  let files = Arc::try_unwrap(files).map_err(|_| Box::new(std::io::Error::other("Failed to unwrap Arc")))?.into_inner().map_err(|e| Box::new(std::io::Error::other(format!("Failed to get inner value: {e:?}"))))?;
409  
410  // 重新压缩文件
411  let mut output = Vec::new();
412  {
413    let cursor = Cursor::new(&mut output);
414    let mut zip_writer = ZipWriter::new(cursor);
415    
416    for entry in files {
417      let (file_name, contents): (String, Vec<u8>) = entry;
418      let options = SimpleFileOptions::default()
419        .compression_method(zip::CompressionMethod::Deflated)
420        .compression_level(Some(6)); // 设置压缩级别
421      
422      zip_writer.start_file(file_name, options)?;
423      zip_writer.write_all(&contents)?;
424    }
425    
426    zip_writer.finish()?;
427  }
428  
429  Ok(output)
430}
431
432fn fix_drawing_with_placeholders(xml_content: String) -> Result<String, Box<dyn std::error::Error>> {
433  let mut fixed_content = xml_content;
434  
435  fixed_content = fixed_content.replace("<w:t><w:drawing>", "<w:drawing>");
436  fixed_content = fixed_content.replace("</w:drawing></w:t>", "</w:drawing>");
437  
438  if fixed_content.contains(REMOVE_TABLE_ROW_KEY) {
439    fixed_content = remove_table_row_simple(
440      &fixed_content,
441      REMOVE_TABLE_ROW_KEY,
442    )?;
443  }
444  
445  Ok(fixed_content)
446}