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
67 pub async fn add_image_url_replacement(
71 &mut self,
72 placeholder: &str,
73 image_url: Option<&str>,
74 ) -> Result<(), DocxError> {
75 match image_url {
76 None => {
77 self.image_replacements
79 .insert(placeholder.to_string(), None);
80 }
81 Some(url) => {
82 let response = self.client.get(url).send().await?;
84 if response.status().is_success() {
86 let image_data = response.bytes().await?.to_vec();
88 self.image_replacements.insert(
90 placeholder.to_string(),
91 Some(DocxImage::new_image_data(url, image_data)?),
92 );
93 }
94 }
95 }
96
97 Ok(())
98 }
99
100 pub fn process_template(
104 &self,
105 template_path: &str,
106 output_path: &str,
107 ) -> Result<(), DocxError> {
108 let template_file = File::open(template_path)?;
110 let mut archive = ZipArchive::new(template_file)?;
111
112 let output_file = File::create(output_path)?;
114 let mut zip_writer = ZipWriter::new(output_file);
115
116 for i in 0..archive.len() {
118 let mut file = archive.by_index(i)?;
119 let mut contents = Vec::new();
121 file.read_to_end(&mut contents)?;
123 match file.name() {
125 "word/document.xml" => {
126 contents = self.process_document_xml(&contents)?;
128 }
129 "word/_rels/document.xml.rels" => {
130 contents = self.process_rels_xml(&contents)?;
132 }
133 &_ => {}
134 }
135
136 let option = SimpleFileOptions::default()
138 .compression_method(file.compression())
139 .unix_permissions(file.unix_mode().unwrap_or(0o644));
140 zip_writer.start_file(file.name(), option)?;
142 zip_writer.write_all(&contents)?;
143 }
144
145 for (_, replacement) in &self.image_replacements {
147 if let Some(replacement) = replacement {
148 let image_path = format!(
149 "word/media/image_{}.{}",
150 replacement.relation_id,
151 DocxTemplate::get_extension(&replacement.image_path)?
152 );
153 zip_writer.start_file(&image_path, SimpleFileOptions::default())?;
155 zip_writer.write_all(&replacement.image_data)?;
156 }
157 }
158 zip_writer.finish()?;
160 Ok(())
161 }
162
163 fn process_element(&self, _element: &mut BytesStart) -> Result<(), DocxError> {
164 Ok(())
166 }
167
168 fn process_document_xml(&self, contents: &[u8]) -> Result<Vec<u8>, DocxError> {
171 let mut xml_writer = Writer::new(Cursor::new(Vec::new()));
173 let mut reader = quick_xml::Reader::from_reader(&contents[..]);
177 let mut buf = Vec::new();
179 let mut current_placeholder = String::new();
181 loop {
183 match reader.read_event_into(&mut buf)? {
185 Event::Start(e) => {
186 let mut element = e.to_owned();
187 self.process_element(&mut element)?;
188 if e.name().as_ref() == b"w:p" {
189 current_placeholder.clear();
190 }
191 xml_writer.write_event(Event::Start(element))?;
192 }
193 Event::Text(e) => {
194 let mut text = e.unescape()?.into_owned();
196 self.process_text(&mut text);
198 if self.image_replacements.contains_key(&text) {
200 current_placeholder.push_str(&text);
201 } else {
202 xml_writer.write_event(Event::Text(BytesText::new(text.as_str())))?;
203 }
204 }
205 Event::End(e) => {
206 if e.name().as_ref() == b"w:p" && !current_placeholder.is_empty() {
208 if let Some(Some(docx_image)) =
209 self.image_replacements.get(¤t_placeholder)
210 {
211 DocxTemplate::create_drawing_element(
213 &mut xml_writer,
214 &docx_image.relation_id,
215 docx_image.width,
216 docx_image.height,
217 )?;
218 } else {
219 xml_writer.write_event(Event::Text(BytesText::from_escaped(
221 "",
223 )))?;
224 }
225 current_placeholder.clear();
226 }
227 xml_writer.write_event(Event::End(e))?;
228 }
229 Event::Eof => break,
230 e => {
231 xml_writer.write_event(e)?
233 }
234 }
235 buf.clear();
236 }
237 Ok(xml_writer.into_inner().into_inner())
239 }
240
241 fn process_rels_xml(&self, xml_data: &[u8]) -> Result<Vec<u8>, DocxError> {
242 let mut writer = Writer::new(Cursor::new(Vec::new()));
244 writer.write_event(Event::Decl(BytesDecl::new(
246 "1.0",
247 Some("UTF-8"),
248 Some("yes"),
249 )))?;
250
251 writer.write_event(Event::Start(
253 BytesStart::new("Relationships").with_attributes([(
254 "xmlns",
255 "http://schemas.openxmlformats.org/package/2006/relationships",
256 )]),
257 ))?;
258
259 let mut reader = quick_xml::Reader::from_reader(xml_data);
261 let mut buf = Vec::new();
262
263 loop {
264 match reader.read_event_into(&mut buf)? {
266 Event::Empty(e) if e.name().as_ref() == b"Relationship" => {
268 writer.write_event(Event::Empty(e))?;
270 }
271 Event::Eof => break,
273 _ => {}
274 }
275 buf.clear();
277 }
278
279 for (_, value) in &self.image_replacements {
281 if let Some(docx_image) = value {
282 let extension = DocxTemplate::get_extension(&docx_image.image_path)?;
284 let image_path = format!("media/image_{}.{}", docx_image.relation_id, extension);
286 let relationship = BytesStart::new("Relationship").with_attributes([
288 ("Id", docx_image.relation_id.as_str()),
289 (
290 "Type",
291 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
292 ),
293 ("Target", &image_path),
294 ]);
295 writer.write_event(Event::Empty(relationship))?;
297 }
298 }
299
300 writer.write_event(Event::End(BytesEnd::new("Relationships")))?;
302 Ok(writer.into_inner().into_inner())
304 }
305
306 fn get_extension(image_path: &str) -> Result<&str, DocxError> {
307 Path::new(image_path)
308 .extension()
309 .and_then(|s| s.to_str())
310 .ok_or_else(|| {
311 DocxError::ImageNotFound("Could not determine image extension".to_string())
312 })
313 }
314 fn process_text(&self, text: &mut String) {
316 for (placeholder, value) in &self.text_replacements {
317 *text = text.replace(placeholder, value);
318 }
319 }
320
321 fn create_drawing_element<T>(
322 writer: &mut Writer<T>,
323 relation_id: &str,
324 width: u64,
325 height: u64,
326 ) -> Result<(), DocxError>
327 where
328 T: Write,
329 {
330 let drawing = format!(
331 r#"
332 <w:drawing>
333 <wp:inline distT="0" distB="0" distL="0" distR="0">
334 <wp:extent cx="{}" cy="{}"/>
335 <wp:docPr id="1" name="Picture 1" descr="Generated image"/>
336 <wp:cNvGraphicFramePr>
337 <a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
338 </wp:cNvGraphicFramePr>
339 <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
340 <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
341 <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
342 <pic:nvPicPr>
343 <pic:cNvPr id="0" name="Picture 1" descr="Generated image"/>
344 <pic:cNvPicPr><a:picLocks noChangeAspect="1"/></pic:cNvPicPr>
345 </pic:nvPicPr>
346 <pic:blipFill>
347 <a:blip r:embed="{}"/>
348 <a:stretch>
349 <a:fillRect/>
350 </a:stretch>
351 </pic:blipFill>
352 <pic:spPr>
353 <a:xfrm>
354 <a:off x="0" y="0"/>
355 <a:ext cx="{}" cy="{}"/>
356 </a:xfrm>
357 <a:prstGeom prst="rect">
358 <a:avLst/>
359 </a:prstGeom>
360 </pic:spPr>
361 </pic:pic>
362 </a:graphicData>
363 </a:graphic>
364 </wp:inline>
365 </w:drawing>
366 "#,
367 width, height, relation_id, width, height,
368 );
369
370 let mut reader = quick_xml::Reader::from_str(&drawing);
371 reader.config_mut().trim_text(true);
372 let mut buf = Vec::new();
373
374 loop {
375 match reader.read_event_into(&mut buf)? {
376 Event::Eof => break,
377 e => {
378 writer.write_event(e)?;
379 }
380 }
381 }
382 Ok(())
383 }
384}
385
386struct DocxImage {
388 pub image_path: String,
390 pub image_data: Vec<u8>,
392 pub relation_id: String,
394 pub width: u64,
396 pub height: u64,
398}
399
400impl DocxImage {
401 pub fn new(image_path: &str) -> Result<Self, DocxError> {
404 Self::new_size(image_path, 6.09, 5.9)
405 }
406 pub fn new_size(image_path: &str, width: f32, height: f32) -> Result<Self, DocxError> {
411 let mut file = File::open(image_path)?;
413 let mut image_data = Vec::new();
414 file.read_to_end(&mut image_data)?;
415 DocxImage::new_image_data_size(image_path, image_data, width, height)
416 }
417
418 pub fn new_image_data(image_url: &str, image_data: Vec<u8>) -> Result<Self, DocxError> {
422 DocxImage::new_image_data_size(image_url, image_data, 6.09, 5.9)
423 }
424
425 pub fn new_image_data_size(
431 image_url: &str,
432 image_data: Vec<u8>,
433 width: f32,
434 height: f32,
435 ) -> Result<Self, DocxError> {
436 Ok(DocxImage {
437 image_path: image_url.to_string(),
438 relation_id: format!("rId{}", Uuid::new_v4().simple()),
439 width: (width * 360000.0) as u64,
440 height: (height * 360000.0) as u64,
441 image_data,
442 })
443 }
444}
445
446#[derive(Error, Debug)]
447pub enum DocxError {
448 #[error("IO error: {0}")]
449 Io(#[from] std::io::Error),
450 #[error("Zip error: {0}")]
451 Zip(#[from] zip::result::ZipError),
452 #[error("XML error: {0}")]
453 Xml(#[from] quick_xml::Error),
454 #[error("UTF-8 error: {0}")]
455 Utf8(#[from] std::string::FromUtf8Error),
456 #[error("Image not found: {0}")]
457 ImageNotFound(String),
458 #[error("Image url not found: {0}")]
459 ImageUrlFound(#[from] reqwest::Error),
460}