docx_handlebars/
template.rs1use 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 validate_docx_format(&zip_bytes)?;
20
21 let cursor = Cursor::new(zip_bytes);
23 let mut archive = ZipArchive::new(cursor)?;
24
25 let files: Arc<Mutex<HashMap<String, Vec<u8>>>> = Arc::new(Mutex::new(HashMap::new()));
27
28 for i in 0..archive.len() {
30 let mut file = archive.by_index(i)?;
31 let file_name = file.name().to_string();
32
33 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 {
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); register_basic_helpers(&mut handlebars)?;
57
58 let files_ref = Arc::clone(&files);
59 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 let parsed_json;
84 let options = if let Some(opt) = options {
85 if opt.is_object() {
86 Some(opt)
88 } else if let Some(json_str) = opt.as_str() {
89 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 let options_anchor = options_anchor.unwrap_or_default().unwrap_or_default();
105 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 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 let rid = Uuid::new_v4().to_string().replace("-", "");
140 let rid = format!("a{rid}");
141
142 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 };
153
154 let anchor_id = Uuid::new_v4().to_string().replace("-", "").to_uppercase();
156 let anchor_id = &anchor_id[..8]; let edit_id = Uuid::new_v4().to_string().replace("-", "").to_uppercase();
158 let edit_id = &edit_id[..8]; 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 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 let image_data = general_purpose::STANDARD.decode(src).map_err(|e| {
190 RenderErrorReason::Other(format!("Failed to decode base64 image: {e}"))
191 })?;
192
193 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 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 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 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 let cx = target_w * 9525; let cy = target_h * 9525; let position_h = if let Some(h) = options_position_h {
224 h * 9525 } else {
226 -((target_w as i64 / 2) * 9525) };
228
229 let position_v = if let Some(v) = options_position_v {
230 v * 9525 } else {
232 -((target_h as i64 / 2) * 9525) };
234
235 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}\" />")); 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}\" />")); 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}\">")); 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(&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 {
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 files_mut.insert(file_name.to_string(), content_types_content.into_bytes());
322 }
323
324 } else {
325 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 handlebars_helper!(removeTableRow: | | {
386 REMOVE_TABLE_ROW_KEY
387 });
388 handlebars.register_helper("removeTableRow", Box::new(removeTableRow));
389
390 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 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 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 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)); 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}