Skip to main content

easydoc_writer/executor/
write_executor.rs

1//! 文档写入执行器 -- 编排完整 DOCX 文件的组装。
2//!
3//! 对应 Java: `com.alibaba.excel.write.ExcelBuilderImpl` 的内部实现
4
5use 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
16/// 将 [`crate::DocBuilder`] 渲染为物理 DOCX 文件的执行器。
17///
18/// 包装 `docx-rs` 进行实际的 OOXML 生成。
19pub struct DocWriteExecutor {
20    path: PathBuf,
21    #[allow(dead_code)]
22    meta: DocumentMeta,
23    elements: Vec<DocumentElement>,
24}
25
26impl DocWriteExecutor {
27    /// Creates a new executor from builder output.
28    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    /// Builds the `docx_rs` document from stored elements.
41    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                    // Header row
92                    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                    // Data rows
105                    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    /// Saves the assembled document to disk.
150    ///
151    /// # Errors
152    ///
153    /// Returns an I/O or ZIP error if the file cannot be written.
154    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    /// Writes the assembled document to a generic writer.
164    ///
165    /// Corresponds to Hutool's `Word07Writer.flush(OutputStream)`.
166    /// The writer must implement both `Write` and `Seek` (required by docx-rs).
167    ///
168    /// # Errors
169    ///
170    /// Returns an I/O or ZIP error.
171    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    /// Writes the assembled document to a `Vec<u8>` buffer.
180    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    // `_` 通配与显式分支体相同是 #[non_exhaustive] 的必然结果
193    #[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        // #[non_exhaustive]:未来新增的标题级别回退到正文
202        _ => "Normal",
203    }
204}
205
206fn heading_outline_level(level: easydoc_core::HeadingLevel) -> usize {
207    // `_` 通配与显式分支体相同是 #[non_exhaustive] 的必然结果
208    #[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        // #[non_exhaustive]:未来新增的标题级别回退到无大纲级别
217        _ => 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    // `_` 通配与显式分支体相同是 #[non_exhaustive] 的必然结果
236    #[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        // #[non_exhaustive]:未来新增的对齐方式默认左对齐
243        _ => docx_rs::AlignmentType::Left,
244    }
245}