restyle_operation_docx.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. #!/usr/bin/env python3
  2. """将操作说明 docx 样式对齐健康评估操作说明参考文档(仅改样式,不改正文)。"""
  3. import shutil
  4. import zipfile
  5. from pathlib import Path
  6. from docx import Document
  7. from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
  8. from docx.oxml import OxmlElement
  9. from docx.oxml.ns import qn
  10. from docx.shared import Pt, RGBColor
  11. REFERENCE_DOCX = Path(__file__).parent / "健康评估-操作说明.docx"
  12. DEFAULT_TARGET_DOCX = Path(__file__).parent / "异常检测及风资源-操作说明.docx"
  13. HEALTH_DOCX = Path(__file__).parent / "健康评估-操作说明.docx"
  14. # 与交付样表一致:深蓝表头 + 浅蓝数据行 + 黑色边框
  15. HEADING_FILL = "1F3864"
  16. BODY_FILL = "EBF1F9"
  17. HEADING_TEXT = RGBColor(0xFF, 0xFF, 0xFF)
  18. BODY_TEXT = RGBColor(0x00, 0x00, 0x00)
  19. BORDER_COLOR = "000000"
  20. STYLE_REMAP = [
  21. ("Heading 3", "Heading 4"),
  22. ("Heading 2", "Heading 3"),
  23. ("Heading 1", "Heading 2"),
  24. ("Title", "Heading 1"),
  25. ]
  26. def set_cell_shading(cell, fill_hex):
  27. tc_pr = cell._tc.get_or_add_tcPr()
  28. for child in tc_pr.findall(qn("w:shd")):
  29. tc_pr.remove(child)
  30. shd = OxmlElement("w:shd")
  31. shd.set(qn("w:val"), "clear")
  32. shd.set(qn("w:color"), "auto")
  33. shd.set(qn("w:fill"), fill_hex)
  34. tc_pr.append(shd)
  35. def clear_run_font_override(run):
  36. r_pr = run._element.rPr
  37. if r_pr is None:
  38. return
  39. for tag in ("w:rFonts",):
  40. for node in r_pr.findall(qn(tag)):
  41. r_pr.remove(node)
  42. def style_title_paragraph(paragraph):
  43. paragraph.paragraph_format.space_before = Pt(0)
  44. paragraph.paragraph_format.space_after = Pt(0)
  45. paragraph.paragraph_format.line_spacing = 1.15
  46. for run in paragraph.runs:
  47. clear_run_font_override(run)
  48. run.font.size = Pt(18)
  49. def set_cell_border(cell, color=BORDER_COLOR, size="4"):
  50. """设置单元格四边黑色实线边框(size 单位为 1/8 pt)。"""
  51. tc_pr = cell._tc.get_or_add_tcPr()
  52. for child in tc_pr.findall(qn("w:tcBorders")):
  53. tc_pr.remove(child)
  54. borders = OxmlElement("w:tcBorders")
  55. for edge in ("top", "left", "bottom", "right"):
  56. element = OxmlElement(f"w:{edge}")
  57. element.set(qn("w:val"), "single")
  58. element.set(qn("w:sz"), size)
  59. element.set(qn("w:space"), "0")
  60. element.set(qn("w:color"), color)
  61. borders.append(element)
  62. tc_pr.append(borders)
  63. def style_table_cell_runs(paragraph, *, bold=False, color=BODY_TEXT, align=WD_PARAGRAPH_ALIGNMENT.LEFT):
  64. paragraph.alignment = align
  65. for run in paragraph.runs:
  66. clear_run_font_override(run)
  67. run.font.bold = bold
  68. run.font.color.rgb = color
  69. run.font.size = Pt(11)
  70. def style_all_tables(doc):
  71. for table in doc.tables:
  72. if not table.rows:
  73. continue
  74. table.style = "Table Grid"
  75. for row_idx, row in enumerate(table.rows):
  76. is_header = row_idx == 0
  77. for cell in row.cells:
  78. set_cell_border(cell)
  79. set_cell_shading(cell, HEADING_FILL if is_header else BODY_FILL)
  80. for paragraph in cell.paragraphs:
  81. if is_header:
  82. style_table_cell_runs(
  83. paragraph,
  84. bold=True,
  85. color=HEADING_TEXT,
  86. align=WD_PARAGRAPH_ALIGNMENT.CENTER,
  87. )
  88. else:
  89. style_table_cell_runs(
  90. paragraph,
  91. bold=False,
  92. color=BODY_TEXT,
  93. align=WD_PARAGRAPH_ALIGNMENT.LEFT,
  94. )
  95. def remap_heading_styles(doc):
  96. for old_style, new_style in STYLE_REMAP:
  97. for paragraph in doc.paragraphs:
  98. if paragraph.style.name == old_style:
  99. paragraph.style = doc.styles[new_style]
  100. def copy_reference_package_parts(target_docx):
  101. if not REFERENCE_DOCX.exists():
  102. raise FileNotFoundError(f"参考文档不存在: {REFERENCE_DOCX}")
  103. backup = target_docx.with_suffix(".docx.bak")
  104. shutil.copy2(target_docx, backup)
  105. temp_path = target_docx.with_suffix(".docx.tmp")
  106. replace_parts = {
  107. "word/styles.xml",
  108. "word/fontTable.xml",
  109. "word/theme/theme1.xml",
  110. }
  111. with zipfile.ZipFile(target_docx, "r") as src_zip:
  112. with zipfile.ZipFile(REFERENCE_DOCX, "r") as ref_zip:
  113. ref_parts = {name: ref_zip.read(name) for name in replace_parts}
  114. with zipfile.ZipFile(temp_path, "w", zipfile.ZIP_DEFLATED) as out_zip:
  115. for item in src_zip.infolist():
  116. data = ref_parts[item.filename] if item.filename in ref_parts else src_zip.read(item.filename)
  117. out_zip.writestr(item, data)
  118. temp_path.replace(target_docx)
  119. return backup
  120. def restyle_docx(target_docx=DEFAULT_TARGET_DOCX, copy_reference=True):
  121. target_docx = Path(target_docx)
  122. backup = None
  123. if copy_reference:
  124. backup = copy_reference_package_parts(target_docx)
  125. doc = Document(str(target_docx))
  126. if copy_reference:
  127. remap_heading_styles(doc)
  128. for paragraph in doc.paragraphs:
  129. if paragraph.style.name == "Heading 1" and paragraph.text.strip():
  130. style_title_paragraph(paragraph)
  131. break
  132. style_all_tables(doc)
  133. doc.save(str(target_docx))
  134. print(f"Restyled: {target_docx}")
  135. if backup:
  136. print(f"Backup: {backup}")
  137. def restyle_tables_only(target_docx=DEFAULT_TARGET_DOCX):
  138. target_docx = Path(target_docx)
  139. doc = Document(str(target_docx))
  140. style_all_tables(doc)
  141. doc.save(str(target_docx))
  142. print(f"Table style applied: {target_docx}")
  143. if __name__ == "__main__":
  144. import sys
  145. args = sys.argv[1:]
  146. tables_only = False
  147. target = DEFAULT_TARGET_DOCX
  148. for arg in args:
  149. if arg == "--tables-only":
  150. tables_only = True
  151. elif arg.endswith(".docx"):
  152. target = Path(arg)
  153. if not target.is_absolute():
  154. target = Path(__file__).parent / target.name
  155. if tables_only:
  156. restyle_tables_only(target)
  157. else:
  158. restyle_docx(target)