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