| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- """
- 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'<w:tblPr {nsdecls("w")}/>')
- )
- borders = parse_xml(
- f'<w:tblBorders {nsdecls("w")}>'
- ' <w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
- ' <w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
- ' <w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
- ' <w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
- ' <w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
- ' <w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/>'
- '</w:tblBorders>'
- )
- 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
|