1use crate::error::DocxError;
2use crate::image::{DOCX_EMU, DocxImage};
3use quick_xml::Writer;
4use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
5use reqwest::Client;
6use std::collections::HashMap;
7use std::fs::File;
8use std::io::{Cursor, Read, Write};
9use std::path::Path;
10use std::time::Duration;
11use zip::write::SimpleFileOptions;
12use zip::{ZipArchive, ZipWriter};
13
14pub struct DocxTemplate {
15 text_replacements: HashMap<String, String>,
17 image_replacements: HashMap<String, Option<DocxImage>>,
19 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)) .build()
31 .unwrap(),
32 }
33 }
34
35 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 pub fn add_image_file_replacement(
47 &mut self,
48 placeholder: &str,
49 image_path: Option<&str>,
50 ) -> Result<(), DocxError> {
51 match image_path {
52 None => {
53 self.image_replacements
55 .insert(placeholder.to_string(), None);
56 }
57 Some(data) => {
58 self.image_replacements
60 .insert(placeholder.to_string(), Some(DocxImage::new(data)?));
61 }
62 }
63
64 Ok(())
65 }
66
67 pub fn add_image_file_size_replacement(
73 &mut self,
74 placeholder: &str,
75 image_path: Option<&str>,
76 width: f32,
77 height: f32,
78 ) -> Result<(), DocxError> {
79 match image_path {
80 None => {
81 self.image_replacements
83 .insert(placeholder.to_string(), None);
84 }
85 Some(file_path) => {
86 let width_emu = (width * DOCX_EMU) as u64;
88 let height_emu = (height * DOCX_EMU) as u64;
89 self.image_replacements.insert(
91 placeholder.to_string(),
92 Some(DocxImage::new_size(file_path, width_emu, height_emu)?),
93 );
94 }
95 }
96
97 Ok(())
98 }
99
100 pub async fn add_image_url_replacement(
104 &mut self,
105 placeholder: &str,
106 image_url: Option<&str>,
107 ) -> Result<(), DocxError> {
108 match image_url {
109 None => {
110 self.image_replacements
112 .insert(placeholder.to_string(), None);
113 }
114 Some(url) => {
115 let response = self.client.get(url).send().await?;
117 if response.status().is_success() {
119 let image_data = response.bytes().await?.to_vec();
121 self.image_replacements.insert(
123 placeholder.to_string(),
124 Some(DocxImage::new_image_data(url, image_data)?),
125 );
126 }
127 }
128 }
129
130 Ok(())
131 }
132
133 pub async fn add_image_url_size_replacement(
139 &mut self,
140 placeholder: &str,
141 image_url: Option<&str>,
142 width: f32,
143 height: f32,
144 ) -> Result<(), DocxError> {
145 match image_url {
146 None => {
147 self.image_replacements
149 .insert(placeholder.to_string(), None);
150 }
151 Some(url) => {
152 let response = self.client.get(url).send().await?;
154 if response.status().is_success() {
156 let image_data = response.bytes().await?.to_vec();
158 let width_emu = (width * DOCX_EMU) as u64;
160 let height_emu = (height * DOCX_EMU) as u64;
161 self.image_replacements.insert(
163 placeholder.to_string(),
164 Some(DocxImage::new_image_data_size(
165 url, image_data, width_emu, height_emu,
166 )?),
167 );
168 }
169 }
170 }
171
172 Ok(())
173 }
174
175 pub fn process_template(
179 &self,
180 template_path: &str,
181 output_path: &str,
182 ) -> Result<(), DocxError> {
183 let template_file = File::open(template_path)?;
185 let mut archive = ZipArchive::new(template_file)?;
186
187 let output_file = File::create(output_path)?;
189 let mut zip_writer = ZipWriter::new(output_file);
190
191 for i in 0..archive.len() {
193 let mut file = archive.by_index(i)?;
194 let mut contents = Vec::new();
196 file.read_to_end(&mut contents)?;
198 match file.name() {
200 "word/document.xml" => {
201 contents = self.process_document_xml(&contents)?;
203 }
204 "word/_rels/document.xml.rels" => {
205 contents = self.process_rels_xml(&contents)?;
207 }
208 &_ => {}
209 }
210
211 let option = SimpleFileOptions::default()
213 .compression_method(file.compression())
214 .unix_permissions(file.unix_mode().unwrap_or(0o644));
215 zip_writer.start_file(file.name(), option)?;
217 zip_writer.write_all(&contents)?;
218 }
219
220 for replacement in self.image_replacements.values().flatten() {
222 let image_path = format!(
223 "word/media/image_{}.{}",
224 replacement.relation_id,
225 DocxTemplate::get_extension(&replacement.image_path)?
226 );
227 zip_writer.start_file(&image_path, SimpleFileOptions::default())?;
229 zip_writer.write_all(&replacement.image_data)?;
230 }
231 zip_writer.finish()?;
233 Ok(())
234 }
235
236 fn process_element(&self, _element: &mut BytesStart) -> Result<(), DocxError> {
237 Ok(())
240 }
241
242 fn process_document_xml(&self, contents: &[u8]) -> Result<Vec<u8>, DocxError> {
245 let mut xml_writer = Writer::new(Cursor::new(Vec::new()));
247 let mut reader = quick_xml::Reader::from_reader(contents);
251 reader.config_mut().trim_text(true);
252 let mut buf = Vec::new();
254 let mut current_placeholder = String::new();
256 loop {
258 match reader.read_event_into(&mut buf)? {
260 Event::Start(e) => {
261 let mut element = e.to_owned();
262 self.process_element(&mut element)?;
263 if current_placeholder.is_empty() {
265 xml_writer.write_event(Event::Start(element))?;
266 }
267 }
268 Event::Text(e) => {
269 let mut text = e.unescape()?.into_owned();
271 if text.starts_with("{{") {
273 if text.ends_with("}}") {
275 self.process_text(&mut text);
277 if self.image_replacements.contains_key(&text) {
279 current_placeholder.push_str(&text);
280 } else {
281 xml_writer
282 .write_event(Event::Text(BytesText::new(text.as_str())))?;
283 }
284 } else {
285 current_placeholder.push_str(&text);
287 }
288 } else {
289 if current_placeholder.is_empty() {
291 xml_writer.write_event(Event::Text(BytesText::new(text.as_str())))?;
293 } else {
294 current_placeholder.push_str(text.as_str());
296 if current_placeholder.ends_with("}}")
298 && current_placeholder.starts_with("{{")
299 {
300 self.process_text(&mut current_placeholder);
302 if !self.image_replacements.contains_key(¤t_placeholder) {
304 xml_writer.write_event(Event::Text(BytesText::new(
305 current_placeholder.as_str(),
306 )))?;
307 current_placeholder.clear();
309 }
310 }
311 }
312 }
313 }
314 Event::End(e) => {
315 if current_placeholder.is_empty() {
317 xml_writer.write_event(Event::End(e))?;
318 }else if current_placeholder.starts_with("{{") && current_placeholder.ends_with("}}"){
319 if e.name().as_ref() == b"w:p" {
321 if let Some(Some(docx_image)) =
323 self.image_replacements.get(¤t_placeholder)
324 {
325 DocxTemplate::create_drawing_element(
327 &mut xml_writer,
328 &docx_image.relation_id,
329 docx_image.width,
330 docx_image.height,
331 )?;
332 }
333 current_placeholder.clear();
335 }
336 xml_writer.write_event(Event::End(e))?;
338 }
339
340 }
341 Event::Eof => break,
342 Event::Empty(e) => {
343 if current_placeholder.is_empty() {
345 xml_writer.write_event(Event::Empty(e))?;
346 }
347 },
348 e => {
349 xml_writer.write_event(e)?;
350 },
351 }
352 buf.clear();
353 }
354 Ok(xml_writer.into_inner().into_inner())
356 }
357
358 fn process_rels_xml(&self, xml_data: &[u8]) -> Result<Vec<u8>, DocxError> {
359 let mut writer = Writer::new(Cursor::new(Vec::new()));
361 writer.write_event(Event::Decl(BytesDecl::new(
363 "1.0",
364 Some("UTF-8"),
365 Some("yes"),
366 )))?;
367
368 writer.write_event(Event::Start(
370 BytesStart::new("Relationships").with_attributes([(
371 "xmlns",
372 "http://schemas.openxmlformats.org/package/2006/relationships",
373 )]),
374 ))?;
375
376 let mut reader = quick_xml::Reader::from_reader(xml_data);
378 reader.config_mut().trim_text(true);
379 let mut buf = Vec::new();
380
381 loop {
382 match reader.read_event_into(&mut buf)? {
384 Event::Empty(e) if e.name().as_ref() == b"Relationship" => {
386 writer.write_event(Event::Empty(e))?;
388 }
389 Event::Eof => break,
391 _ => {}
392 }
393 buf.clear();
395 }
396
397 for docx_image in self.image_replacements.values().flatten() {
399 let extension = DocxTemplate::get_extension(&docx_image.image_path)?;
401 let image_path = format!("media/image_{}.{}", docx_image.relation_id, extension);
403 let relationship = BytesStart::new("Relationship").with_attributes([
405 ("Id", docx_image.relation_id.as_str()),
406 (
407 "Type",
408 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
409 ),
410 ("Target", &image_path),
411 ]);
412 writer.write_event(Event::Empty(relationship))?;
414 }
415
416 writer.write_event(Event::End(BytesEnd::new("Relationships")))?;
418 Ok(writer.into_inner().into_inner())
420 }
421
422 fn get_extension(image_path: &str) -> Result<&str, DocxError> {
423 Path::new(image_path)
424 .extension()
425 .and_then(|s| s.to_str())
426 .ok_or_else(|| {
427 DocxError::ImageNotFound("Could not determine image extension".to_string())
428 })
429 }
430 fn process_text(&self, text: &mut String) {
432 for (placeholder, value) in &self.text_replacements {
433 *text = text.replace(placeholder, value);
434 }
435 }
436
437 fn create_drawing_element<T>(
438 writer: &mut Writer<T>,
439 relation_id: &str,
440 width: u64,
441 height: u64,
442 ) -> Result<(), DocxError>
443 where
444 T: Write,
445 {
446 let drawing = format!(
447 r#"
448 <w:drawing>
449 <wp:inline distT="0" distB="0" distL="0" distR="0">
450 <wp:extent cx="{}" cy="{}"/>
451 <wp:docPr id="1" name="Picture 1" descr="Generated image"/>
452 <wp:cNvGraphicFramePr>
453 <a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
454 </wp:cNvGraphicFramePr>
455 <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
456 <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
457 <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
458 <pic:nvPicPr>
459 <pic:cNvPr id="0" name="Picture 1" descr="Generated image"/>
460 <pic:cNvPicPr><a:picLocks noChangeAspect="1"/></pic:cNvPicPr>
461 </pic:nvPicPr>
462 <pic:blipFill>
463 <a:blip r:embed="{}"/>
464 <a:stretch>
465 <a:fillRect/>
466 </a:stretch>
467 </pic:blipFill>
468 <pic:spPr>
469 <a:xfrm>
470 <a:off x="0" y="0"/>
471 <a:ext cx="{}" cy="{}"/>
472 </a:xfrm>
473 <a:prstGeom prst="rect">
474 <a:avLst/>
475 </a:prstGeom>
476 </pic:spPr>
477 </pic:pic>
478 </a:graphicData>
479 </a:graphic>
480 </wp:inline>
481 </w:drawing>
482 "#,
483 width, height, relation_id, width, height,
484 );
485
486 let mut reader = quick_xml::Reader::from_str(&drawing);
487 reader.config_mut().trim_text(true);
488 let mut buf = Vec::new();
489
490 loop {
491 match reader.read_event_into(&mut buf)? {
492 Event::Eof => break,
493 e => {
494 writer.write_event(e)?;
495 }
496 }
497 }
498 Ok(())
499 }
500}
501
502impl Default for DocxTemplate {
503 fn default() -> Self {
504 Self::new()
505 }
506}