Files
Pier_v1/도면작성/dwg_export.py
T

147 lines
9.3 KiB
Python

"""Validated DWG 2000 writer using the project's existing LibreDWG tools.
No source/project files are modified. Conversion intermediates and reports stay beside the requested output.
"""
from collections import Counter
import contextlib
import hashlib
import io
import json
import math
from pathlib import Path
import subprocess
import sys
import ezdxf
from ezdxf import recover, colors
ROOT=Path(__file__).resolve().parent
TOOLS=ROOT.parent/'pierzainer_ground/tools/libredwg'
def compatibility_angles(path):
# LibreDWG imports the linear angle from AcDbRotatedDimension whereas standard
# DXF places it in AcDbAlignedDimension. Mirror the value in the converter input only.
lines=path.read_text(encoding='cp949').splitlines()
pairs=list(zip(lines[::2],lines[1::2]));groups=[];group=[]
for code,value in pairs:
if code.strip()=='0' and group:groups.append(group);group=[]
group.append((code,value))
if group:groups.append(group)
output=[]
for group in groups:
angle=next((v for c,v in group if c.strip()=='50'),'0')
for c,v in group:
output.extend([c,v])
if group[0][1]=='DIMENSION' and c.strip()=='100' and v=='AcDbRotatedDimension':output.extend([' 50',angle])
path.write_text('\n'.join(output)+'\n',encoding='cp949')
def fingerprint(entity):
def point(p):return tuple(round(float(v),5) for v in p)
d=entity.dxf;t=entity.dxftype();link=tuple((tag.code,tag.value) for tag in entity.get_xdata('PZ_GA')) if entity.has_xdata('PZ_GA') else ();base=(t,d.layer,link)
if t=='LINE':return base+(point(d.start),point(d.end))
if t=='CIRCLE':return base+(point(d.center),round(d.radius,5))
if t=='LWPOLYLINE':return base+(bool(entity.closed),tuple(point(p[:2]) for p in entity.get_points()))
if t=='TEXT':return base+(d.text,point(d.insert),round(d.height,5),round(d.get('rotation',0),5),d.get('style','STANDARD').casefold())
if t=='DIMENSION':return base+(round(float(entity.get_measurement()),5),point(d.defpoint2),point(d.defpoint3))
if t in ('ATTDEF','ATTRIB'):return base+(d.tag,d.text,point(d.insert),round(d.height,5))
if t=='INSERT':return base+(d.name,point(d.insert),tuple(fingerprint(a) for a in entity.attribs))
if t=='HATCH':return base+(d.pattern_name,tuple(tuple(point(p[:2]) for p in loop.vertices) for loop in entity.paths if hasattr(loop,'vertices')),
tuple((round(line.angle,5),point(line.base_point),point(line.offset),tuple(round(v,5) for v in line.dash_length_items)) for line in entity.pattern.lines) if entity.pattern else ())
return base
def convert(source,output):
source=Path(source).resolve();output=Path(output).resolve()
if output.suffix.lower()!='.dwg' or not output.is_relative_to(ROOT):raise ValueError('DWG output must stay inside 도면작성')
if output.exists():raise ValueError('Existing DWG is preserved; choose another output file')
output.parent.mkdir(parents=True,exist_ok=True)
original=ezdxf.readfile(source);audit=original.audit()
if audit.errors:raise ValueError('Input DXF audit failed')
doc=original
# R2000 has no MATERIAL objects. LibreDWG used to turn the default material
# dictionary into invalid ACDBPLACEHOLDER entries that its own reader repairs.
# Remove unsupported metadata before encoding, preserving indexed colours.
materials=[e for e in doc.objects if e.dxftype()=='MATERIAL']
material_dictionary=doc.rootdict.get('ACAD_MATERIAL')
unsupported_handles={e.dxf.handle for e in materials}
if material_dictionary:unsupported_handles.add(material_dictionary.dxf.handle)
# The bundled writer's R2004 path drops entities. R2000 is used after a full round-trip check.
# R2000 uses indexed colours; retain the intended greys with the nearest ACI colour.
colour_count=0
for entity in doc.entitydb.values():
if entity.is_alive and getattr(entity,'rgb',None):
rgb=entity.rgb
nearest=min(range(1,256),key=lambda i:sum((a-b)**2 for a,b in zip(rgb,colors.aci2rgb(i))))
entity.dxf.color=nearest;entity.dxf.discard('true_color');colour_count+=1
doc.dxfversion='AC1015';doc.encoding='cp949';doc.header['$DWGCODEPAGE']='ANSI_949'
intermediate=output.with_suffix('.conversion.dxf');doc.saveas(intermediate)
expected={layout.name:Counter(fingerprint(e) for e in layout) for layout in doc.layouts}
expected_blocks={block.name:Counter(fingerprint(e) for e in block) for block in doc.blocks if not block.name.startswith('*')}
compatibility_angles(intermediate)
# Strip only converter-input metadata after ezdxf has finished serialization;
# ezdxf requires its default materials while saving even an R2000 document.
lines=intermediate.read_text(encoding='cp949').splitlines()
pairs=list(zip(lines[::2],lines[1::2]));groups=[];group=[]
for code,value in pairs:
if code.strip()=='0' and group:groups.append(group);group=[]
group.append((code,value))
if group:groups.append(group)
cleaned=[]
for group in groups:
handle=next((v for c,v in group if c.strip()=='5'),None)
if handle in unsupported_handles:continue
skip=False
for i,(code,value) in enumerate(group):
if skip:skip=False;continue
if code.strip()=='3' and value=='ACAD_MATERIAL':skip=True;continue
if code.strip()=='347':value='0'
cleaned.extend([code,value])
intermediate.write_text('\n'.join(cleaned)+'\n',encoding='cp949')
logs=[]
for args in [[str(TOOLS/'dxf2dwg.exe'),'--as','r2000','-o',str(output),str(intermediate)],
[str(TOOLS/'dwgread.exe'),'-O','DXF','-o',str(output.with_suffix('.roundtrip.dxf')),str(output)]]:
result=subprocess.run(args,capture_output=True,timeout=90)
logs.append(result.stderr.decode('utf-8','replace')[-12000:])
if not output.exists() or output.stat().st_size<100:raise RuntimeError('DWG writer did not create a valid file')
if output.read_bytes()[:6]!=b'AC1015':raise ValueError('Invalid DWG 2000 signature')
roundtrip,reader_audit=recover.readfile(output.with_suffix('.roundtrip.dxf'))
audit=roundtrip.audit()
if audit.errors:raise ValueError('DWG round-trip audit failed')
actual={layout.name:Counter(fingerprint(e) for e in layout) for layout in roundtrip.layouts}
differences=[]
for name,wanted in expected.items():
got=actual.get(name,Counter())
if wanted!=got:differences.append(dict(layout=name,missing=sum((wanted-got).values()),extra=sum((got-wanted).values())))
if differences:raise ValueError('DWG round-trip changed objects: '+json.dumps(differences))
for name,wanted in expected_blocks.items():
if name not in roundtrip.blocks or Counter(fingerprint(e) for e in roundtrip.blocks[name])!=wanted:raise ValueError('DWG block/attribute definition changed: '+name)
source_dimensions=sorted(float(e.get_measurement()) for e in doc.modelspace().query('DIMENSION'))
target_dimensions=sorted(float(e.get_measurement()) for e in roundtrip.modelspace().query('DIMENSION'))
max_error=max((abs(a-b) for a,b in zip(source_dimensions,target_dimensions)),default=0)
if max_error>1e-6:raise ValueError('DWG dimension measurement differs by more than 0.000001mm')
for layer in doc.layers:
target=roundtrip.layers.get(layer.dxf.name)
if layer.dxf.lineweight!=target.dxf.lineweight:raise ValueError('DWG layer lineweight changed')
for style in doc.dimstyles:
target=roundtrip.dimstyles.get(style.dxf.name)
for key in ['dimtxt','dimasz','dimlfac','dimscale','dimexe','dimexo']:
if abs(float(style.dxf.get(key,0))-float(target.dxf.get(key,0)))>1e-6:raise ValueError('DWG dimension style changed: '+style.dxf.name+'/'+key)
for style in doc.styles:
if style.dxf.font.casefold()!=roundtrip.styles.get(style.dxf.name).dxf.font.casefold():raise ValueError('DWG text font changed')
for layout in doc.layouts:
target=roundtrip.layouts.get(layout.name)
for key in ['paper_width','paper_height','scale_numerator','scale_denominator','plot_layout_flags']:
if layout.dxf.get(key)!=target.dxf.get(key):raise ValueError('DWG paper/plot setting changed: '+key)
report=dict(status='validated',format='DWG 2000 / AC1015',sha256=hashlib.sha256(output.read_bytes()).hexdigest(),
layouts={l.name:len(l) for l in roundtrip.layouts},dimensions=len(roundtrip.modelspace().query('DIMENSION')),
object_fingerprints_match=True,source_links_match=True,dimension_measurements_match=True,max_dimension_error_mm=max_error,lineweights_match=True,dimstyles_match=True,paper_plot_settings_match=True,font_styles_match=True,audit_errors=len(audit.errors),
decoder_metadata_repairs=[str(e.message) for e in reader_audit.fixes],indexed_colour_conversions=colour_count,
native_autocad_open_tested=False)
report['unsupported_materials_removed']=len(materials)
output.with_suffix('.validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8')
output.with_suffix('.conversion.log').write_text('\n'.join(logs),encoding='utf-8')
return report
if __name__=='__main__':
try:print(json.dumps(convert(sys.argv[1],sys.argv[2]),ensure_ascii=False))
except Exception as error:
print(json.dumps(dict(status='error',message=str(error)),ensure_ascii=False));sys.exit(1)