"""
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