Files
guangdong-red-base/build_professional_docx.py
T

181 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from pathlib import Path
import re, os
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.section import WD_SECTION_START
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from PIL import Image, ImageDraw, ImageFont
ROOT=Path(__file__).parent
SRC=ROOT/'终极目标导向创新升级方案.md'
OUT=ROOT/'延安精神(大湾区)学习展示中心—终极目标导向创新升级方案.docx'
AS=ROOT/'docx_assets'; AS.mkdir(exist_ok=True)
RED='9E1B1B'; DARK='4A0F12'; GOLD='C59B53'; INK='252525'; MUTED='666666'; PALE='F7F2EC'; GRID='D8CCC0'
def shade(cell, fill):
pr=cell._tc.get_or_add_tcPr(); x=pr.find(qn('w:shd'))
if x is None: x=OxmlElement('w:shd'); pr.append(x)
x.set(qn('w:fill'),fill)
def margins(cell,t=90,b=90,s=120,e=120):
pr=cell._tc.get_or_add_tcPr(); tc=pr.first_child_found_in('w:tcMar')
if tc is None: tc=OxmlElement('w:tcMar'); pr.append(tc)
for k,v in [('top',t),('bottom',b),('start',s),('end',e)]:
n=tc.find(qn('w:'+k))
if n is None: n=OxmlElement('w:'+k); tc.append(n)
n.set(qn('w:w'),str(v)); n.set(qn('w:type'),'dxa')
def set_repeat(row):
trPr=row._tr.get_or_add_trPr(); e=OxmlElement('w:tblHeader'); e.set(qn('w:val'),'true'); trPr.append(e)
def cant_split(row):
trPr=row._tr.get_or_add_trPr(); trPr.append(OxmlElement('w:cantSplit'))
def set_cell_width(cell,dxa):
tcPr=cell._tc.get_or_add_tcPr(); tcW=tcPr.find(qn('w:tcW'))
if tcW is None: tcW=OxmlElement('w:tcW'); tcPr.append(tcW)
tcW.set(qn('w:w'),str(dxa)); tcW.set(qn('w:type'),'dxa')
def font(run,name='Arial Unicode MS',size=None,bold=None,color=None):
run.font.name=name
rf=run._element.get_or_add_rPr().rFonts
for key in ('ascii','hAnsi','eastAsia','cs'): rf.set(qn('w:'+key),name)
if size: run.font.size=Pt(size)
if bold is not None: run.bold=bold
if color: run.font.color.rgb=RGBColor.from_string(color)
def keep(p,next_=False):
pr=p._p.get_or_add_pPr(); pr.append(OxmlElement('w:keepNext' if next_ else 'w:keepLines'))
def add_runs(p,text,base_size=10.5,color=INK):
parts=re.split(r'(\*\*.*?\*\*)',text)
for x in parts:
if not x: continue
bold=x.startswith('**') and x.endswith('**'); val=x[2:-2] if bold else x
r=p.add_run(val); font(r,size=base_size,bold=bold,color=(RED if bold else color))
def border_bottom(p,color=GOLD,size='16'):
pPr=p._p.get_or_add_pPr(); pbdr=OxmlElement('w:pBdr'); b=OxmlElement('w:bottom')
for k,v in [('val','single'),('sz',size),('space','5'),('color',color)]: b.set(qn('w:'+k),v)
pbdr.append(b); pPr.append(pbdr)
def diagram(lines,idx):
items=[]
raw='\n'.join(lines).strip()
if '' in raw and '' in raw and '\n' in raw:
for ln in lines:
if '' in ln: items.append((ln.split('',1)[0], [x.strip() for x in ln.split('',1)[1].split('')]))
else:
toks=[x.strip() for x in re.split(r'→|↓|\n\s*\+\s*|\n',raw) if x.strip() and x.strip()!='+']
items=[('',toks)]
W=1400; pad=70; boxh=74; gap=30; titleh=38
H=pad*2+sum((titleh if a else 0)+len(b)*(boxh+gap) for a,b in items)
im=Image.new('RGB',(W,H),'white'); d=ImageDraw.Draw(im)
candidates=['/System/Library/Fonts/PingFang.ttc','/System/Library/Fonts/STHeiti Light.ttc']
fp=next((x for x in candidates if os.path.exists(x)),None)
f=ImageFont.truetype(fp,28) if fp else ImageFont.load_default(); fs=ImageFont.truetype(fp,24) if fp else f
y=pad
for label,seq in items:
if label: d.text((pad,y),label,fill='#9E1B1B',font=f); y+=titleh
n=len(seq); bw=min(1120,max(400,1000)); x=(W-bw)//2
for j,t in enumerate(seq):
fill='#9E1B1B' if j==0 else ('#4A0F12' if j==n-1 else '#F7F2EC')
tc='white' if j in (0,n-1) else '#252525'
d.rounded_rectangle((x,y,x+bw,y+boxh),radius=16,fill=fill,outline='#C59B53',width=3)
bb=d.textbbox((0,0),t,font=fs); d.text((W/2-(bb[2]-bb[0])/2,y+(boxh-(bb[3]-bb[1]))/2-3),t,fill=tc,font=fs)
if j<n-1:
cx=W//2; d.line((cx,y+boxh,cx,y+boxh+gap-7),fill='#C59B53',width=4)
d.polygon([(cx-8,y+boxh+gap-14),(cx+8,y+boxh+gap-14),(cx,y+boxh+gap)],fill='#C59B53')
y+=boxh+gap
path=AS/f'diagram_{idx}.png'; im.save(path,dpi=(180,180)); return path
doc=Document(); sec=doc.sections[0]
sec.page_width=Inches(8.27); sec.page_height=Inches(11.69)
sec.top_margin=Inches(.78); sec.bottom_margin=Inches(.72); sec.left_margin=Inches(.85); sec.right_margin=Inches(.75)
sec.header_distance=Inches(.35); sec.footer_distance=Inches(.35)
styles=doc.styles
normal=styles['Normal']; normal.font.name='Arial Unicode MS'; normal._element.rPr.rFonts.set(qn('w:eastAsia'),'Arial Unicode MS'); normal.font.size=Pt(10.5)
normal.paragraph_format.space_after=Pt(6); normal.paragraph_format.line_spacing=1.35
for nm,sz,col,bef,aft in [('Title',25,DARK,0,14),('Heading 1',17,RED,18,9),('Heading 2',13.5,DARK,13,6),('Heading 3',11.5,RED,9,4),('Heading 4',10.5,DARK,7,3)]:
s=styles[nm]; s.font.name='Arial Unicode MS'; s._element.rPr.rFonts.set(qn('w:eastAsia'),'Arial Unicode MS'); s.font.size=Pt(sz); s.font.bold=True; s.font.color.rgb=RGBColor.from_string(col)
s.paragraph_format.space_before=Pt(bef); s.paragraph_format.space_after=Pt(aft); s.paragraph_format.keep_with_next=True
# Cover
p=doc.add_paragraph(); p.paragraph_format.space_before=Pt(72); p.alignment=WD_ALIGN_PARAGRAPH.CENTER
r=p.add_run('延安精神(大湾区)\n学习展示中心'); font(r,size=29,bold=True,color=DARK)
p=doc.add_paragraph(); p.alignment=WD_ALIGN_PARAGRAPH.CENTER; border_bottom(p,GOLD,'22')
r=p.add_run('终极目标导向创新升级方案'); font(r,size=21,bold=True,color=RED)
p.paragraph_format.space_before=Pt(22); p.paragraph_format.space_after=Pt(22)
p=doc.add_paragraph(); p.alignment=WD_ALIGN_PARAGRAPH.CENTER
r=p.add_run('从数字化展馆到可复制的精神传承操作系统'); font(r,size=12,color=MUTED)
p=doc.add_paragraph(); p.paragraph_format.space_before=Pt(155); p.alignment=WD_ALIGN_PARAGRAPH.CENTER
r=p.add_run('专业方案文本 · 2026年7月'); font(r,size=10.5,color=MUTED)
doc.add_page_break()
# contents
p=doc.add_paragraph('目录',style='Heading 1'); border_bottom(p)
for n,t in [('','先重新定义项目的终极目标'),('','创新总体架构'),('','八个旗舰创新产品'),('','可信AI不是一个展项,而是全馆公共能力'),('','六类人群的完整体验旅程'),('','对现有八大子系统的创新升级'),('','内容与技术的投资优先级'),('','分阶段实施建议'),('','建议首个重点原型'),('','评价体系'),('十一','最终形成的项目壁垒')]:
p=doc.add_paragraph(); p.paragraph_format.space_after=Pt(5); r=p.add_run(f'{n} {t}'); font(r,size=11,bold=True if n in ('','','') else False,color=DARK)
doc.add_page_break()
lines=SRC.read_text(encoding='utf-8').splitlines(); i=0; dnum=0; first_title=True
while i<len(lines):
ln=lines[i].rstrip()
if first_title and ln.startswith('# '): first_title=False; i+=1; continue
if ln.strip() in ('---',''): i+=1; continue
if ln.startswith('```'):
block=[]; i+=1
while i<len(lines) and not lines[i].startswith('```'): block.append(lines[i]); i+=1
dnum+=1; path=diagram(block,dnum); p=doc.add_paragraph(); p.alignment=WD_ALIGN_PARAGRAPH.CENTER; keep(p)
p.add_run().add_picture(str(path),width=Inches(5.65)); i+=1; continue
if ln.startswith('|'):
rows=[]
while i<len(lines) and lines[i].startswith('|'):
cells=[x.strip() for x in lines[i].strip().strip('|').split('|')]
if not all(re.fullmatch(r':?-+:?',x) for x in cells): rows.append(cells)
i+=1
cols=max(map(len,rows)); tbl=doc.add_table(rows=0,cols=cols); tbl.alignment=WD_TABLE_ALIGNMENT.LEFT; tbl.autofit=False
# narrative columns get more width
lens=[max(len(r[c]) if c<len(r) else 0 for r in rows) for c in range(cols)]; total=sum(max(6,x) for x in lens)
widths=[int(9360*max(6,x)/total) for x in lens]; widths[-1]+=9360-sum(widths)
for ri,row in enumerate(rows):
cells=tbl.add_row().cells; cant_split(tbl.rows[-1])
if ri==0: set_repeat(tbl.rows[-1])
for c in range(cols):
set_cell_width(cells[c],widths[c]); margins(cells[c]); cells[c].vertical_alignment=WD_CELL_VERTICAL_ALIGNMENT.CENTER
if ri==0: shade(cells[c],RED)
elif ri%2==0: shade(cells[c],PALE)
p=cells[c].paragraphs[0]; p.paragraph_format.space_after=Pt(0); p.paragraph_format.line_spacing=1.15
add_runs(p,row[c] if c<len(row) else '',9.2,'FFFFFF' if ri==0 else INK)
if ri==0:
for rr in p.runs: rr.bold=True
doc.add_paragraph().paragraph_format.space_after=Pt(1); continue
m=re.match(r'^(#{2,5})\s+(.*)',ln)
if m:
level=len(m.group(1))-1; text=m.group(2)
if level==1 and doc.paragraphs[-1].text: doc.add_page_break()
p=doc.add_paragraph(style=f'Heading {min(level,4)}'); add_runs(p,text,styles[f'Heading {min(level,4)}'].font.size.pt)
if level==1: border_bottom(p)
i+=1; continue
if ln.startswith('>'):
vals=[]
while i<len(lines) and lines[i].startswith('>'): vals.append(lines[i].lstrip('> ').strip()); i+=1
t=doc.add_table(rows=1,cols=1); t.alignment=WD_TABLE_ALIGNMENT.LEFT; t.autofit=False; set_cell_width(t.cell(0,0),9360); margins(t.cell(0,0),170,170,220,220); shade(t.cell(0,0),PALE)
p=t.cell(0,0).paragraphs[0]; p.paragraph_format.space_after=Pt(0); p.paragraph_format.line_spacing=1.3; add_runs(p,' '.join(vals),11,DARK)
for r in p.runs: r.bold=True
continue
lm=re.match(r'^\s*(-|\d+\.)\s+(.*)',ln)
if lm:
style='List Bullet' if lm.group(1)=='-' else 'List Number'; p=doc.add_paragraph(style=style); add_runs(p,lm.group(2)); p.paragraph_format.left_indent=Inches(.28); p.paragraph_format.first_line_indent=Inches(-.18); p.paragraph_format.space_after=Pt(3); i+=1; continue
p=doc.add_paragraph(); p.alignment=WD_ALIGN_PARAGRAPH.JUSTIFY; add_runs(p,ln); keep(p); i+=1
# header/footer
for s in doc.sections:
hp=s.header.paragraphs[0]; hp.alignment=WD_ALIGN_PARAGRAPH.RIGHT; r=hp.add_run('延安精神(大湾区)学习展示中心 · 创新升级方案'); font(r,size=8.5,color=MUTED); border_bottom(hp,GRID,'6')
fp=s.footer.paragraphs[0]; fp.alignment=WD_ALIGN_PARAGRAPH.CENTER
r=fp.add_run(''); font(r,size=8,color=MUTED)
fld=OxmlElement('w:fldSimple'); fld.set(qn('w:instr'),'PAGE'); fp._p.append(fld)
r=fp.add_run(''); font(r,size=8,color=MUTED)
# cover no header
doc.sections[0].different_first_page_header_footer=True
doc.core_properties.title='延安精神(大湾区)学习展示中心—终极目标导向创新升级方案'
doc.core_properties.subject='专业创新升级方案'
doc.save(OUT)
print(OUT)