Improve drawing export and foundation quantity views
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
a0ec74b09b
commit
d22c5008f0
72 files changed
+24346
-12
No files matched your search
@@ -0,0 +1,150 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user