docx_helpers.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. # -*- coding: utf-8 -*-
  2. """Shared DOCX helpers for the reference document."""
  3. import docx.opc.constants
  4. from docx import Document
  5. from docx.shared import Pt, Cm, RGBColor
  6. from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
  7. from docx.enum.table import WD_TABLE_ALIGNMENT
  8. from docx.oxml.ns import qn
  9. from docx.oxml import OxmlElement
  10. BLACK = RGBColor(0, 0, 0)
  11. GREY = RGBColor(0x40, 0x40, 0x40)
  12. def set_cn_font(target, name_cn, name_ascii=None, size=None, bold=None, color=None):
  13. f = target.font
  14. f.name = name_ascii or name_cn
  15. if size is not None:
  16. f.size = Pt(size)
  17. if bold is not None:
  18. f.bold = bold
  19. if color is not None:
  20. f.color.rgb = color
  21. el = target.element
  22. rPr = el.get_or_add_rPr()
  23. rFonts = rPr.find(qn("w:rFonts"))
  24. if rFonts is None:
  25. rFonts = OxmlElement("w:rFonts")
  26. rPr.append(rFonts)
  27. rFonts.set(qn("w:ascii"), name_ascii or name_cn)
  28. rFonts.set(qn("w:hAnsi"), name_ascii or name_cn)
  29. rFonts.set(qn("w:eastAsia"), name_cn)
  30. def init_doc():
  31. doc = Document()
  32. sec = doc.sections[0]
  33. sec.page_width = Cm(21.0)
  34. sec.page_height = Cm(29.7)
  35. sec.top_margin = Cm(2.54)
  36. sec.bottom_margin = Cm(2.54)
  37. sec.left_margin = Cm(2.8)
  38. sec.right_margin = Cm(2.8)
  39. normal = doc.styles["Normal"]
  40. set_cn_font(normal, "宋体", "Times New Roman", 10.5)
  41. pf = normal.paragraph_format
  42. pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
  43. pf.space_before = Pt(0)
  44. pf.space_after = Pt(4)
  45. for name, size, before, after in (("Heading 1", 16, 18, 8),
  46. ("Heading 2", 13, 14, 6),
  47. ("Heading 3", 11, 10, 4)):
  48. st = doc.styles[name]
  49. set_cn_font(st, "黑体", "Times New Roman", size, True, BLACK)
  50. st.paragraph_format.space_before = Pt(before)
  51. st.paragraph_format.space_after = Pt(after)
  52. st.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
  53. # let Word refresh the TOC field when the document is opened
  54. settings = doc.settings.element
  55. uf = OxmlElement("w:updateFields")
  56. uf.set(qn("w:val"), "true")
  57. settings.append(uf)
  58. footer = sec.footer
  59. fp = footer.paragraphs[0]
  60. fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
  61. run = fp.add_run()
  62. f1 = OxmlElement("w:fldChar"); f1.set(qn("w:fldCharType"), "begin")
  63. it = OxmlElement("w:instrText"); it.set(qn("xml:space"), "preserve"); it.text = " PAGE "
  64. f2 = OxmlElement("w:fldChar"); f2.set(qn("w:fldCharType"), "end")
  65. run._r.append(f1); run._r.append(it); run._r.append(f2)
  66. set_cn_font(run, "宋体", "Times New Roman", 9)
  67. return doc
  68. def add_hyperlink(paragraph, url, text, size=8.5, bold=False):
  69. part = paragraph.part
  70. r_id = part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True)
  71. link = OxmlElement("w:hyperlink")
  72. link.set(qn("r:id"), r_id)
  73. r = OxmlElement("w:r")
  74. rPr = OxmlElement("w:rPr")
  75. rFonts = OxmlElement("w:rFonts")
  76. rFonts.set(qn("w:ascii"), "Consolas")
  77. rFonts.set(qn("w:hAnsi"), "Consolas")
  78. rFonts.set(qn("w:eastAsia"), "宋体")
  79. rPr.append(rFonts)
  80. if bold:
  81. rPr.append(OxmlElement("w:b"))
  82. color = OxmlElement("w:color"); color.set(qn("w:val"), "1F4E79"); rPr.append(color)
  83. u = OxmlElement("w:u"); u.set(qn("w:val"), "single"); rPr.append(u)
  84. sz = OxmlElement("w:sz"); sz.set(qn("w:val"), str(int(size * 2))); rPr.append(sz)
  85. r.append(rPr)
  86. t = OxmlElement("w:t"); t.text = text; t.set(qn("xml:space"), "preserve")
  87. r.append(t)
  88. link.append(r)
  89. paragraph._p.append(link)
  90. def shade(cell, hexcolor):
  91. tcPr = cell._tc.get_or_add_tcPr()
  92. shd = OxmlElement("w:shd")
  93. shd.set(qn("w:val"), "clear")
  94. shd.set(qn("w:color"), "auto")
  95. shd.set(qn("w:fill"), hexcolor)
  96. tcPr.append(shd)
  97. def cell_margins(table, top=55, bottom=55, left=85, right=85):
  98. tblPr = table._tbl.tblPr
  99. mar = OxmlElement("w:tblCellMar")
  100. for tag, val in (("top", top), ("left", left), ("bottom", bottom), ("right", right)):
  101. el = OxmlElement("w:" + tag)
  102. el.set(qn("w:w"), str(val))
  103. el.set(qn("w:type"), "dxa")
  104. mar.append(el)
  105. tblPr.append(mar)
  106. def repeat_header(row):
  107. trPr = row._tr.get_or_add_trPr()
  108. el = OxmlElement("w:tblHeader"); el.set(qn("w:val"), "true")
  109. trPr.append(el)
  110. def cant_split(row):
  111. trPr = row._tr.get_or_add_trPr()
  112. el = OxmlElement("w:cantSplit"); el.set(qn("w:val"), "true")
  113. trPr.append(el)
  114. def set_cell_text(cell, text, size=8.5, bold=False, align=None, color=None):
  115. cell.text = ""
  116. p = cell.paragraphs[0]
  117. p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
  118. p.paragraph_format.space_before = Pt(1)
  119. p.paragraph_format.space_after = Pt(1)
  120. if align is not None:
  121. p.alignment = align
  122. run = p.add_run(text)
  123. set_cn_font(run, "宋体", "Times New Roman", size, bold, color)
  124. def make_table(doc, headers, rows, widths, link_col=None, font=8.5, head_font=8.5):
  125. table = doc.add_table(rows=1, cols=len(headers))
  126. table.style = "Table Grid"
  127. table.alignment = WD_TABLE_ALIGNMENT.CENTER
  128. table.autofit = False
  129. cell_margins(table)
  130. hdr = table.rows[0]
  131. repeat_header(hdr)
  132. cant_split(hdr)
  133. for i, h in enumerate(headers):
  134. c = hdr.cells[i]
  135. c.width = Cm(widths[i])
  136. shade(c, "D9E2F3")
  137. set_cell_text(c, h, head_font, True, WD_ALIGN_PARAGRAPH.CENTER)
  138. for r in rows:
  139. row = table.add_row()
  140. cant_split(row)
  141. for i, val in enumerate(r):
  142. c = row.cells[i]
  143. c.width = Cm(widths[i])
  144. if i == 0:
  145. cell_no_shade = c
  146. if link_col is not None and i == link_col:
  147. c.text = ""
  148. p = c.paragraphs[0]
  149. p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
  150. p.paragraph_format.space_before = Pt(1)
  151. p.paragraph_format.space_after = Pt(1)
  152. add_hyperlink(p, val[1], val[0], size=font)
  153. else:
  154. set_cell_text(c, val, font)
  155. for row in table.rows:
  156. for i, c in enumerate(row.cells):
  157. c.width = Cm(widths[i])
  158. return table
  159. def para(doc, text, size=10.5, bold=False, first_indent=True, space_after=4,
  160. align=None, color=None, left=0.0, line=1.5, font_cn="宋体"):
  161. p = doc.add_paragraph()
  162. pf = p.paragraph_format
  163. pf.space_after = Pt(space_after)
  164. if left:
  165. pf.left_indent = Cm(left)
  166. if line == 1.5:
  167. pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
  168. else:
  169. pf.line_spacing = line
  170. if align is not None:
  171. p.alignment = align
  172. if first_indent:
  173. pf.first_line_indent = Pt(21)
  174. run = p.add_run(text)
  175. set_cn_font(run, font_cn, "Times New Roman", size, bold, color)
  176. return p
  177. def rich(doc, chunks, size=10.5, first_indent=True, space_after=4, left=0.0):
  178. """chunks: list of (text, bold) tuples, all one paragraph."""
  179. p = doc.add_paragraph()
  180. pf = p.paragraph_format
  181. pf.space_after = Pt(space_after)
  182. pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
  183. if left:
  184. pf.left_indent = Cm(left)
  185. if first_indent:
  186. pf.first_line_indent = Pt(21)
  187. for text, bold in chunks:
  188. run = p.add_run(text)
  189. set_cn_font(run, "宋体" if not bold else "黑体", "Times New Roman", size, bold, None)
  190. return p
  191. def bullet(doc, text, size=10.5, level=0, space_after=2):
  192. p = doc.add_paragraph()
  193. pf = p.paragraph_format
  194. pf.left_indent = Cm(0.75 + 0.6 * level)
  195. pf.first_line_indent = Cm(-0.45)
  196. pf.space_after = Pt(space_after)
  197. pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
  198. run = p.add_run("· " if level == 0 else "– ")
  199. set_cn_font(run, "宋体", "Times New Roman", size)
  200. run2 = p.add_run(text)
  201. set_cn_font(run2, "宋体", "Times New Roman", size)
  202. return p
  203. def bullet_rich(doc, chunks, size=10.5, level=0, space_after=2):
  204. p = doc.add_paragraph()
  205. pf = p.paragraph_format
  206. pf.left_indent = Cm(0.75 + 0.6 * level)
  207. pf.first_line_indent = Cm(-0.45)
  208. pf.space_after = Pt(space_after)
  209. pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
  210. run = p.add_run("· " if level == 0 else "– ")
  211. set_cn_font(run, "宋体", "Times New Roman", size)
  212. for text, bold in chunks:
  213. r = p.add_run(text)
  214. set_cn_font(r, "宋体" if not bold else "黑体", "Times New Roman", size, bold, None)
  215. return p
  216. def heading(doc, text, level=1):
  217. h = doc.add_heading(level=level)
  218. h.paragraph_format.space_before = Pt({1: 18, 2: 14, 3: 10}[level])
  219. h.paragraph_format.space_after = Pt({1: 8, 2: 6, 3: 4}[level])
  220. run = h.add_run(text)
  221. set_cn_font(run, "黑体", "Times New Roman", {1: 16, 2: 13, 3: 11}[level], True, BLACK)
  222. return h
  223. def caption(doc, text):
  224. p = doc.add_paragraph()
  225. p.alignment = WD_ALIGN_PARAGRAPH.CENTER
  226. p.paragraph_format.space_before = Pt(8)
  227. p.paragraph_format.space_after = Pt(3)
  228. p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE
  229. run = p.add_run(text)
  230. set_cn_font(run, "黑体", "Times New Roman", 9, True)
  231. return p
  232. def note(doc, text):
  233. p = doc.add_paragraph()
  234. pf = p.paragraph_format
  235. pf.space_before = Pt(4)
  236. pf.space_after = Pt(10)
  237. pf.left_indent = Cm(0.2)
  238. pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
  239. run = p.add_run(text)
  240. set_cn_font(run, "楷体", "Times New Roman", 9.5, False, GREY)
  241. return p
  242. def quote(doc, lines):
  243. for ln in lines:
  244. p = doc.add_paragraph()
  245. pf = p.paragraph_format
  246. pf.left_indent = Cm(0.9)
  247. pf.right_indent = Cm(0.4)
  248. pf.space_after = Pt(3)
  249. pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
  250. run = p.add_run(ln)
  251. set_cn_font(run, "楷体", "Times New Roman", 10)
  252. return
  253. def link_para(doc, pairs, size=9.5, left=0.75, space_after=2):
  254. """pairs: list of (label, url) rendered as 'label url' with url clickable."""
  255. p = doc.add_paragraph()
  256. pf = p.paragraph_format
  257. pf.left_indent = Cm(left)
  258. pf.first_line_indent = Cm(-0.45)
  259. pf.space_after = Pt(space_after)
  260. pf.line_spacing_rule = WD_LINE_SPACING.SINGLE
  261. r = p.add_run("· ")
  262. set_cn_font(r, "宋体", "Times New Roman", size)
  263. for label, url in pairs:
  264. rl = p.add_run(label + " ")
  265. set_cn_font(rl, "宋体", "Times New Roman", size)
  266. add_hyperlink(p, url, url.replace("https://", "").replace("http://", ""), size=size - 0.5)
  267. return p
  268. def toc(doc):
  269. hint = doc.add_paragraph()
  270. hint.paragraph_format.space_after = Pt(6)
  271. hr = hint.add_run("(本页为自动目录域。用 Word 打开时若提示更新域,选择「是」即可生成带页码的目录;也可按 Ctrl+A 后按 F9 手动更新。)")
  272. set_cn_font(hr, "楷体", "Times New Roman", 9, False, GREY)
  273. p = doc.add_paragraph()
  274. run = p.add_run()
  275. f1 = OxmlElement("w:fldChar"); f1.set(qn("w:fldCharType"), "begin"); f1.set(qn("w:dirty"), "true")
  276. it = OxmlElement("w:instrText"); it.set(qn("xml:space"), "preserve")
  277. it.text = r' TOC \o "1-2" \h \z \u '
  278. f2 = OxmlElement("w:fldChar"); f2.set(qn("w:fldCharType"), "separate")
  279. f3 = OxmlElement("w:fldChar"); f3.set(qn("w:fldCharType"), "end")
  280. run._r.append(f1); run._r.append(it); run._r.append(f2)
  281. ph = OxmlElement("w:r")
  282. pt = OxmlElement("w:t")
  283. pt.text = "目录将在此处生成"
  284. pt.set(qn("xml:space"), "preserve")
  285. ph.append(pt)
  286. run._r.append(ph)
  287. run._r.append(f3)
  288. return p