파이썬으로 dxf파일 만들고 이미지,SVG파일로 변환(ft. ezdxf)
토목 엔지니어링에서 빼놓을 수 없는 도구가 CAD다.
파이썬으로 dxf파일을 핸들링 할 수 있는 모듈이 있어서 소개한다.
ezdxf다. 아래 url참조
https://ezdxf.readthedocs.io/en/stable/index.html
https://github.com/mozman/ezdxf/tree/stable
생각보다 기능이 많다.
파이썬에서 dxf파일을 만들고 그것을 저장할 수 있다. 그리고 다른 형식으로 변환도 가능하다.
사용법은 의외로 간단하다.
우선 아래와 같이 pip를 이용해서 설치한다.
pip install ezdxf |
그리고 코드에서 아래와 같이 입력해서 ezdxf를 import한다.
import ezdxf |
dxf를 만드는 건 간단하다.
doc을 정의 하고(dxf버전 정의) 난 다음 modelspace를 정의한 후 add_로 요소들을 추가해주면 된다.
코드를 보면 원은 중심점, 반지름을 지정하고 color를 지정했다.
선은 시작점과 끝점을, 텍스트는 insert point와 높이, 색, 회전을 지정했다.
doc을 saveas 메소드로 저장하면 파일로 생성된다.
doc = ezdxf.new('R2010') msp = doc.modelspace() msp.add_circle((0, 0), 5, dxfattribs={'color': 1}) # 빨간색으로 설정 msp.add_line((0, 0), (50,50), dxfattribs={'color': 1}) msp.add_text("Sample Text", dxfattribs={ 'insert': (0, 5), 'height': 0.5, 'color': 2, # 노란색 'rotation': 30 # 30도 회전 }) doc.saveas(filename) |
필자는 claude 3.5 sonnet을 이용해서 몇가지 기능을 넣어봤다.
필자가 필요한 기능은 GUI에서 dxf파일을 보여주는 기능인데, dxf파일을 GUI에서 직접 보여주는 모듈을 못찾았다.
dxf파일을 분석해서 요소별로 그래픽으로 구현하는 것도 가능하다.
그렇게 하는 것도 나쁘지 않은 방법인데 dxf형식을 GUI환경에서 지원하는 이미지(래스터,벡터)형식으로 변환하면 어떨까 하는 생각으로 만들어본 코드다.
GUI환경까지는 만들지 않았고 일단 dxf를 PNG나 SVG로 만드는 것까지 해본 것이다.
svg형식은 text형식(XML)으로 되어있다. 그래서 dxf요소별로 해당 코드를 만드는 것으로 보인다. 이 코드에 없는 요소는 별도로 추가해줘야 하는 것으로 보인다. 현재 cicle,line,text가 포함되어 있다. arc와 dimension이 포함되면 어느정도 쓸만해질 것 같다.
그리고 dxf를 png로 변환하는 것은 matplotlib모듈을 이용해서 변환한다. matplotlib가 그래프 그리는 기능만 있는 줄 알았는데 다양한 기능이 있는 것으로 보인다.
사용법은 다음과 같다. python으로 스크립트를 실행한다.
python ezdxf2pngsvg.py |
그러면 아래와 같이 sample.dxf를 만들 것인지 아니면 기존 dxf파일을 읽을 것인지를 물어본다. 1을 선택하면 사용자가 지정한 dxf파일을 연다. 현재 디렉토리에 있는 파일만 가능하다. 2를 선택하면 sample.dxf를 생성한다.
그런 다음 출력형식을 묻는다. png 또는 svg를 선택하면 된다.
현재 작업 디렉토리: C:\Users\PCuser\OneDrive\dev\dxf 1: 기존 DXF 파일 열기 2: 샘플 DXF 파일 생성 선택하세요 (1 또는 2): 2 샘플 DXF 파일 'C:\Users\PCuser\OneDrive\dev\dxf\sample.dxf' 생성 완료 출력 형식을 선택하세요 (png 또는 svg): svg SVG 파일 저장 완료: C:\Users\PCuser\OneDrive\dev\dxf\sample.svg 변환 완료: 'sample.dxf' -> 'sample.svg' |
sample.dxf는 아래와 같이 원,선,텍스트 3개 요소로 이루어진 dxf파일이다.
sample.svg파일은 아래와 같이 xml형식의 text파일이다.
<?xml version='1.0' encoding='utf-8'?> <svg width="3.3" height="1" viewBox="0.0 -5.5 3.3 1" xmlns="http://www.w3.org/2000/svg"> <circle cx="0.0" cy="-0.0" r="5.0" stroke="black" stroke-width="0.1%" fill="none" /> <line x1="0.0" y1="-0.0" x2="50.0" y2="-50.0" stroke="black" stroke-width="0.1%" /> <text x="0.0" y="-5.0" font-size="100.0%" fill="black" text-anchor="start" dominant-baseline="auto" transform="rotate(-30.0 0.0 -5.0)">Sample Text</text> </svg> |
아래의 내용은 인공지능에게 이 코드를 설명해달라고 해서 받은 내용이다.
제공된 코드는 DXF (Drawing Exchange Format) 파일을 생성, 조작 및 PNG 또는 SVG 형식으로 변환하는 Python 스크립트입니다. DXF 파일 처리에는 ezdxf 라이브러리를 사용하고, 이미지 렌더링에는 matplotlib을 사용합니다. 스크립트는 각각 특정 작업을 담당하는 여러 함수로 구성됩니다.
create_sample_dxf 함수는 원, 선 및 텍스트 요소가 포함된 샘플 DXF 파일을 생성합니다. 이러한 요소에 대한 색상 및 회전과 같은 특정 속성을 설정하고 지정된 파일 이름으로 파일을 저장합니다.
get_entity_color 함수는 DXF 엔티티에서 색상 속성을 추출하고 이를 RGB 16진수 문자열로 변환합니다. 정수 기반 AutoCAD 색상 인덱스와 직접 RGB 튜플을 모두 처리하고, 색상이 지정되지 않았거나 오류가 발생하면 기본적으로 검정색을 사용합니다.
create_svg_element 함수는 DXF 엔티티를 해당 SVG 요소로 변환합니다. 선, 원, 호 및 텍스트를 지원하며, 위치, 크기, 색상 및 회전과 같은 속성을 SVG 형식에 맞게 조정합니다. 이 함수는 SVG 표현이 원래 DXF 엔티티의 시각적 속성을 유지하도록 합니다.
calculate_bounds 함수는 DXF 문서의 모델 공간에 있는 모든 엔티티의 경계 상자를 계산합니다. 엔티티를 반복하여 최소 및 최대 좌표를 업데이트하여 도면의 전체 치수를 결정합니다.
convert_dxf 함수는 변환 프로세스를 처리합니다. DXF 파일을 읽고 오류를 확인하고 도면 경계를 계산한 다음 matplotlib을 사용하여 PNG 이미지로 렌더링하거나 SVG 파일을 생성합니다. 최종 이미지 또는 SVG가 적절하게 확장되도록 출력 치수 및 해상도를 조정합니다.
get_user_choice, get_dxf_file_name 및 get_output_format 함수는 사용자 입력을 처리하고, 기존 DXF 파일을 열지 아니면 샘플 파일을 생성할지를 선택하고, DXF 파일 이름을 지정하고, 출력 형식(PNG 또는 SVG)을 선택하도록 사용자에게 요청합니다.
마지막으로 스크립트의 기본 블록은 워크플로를 조정합니다. 현재 작업 디렉토리를 설정하고, 사용자에게 입력을 요청하고, DXF 파일을 생성하거나 열고, 변환을 수행합니다. 성공하면 사용자에게 변환 완료를 알리고, 그렇지 않으면 실패를 보고합니다.
소스코드 https://github.com/dolljong/ezdxf2pngsvg
import ezdxf import matplotlib.pyplot as plt from ezdxf import recover from ezdxf.addons.drawing import Frontend, RenderContext from ezdxf.addons.drawing.matplotlib import MatplotlibBackend import os import sys import xml.etree.ElementTree as ET import math def create_sample_dxf(filename): doc = ezdxf.new('R2010') msp = doc.modelspace() msp.add_circle((0, 0), 5, dxfattribs={'color': 1}) # 색상을 빨간색으로 설정 msp.add_line((0, 0), (50,50), dxfattribs={'color': 1}) msp.add_text("Sample Text", dxfattribs={ 'insert': (0, 5), 'height': 0.5, 'color': 2, # 노란색 'rotation': 30 # 30도 회전 }) doc.saveas(filename) print(f"샘플 DXF 파일 '{filename}' 생성 완료") def get_entity_color(entity): try: if hasattr(entity, 'dxf'): color = entity.dxf.color if isinstance(color, int): # AutoCAD 색상 인덱스를 RGB로 변환 rgb = ezdxf.colors.aci_to_true_color(color) return f"#{rgb:06x}" # RGB를 16진수 문자열로 변환 elif isinstance(color, tuple) and len(color) == 3: return f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}" return 'black' # 기본 색상 except Exception: return 'black' # 오류 발생 시 기본 색상 반환 def create_svg_element(entity, svg, min_x, min_y, max_x, max_y): color = get_entity_color(entity) stroke_width = '0.1%' # 선 두께를 SVG 뷰포트의 0.1%로 설정 if isinstance(entity, ezdxf.entities.Line): ET.SubElement(svg, 'line', { 'x1': str(entity.dxf.start[0]), 'y1': str(-entity.dxf.start[1]), # SVG에서 Y축이 반대 'x2': str(entity.dxf.end[0]), 'y2': str(-entity.dxf.end[1]), 'stroke': color, 'stroke-width': stroke_width }) elif isinstance(entity, ezdxf.entities.Circle): ET.SubElement(svg, 'circle', { 'cx': str(entity.dxf.center[0]), 'cy': str(-entity.dxf.center[1]), 'r': str(entity.dxf.radius), 'stroke': color, 'stroke-width': stroke_width, 'fill': 'none' }) elif isinstance(entity, ezdxf.entities.Arc): center = entity.dxf.center radius = entity.dxf.radius start_angle = entity.dxf.start_angle end_angle = entity.dxf.end_angle path = f"M {center[0] + radius * math.cos(math.radians(start_angle))} {-center[1] - radius * math.sin(math.radians(start_angle))} " \ f"A {radius} {radius} 0 0 0 {center[0] + radius * math.cos(math.radians(end_angle))} {-center[1] - radius * math.sin(math.radians(end_angle))}" ET.SubElement(svg, 'path', { 'd': path, 'stroke': color, 'stroke-width': stroke_width, 'fill': 'none' }) elif isinstance(entity, ezdxf.entities.Text): # 텍스트 크기를 SVG 뷰포트에 맞게 조정 font_size = entity.dxf.height / (max_y - min_y) * 100 # 회전 각도 적용 rotation = entity.dxf.rotation if hasattr(entity.dxf, 'rotation') else 0 # 텍스트 정렬 처리 alignment = entity.dxf.insert text_anchor = 'start' if hasattr(entity.dxf, 'halign'): if entity.dxf.halign > 0: text_anchor = 'middle' if entity.dxf.halign == 1 else 'end' baseline = 'auto' if hasattr(entity.dxf, 'valign'): if entity.dxf.valign > 0: baseline = 'middle' if entity.dxf.valign == 1 else 'hanging' text_element = ET.SubElement(svg, 'text', { 'x': str(alignment[0]), 'y': str(-alignment[1]), 'font-size': f"{font_size}%", 'fill': color, 'text-anchor': text_anchor, 'dominant-baseline': baseline, 'transform': f"rotate({-rotation} {alignment[0]} {-alignment[1]})" }) text_element.text = entity.dxf.text def calculate_bounds(msp): min_x, min_y, max_x, max_y = float('inf'), float('inf'), float('-inf'), float('-inf') for entity in msp: if isinstance(entity, ezdxf.entities.Text): insert = entity.dxf.insert min_x = min(min_x, insert[0]) min_y = min(min_y, insert[1]) # 텍스트의 너비와 높이 정보가 없으므로 대략적인 크기 추정 max_x = max(max_x, insert[0] + entity.dxf.height * len(entity.dxf.text) * 0.6) max_y = max(max_y, insert[1] + entity.dxf.height) elif hasattr(entity, 'get_bbox'): bbox = entity.get_bbox() if bbox: min_x = min(min_x, bbox.extmin[0]) min_y = min(min_y, bbox.extmin[1]) max_x = max(max_x, bbox.extmax[0]) max_y = max(max_y, bbox.extmax[1]) # 경계가 유효하지 않은 경우 기본값 설정 if min_x == float('inf') or min_y == float('inf') or max_x == float('-inf') or max_y == float('-inf'): min_x, min_y, max_x, max_y = -10, -10, 10, 10 return min_x, min_y, max_x, max_y def convert_dxf(dxf_file, output_file, output_format): try: # DXF 파일 불러오기 doc, auditor = recover.readfile(dxf_file) if auditor.has_errors: print(f"DXF 파일 '{dxf_file}'에 오류가 있습니다:") for error in auditor.errors: print(f" - {error}") return False # 도면의 경계 계산 msp = doc.modelspace() min_x, min_y, max_x, max_y = calculate_bounds(msp) width = max(max_x - min_x, 1) # 최소 너비 1 height = max(max_y - min_y, 1) # 최소 높이 1 if output_format == 'png': # 이미지 크기 제한 max_size = 65000 # 픽셀 scale = min(max_size / width, max_size / height) fig_width = int(width * scale) fig_height = int(height * scale) # DPI 계산 (최대 300으로 제한) dpi = min(300, fig_width / width) # matplotlib 설정 fig = plt.figure(figsize=(fig_width/dpi, fig_height/dpi), dpi=dpi) ax = fig.add_axes([0, 0, 1, 1]) ax.set_axis_off() # 렌더링 컨텍스트와 백엔드 설정 ctx = RenderContext(doc) ctx.set_current_layout(doc.modelspace()) ctx.bounds = (min_x, min_y, max_x, max_y) out = MatplotlibBackend(ax) # 엔티티 렌더링 Frontend(ctx, out).draw_layout(msp, finalize=True) # PNG 파일로 저장 plt.savefig(output_file, dpi=dpi, bbox_inches='tight', pad_inches=0, facecolor='white') plt.close(fig) # 명시적으로 figure 객체 닫기 print(f"이미지 크기: {fig_width}x{fig_height} 픽셀, DPI: {dpi:.2f}") elif output_format == 'svg': # SVG 생성 svg = ET.Element('svg', { 'width': f"{width}", 'height': f"{height}", 'viewBox': f"{min_x} {-max_y} {width} {height}", 'xmlns': "http://www.w3.org/2000/svg" }) for entity in msp: create_svg_element(entity, svg, min_x, min_y, max_x, max_y) # SVG 파일로 저장 tree = ET.ElementTree(svg) tree.write(output_file, encoding='utf-8', xml_declaration=True) print(f"SVG 파일 저장 완료: {output_file}") return True except Exception as e: print(f"오류 발생: {str(e)}") return False def get_user_choice(): while True: choice = input("1: 기존 DXF 파일 열기\n2: 샘플 DXF 파일 생성\n선택하세요 (1 또는 2): ").strip() if choice in ['1', '2']: return choice print("잘못된 선택입니다. 1 또는 2를 입력하세요.") def get_dxf_file_name(): file_name = input("DXF 파일의 이름을 입력하세요 (확장자 포함): ").strip() if not file_name.lower().endswith('.dxf'): file_name += '.dxf' return file_name def get_output_format(): while True: choice = input("출력 형식을 선택하세요 (png 또는 svg): ").strip().lower() if choice in ['png', 'svg']: return choice print("잘못된 선택입니다. 'png' 또는 'svg'를 입력하세요.") if __name__ == "__main__": current_dir = os.getcwd() print(f"현재 작업 디렉토리: {current_dir}") choice = get_user_choice() if choice == '1': dxf_file = get_dxf_file_name() full_path = os.path.join(current_dir, dxf_file) if not os.path.exists(full_path): print(f"오류: '{dxf_file}' 파일이 현재 디렉토리에 없습니다.") sys.exit(1) else: dxf_file = "sample.dxf" full_path = os.path.join(current_dir, dxf_file) create_sample_dxf(full_path) output_format = get_output_format() output_file = os.path.splitext(full_path)[0] + f".{output_format}" # DXF를 선택한 형식으로 변환 if convert_dxf(full_path, output_file, output_format): print(f"변환 완료: '{dxf_file}' -> '{os.path.basename(output_file)}'") else: print("변환 실패") |