1use quick_xml::Writer;
2use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
3use reqwest::Client;
4use std::collections::HashMap;
5use std::fs::File;
6use std::io::{Cursor, Read, Write};
7use std::path::Path;
8use std::time::Duration;
9use thiserror::Error;
10use uuid::Uuid;
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_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 pub fn add_image_size_replacement(
72 &mut self,
73 placeholder: &str,
74 image_path: Option<&str>,
75 width: f32,
76 height: f32,
77 ) -> Result<(), DocxError> {
78 match image_path {
79 None => {
80 self.image_replacements
82 .insert(placeholder.to_string(), None);
83 }
84 Some(file_path) => {
85 self.image_replacements.insert(
87 placeholder.to_string(),
88 Some(DocxImage::new_size(file_path, width, height)?),
89 );
90 }
91 }
92
93 Ok(())
94 }
95
96 pub async fn add_image_url_replacement(
100 &mut self,
101 placeholder: &str,
102 image_url: Option<&str>,
103 ) -> Result<(), DocxError> {
104 match image_url {
105 None => {
106 self.image_replacements
108 .insert(placeholder.to_string(), None);
109 }
110 Some(url) => {
111 let response = self.client.get(url).send().await?;
113 if response.status().is_success() {
115 let image_data = response.bytes().await?.to_vec();
117 self.image_replacements.insert(
119 placeholder.to_string(),
120 Some(DocxImage::new_image_data(url, image_data)?),
121 );
122 }
123 }
124 }
125
126 Ok(())
127 }
128
129 pub async fn add_image_size_url_replacement(
135 &mut self,
136 placeholder: &str,
137 image_url: Option<&str>,
138 width: f32,
139 height: f32,
140 ) -> Result<(), DocxError> {
141 match image_url {
142 None => {
143 self.image_replacements
145 .insert(placeholder.to_string(), None);
146 }
147 Some(url) => {
148 let response = self.client.get(url).send().await?;
150 if response.status().is_success() {
152 let image_data = response.bytes().await?.to_vec();
154 self.image_replacements.insert(
156 placeholder.to_string(),
157 Some(DocxImage::new_image_data_size(
158 url, image_data, width, height,
159 )?),
160 );
161 }
162 }
163 }
164
165 Ok(())
166 }
167
168 pub fn process_template(
172 &self,
173 template_path: &str,
174 output_path: &str,
175 ) -> Result<(), DocxError> {
176 let template_file = File::open(template_path)?;
178 let mut archive = ZipArchive::new(template_file)?;
179
180 let output_file = File::create(output_path)?;
182 let mut zip_writer = ZipWriter::new(output_file);
183
184 for i in 0..archive.len() {
186 let mut file = archive.by_index(i)?;
187 let mut contents = Vec::new();
189 file.read_to_end(&mut contents)?;
191 match file.name() {
193 "word/document.xml" => {
194 contents = self.process_document_xml(&contents)?;
196 }
197 "word/_rels/document.xml.rels" => {
198 contents = self.process_rels_xml(&contents)?;
200 }
201 &_ => {}
202 }
203
204 let option = SimpleFileOptions::default()
206 .compression_method(file.compression())
207 .unix_permissions(file.unix_mode().unwrap_or(0o644));
208 zip_writer.start_file(file.name(), option)?;
210 zip_writer.write_all(&contents)?;
211 }
212
213 for (_, replacement) in &self.image_replacements {
215 if let Some(replacement) = replacement {
216 let image_path = format!(
217 "word/media/image_{}.{}",
218 replacement.relation_id,
219 DocxTemplate::get_extension(&replacement.image_path)?
220 );
221 zip_writer.start_file(&image_path, SimpleFileOptions::default())?;
223 zip_writer.write_all(&replacement.image_data)?;
224 }
225 }
226 zip_writer.finish()?;
228 Ok(())
229 }
230
231 fn process_element(&self, _element: &mut BytesStart) -> Result<(), DocxError> {
232 Ok(())
234 }
235
236 fn process_document_xml(&self, contents: &[u8]) -> Result<Vec<u8>, DocxError> {
239 let mut xml_writer = Writer::new(Cursor::new(Vec::new()));
241 let mut reader = quick_xml::Reader::from_reader(&contents[..]);
245 reader.config_mut().trim_text(true);
246 let mut buf = Vec::new();
248 let mut current_placeholder = String::new();
250 loop {
252 match reader.read_event_into(&mut buf)? {
254 Event::Start(e) => {
255 let mut element = e.to_owned();
256 self.process_element(&mut element)?;
257 if e.name().as_ref() == b"w:p" {
258 current_placeholder.clear();
259 }
260 xml_writer.write_event(Event::Start(element))?;
261 }
262 Event::Text(e) => {
263 let mut text = e.unescape()?.into_owned();
265 self.process_text(&mut text);
267 if self.image_replacements.contains_key(&text) {
269 current_placeholder.push_str(&text);
270 } else {
271 xml_writer.write_event(Event::Text(BytesText::new(text.as_str())))?;
272 }
273 }
274 Event::End(e) => {
275 if e.name().as_ref() == b"w:p" && !current_placeholder.is_empty() {
277 if let Some(Some(docx_image)) =
278 self.image_replacements.get(¤t_placeholder)
279 {
280 DocxTemplate::create_drawing_element(
282 &mut xml_writer,
283 &docx_image.relation_id,
284 docx_image.width,
285 docx_image.height,
286 )?;
287 } else {
288 xml_writer.write_event(Event::Text(BytesText::from_escaped(
290 "",
292 )))?;
293 }
294 current_placeholder.clear();
295 }
296 xml_writer.write_event(Event::End(e))?;
297 }
298 Event::Eof => break,
299 e => {
300 xml_writer.write_event(e)?
302 }
303 }
304 buf.clear();
305 }
306 Ok(xml_writer.into_inner().into_inner())
308 }
309
310 fn process_rels_xml(&self, xml_data: &[u8]) -> Result<Vec<u8>, DocxError> {
311 let mut writer = Writer::new(Cursor::new(Vec::new()));
313 writer.write_event(Event::Decl(BytesDecl::new(
315 "1.0",
316 Some("UTF-8"),
317 Some("yes"),
318 )))?;
319
320 writer.write_event(Event::Start(
322 BytesStart::new("Relationships").with_attributes([(
323 "xmlns",
324 "http://schemas.openxmlformats.org/package/2006/relationships",
325 )]),
326 ))?;
327
328 let mut reader = quick_xml::Reader::from_reader(xml_data);
330 reader.config_mut().trim_text(true);
331 let mut buf = Vec::new();
332
333 loop {
334 match reader.read_event_into(&mut buf)? {
336 Event::Empty(e) if e.name().as_ref() == b"Relationship" => {
338 writer.write_event(Event::Empty(e))?;
340 }
341 Event::Eof => break,
343 _ => {}
344 }
345 buf.clear();
347 }
348
349 for (_, value) in &self.image_replacements {
351 if let Some(docx_image) = value {
352 let extension = DocxTemplate::get_extension(&docx_image.image_path)?;
354 let image_path = format!("media/image_{}.{}", docx_image.relation_id, extension);
356 let relationship = BytesStart::new("Relationship").with_attributes([
358 ("Id", docx_image.relation_id.as_str()),
359 (
360 "Type",
361 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
362 ),
363 ("Target", &image_path),
364 ]);
365 writer.write_event(Event::Empty(relationship))?;
367 }
368 }
369
370 writer.write_event(Event::End(BytesEnd::new("Relationships")))?;
372 Ok(writer.into_inner().into_inner())
374 }
375
376 fn get_extension(image_path: &str) -> Result<&str, DocxError> {
377 Path::new(image_path)
378 .extension()
379 .and_then(|s| s.to_str())
380 .ok_or_else(|| {
381 DocxError::ImageNotFound("Could not determine image extension".to_string())
382 })
383 }
384 fn process_text(&self, text: &mut String) {
386 for (placeholder, value) in &self.text_replacements {
387 *text = text.replace(placeholder, value);
388 }
389 }
390
391 fn create_drawing_element<T>(
392 writer: &mut Writer<T>,
393 relation_id: &str,
394 width: u64,
395 height: u64,
396 ) -> Result<(), DocxError>
397 where
398 T: Write,
399 {
400 let drawing = format!(
401 r#"
402 <w:drawing>
403 <wp:inline distT="0" distB="0" distL="0" distR="0">
404 <wp:extent cx="{}" cy="{}"/>
405 <wp:docPr id="1" name="Picture 1" descr="Generated image"/>
406 <wp:cNvGraphicFramePr>
407 <a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
408 </wp:cNvGraphicFramePr>
409 <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
410 <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
411 <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
412 <pic:nvPicPr>
413 <pic:cNvPr id="0" name="Picture 1" descr="Generated image"/>
414 <pic:cNvPicPr><a:picLocks noChangeAspect="1"/></pic:cNvPicPr>
415 </pic:nvPicPr>
416 <pic:blipFill>
417 <a:blip r:embed="{}"/>
418 <a:stretch>
419 <a:fillRect/>
420 </a:stretch>
421 </pic:blipFill>
422 <pic:spPr>
423 <a:xfrm>
424 <a:off x="0" y="0"/>
425 <a:ext cx="{}" cy="{}"/>
426 </a:xfrm>
427 <a:prstGeom prst="rect">
428 <a:avLst/>
429 </a:prstGeom>
430 </pic:spPr>
431 </pic:pic>
432 </a:graphicData>
433 </a:graphic>
434 </wp:inline>
435 </w:drawing>
436 "#,
437 width, height, relation_id, width, height,
438 );
439
440 let mut reader = quick_xml::Reader::from_str(&drawing);
441 reader.config_mut().trim_text(true);
442 let mut buf = Vec::new();
443
444 loop {
445 match reader.read_event_into(&mut buf)? {
446 Event::Eof => break,
447 e => {
448 writer.write_event(e)?;
449 }
450 }
451 }
452 Ok(())
453 }
454}
455
456struct DocxImage {
458 pub image_path: String,
460 pub image_data: Vec<u8>,
462 pub relation_id: String,
464 pub width: u64,
466 pub height: u64,
468}
469
470impl DocxImage {
471 pub fn new(image_path: &str) -> Result<Self, DocxError> {
474 Self::new_size(image_path, 6.09, 5.9)
475 }
476 pub fn new_size(image_path: &str, width: f32, height: f32) -> Result<Self, DocxError> {
481 let mut file = File::open(image_path)?;
483 let mut image_data = Vec::new();
484 file.read_to_end(&mut image_data)?;
485 DocxImage::new_image_data_size(image_path, image_data, width, height)
486 }
487
488 pub fn new_image_data(image_url: &str, image_data: Vec<u8>) -> Result<Self, DocxError> {
492 DocxImage::new_image_data_size(image_url, image_data, 6.09, 5.9)
493 }
494
495 pub fn new_image_data_size(
501 image_url: &str,
502 image_data: Vec<u8>,
503 width: f32,
504 height: f32,
505 ) -> Result<Self, DocxError> {
506 Ok(DocxImage {
507 image_path: image_url.to_string(),
508 relation_id: format!("rId{}", Uuid::new_v4().simple()),
509 width: (width * 360000.0) as u64,
510 height: (height * 360000.0) as u64,
511 image_data,
512 })
513 }
514}
515
516#[derive(Error, Debug)]
517pub enum DocxError {
518 #[error("IO error: {0}")]
519 Io(#[from] std::io::Error),
520 #[error("Zip error: {0}")]
521 Zip(#[from] zip::result::ZipError),
522 #[error("XML error: {0}")]
523 Xml(#[from] quick_xml::Error),
524 #[error("UTF-8 error: {0}")]
525 Utf8(#[from] std::string::FromUtf8Error),
526 #[error("Image not found: {0}")]
527 ImageNotFound(String),
528 #[error("Image url not found: {0}")]
529 ImageUrlFound(#[from] reqwest::Error),
530}