Files
Pier_v1/수량산출/tools/preserve_template.py
T

69 lines
5.8 KiB
Python

"""Keep the original OOXML package (including WMF and shapes); patch selected cells only."""
from pathlib import Path
import zipfile,json,sys,re,hashlib,html,base64
from xml.etree import ElementTree as ET
ROOT=Path(__file__).resolve().parent.parent
NS={'s':'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
def write(payload_path,output_path):
source=ROOT/'교각수량산출.xlsx';output=Path(output_path).resolve()
if not output.is_relative_to((ROOT/'outputs').resolve()) or output.exists():raise ValueError('결과 경로는 수량산출/outputs 내부의 새 파일이어야 합니다.')
data=json.loads(Path(payload_path).read_text(encoding='utf-8-sig'))
if hashlib.sha256(source.read_bytes()).hexdigest()!=data['templateHash']:raise ValueError('원본 템플릿이 변경되었습니다. 분석을 다시 실행하세요.')
with zipfile.ZipFile(source) as zin:
wb=ET.fromstring(zin.read('xl/workbook.xml'));rels=ET.fromstring(zin.read('xl/_rels/workbook.xml.rels'))
targets={r.attrib['Id']:r.attrib['Target'] for r in rels}
paths={s.attrib['name']:('xl/'+targets[s.attrib['{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id']].lstrip('/').removeprefix('xl/')) for s in wb.find('s:sheets',NS)}
updates={}
for sheet,edits in data['edits'].items():
path=paths[sheet];xml=zin.read(path).decode('utf8')
for cell,change in edits.items():
pattern=r'<c\b([^>]*\br="'+re.escape(cell)+r'"[^>]*)(?:>(.*?)</c>|/>)'
def replace(match):
attrs=match[1];body=match[2] or ''
if 'value' in change:
attrs=re.sub(r'\s+t="[^"]*"','',attrs);v=change['value']
if v is None:return '<c'+attrs+'/>'
if isinstance(v,str):return '<c'+attrs+' t="inlineStr"><is><t xml:space="preserve">'+html.escape(v)+'</t></is></c>'
return '<c'+attrs+'><v>'+str(v)+'</v></c>'
if '<f' not in body:raise ValueError('수식 캐시 대상에 수식이 없습니다: '+sheet+'!'+cell)
if 'formula' in change:body=re.sub(r'<f\b[^>]*>.*?</f>',lambda _: '<f>'+html.escape(change['formula'].removeprefix('='))+'</f>',body,flags=re.S)
body=re.sub(r'<v(?:\s[^>]*)?>.*?</v>|<v\s*/>','',body,flags=re.S)
return '<c'+attrs+'>'+body+'<v>'+str(change['cached'])+'</v></c>'
xml,count=re.subn(pattern,replace,xml,count=1,flags=re.S)
if count!=1:raise ValueError('기존 입력셀을 찾을 수 없습니다: '+sheet+'!'+cell)
updates[path]=xml.encode('utf8')
xml=zin.read('xl/workbook.xml').decode('utf8')
xml=re.sub(r'<calcPr\b[^>]*/>', '<calcPr calcId="191029" fullCalcOnLoad="1" forceFullCalc="1"/>',xml)
updates['xl/workbook.xml']=xml.encode('utf8')
updates['xl/_rels/workbook.xml.rels']=re.sub(r'<Relationship\b[^>]*Type="[^"]*/calcChain"[^>]*/>','',zin.read('xl/_rels/workbook.xml.rels').decode('utf8')).encode('utf8')
updates['[Content_Types].xml']=re.sub(r'<Override\b[^>]*PartName="/xl/calcChain.xml"[^>]*/>','',zin.read('[Content_Types].xml').decode('utf8')).encode('utf8')
additions={}
if data.get('planPng'):
image=base64.b64decode(data['planPng'],validate=True)
if not image.startswith(b'\x89PNG\r\n\x1a\n') or len(image)>1200000:raise ValueError('KEY PLAN PNG가 올바르지 않습니다.')
# Only the visible 3.2 sample alignment picture is replaced. Source
# workbook and all calculation cells / other figures remain intact.
drawing=ET.fromstring(zin.read('xl/drawings/drawing5.xml'))
dns={'x':'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing','a':'http://schemas.openxmlformats.org/drawingml/2006/main'}
target=next(a for a in drawing if a.find('x:from/x:col',dns).text=='0')
rid=target.find('.//a:blip',dns).get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
relpath='xl/drawings/_rels/drawing5.xml.rels'
relxml=zin.read(relpath).decode('utf8')
pattern=r'<Relationship\b(?=[^>]*\bId="'+re.escape(rid)+r'")[^>]*/>'
relxml,count=re.subn(pattern,lambda m:re.sub(r'Target="[^"]*"','Target="../media/quantity-project-plan.png"',m[0]),relxml)
if count!=1:raise ValueError('3.2 KEY PLAN 연결을 찾지 못했습니다.')
updates[relpath]=relxml.encode('utf8')
additions['xl/media/quantity-project-plan.png']=image
output.parent.mkdir(parents=True,exist_ok=True)
with zipfile.ZipFile(output,'w',zipfile.ZIP_DEFLATED) as zout:
for info in zin.infolist():
if info.filename=='xl/calcChain.xml':continue
zout.writestr(info,updates.get(info.filename,zin.read(info.filename)))
for name,content in additions.items():zout.writestr(name,content)
report={'status':'generated','sheets':len(paths),'patched_cells':sum(map(len,data['edits'].values())),'original_preserved':True,'straight_limit_formulas':sum('formula' in c for cells in data['edits'].values() for c in cells.values()),'unchanged_package_parts':len(zin.namelist())-len(updates)-('xl/calcChain.xml' in zin.namelist()),'formula_policy':'P1 source formulas with explicit straight-limit overrides when applicable; P2-P6 unused blank; native full recalculation on open'}
output.with_suffix('.validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf8')
return report
if __name__=='__main__':
try:print(json.dumps(write(sys.argv[1],sys.argv[2]),ensure_ascii=False))
except Exception as e:print(json.dumps({'error':str(e)},ensure_ascii=False));sys.exit(1)