Skill-BidCreater/scripts/render_outline_docx.py
2026-03-09 22:20:38 +08:00

132 lines
4.5 KiB
Python

from __future__ import annotations
import argparse
from pathlib import Path
from typing import Any
from docx import Document
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Pt
from common import get_bundle_defaults, normalize_bundle, read_json
FONT_NAME = "Microsoft YaHei"
TOC_HINT = "打开文档后右键更新目录"
HEADING_FONT_SIZES = {1: 16, 2: 14, 3: 12}
MAX_HEADING_LEVEL = 9
def set_run_font(run, size: int = 12, bold: bool = False) -> None:
run.bold = bold
run.font.size = Pt(size)
run.font.name = FONT_NAME
rpr = run._element.get_or_add_rPr()
rfonts = rpr.rFonts
if rfonts is None:
rfonts = OxmlElement("w:rFonts")
rpr.append(rfonts)
rfonts.set(qn("w:eastAsia"), FONT_NAME)
rfonts.set(qn("w:ascii"), FONT_NAME)
rfonts.set(qn("w:hAnsi"), FONT_NAME)
def configure_style(style, size: int, bold: bool = False) -> None:
style.font.name = FONT_NAME
style.font.size = Pt(size)
style.font.bold = bold
rpr = style._element.get_or_add_rPr()
rfonts = rpr.rFonts
if rfonts is None:
rfonts = OxmlElement("w:rFonts")
rpr.append(rfonts)
rfonts.set(qn("w:eastAsia"), FONT_NAME)
rfonts.set(qn("w:ascii"), FONT_NAME)
rfonts.set(qn("w:hAnsi"), FONT_NAME)
def add_toc(paragraph) -> None:
run = paragraph.add_run()
fld_begin = OxmlElement("w:fldChar")
fld_begin.set(qn("w:fldCharType"), "begin")
instr = OxmlElement("w:instrText")
instr.set(qn("xml:space"), "preserve")
instr.text = 'TOC \\o "1-9" \\h \\z \\u'
fld_sep = OxmlElement("w:fldChar")
fld_sep.set(qn("w:fldCharType"), "separate")
text = OxmlElement("w:t")
text.text = TOC_HINT
fld_end = OxmlElement("w:fldChar")
fld_end.set(qn("w:fldCharType"), "end")
run._r.append(fld_begin)
run._r.append(instr)
run._r.append(fld_sep)
run._r.append(text)
run._r.append(fld_end)
def add_cover(doc: Document, project_name: str, doc_title: str) -> None:
p = doc.add_paragraph()
p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
set_run_font(p.add_run(project_name), 18, True)
p = doc.add_paragraph()
p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
set_run_font(p.add_run(doc_title), 22, True)
def normalize_outline_item(item: dict[str, Any]) -> dict[str, Any]:
return {
"title": item["title"],
"children": [normalize_outline_item(child) for child in item.get("children", [])],
}
def render_outline_nodes(doc: Document, items: list[dict[str, Any]], prefix: tuple[int, ...] = ()) -> None:
for index, item in enumerate(items, start=1):
numbers = prefix + (index,)
level = min(len(numbers), MAX_HEADING_LEVEL)
font_size = HEADING_FONT_SIZES.get(level, 12)
paragraph = doc.add_paragraph(style=f"Heading {level}")
set_run_font(paragraph.add_run(f"{'.'.join(str(number) for number in numbers)} {item['title']}"), font_size, True)
render_outline_nodes(doc, item.get("children", []), numbers)
def build_docx(outline_spec: dict[str, Any], out_path: Path) -> None:
doc = Document()
configure_style(doc.styles["Normal"], 12)
for level in range(1, MAX_HEADING_LEVEL + 1):
configure_style(doc.styles[f"Heading {level}"], HEADING_FONT_SIZES.get(level, 12), True)
bundle = normalize_bundle(outline_spec.get("bundle"))
bundle_defaults = get_bundle_defaults(bundle) if bundle else None
doc_title = outline_spec.get("doc_title") or (bundle_defaults["outline_doc_title"] if bundle_defaults else "投标文件(目录版)")
toc_title = outline_spec.get("toc_title") or (bundle_defaults["outline_toc_title"] if bundle_defaults else "目录")
add_cover(doc, outline_spec.get("project_name", "项目名称待补充"), doc_title)
doc.add_page_break()
title = doc.add_paragraph()
set_run_font(title.add_run(toc_title), 16, True)
add_toc(doc.add_paragraph())
doc.add_page_break()
sections = [normalize_outline_item(section) for section in outline_spec.get("sections", [])]
render_outline_nodes(doc, sections)
out_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(out_path))
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--outline", required=True)
parser.add_argument("--out", required=True)
args = parser.parse_args()
build_docx(read_json(Path(args.outline).resolve()), Path(args.out).resolve())
if __name__ == "__main__":
main()