181 lines
11 KiB
Python
181 lines
11 KiB
Python
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)
|