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 {
222 if let Some(replacement) = replacement {
223 let image_path = format!(
224 "word/media/image_{}.{}",
225 replacement.relation_id,
226 DocxTemplate::get_extension(&replacement.image_path)?
227 );
228 zip_writer.start_file(&image_path, SimpleFileOptions::default())?;
230 zip_writer.write_all(&replacement.image_data)?;
231 }
232 }
233 zip_writer.finish()?;
235 Ok(())
236 }
237
238 fn process_element(&self, _element: &mut BytesStart) -> Result<(), DocxError> {
239 Ok(())
241 }
242
243 fn process_document_xml(&self, contents: &[u8]) -> Result<Vec<u8>, DocxError> {
246 let mut xml_writer = Writer::new(Cursor::new(Vec::new()));
248 let mut reader = quick_xml::Reader::from_reader(&contents[..]);
252 reader.config_mut().trim_text(true);
253 let mut buf = Vec::new();
255 let mut current_placeholder = String::new();
257 loop {
259 match reader.read_event_into(&mut buf)? {
261 Event::Start(e) => {
262 let mut element = e.to_owned();
263 self.process_element(&mut element)?;
264 if e.name().as_ref() == b"w:p" {
265 current_placeholder.clear();
266 }
267 xml_writer.write_event(Event::Start(element))?;
268 }
269 Event::Text(e) => {
270 let mut text = e.unescape()?.into_owned();
272 self.process_text(&mut text);
274 if self.image_replacements.contains_key(&text) {
276 current_placeholder.push_str(&text);
277 } else {
278 xml_writer.write_event(Event::Text(BytesText::new(text.as_str())))?;
279 }
280 }
281 Event::End(e) => {
282 if e.name().as_ref() == b"w:p" && !current_placeholder.is_empty() {
284 if let Some(Some(docx_image)) =
285 self.image_replacements.get(¤t_placeholder)
286 {
287 DocxTemplate::create_drawing_element(
289 &mut xml_writer,
290 &docx_image.relation_id,
291 docx_image.width,
292 docx_image.height,
293 )?;
294 } else {
295 xml_writer.write_event(Event::Text(BytesText::from_escaped(
297 "",
299 )))?;
300 }
301 current_placeholder.clear();
302 }
303 xml_writer.write_event(Event::End(e))?;
304 }
305 Event::Eof => break,
306 e => {
307 xml_writer.write_event(e)?
309 }
310 }
311 buf.clear();
312 }
313 Ok(xml_writer.into_inner().into_inner())
315 }
316
317 fn process_rels_xml(&self, xml_data: &[u8]) -> Result<Vec<u8>, DocxError> {
318 let mut writer = Writer::new(Cursor::new(Vec::new()));
320 writer.write_event(Event::Decl(BytesDecl::new(
322 "1.0",
323 Some("UTF-8"),
324 Some("yes"),
325 )))?;
326
327 writer.write_event(Event::Start(
329 BytesStart::new("Relationships").with_attributes([(
330 "xmlns",
331 "http://schemas.openxmlformats.org/package/2006/relationships",
332 )]),
333 ))?;
334
335 let mut reader = quick_xml::Reader::from_reader(xml_data);
337 reader.config_mut().trim_text(true);
338 let mut buf = Vec::new();
339
340 loop {
341 match reader.read_event_into(&mut buf)? {
343 Event::Empty(e) if e.name().as_ref() == b"Relationship" => {
345 writer.write_event(Event::Empty(e))?;
347 }
348 Event::Eof => break,
350 _ => {}
351 }
352 buf.clear();
354 }
355
356 for (_, value) in &self.image_replacements {
358 if let Some(docx_image) = value {
359 let extension = DocxTemplate::get_extension(&docx_image.image_path)?;
361 let image_path = format!("media/image_{}.{}", docx_image.relation_id, extension);
363 let relationship = BytesStart::new("Relationship").with_attributes([
365 ("Id", docx_image.relation_id.as_str()),
366 (
367 "Type",
368 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
369 ),
370 ("Target", &image_path),
371 ]);
372 writer.write_event(Event::Empty(relationship))?;
374 }
375 }
376
377 writer.write_event(Event::End(BytesEnd::new("Relationships")))?;
379 Ok(writer.into_inner().into_inner())
381 }
382
383 fn get_extension(image_path: &str) -> Result<&str, DocxError> {
384 Path::new(image_path)
385 .extension()
386 .and_then(|s| s.to_str())
387 .ok_or_else(|| {
388 DocxError::ImageNotFound("Could not determine image extension".to_string())
389 })
390 }
391 fn process_text(&self, text: &mut String) {
393 for (placeholder, value) in &self.text_replacements {
394 *text = text.replace(placeholder, value);
395 }
396 }
397
398 fn create_drawing_element<T>(
399 writer: &mut Writer<T>,
400 relation_id: &str,
401 width: u64,
402 height: u64,
403 ) -> Result<(), DocxError>
404 where
405 T: Write,
406 {
407 let drawing = format!(
408 r#"
409 <w:drawing>
410 <wp:inline distT="0" distB="0" distL="0" distR="0">
411 <wp:extent cx="{}" cy="{}"/>
412 <wp:docPr id="1" name="Picture 1" descr="Generated image"/>
413 <wp:cNvGraphicFramePr>
414 <a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
415 </wp:cNvGraphicFramePr>
416 <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
417 <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
418 <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
419 <pic:nvPicPr>
420 <pic:cNvPr id="0" name="Picture 1" descr="Generated image"/>
421 <pic:cNvPicPr><a:picLocks noChangeAspect="1"/></pic:cNvPicPr>
422 </pic:nvPicPr>
423 <pic:blipFill>
424 <a:blip r:embed="{}"/>
425 <a:stretch>
426 <a:fillRect/>
427 </a:stretch>
428 </pic:blipFill>
429 <pic:spPr>
430 <a:xfrm>
431 <a:off x="0" y="0"/>
432 <a:ext cx="{}" cy="{}"/>
433 </a:xfrm>
434 <a:prstGeom prst="rect">
435 <a:avLst/>
436 </a:prstGeom>
437 </pic:spPr>
438 </pic:pic>
439 </a:graphicData>
440 </a:graphic>
441 </wp:inline>
442 </w:drawing>
443 "#,
444 width, height, relation_id, width, height,
445 );
446
447 let mut reader = quick_xml::Reader::from_str(&drawing);
448 reader.config_mut().trim_text(true);
449 let mut buf = Vec::new();
450
451 loop {
452 match reader.read_event_into(&mut buf)? {
453 Event::Eof => break,
454 e => {
455 writer.write_event(e)?;
456 }
457 }
458 }
459 Ok(())
460 }
461}