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