"""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''+''.join(render(c,rels) for c in n if c.tag.split('}')[-1] in ('grpSp','pic','sp','cxnSp'))+'' 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'') 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'') preset=child(sp,'a:prstGeom');prst=preset.get('prst') if preset is not None else '' if prst=='ellipse':out.append(f'') elif prst=='line':out.append(f'') elif prst and not paths:out.append(f'') 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'{esc(t.text or "") if t is not None else ""}') out.append(f'{"".join(spans)}') lineY+=fs*.12 return f''+''.join(out)+'' 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'') 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'') 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'{esc(cell.value)}') 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'{render(shape,rels)}') svg=f'{"".join(out)}' (ROOT/'.cache'/f'{name}.svg').write_text(svg,encoding='utf-8') print(name,xs[-1],ys[-1]-offset)