easydoc_writer/executor/
write_executor.rs1use std::io::{Seek, Write};
6use std::path::PathBuf;
7
8use easydoc_core::metadata::DocumentMeta;
9use easydoc_core::{DocError, Result};
10use easydoc_ooxml::AtomicFile;
11
12use crate::builder::doc_builder::DocumentElement;
13
14use docx_rs::{BreakType, Docx, Pic, RunFonts};
15
16pub struct DocWriteExecutor {
20 path: PathBuf,
21 #[allow(dead_code)]
22 meta: DocumentMeta,
23 elements: Vec<DocumentElement>,
24}
25
26impl DocWriteExecutor {
27 pub(crate) fn new(
29 path: PathBuf,
30 meta: DocumentMeta,
31 elements: Vec<DocumentElement>,
32 ) -> Result<Self> {
33 Ok(Self {
34 path,
35 meta,
36 elements,
37 })
38 }
39
40 fn build_docx(&self) -> Result<Docx> {
42 let mut docx = Docx::new();
43
44 for element in &self.elements {
45 match element {
46 DocumentElement::Heading { text, level } => {
47 let run = docx_rs::Run::new().add_text(text.as_str()).bold().size(28);
48 let p = docx_rs::Paragraph::new()
49 .style(heading_style(*level))
50 .outline_lvl(heading_outline_level(*level))
51 .add_run(run);
52 docx = docx.add_paragraph(p);
53 }
54 DocumentElement::Paragraph(para) => {
55 let mut p = docx_rs::Paragraph::new();
56 for run in para.clone().into_runs() {
57 let mut r = docx_rs::Run::new();
58 r = r.add_text(run.run_text());
59 if let Some(font) = run.font_config() {
60 if font.bold {
61 r = r.bold();
62 }
63 if font.italic {
64 r = r.italic();
65 }
66 if let Some(size) = font.size {
67 r = r.size(size as usize);
68 }
69 if let Some(color) = font.color {
70 r = r.color(format!("{:06X}", color.to_hex()));
71 }
72 if let Some(name) = &font.name {
73 r = r.fonts(RunFonts::new().ascii(name.as_str()));
74 }
75 if font.underline {
76 r = r.underline("single");
77 }
78 }
79 p = p.add_run(r);
80 }
81 if let Some(style) = para.paragraph_style()
82 && let Some(alignment) = style.alignment
83 {
84 p = p.align(convert_alignment(alignment));
85 }
86 docx = docx.add_paragraph(p);
87 }
88 DocumentElement::Table(table) => {
89 let mut rows: Vec<docx_rs::TableRow> = Vec::new();
90
91 let header_cells: Vec<docx_rs::TableCell> = table
93 .headers()
94 .iter()
95 .map(|h| {
96 docx_rs::TableCell::new().add_paragraph(
97 docx_rs::Paragraph::new()
98 .add_run(docx_rs::Run::new().add_text(h.as_str()).bold()),
99 )
100 })
101 .collect();
102 rows.push(docx_rs::TableRow::new(header_cells));
103
104 for row in table.rows() {
106 let cells: Vec<docx_rs::TableCell> = row
107 .iter()
108 .map(|cell| {
109 let text = doc_value_to_string(&cell.value);
110 docx_rs::TableCell::new().add_paragraph(
111 docx_rs::Paragraph::new()
112 .add_run(docx_rs::Run::new().add_text(text)),
113 )
114 })
115 .collect();
116 rows.push(docx_rs::TableRow::new(cells));
117 }
118
119 docx = docx.add_table(docx_rs::Table::new(rows));
120 }
121 DocumentElement::Image(image) => {
122 let bytes = std::fs::read(&image.path).map_err(|e| {
123 DocError::Document(format!(
124 "cannot read image {}: {e}",
125 image.path.display()
126 ))
127 })?;
128 let pic = if let (Some(w), Some(h)) = (image.width, image.height) {
129 Pic::new_with_dimensions(bytes, w, h)
130 } else {
131 Pic::new(&bytes)
132 };
133 docx = docx.add_paragraph(
134 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_image(pic)),
135 );
136 }
137 DocumentElement::PageBreak => {
138 docx = docx.add_paragraph(
139 docx_rs::Paragraph::new()
140 .add_run(docx_rs::Run::new().add_break(BreakType::Page)),
141 );
142 }
143 }
144 }
145
146 Ok(docx)
147 }
148
149 pub fn save(self) -> Result<()> {
155 let docx = self.build_docx()?;
156 AtomicFile::write(&self.path, |file| {
157 docx.build()
158 .pack(file)
159 .map_err(|error| DocError::Zip(error.to_string()))
160 })
161 }
162
163 pub fn save_to_writer<W: Write + Seek>(self, writer: W) -> Result<()> {
172 let docx = self.build_docx()?;
173 docx.build()
174 .pack(writer)
175 .map_err(|e| DocError::Zip(e.to_string()))?;
176 Ok(())
177 }
178
179 pub fn save_to_bytes(self) -> Result<Vec<u8>> {
181 let mut buf = Vec::new();
182 let cursor = std::io::Cursor::new(&mut buf);
183 let docx = self.build_docx()?;
184 docx.build()
185 .pack(cursor)
186 .map_err(|e| DocError::Zip(e.to_string()))?;
187 Ok(buf)
188 }
189}
190
191fn heading_style(level: easydoc_core::HeadingLevel) -> &'static str {
192 #[allow(clippy::match_same_arms)]
194 match level {
195 easydoc_core::HeadingLevel::H1 => "Heading1",
196 easydoc_core::HeadingLevel::H2 => "Heading2",
197 easydoc_core::HeadingLevel::H3 => "Heading3",
198 easydoc_core::HeadingLevel::H4 => "Heading4",
199 easydoc_core::HeadingLevel::H5 => "Heading5",
200 easydoc_core::HeadingLevel::H6 => "Heading6",
201 _ => "Normal",
203 }
204}
205
206fn heading_outline_level(level: easydoc_core::HeadingLevel) -> usize {
207 #[allow(clippy::match_same_arms)]
209 match level {
210 easydoc_core::HeadingLevel::H1 => 0,
211 easydoc_core::HeadingLevel::H2 => 1,
212 easydoc_core::HeadingLevel::H3 => 2,
213 easydoc_core::HeadingLevel::H4 => 3,
214 easydoc_core::HeadingLevel::H5 => 4,
215 easydoc_core::HeadingLevel::H6 => 5,
216 _ => 0,
218 }
219}
220
221fn doc_value_to_string(value: &easydoc_core::DocValue) -> String {
222 match value {
223 easydoc_core::DocValue::String(s) => s.clone(),
224 easydoc_core::DocValue::Int(n) => n.to_string(),
225 easydoc_core::DocValue::Float(n) => n.to_string(),
226 easydoc_core::DocValue::Bool(b) => b.to_string(),
227 easydoc_core::DocValue::Empty => String::new(),
228 other => format!("{other:?}"),
229 }
230}
231
232fn convert_alignment(
233 alignment: easydoc_core::types::HorizontalAlignment,
234) -> docx_rs::AlignmentType {
235 #[allow(clippy::match_same_arms)]
237 match alignment {
238 easydoc_core::types::HorizontalAlignment::Left => docx_rs::AlignmentType::Left,
239 easydoc_core::types::HorizontalAlignment::Center => docx_rs::AlignmentType::Center,
240 easydoc_core::types::HorizontalAlignment::Right => docx_rs::AlignmentType::Right,
241 easydoc_core::types::HorizontalAlignment::Both => docx_rs::AlignmentType::Both,
242 _ => docx_rs::AlignmentType::Left,
244 }
245}