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'): vals=[] while i'): 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)