use crate::ExportDocxDto;
use crate::ExportDocxResultDto;
use anyhow::{Result, anyhow};
use common::database::QueryUnitOfWork;
use common::entities::{
Block, Document, Frame, InlineContent, InlineElement, List, Root, Table, TableCell,
};
use common::long_operation::LongOperation;
use common::types::{EntityId, ROOT_ENTITY_ID};
use std::collections::HashSet;
use std::sync::Arc;
pub trait ExportDocxUnitOfWorkFactoryTrait: Send + Sync {
fn create(&self) -> Box<dyn ExportDocxUnitOfWorkTrait>;
}
#[macros::uow_action(entity = "Root", action = "GetRO", thread_safe = true)]
#[macros::uow_action(entity = "Root", action = "GetRelationshipRO", thread_safe = true)]
#[macros::uow_action(entity = "Document", action = "GetRO", thread_safe = true)]
#[macros::uow_action(entity = "Document", action = "GetRelationshipRO", thread_safe = true)]
#[macros::uow_action(entity = "Frame", action = "GetRO", thread_safe = true)]
#[macros::uow_action(entity = "Frame", action = "GetRelationshipRO", thread_safe = true)]
#[macros::uow_action(entity = "Block", action = "GetMultiRO", thread_safe = true)]
#[macros::uow_action(entity = "Block", action = "GetRelationshipRO", thread_safe = true)]
#[macros::uow_action(entity = "InlineElement", action = "GetMultiRO", thread_safe = true)]
#[macros::uow_action(entity = "List", action = "GetRO", thread_safe = true)]
#[macros::uow_action(entity = "Table", action = "GetRO", thread_safe = true)]
#[macros::uow_action(entity = "Table", action = "GetRelationshipRO", thread_safe = true)]
#[macros::uow_action(entity = "TableCell", action = "GetMultiRO", thread_safe = true)]
pub trait ExportDocxUnitOfWorkTrait: QueryUnitOfWork + Send + Sync {}
pub struct ExportDocxUseCase {
uow_factory: Box<dyn ExportDocxUnitOfWorkFactoryTrait>,
dto: ExportDocxDto,
}
impl ExportDocxUseCase {
pub fn new(
uow_factory: Box<dyn ExportDocxUnitOfWorkFactoryTrait>,
dto: &ExportDocxDto,
) -> Self {
ExportDocxUseCase {
uow_factory,
dto: dto.clone(),
}
}
}
impl LongOperation for ExportDocxUseCase {
type Output = ExportDocxResultDto;
fn execute(
&self,
progress_callback: Box<dyn Fn(common::long_operation::OperationProgress) + Send>,
cancel_flag: Arc<std::sync::atomic::AtomicBool>,
) -> Result<Self::Output> {
use docx_rs::*;
use std::sync::atomic::Ordering;
let output_path = std::path::Path::new(&self.dto.output_path);
if let Some(parent) = output_path.parent()
&& !parent.as_os_str().is_empty()
&& !parent.exists()
{
return Err(anyhow!(
"Output directory does not exist: '{}'",
parent.display()
));
}
progress_callback(common::long_operation::OperationProgress::new(
0.0,
Some("Starting DOCX export...".to_string()),
));
let uow = self.uow_factory.create();
uow.begin_transaction()?;
let root = uow
.get_root(&ROOT_ENTITY_ID)?
.ok_or_else(|| anyhow!("Root entity not found"))?;
let doc_ids = uow.get_root_relationship(
&root.id,
&common::direct_access::root::RootRelationshipField::Document,
)?;
let doc_id = *doc_ids
.first()
.ok_or_else(|| anyhow!("Root has no associated Document"))?;
let frame_ids = uow.get_document_relationship(
&doc_id,
&common::direct_access::document::DocumentRelationshipField::Frames,
)?;
let table_ids = uow.get_document_relationship(
&doc_id,
&common::direct_access::document::DocumentRelationshipField::Tables,
)?;
let mut cell_frame_ids: HashSet<EntityId> = HashSet::new();
for tid in &table_ids {
let cell_ids = uow.get_table_relationship(
tid,
&common::direct_access::table::TableRelationshipField::Cells,
)?;
let cells_opt = uow.get_table_cell_multi(&cell_ids)?;
for cell in cells_opt.into_iter().flatten() {
if let Some(cf_id) = cell.cell_frame {
cell_frame_ids.insert(cf_id);
}
}
}
let mut docx = Docx::new();
let mut paragraph_count: i64 = 0;
progress_callback(common::long_operation::OperationProgress::new(
10.0,
Some("Walking document tree...".to_string()),
));
for frame_id in &frame_ids {
if cancel_flag.load(Ordering::Relaxed) {
uow.end_transaction()?;
return Err(anyhow!("Operation was cancelled"));
}
if cell_frame_ids.contains(frame_id) {
continue;
}
let frame = uow.get_frame(frame_id)?;
if let Some(ref f) = frame
&& let Some(table_id) = f.table
{
let table = self.render_table_docx(&*uow, &table_id)?;
docx = docx.add_table(table);
paragraph_count += 1;
continue;
}
let block_ids = uow.get_frame_relationship(
frame_id,
&common::direct_access::frame::FrameRelationshipField::Blocks,
)?;
if block_ids.is_empty() {
continue;
}
let blocks_opt = uow.get_block_multi(&block_ids)?;
let mut blocks: Vec<Block> = blocks_opt.into_iter().flatten().collect();
blocks.sort_by_key(|b| b.document_position);
let total = blocks.len();
for (idx, block) in blocks.iter().enumerate() {
if cancel_flag.load(Ordering::Relaxed) {
uow.end_transaction()?;
return Err(anyhow!("Operation was cancelled"));
}
let element_ids = uow.get_block_relationship(
&block.id,
&common::direct_access::block::BlockRelationshipField::Elements,
)?;
let elements_opt = uow.get_inline_element_multi(&element_ids)?;
let elements: Vec<InlineElement> = elements_opt.into_iter().flatten().collect();
let mut paragraph = Paragraph::new();
if let Some(level) = block.fmt_heading_level {
let style_name = format!("Heading{}", level.clamp(1, 6));
paragraph = paragraph.style(&style_name);
}
if let Some(lh) = block.fmt_line_height {
let twips = (lh as f64 / 1000.0 * 240.0) as i32;
paragraph = paragraph.line_spacing(
LineSpacing::new()
.line_rule(LineSpacingType::Auto)
.line(twips),
);
}
if block.fmt_non_breakable_lines == Some(true) {
paragraph = paragraph.keep_lines(true);
}
for elem in &elements {
let text = match &elem.content {
InlineContent::Text(t) => t.clone(),
InlineContent::Image { name, .. } => {
format!("[Image: {}]", name)
}
InlineContent::Empty => String::new(),
};
if text.is_empty() {
continue;
}
let mut run = Run::new().add_text(text);
if elem.fmt_font_bold == Some(true) {
run = run.bold();
}
if elem.fmt_font_italic == Some(true) {
run = run.italic();
}
if elem.fmt_font_underline == Some(true) {
run = run.underline("single");
}
if elem.fmt_font_strikeout == Some(true) {
run = run.strike();
}
if elem.fmt_font_family.as_deref() == Some("monospace") {
run = run.fonts(RunFonts::new().ascii("Courier New"));
}
paragraph = paragraph.add_run(run);
}
docx = docx.add_paragraph(paragraph);
paragraph_count += 1;
if idx % 10 == 0 {
let pct = 10.0 + (idx as f32 / total as f32) * 80.0;
progress_callback(common::long_operation::OperationProgress::new(
pct,
Some(format!("Processing paragraph {}/{}", idx + 1, total)),
));
}
}
}
uow.end_transaction()?;
progress_callback(common::long_operation::OperationProgress::new(
90.0,
Some("Writing DOCX file...".to_string()),
));
let file = std::fs::File::create(&self.dto.output_path).map_err(|e| {
anyhow!(
"Failed to create output file '{}': {}",
self.dto.output_path,
e
)
})?;
docx.build()
.pack(file)
.map_err(|e| anyhow!("Failed to write DOCX: {}", e))?;
progress_callback(common::long_operation::OperationProgress::new(
100.0,
Some("completed".to_string()),
));
Ok(ExportDocxResultDto {
file_path: self.dto.output_path.clone(),
paragraph_count,
})
}
}
impl ExportDocxUseCase {
fn render_table_docx(
&self,
uow: &dyn ExportDocxUnitOfWorkTrait,
table_id: &EntityId,
) -> Result<docx_rs::Table> {
use docx_rs::*;
let table = uow
.get_table(table_id)?
.ok_or_else(|| anyhow!("Table not found"))?;
let cell_ids = uow.get_table_relationship(
table_id,
&common::direct_access::table::TableRelationshipField::Cells,
)?;
let cells_opt = uow.get_table_cell_multi(&cell_ids)?;
let mut cells: Vec<common::entities::TableCell> = cells_opt.into_iter().flatten().collect();
cells.sort_by(|a, b| a.row.cmp(&b.row).then(a.column.cmp(&b.column)));
let rows = table.rows as usize;
let cols = table.columns as usize;
let mut covered = vec![vec![false; cols]; rows];
let grid: Vec<usize> = table.column_widths.iter().map(|w| *w as usize).collect();
let mut docx_rows: Vec<TableRow> = Vec::new();
for r in 0..rows {
let mut docx_cells: Vec<docx_rs::TableCell> = Vec::new();
for c in 0..cols {
if covered[r][c] {
let needs_vmerge_continue = r > 0 && {
cells.iter().any(|cell| {
cell.column == c as i64
&& cell.row < r as i64
&& (cell.row + cell.row_span) > r as i64
})
};
if needs_vmerge_continue {
let cont_cell =
docx_rs::TableCell::new().vertical_merge(VMergeType::Continue);
docx_cells.push(cont_cell);
}
continue;
}
let cell = cells
.iter()
.find(|cell| cell.row == r as i64 && cell.column == c as i64);
if let Some(cell) = cell {
let mut docx_cell = docx_rs::TableCell::new();
if cell.column_span > 1 {
docx_cell = docx_cell.grid_span(cell.column_span as usize);
}
if cell.row_span > 1 {
docx_cell = docx_cell.vertical_merge(VMergeType::Restart);
}
if let Some(cf_id) = cell.cell_frame {
let block_ids = uow.get_frame_relationship(
&cf_id,
&common::direct_access::frame::FrameRelationshipField::Blocks,
)?;
let blocks_opt = uow.get_block_multi(&block_ids)?;
let mut blocks: Vec<Block> = blocks_opt.into_iter().flatten().collect();
blocks.sort_by_key(|b| b.document_position);
for block in &blocks {
let element_ids = uow.get_block_relationship(
&block.id,
&common::direct_access::block::BlockRelationshipField::Elements,
)?;
let elements_opt = uow.get_inline_element_multi(&element_ids)?;
let elements: Vec<InlineElement> =
elements_opt.into_iter().flatten().collect();
let mut paragraph = Paragraph::new();
for elem in &elements {
let text = match &elem.content {
InlineContent::Text(t) => t.clone(),
InlineContent::Image { name, .. } => {
format!("[Image: {}]", name)
}
InlineContent::Empty => String::new(),
};
if text.is_empty() {
continue;
}
let mut run = Run::new().add_text(text);
if elem.fmt_font_bold == Some(true) {
run = run.bold();
}
if elem.fmt_font_italic == Some(true) {
run = run.italic();
}
if elem.fmt_font_underline == Some(true) {
run = run.underline("single");
}
if elem.fmt_font_strikeout == Some(true) {
run = run.strike();
}
if elem.fmt_font_family.as_deref() == Some("monospace") {
run = run.fonts(RunFonts::new().ascii("Courier New"));
}
paragraph = paragraph.add_run(run);
}
docx_cell = docx_cell.add_paragraph(paragraph);
}
}
docx_cells.push(docx_cell);
for sr in 0..cell.row_span as usize {
for sc in 0..cell.column_span as usize {
if sr == 0 && sc == 0 {
continue;
}
if r + sr < rows && c + sc < cols {
covered[r + sr][c + sc] = true;
}
}
}
} else {
let docx_cell = docx_rs::TableCell::new().add_paragraph(Paragraph::new());
docx_cells.push(docx_cell);
}
}
docx_rows.push(TableRow::new(docx_cells));
}
let mut docx_table = docx_rs::Table::new(docx_rows);
if !grid.is_empty() {
docx_table = docx_table.set_grid(grid);
}
Ok(docx_table)
}
}