151 lines
9.4 KiB
Python
151 lines
9.4 KiB
Python
"""Read-only capture preparation: original DrawingML + native rasterized EMF/WMF.
|
|
|
|
Builds header-free SVG pages for the browser to capture; never edits the workbook.
|
|
"""
|
|
import base64, html, json, math, posixpath, re, zipfile
|
|
from pathlib import Path
|
|
from xml.etree import ElementTree as E
|
|
import openpyxl
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
NS={'a':'http://schemas.openxmlformats.org/drawingml/2006/main','x':'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing','s':'http://schemas.openxmlformats.org/spreadsheetml/2006/main','r':'http://schemas.openxmlformats.org/officeDocument/2006/relationships'}
|
|
Z=zipfile.ZipFile(ROOT/'교각수량산출.xlsx')
|
|
WB=openpyxl.load_workbook(ROOT/'교각수량산출.xlsx',data_only=True)
|
|
THEME={}
|
|
for entry in E.fromstring(Z.read('xl/theme/theme1.xml')).find('a:themeElements/a:clrScheme',NS):
|
|
value=next(iter(entry));THEME[entry.tag.split('}')[-1]]='#'+value.get('val',value.get('lastClr','000000'))
|
|
esc=lambda s:html.escape(str(s),quote=True)
|
|
def child(n,path): return n.find(path,NS) if n is not None else None
|
|
def num(n,key,default=0): return float(n.get(key,default)) if n is not None else default
|
|
def color(n,default='black'):
|
|
if n is None:return default
|
|
rgb=child(n,'a:srgbClr'); scheme=child(n,'a:schemeClr')
|
|
if rgb is not None:return '#'+rgb.get('val')
|
|
if scheme is not None:
|
|
val=THEME.get({'tx1':'dk1','bg1':'lt1'}.get(scheme.get('val'),scheme.get('val')),default)
|
|
mod=child(scheme,'a:lumMod')
|
|
if mod is not None and val.startswith('#'):val='#'+''.join(f'{round(int(val[i:i+2],16)*num(mod,"val")/100000):02x}' for i in (1,3,5))
|
|
return val
|
|
return default
|
|
def frame(n):
|
|
f=child(n,'a:off');e=child(n,'a:ext')
|
|
return num(f,'x'),num(f,'y'),num(e,'cx',1),num(e,'cy',1)
|
|
def render(n,rels):
|
|
kind=n.tag.split('}')[-1]
|
|
if kind=='grpSp':
|
|
xf=child(n,'x:grpSpPr/a:xfrm');x,y,w,h=frame(xf);co=child(xf,'a:chOff');ce=child(xf,'a:chExt')
|
|
tr=f'translate({x} {y}) '
|
|
rot=num(xf,'rot')/60000
|
|
if rot:tr+=f'translate({w/2} {h/2}) rotate({rot}) translate({-w/2} {-h/2}) '
|
|
tr+=f'scale({w/max(1,num(ce,"cx",w))} {h/max(1,num(ce,"cy",h))}) translate({-num(co,"x")} {-num(co,"y")})'
|
|
return f'<g transform="{tr}">'+''.join(render(c,rels) for c in n if c.tag.split('}')[-1] in ('grpSp','pic','sp','cxnSp'))+'</g>'
|
|
sp=child(n,'x:spPr');xf=child(sp,'a:xfrm');x,y,w,h=frame(xf)
|
|
tr=f'translate({x} {y})';rot=num(xf,'rot')/60000
|
|
if rot:tr+=f' translate({w/2} {h/2}) rotate({rot}) translate({-w/2} {-h/2})'
|
|
if xf is not None and (xf.get('flipH')=='1' or xf.get('flipV')=='1'):
|
|
sx=-1 if xf.get('flipH')=='1' else 1;sy=-1 if xf.get('flipV')=='1' else 1
|
|
tr+=f' translate({w if sx<0 else 0} {h if sy<0 else 0}) scale({sx} {sy})'
|
|
out=[]
|
|
if kind=='pic':
|
|
blip=child(n,'x:blipFill/a:blip');rid=blip.get('{'+NS['r']+'}embed') if blip is not None else None
|
|
if rid in rels:
|
|
name=posixpath.basename(rels[rid]);p=ROOT/'.cache/source-media'/name
|
|
if p.suffix in ('.emf','.wmf'):p=Path(str(p)+'.png')
|
|
mime='image/png' if p.suffix=='.png' else 'image/jpeg'
|
|
out.append(f'<image width="{w}" height="{h}" preserveAspectRatio="none" href="data:{mime};base64,{base64.b64encode(p.read_bytes()).decode()}"/>')
|
|
else:
|
|
fill='none' if child(sp,'a:noFill') is not None else color(child(sp,'a:solidFill'),'none')
|
|
ln=child(sp,'a:ln');stroke='none' if ln is None or child(ln,'a:noFill') is not None else color(child(ln,'a:solidFill'))
|
|
width=num(ln,'w',9525)
|
|
attr=f'fill="{fill}" stroke="{stroke}" stroke-width="{width}"'
|
|
paths=sp.findall('a:custGeom/a:pathLst/a:path',NS) if sp is not None else []
|
|
for path in paths:
|
|
cmds=[]
|
|
for cmd in path:
|
|
t=cmd.tag.split('}')[-1];pts=cmd.findall('a:pt',NS);v=' '.join(f'{num(p,"x")} {num(p,"y")}' for p in pts)
|
|
if t in ('moveTo','lnTo','cubicBezTo','quadBezTo'):cmds.append({'moveTo':'M','lnTo':'L','cubicBezTo':'C','quadBezTo':'Q'}[t]+v)
|
|
elif t=='close':cmds.append('Z')
|
|
pw=num(path,'w',w);ph=num(path,'h',h)
|
|
out.append(f'<path transform="scale({w/max(1,pw)} {h/max(1,ph)})" d="{" ".join(cmds)}" {attr}/>')
|
|
preset=child(sp,'a:prstGeom');prst=preset.get('prst') if preset is not None else ''
|
|
if prst=='ellipse':out.append(f'<ellipse cx="{w/2}" cy="{h/2}" rx="{w/2}" ry="{h/2}" {attr}/>')
|
|
elif prst=='line':out.append(f'<path d="M0 0 L{w} {h}" {attr}/>')
|
|
elif prst and not paths:out.append(f'<rect width="{w}" height="{h}" {attr}/>')
|
|
body=child(n,'x:txBody')
|
|
if body is not None:
|
|
bp=child(body,'a:bodyPr');paras=body.findall('a:p',NS);fs=127000;lineY=num(bp,'tIns',45720)
|
|
for para in paras:
|
|
runs=para.findall('a:r',NS);defaults=child(para,'a:pPr/a:defRPr');end=child(para,'a:endParaRPr')
|
|
if runs:fs=num(child(runs[0],'a:rPr'),'sz',1000)*127
|
|
else:fs=num(defaults if defaults is not None else end,'sz',1000)*127
|
|
lineY+=fs
|
|
pp=child(para,'a:pPr');align=pp.get('algn','l') if pp is not None else 'l'
|
|
tx=w/2 if align=='ctr' else w-num(bp,'rIns',91440) if align=='r' else num(bp,'lIns',91440)
|
|
spans=[]
|
|
for run in runs:
|
|
rp=child(run,'a:rPr');t=child(run,'a:t');size=num(rp,'sz',fs/127)*127
|
|
spans.append(f'<tspan font-size="{size/9525}" font-weight="{"700" if rp is not None and rp.get("b")=="1" else "400"}" fill="{color(child(rp,"a:solidFill"))}">{esc(t.text or "") if t is not None else ""}</tspan>')
|
|
out.append(f'<text transform="scale(9525)" x="{tx/9525}" y="{lineY/9525}" text-anchor="{ "middle" if align=="ctr" else "end" if align=="r" else "start"}" font-family="Malgun Gothic,Arial" font-size="{fs/9525}">{"".join(spans)}</text>')
|
|
lineY+=fs*.12
|
|
return f'<g transform="{tr}">'+''.join(out)+'</g>'
|
|
|
|
items=[(3,'criteria-earth',95,27),(4,'criteria-status',51,29),(5,'criteria-structure',102,29),(6,'variables-diagram',16,27)]
|
|
for index,name,last,colcount in items:
|
|
ws=WB.worksheets[index-1];widths=[]
|
|
for c in range(1,colcount+1):
|
|
letter=openpyxl.utils.get_column_letter(c);dim=ws.column_dimensions.get(letter)
|
|
# Grouped column ranges share the first dimension entry.
|
|
if dim is None:dim=next((d for d in ws.column_dimensions.values() if d.min and d.min<=c<=d.max),None)
|
|
width=dim.width if dim is not None else ws.sheet_format.defaultColWidth or 8.43
|
|
widths.append(math.floor(width*7+5))
|
|
heights=[(ws.row_dimensions[r].height or ws.sheet_format.defaultRowHeight or 15)*4/3 for r in range(1,last+1)]
|
|
xs=[0];ys=[0]
|
|
for v in widths:xs.append(xs[-1]+v)
|
|
for v in heights:ys.append(ys[-1]+v)
|
|
offset=ys[1];out=[];covered=set();merges={}
|
|
for m in ws.merged_cells.ranges:
|
|
merges[(m.min_row,m.min_col)]=(m.max_row,m.max_col)
|
|
covered.update((r,c) for r in range(m.min_row,m.max_row+1) for c in range(m.min_col,m.max_col+1) if (r,c)!=(m.min_row,m.min_col))
|
|
for row in ws.iter_rows(min_row=2,max_row=last,max_col=colcount):
|
|
for cell in row:
|
|
r,c=cell.row,cell.column
|
|
if (r,c) in covered:continue
|
|
rr,cc=merges.get((r,c),(r,c));cc=min(cc,colcount);rr=min(rr,last)
|
|
x,y,w,h=xs[c-1],ys[r-1]-offset,xs[cc]-xs[c-1],ys[rr]-ys[r-1]
|
|
fill=cell.fill.fgColor.rgb if cell.fill.patternType=='solid' and cell.fill.fgColor.type=='rgb' else None
|
|
if fill:out.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="#{fill[-6:]}"/>')
|
|
for side,x1,y1,x2,y2 in [('left',x,y,x,y+h),('right',x+w,y,x+w,y+h),('top',x,y,x+w,y),('bottom',x,y+h,x+w,y+h)]:
|
|
b=getattr(cell.border,side)
|
|
if b and b.style:out.append(f'<path d="M{x1} {y1} L{x2} {y2}" stroke="black" stroke-width="{1 if b.style=="thin" else 1.4}"/>')
|
|
if cell.value is not None:
|
|
size=(cell.font.sz or 10)*4/3;align=cell.alignment.horizontal
|
|
tx=x+w/2 if align=='center' else x+w-3 if align=='right' else x+3
|
|
fc=cell.font.color;rgb='#'+fc.rgb[-6:] if fc and fc.type=='rgb' else 'black'
|
|
out.append(f'<text x="{tx}" y="{y+h/2}" dominant-baseline="central" text-anchor="{"middle" if align=="center" else "end" if align=="right" else "start"}" font-family="Malgun Gothic,Arial" font-size="{size}" font-weight="{700 if cell.font.b else 400}" fill="{rgb}">{esc(cell.value)}</text>')
|
|
relfile=f'xl/worksheets/_rels/sheet{index}.xml.rels';dre=None
|
|
for rel in E.fromstring(Z.read(relfile)):
|
|
if rel.get('Type','').endswith('/drawing'):dre=posixpath.normpath(posixpath.join('xl/worksheets',rel.get('Target')))
|
|
rels={}
|
|
if dre:
|
|
for rel in E.fromstring(Z.read(posixpath.dirname(dre)+'/_rels/'+posixpath.basename(dre)+'.rels')):
|
|
if rel.get('TargetMode')!='External':rels[rel.get('Id')]=rel.get('Target')
|
|
for anchor in E.fromstring(Z.read(dre)):
|
|
fr=child(anchor,'x:from');to=child(anchor,'x:to')
|
|
def point(p):
|
|
c=int(child(p,'x:col').text);r=int(child(p,'x:row').text)
|
|
return sum(widths[:c])+float(child(p,'x:colOff').text)/9525,sum(heights[:r])+float(child(p,'x:rowOff').text)/9525-offset
|
|
if fr is None:continue
|
|
left,top=point(fr)
|
|
if to is not None:right,bottom=point(to);aw,ah=right-left,bottom-top
|
|
else:ext=child(anchor,'x:ext');aw,ah=num(ext,'cx')/9525,num(ext,'cy')/9525
|
|
for shape in anchor:
|
|
kind=shape.tag.split('}')[-1]
|
|
if kind not in ('grpSp','sp','pic','cxnSp'):continue
|
|
xf=child(shape,'x:grpSpPr/a:xfrm' if kind=='grpSp' else 'x:spPr/a:xfrm');x,y,w,h=frame(xf)
|
|
# xfrm stores the unrotated source geometry. Cell anchors describe its rotated
|
|
# bounding box; rescaling to that box distorts rotated text and grouped figures.
|
|
out.append(f'<g transform="translate(0 {-offset}) scale({1/9525})">{render(shape,rels)}</g>')
|
|
svg=f'<svg xmlns="http://www.w3.org/2000/svg" width="{xs[-1]}" height="{ys[-1]-offset}" viewBox="0 0 {xs[-1]} {ys[-1]-offset}"><rect width="100%" height="100%" fill="white"/>{"".join(out)}</svg>'
|
|
(ROOT/'.cache'/f'{name}.svg').write_text(svg,encoding='utf-8')
|
|
print(name,xs[-1],ys[-1]-offset)
|