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 let mut handlebars = Handlebars::new();
45 handlebars.set_strict_mode(false); register_basic_helpers(&mut handlebars)?;
47
48 {
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 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 {
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 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 let parsed_json;
113 let options = if let Some(opt) = options {
114 if opt.is_object() {
115 Some(opt)
117 } else if let Some(json_str) = opt.as_str() {
118 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 let options_anchor = options_anchor.unwrap_or_default().unwrap_or_default();
134 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 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 let rid = Uuid::new_v4().to_string().replace("-", "");
169 let rid = format!("a{rid}");
170
171 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 };
182
183 let anchor_id = Uuid::new_v4().to_string().replace("-", "").to_uppercase();
185 let anchor_id = &anchor_id[..8]; let edit_id = Uuid::new_v4().to_string().replace("-", "").to_uppercase();
187 let edit_id = &edit_id[..8]; 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 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 let image_data = general_purpose::STANDARD.decode(src).map_err(|e| {
219 RenderErrorReason::Other(format!("Failed to decode base64 image: {e}"))
220 })?;
221
222 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 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 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 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 let cx = target_w * 9525; let cy = target_h * 9525; let position_h = if let Some(h) = options_position_h {
253 h * 9525 } else {
255 -((target_w as i64 / 2) * 9525) };
257
258 let position_v = if let Some(v) = options_position_v {
259 v * 9525 } else {
261 -((target_h as i64 / 2) * 9525) };
263
264 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}\" />")); 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}\" />")); 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}\">")); 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(&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 {
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 files_mut.insert(file_name.to_string(), content_types_content.into_bytes());
351 }
352
353 } else {
354 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 handlebars_helper!(removeTableRow: | | {
415 REMOVE_TABLE_ROW_KEY
416 });
417 handlebars.register_helper("removeTableRow", Box::new(removeTableRow));
418
419 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 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 {
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 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 drop(handlebars);
468
469 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 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)); 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}