""" docx_writer.py - DOCX 文档生成模块 负责: - 使用 python-docx 创建 Word 文档 - 根据 TableInfo 在文档中重建表格 - 设置合并单元格(跨行/跨列) - 设置单元格对齐、边框等基本格式 """ import logging from typing import Optional from docx import Document from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.ns import qn, nsdecls from docx.oxml import parse_xml from docx.shared import Emu, Pt, Cm from docx.table import _Cell from .table_parser import TableInfo logger = logging.getLogger(__name__) def _get_alignment(text: str) -> int: """从文本内容中猜测对齐方式。""" if not text or not text.strip(): return WD_ALIGN_PARAGRAPH.LEFT stripped = text.strip() if stripped.replace(",", "").replace(".", "").replace("-", "").isdigit(): return WD_ALIGN_PARAGRAPH.RIGHT if len(stripped) <= 5: return WD_ALIGN_PARAGRAPH.CENTER return WD_ALIGN_PARAGRAPH.LEFT def _convert_cm_to_emu(cm_value: float) -> int: """将厘米转换为 EMU(docx 内部单位)。""" return int(round(cm_value * 360000)) def _merge_cells(table, r: int, c: int, row_span: int, col_span: int): """合并表格中的单元格。""" if row_span <= 1 and col_span <= 1: return end_r = r + row_span - 1 end_c = c + col_span - 1 start_cell = table.cell(r, c) end_cell = table.cell(end_r, end_c) start_cell.merge(end_cell) def write_table_to_doc(doc, table_info: TableInfo, body_width_cm: float): """将单个 TableInfo 写入到已打开的 doc 对象中。 Args: doc: Document 对象 table_info: 要写入的表格 body_width_cm: 正文宽度(厘米),用于计算列宽 """ if table_info.rows == 0 or table_info.cols == 0: return doc_table = doc.add_table( rows=table_info.rows, cols=table_info.cols, ) doc_table.autofit = False doc_table.allow_autofit = False # ---- 设置列宽 ---- usable_width = body_width_cm * 0.95 if table_info.col_widths: for c_idx, ratio in enumerate(table_info.col_widths): width_emu = _convert_cm_to_emu(usable_width * ratio) for row in doc_table.rows: row.cells[c_idx].width = Emu(width_emu) # ---- 填充内容、合并单元格 ---- for r in range(table_info.rows): for c in range(table_info.cols): cell_info = table_info.cells[r][c] if cell_info.row_span == 0 or cell_info.col_span == 0: continue _merge_cells(doc_table, r, c, cell_info.row_span, cell_info.col_span) cell = doc_table.cell(r, c) text = cell_info.text or "" cell.text = "" lines = text.split("\n") for li, line in enumerate(lines): if li > 0: cell.add_paragraph() run = cell.paragraphs[li].add_run(line.strip()) run.font.size = Pt(10) run.font.name = "宋体" run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体") alignment = _get_alignment(text) for para in cell.paragraphs: para.alignment = alignment cell.vertical_alignment = 1 # CENTER # ---- 设置表格边框 ---- tbl = doc_table._tbl tblPr = ( tbl.tblPr if tbl.tblPr is not None else parse_xml(f'') ) borders = parse_xml( f'' ' ' ' ' ' ' ' ' ' ' ' ' '' ) tblPr.append(borders) logger.info( f"写入表格: {table_info.rows} 行 x {table_info.cols} 列(含合并单元格)" ) def build_document( tables: list, output_path: str, page_width_cm: float = 21.0, page_height_cm: float = 29.7, margin_cm: float = 2.0, ) -> str: """将所有表格依次写入一个 DOCX 文件。 Args: tables: list[TableInfo] output_path: 输出路径 page_width_cm: 页面宽度 page_height_cm: 页面高度 margin_cm: 页边距 Returns: str: 输出文件路径 """ doc = Document() section = doc.sections[0] section.page_width = Cm(page_width_cm) section.page_height = Cm(page_height_cm) section.top_margin = Cm(margin_cm) section.bottom_margin = Cm(margin_cm) section.left_margin = Cm(margin_cm) section.right_margin = Cm(margin_cm) body_width = page_width_cm - 2 * margin_cm for t_idx, table_info in enumerate(tables): if table_info.rows == 0 or table_info.cols == 0: continue if t_idx > 0: doc.add_paragraph() write_table_to_doc(doc, table_info, body_width) doc.save(output_path) logger.info(f"文档已保存至: {output_path}") return output_path def build_single_table_document( table_info: TableInfo, output_path: str, page_width_cm: float = 21.0, page_height_cm: float = 29.7, margin_cm: float = 2.0, table_label: str = "", ) -> str: """将单个表格写入独立的 DOCX 文件。 Args: table_info: 要写入的表格 output_path: 输出路径 page_width_cm: 页面宽度 page_height_cm: 页面高度 margin_cm: 页边距 table_label: 可选标签,写入文档标题 Returns: str: 输出文件路径 """ doc = Document() section = doc.sections[0] section.page_width = Cm(page_width_cm) section.page_height = Cm(page_height_cm) section.top_margin = Cm(margin_cm) section.bottom_margin = Cm(margin_cm) section.left_margin = Cm(margin_cm) section.right_margin = Cm(margin_cm) body_width = page_width_cm - 2 * margin_cm if table_label: p = doc.add_paragraph() run = p.add_run(table_label) run.font.size = Pt(12) run.font.bold = True write_table_to_doc(doc, table_info, body_width) doc.save(output_path) logger.info(f"单表文档已保存至: {output_path}") return output_path