Files
fletimageanalysis/dxf_processor.py
2025-07-16 17:33:20 +09:00

872 lines
36 KiB
Python

# -*- coding: utf-8 -*-
"""
향상된 DXF 파일 처리 모듈
ezdxf 라이브러리를 사용하여 DXF 파일에서 도곽 정보, 텍스트 엔티티 및 모든 Block Reference/Attribute Reference를 추출
"""
import os
import json
import logging
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass, asdict, field
try:
import ezdxf
from ezdxf.document import Drawing
from ezdxf.entities import Insert, Attrib, AttDef, Text, MText
from ezdxf.layouts import BlockLayout, Modelspace
from ezdxf import bbox, disassemble
EZDXF_AVAILABLE = True
except ImportError:
EZDXF_AVAILABLE = False
logging.warning("ezdxf 라이브러리가 설치되지 않았습니다. DXF 기능이 비활성화됩니다.")
from config import Config
@dataclass
class BoundingBox:
"""바운딩 박스 정보를 담는 데이터 클래스"""
min_x: float
min_y: float
max_x: float
max_y: float
@property
def width(self) -> float:
return self.max_x - self.min_x
@property
def height(self) -> float:
return self.max_y - self.min_y
@property
def center(self) -> Tuple[float, float]:
return ((self.min_x + self.max_x) / 2, (self.min_y + self.max_y) / 2)
def merge(self, other: 'BoundingBox') -> 'BoundingBox':
"""다른 바운딩 박스와 병합하여 가장 큰 외곽 박스 반환"""
return BoundingBox(
min_x=min(self.min_x, other.min_x),
min_y=min(self.min_y, other.min_y),
max_x=max(self.max_x, other.max_x),
max_y=max(self.max_y, other.max_y)
)
@dataclass
class TextInfo:
"""텍스트 엔티티 정보를 담는 데이터 클래스"""
entity_type: str # TEXT, MTEXT, ATTRIB
text: str
position: Tuple[float, float, float]
height: float
rotation: float
layer: str
bounding_box: Optional[BoundingBox] = None
entity_handle: Optional[str] = None
style: Optional[str] = None
color: Optional[int] = None
@dataclass
class AttributeInfo:
"""속성 정보를 담는 데이터 클래스 - 모든 DXF 속성 포함"""
tag: str
text: str
position: Tuple[float, float, float] # insert point (x, y, z)
height: float
width: float
rotation: float
layer: str
bounding_box: Optional[BoundingBox] = None
# 추가 DXF 속성들
prompt: Optional[str] = None
style: Optional[str] = None
invisible: bool = False
const: bool = False
verify: bool = False
preset: bool = False
align_point: Optional[Tuple[float, float, float]] = None
halign: int = 0
valign: int = 0
text_generation_flag: int = 0
oblique_angle: float = 0.0
width_factor: float = 1.0
color: Optional[int] = None
linetype: Optional[str] = None
lineweight: Optional[int] = None
# 좌표 정보
insert_x: float = 0.0
insert_y: float = 0.0
insert_z: float = 0.0
# 계산된 정보
estimated_width: float = 0.0
entity_handle: Optional[str] = None
@dataclass
class BlockInfo:
"""블록 정보를 담는 데이터 클래스"""
name: str
position: Tuple[float, float, float]
scale: Tuple[float, float, float]
rotation: float
layer: str
attributes: List[AttributeInfo]
bounding_box: Optional[BoundingBox] = None
@dataclass
class TitleBlockInfo:
"""도곽 정보를 담는 데이터 클래스"""
drawing_name: Optional[str] = None
drawing_number: Optional[str] = None
construction_field: Optional[str] = None
construction_stage: Optional[str] = None
scale: Optional[str] = None
project_name: Optional[str] = None
designer: Optional[str] = None
date: Optional[str] = None
revision: Optional[str] = None
location: Optional[str] = None
bounding_box: Optional[BoundingBox] = None
block_name: Optional[str] = None
# 모든 attributes 정보 저장
all_attributes: List[AttributeInfo] = field(default_factory=list)
attributes_count: int = 0
# 추가 메타데이터
block_position: Optional[Tuple[float, float, float]] = None
block_scale: Optional[Tuple[float, float, float]] = None
block_rotation: float = 0.0
block_layer: Optional[str] = None
def __post_init__(self):
"""초기화 후 처리"""
self.attributes_count = len(self.all_attributes)
@dataclass
class ComprehensiveExtractionResult:
"""종합적인 추출 결과를 담는 데이터 클래스"""
text_entities: List[TextInfo] = field(default_factory=list)
all_block_references: List[BlockInfo] = field(default_factory=list)
title_block: Optional[TitleBlockInfo] = None
overall_bounding_box: Optional[BoundingBox] = None
summary: Dict[str, Any] = field(default_factory=dict)
class EnhancedDXFProcessor:
"""향상된 DXF 파일 처리 클래스"""
# 도곽 식별을 위한 키워드 정의
TITLE_BLOCK_KEYWORDS = {
'건설분야': ['construction_field', 'field', '분야', '공사', 'category'],
'건설단계': ['construction_stage', 'stage', '단계', 'phase'],
'도면명': ['drawing_name', 'title', '제목', 'name', ''],
'축척': ['scale', '축척', 'ratio', '비율'],
'도면번호': ['drawing_number', 'number', '번호', 'no', 'dwg'],
'설계자': ['designer', '설계', 'design', 'drawn'],
'프로젝트': ['project', '사업', '공사명'],
'날짜': ['date', '일자', '작성일'],
'리비전': ['revision', 'rev', '개정'],
'위치': ['location', '위치', '지역']
}
def __init__(self):
"""DXF 처리기 초기화"""
self.logger = logging.getLogger(__name__)
if not EZDXF_AVAILABLE:
raise ImportError("ezdxf 라이브러리가 필요합니다. 'pip install ezdxf'로 설치하세요.")
def validate_dxf_file(self, file_path: str) -> bool:
"""DXF 파일 유효성 검사"""
try:
if not os.path.exists(file_path):
self.logger.error(f"파일이 존재하지 않습니다: {file_path}")
return False
if not file_path.lower().endswith('.dxf'):
self.logger.error(f"DXF 파일이 아닙니다: {file_path}")
return False
# ezdxf로 파일 읽기 시도
doc = ezdxf.readfile(file_path)
if doc is None:
return False
self.logger.info(f"DXF 파일 유효성 검사 성공: {file_path}")
return True
except ezdxf.DXFStructureError as e:
self.logger.error(f"DXF 구조 오류: {e}")
return False
except Exception as e:
self.logger.error(f"DXF 파일 검증 중 오류: {e}")
return False
def load_dxf_document(self, file_path: str) -> Optional[Drawing]:
"""DXF 문서 로드"""
try:
doc = ezdxf.readfile(file_path)
self.logger.info(f"DXF 문서 로드 성공: {file_path}")
return doc
except Exception as e:
self.logger.error(f"DXF 문서 로드 실패: {e}")
return None
def _is_empty_text(self, text: str) -> bool:
"""텍스트가 비어있는지 확인 (공백 문자만 있거나 완전히 비어있는 경우)"""
return not text or text.strip() == ""
def calculate_comprehensive_bounding_box(self, doc: Drawing) -> Optional[BoundingBox]:
"""전체 문서의 종합적인 바운딩 박스 계산 (ezdxf.bbox 사용)"""
try:
msp = doc.modelspace()
# ezdxf의 bbox 모듈을 사용하여 전체 바운딩 박스 계산
cache = bbox.Cache()
overall_bbox = bbox.extents(msp, cache=cache)
if overall_bbox:
self.logger.info(f"전체 바운딩 박스: {overall_bbox}")
return BoundingBox(
min_x=overall_bbox.extmin.x,
min_y=overall_bbox.extmin.y,
max_x=overall_bbox.extmax.x,
max_y=overall_bbox.extmax.y
)
else:
self.logger.warning("바운딩 박스 계산 실패")
return None
except Exception as e:
self.logger.warning(f"바운딩 박스 계산 중 오류: {e}")
return None
def extract_all_text_entities(self, doc: Drawing) -> List[TextInfo]:
"""모든 텍스트 엔티티 추출 (TEXT, MTEXT, DBTEXT)"""
text_entities = []
try:
msp = doc.modelspace()
# TEXT 엔티티 추출
for text_entity in msp.query('TEXT'):
text_content = getattr(text_entity.dxf, 'text', '')
if not self._is_empty_text(text_content):
text_info = self._extract_text_info(text_entity, 'TEXT')
if text_info:
text_entities.append(text_info)
# MTEXT 엔티티 추출
for mtext_entity in msp.query('MTEXT'):
# MTEXT는 .text 속성 사용
text_content = getattr(mtext_entity, 'text', '') or getattr(mtext_entity.dxf, 'text', '')
if not self._is_empty_text(text_content):
text_info = self._extract_text_info(mtext_entity, 'MTEXT')
if text_info:
text_entities.append(text_info)
# ATTRIB 엔티티 추출 (블록 외부의 독립적인 속성)
for attrib_entity in msp.query('ATTRIB'):
text_content = getattr(attrib_entity.dxf, 'text', '')
if not self._is_empty_text(text_content):
text_info = self._extract_text_info(attrib_entity, 'ATTRIB')
if text_info:
text_entities.append(text_info)
# 페이퍼스페이스도 확인
for layout_name in doc.layout_names_in_taborder():
if layout_name.startswith('*'): # 모델스페이스 제외
continue
try:
layout = doc.paperspace(layout_name)
# TEXT, MTEXT, ATTRIB 추출
for entity_type in ['TEXT', 'MTEXT', 'ATTRIB']:
for entity in layout.query(entity_type):
if entity_type == 'MTEXT':
text_content = getattr(entity, 'text', '') or getattr(entity.dxf, 'text', '')
else:
text_content = getattr(entity.dxf, 'text', '')
if not self._is_empty_text(text_content):
text_info = self._extract_text_info(entity, entity_type)
if text_info:
text_entities.append(text_info)
except Exception as e:
self.logger.warning(f"레이아웃 {layout_name} 처리 중 오류: {e}")
self.logger.info(f"{len(text_entities)}개의 텍스트 엔티티를 찾았습니다.")
return text_entities
except Exception as e:
self.logger.error(f"텍스트 엔티티 추출 중 오류: {e}")
return []
def _extract_text_info(self, entity, entity_type: str) -> Optional[TextInfo]:
"""텍스트 엔티티에서 정보 추출"""
try:
# 텍스트 내용 추출
if entity_type == 'MTEXT':
text_content = getattr(entity, 'text', '') or getattr(entity.dxf, 'text', '')
else:
text_content = getattr(entity.dxf, 'text', '')
# 위치 정보
insert_point = getattr(entity.dxf, 'insert', (0, 0, 0))
position = (
insert_point.x if hasattr(insert_point, 'x') else insert_point[0],
insert_point.y if hasattr(insert_point, 'y') else insert_point[1],
insert_point.z if hasattr(insert_point, 'z') else insert_point[2]
)
# 기본 속성
height = getattr(entity.dxf, 'height', 1.0)
rotation = getattr(entity.dxf, 'rotation', 0.0)
layer = getattr(entity.dxf, 'layer', '0')
entity_handle = getattr(entity.dxf, 'handle', None)
style = getattr(entity.dxf, 'style', None)
color = getattr(entity.dxf, 'color', None)
# 바운딩 박스 계산
bounding_box = self._calculate_text_bounding_box(entity)
return TextInfo(
entity_type=entity_type,
text=text_content,
position=position,
height=height,
rotation=rotation,
layer=layer,
bounding_box=bounding_box,
entity_handle=entity_handle,
style=style,
color=color
)
except Exception as e:
self.logger.warning(f"텍스트 정보 추출 중 오류: {e}")
return None
def _calculate_text_bounding_box(self, entity) -> Optional[BoundingBox]:
"""텍스트 엔티티의 바운딩 박스 계산"""
try:
# ezdxf bbox 모듈 사용
entity_bbox = bbox.extents([entity])
if entity_bbox:
return BoundingBox(
min_x=entity_bbox.extmin.x,
min_y=entity_bbox.extmin.y,
max_x=entity_bbox.extmax.x,
max_y=entity_bbox.extmax.y
)
except Exception as e:
self.logger.debug(f"바운딩 박스 계산 실패, 추정값 사용: {e}")
# 대안: 추정 계산
try:
if hasattr(entity, 'dxf'):
insert_point = getattr(entity.dxf, 'insert', (0, 0, 0))
height = getattr(entity.dxf, 'height', 1.0)
# 텍스트 내용 길이 추정
if hasattr(entity, 'text'):
text_content = entity.text
elif hasattr(entity.dxf, 'text'):
text_content = entity.dxf.text
else:
text_content = ""
# 텍스트 너비 추정 (높이의 0.6배 * 글자 수)
estimated_width = len(text_content) * height * 0.6
x, y = insert_point[0], insert_point[1]
return BoundingBox(
min_x=x,
min_y=y,
max_x=x + estimated_width,
max_y=y + height
)
except Exception as e:
self.logger.warning(f"텍스트 바운딩 박스 계산 실패: {e}")
return None
def extract_all_block_references(self, doc: Drawing) -> List[BlockInfo]:
"""모든 Block Reference 추출 (재귀적으로 중첩된 블록도 포함)"""
block_refs = []
try:
# 모델스페이스에서 INSERT 엔티티 찾기
msp = doc.modelspace()
for insert in msp.query('INSERT'):
block_info = self._process_block_reference(doc, insert)
if block_info:
block_refs.append(block_info)
# 페이퍼스페이스도 확인
for layout_name in doc.layout_names_in_taborder():
if layout_name.startswith('*'): # 모델스페이스 제외
continue
try:
layout = doc.paperspace(layout_name)
for insert in layout.query('INSERT'):
block_info = self._process_block_reference(doc, insert)
if block_info:
block_refs.append(block_info)
except Exception as e:
self.logger.warning(f"레이아웃 {layout_name} 처리 중 오류: {e}")
# 블록 정의 내부도 재귀적으로 검사
for block_layout in doc.blocks:
if not block_layout.name.startswith('*'): # 시스템 블록 제외
for insert in block_layout.query('INSERT'):
block_info = self._process_block_reference(doc, insert)
if block_info:
block_refs.append(block_info)
self.logger.info(f"{len(block_refs)}개의 블록 참조를 찾았습니다.")
return block_refs
except Exception as e:
self.logger.error(f"블록 참조 추출 중 오류: {e}")
return []
def _process_block_reference(self, doc: Drawing, insert: Insert) -> Optional[BlockInfo]:
"""개별 Block Reference 처리 - ATTDEF 정보도 함께 수집"""
try:
# 블록 정보 추출
block_name = insert.dxf.name
position = (insert.dxf.insert.x, insert.dxf.insert.y, insert.dxf.insert.z)
scale = (
getattr(insert.dxf, 'xscale', 1.0),
getattr(insert.dxf, 'yscale', 1.0),
getattr(insert.dxf, 'zscale', 1.0)
)
rotation = getattr(insert.dxf, 'rotation', 0.0)
layer = getattr(insert.dxf, 'layer', '0')
# ATTDEF 정보 수집 (프롬프트 정보 포함)
attdef_info = {}
try:
block_layout = doc.blocks.get(block_name)
if block_layout:
for attdef in block_layout.query('ATTDEF'):
tag = getattr(attdef.dxf, 'tag', '')
prompt = getattr(attdef.dxf, 'prompt', '')
if tag:
attdef_info[tag] = {
'prompt': prompt,
'default_text': getattr(attdef.dxf, 'text', ''),
'position': (attdef.dxf.insert.x, attdef.dxf.insert.y, attdef.dxf.insert.z),
'height': getattr(attdef.dxf, 'height', 1.0),
'style': getattr(attdef.dxf, 'style', 'Standard'),
'invisible': getattr(attdef.dxf, 'invisible', False),
'const': getattr(attdef.dxf, 'const', False),
'verify': getattr(attdef.dxf, 'verify', False),
'preset': getattr(attdef.dxf, 'preset', False)
}
except Exception as e:
self.logger.debug(f"ATTDEF 정보 수집 실패: {e}")
# ATTRIB 속성 추출 및 ATTDEF 정보와 결합 (빈 텍스트 제외)
attributes = []
for attrib in insert.attribs:
attr_info = self._extract_attribute_info(attrib)
if attr_info and not self._is_empty_text(attr_info.text):
# ATTDEF에서 프롬프트 정보 추가
if attr_info.tag in attdef_info:
attr_info.prompt = attdef_info[attr_info.tag]['prompt']
attributes.append(attr_info)
# 블록 바운딩 박스 계산
block_bbox = self._calculate_block_bounding_box(insert)
return BlockInfo(
name=block_name,
position=position,
scale=scale,
rotation=rotation,
layer=layer,
attributes=attributes,
bounding_box=block_bbox
)
except Exception as e:
self.logger.warning(f"블록 참조 처리 중 오류: {e}")
return None
def _calculate_block_bounding_box(self, insert: Insert) -> Optional[BoundingBox]:
"""블록의 바운딩 박스 계산"""
try:
# ezdxf bbox 모듈 사용
block_bbox = bbox.extents([insert])
if block_bbox:
return BoundingBox(
min_x=block_bbox.extmin.x,
min_y=block_bbox.extmin.y,
max_x=block_bbox.extmax.x,
max_y=block_bbox.extmax.y
)
except Exception as e:
self.logger.debug(f"블록 바운딩 박스 계산 실패: {e}")
return None
def _extract_attribute_info(self, attrib: Attrib) -> Optional[AttributeInfo]:
"""Attribute Reference에서 모든 정보 추출 (빈 텍스트 포함)"""
try:
# 기본 속성
tag = getattr(attrib.dxf, 'tag', '')
text = getattr(attrib.dxf, 'text', '')
# 위치 정보
insert_point = getattr(attrib.dxf, 'insert', (0, 0, 0))
position = (insert_point.x if hasattr(insert_point, 'x') else insert_point[0],
insert_point.y if hasattr(insert_point, 'y') else insert_point[1],
insert_point.z if hasattr(insert_point, 'z') else insert_point[2])
# 텍스트 속성
height = getattr(attrib.dxf, 'height', 1.0)
width = getattr(attrib.dxf, 'width', 1.0)
rotation = getattr(attrib.dxf, 'rotation', 0.0)
# 레이어 및 스타일
layer = getattr(attrib.dxf, 'layer', '0')
style = getattr(attrib.dxf, 'style', 'Standard')
# 속성 플래그
invisible = getattr(attrib.dxf, 'invisible', False)
const = getattr(attrib.dxf, 'const', False)
verify = getattr(attrib.dxf, 'verify', False)
preset = getattr(attrib.dxf, 'preset', False)
# 정렬 정보
align_point_data = getattr(attrib.dxf, 'align_point', None)
align_point = None
if align_point_data:
align_point = (align_point_data.x if hasattr(align_point_data, 'x') else align_point_data[0],
align_point_data.y if hasattr(align_point_data, 'y') else align_point_data[1],
align_point_data.z if hasattr(align_point_data, 'z') else align_point_data[2])
halign = getattr(attrib.dxf, 'halign', 0)
valign = getattr(attrib.dxf, 'valign', 0)
# 텍스트 형식
text_generation_flag = getattr(attrib.dxf, 'text_generation_flag', 0)
oblique_angle = getattr(attrib.dxf, 'oblique_angle', 0.0)
width_factor = getattr(attrib.dxf, 'width_factor', 1.0)
# 시각적 속성
color = getattr(attrib.dxf, 'color', None)
linetype = getattr(attrib.dxf, 'linetype', None)
lineweight = getattr(attrib.dxf, 'lineweight', None)
# 엔티티 핸들
entity_handle = getattr(attrib.dxf, 'handle', None)
# 텍스트 폭 추정
estimated_width = len(text) * height * 0.6 * width_factor
# 바운딩 박스 계산
bounding_box = self._calculate_text_bounding_box(attrib)
return AttributeInfo(
tag=tag,
text=text,
position=position,
height=height,
width=width,
rotation=rotation,
layer=layer,
bounding_box=bounding_box,
prompt=None, # 나중에 ATTDEF에서 설정
style=style,
invisible=invisible,
const=const,
verify=verify,
preset=preset,
align_point=align_point,
halign=halign,
valign=valign,
text_generation_flag=text_generation_flag,
oblique_angle=oblique_angle,
width_factor=width_factor,
color=color,
linetype=linetype,
lineweight=lineweight,
insert_x=position[0],
insert_y=position[1],
insert_z=position[2],
estimated_width=estimated_width,
entity_handle=entity_handle
)
except Exception as e:
self.logger.warning(f"속성 정보 추출 중 오류: {e}")
return None
def identify_title_block(self, block_refs: List[BlockInfo]) -> Optional[TitleBlockInfo]:
"""블록 참조들 중에서 도곽을 식별하고 정보 추출"""
title_block_candidates = []
for block_ref in block_refs:
# 도곽 키워드를 포함한 속성이 있는지 확인
keyword_matches = 0
for attr in block_ref.attributes:
for keyword_group in self.TITLE_BLOCK_KEYWORDS.keys():
if self._contains_keyword(attr.tag, keyword_group) or \
self._contains_keyword(attr.text, keyword_group):
keyword_matches += 1
break
# 충분한 키워드가 매칭되면 도곽 후보로 추가
if keyword_matches >= 2: # 최소 2개 이상의 키워드 매칭
title_block_candidates.append((block_ref, keyword_matches))
if not title_block_candidates:
self.logger.warning("도곽 블록을 찾을 수 없습니다.")
return None
# 가장 많은 키워드를 포함한 블록을 도곽으로 선택
title_block_candidates.sort(key=lambda x: x[1], reverse=True)
best_candidate = title_block_candidates[0][0]
self.logger.info(f"도곽 블록 발견: {best_candidate.name} (키워드 매칭: {title_block_candidates[0][1]})")
return self._extract_title_block_info(best_candidate)
def _contains_keyword(self, text: str, keyword_group: str) -> bool:
"""텍스트에 특정 키워드 그룹의 단어가 포함되어 있는지 확인"""
if not text:
return False
text_lower = text.lower()
keywords = self.TITLE_BLOCK_KEYWORDS.get(keyword_group, [])
return any(keyword.lower() in text_lower for keyword in keywords)
def _extract_title_block_info(self, block_ref: BlockInfo) -> TitleBlockInfo:
"""도곽 블록에서 상세 정보 추출"""
# TitleBlockInfo 객체 생성
title_block = TitleBlockInfo(
block_name=block_ref.name,
all_attributes=block_ref.attributes.copy(),
block_position=block_ref.position,
block_scale=block_ref.scale,
block_rotation=block_ref.rotation,
block_layer=block_ref.layer
)
# 속성들을 분석하여 도곽 정보 매핑
for attr in block_ref.attributes:
text_value = attr.text.strip()
if not text_value:
continue
# 각 키워드 그룹별로 매칭 시도
if self._contains_keyword(attr.tag, '도면명') or self._contains_keyword(attr.text, '도면명'):
title_block.drawing_name = text_value
elif self._contains_keyword(attr.tag, '도면번호') or self._contains_keyword(attr.text, '도면번호'):
title_block.drawing_number = text_value
elif self._contains_keyword(attr.tag, '건설분야') or self._contains_keyword(attr.text, '건설분야'):
title_block.construction_field = text_value
elif self._contains_keyword(attr.tag, '건설단계') or self._contains_keyword(attr.text, '건설단계'):
title_block.construction_stage = text_value
elif self._contains_keyword(attr.tag, '축척') or self._contains_keyword(attr.text, '축척'):
title_block.scale = text_value
elif self._contains_keyword(attr.tag, '설계자') or self._contains_keyword(attr.text, '설계자'):
title_block.designer = text_value
elif self._contains_keyword(attr.tag, '프로젝트') or self._contains_keyword(attr.text, '프로젝트'):
title_block.project_name = text_value
elif self._contains_keyword(attr.tag, '날짜') or self._contains_keyword(attr.text, '날짜'):
title_block.date = text_value
elif self._contains_keyword(attr.tag, '리비전') or self._contains_keyword(attr.text, '리비전'):
title_block.revision = text_value
elif self._contains_keyword(attr.tag, '위치') or self._contains_keyword(attr.text, '위치'):
title_block.location = text_value
# 도곽 바운딩 박스는 블록의 바운딩 박스 사용
title_block.bounding_box = block_ref.bounding_box
# 속성 개수 업데이트
title_block.attributes_count = len(title_block.all_attributes)
self.logger.info(f"도곽 '{block_ref.name}'에서 {title_block.attributes_count}개의 속성 추출 완료")
return title_block
def process_dxf_file_comprehensive(self, file_path: str) -> Dict[str, Any]:
"""DXF 파일 종합적인 처리"""
result = {
'success': False,
'error': None,
'file_path': file_path,
'comprehensive_result': None,
'summary': {}
}
try:
# 파일 유효성 검사
if not self.validate_dxf_file(file_path):
result['error'] = "유효하지 않은 DXF 파일입니다."
return result
# DXF 문서 로드
doc = self.load_dxf_document(file_path)
if not doc:
result['error'] = "DXF 문서를 로드할 수 없습니다."
return result
# 종합적인 추출 시작
comprehensive_result = ComprehensiveExtractionResult()
# 1. 모든 텍스트 엔티티 추출
self.logger.info("텍스트 엔티티 추출 중...")
comprehensive_result.text_entities = self.extract_all_text_entities(doc)
# 2. 모든 블록 참조 추출
self.logger.info("블록 참조 추출 중...")
comprehensive_result.all_block_references = self.extract_all_block_references(doc)
# 3. 도곽 정보 추출
self.logger.info("도곽 정보 추출 중...")
comprehensive_result.title_block = self.identify_title_block(comprehensive_result.all_block_references)
# 4. 전체 바운딩 박스 계산
self.logger.info("전체 바운딩 박스 계산 중...")
comprehensive_result.overall_bounding_box = self.calculate_comprehensive_bounding_box(doc)
# 5. 요약 정보 생성
comprehensive_result.summary = {
'total_text_entities': len(comprehensive_result.text_entities),
'total_block_references': len(comprehensive_result.all_block_references),
'title_block_found': comprehensive_result.title_block is not None,
'title_block_name': comprehensive_result.title_block.block_name if comprehensive_result.title_block else None,
'total_attributes': sum(len(br.attributes) for br in comprehensive_result.all_block_references),
'non_empty_attributes': sum(len([attr for attr in br.attributes if not self._is_empty_text(attr.text)])
for br in comprehensive_result.all_block_references),
'overall_bounding_box': comprehensive_result.overall_bounding_box.__dict__ if comprehensive_result.overall_bounding_box else None
}
# 결과 저장
result['comprehensive_result'] = asdict(comprehensive_result)
result['summary'] = comprehensive_result.summary
result['success'] = True
self.logger.info(f"DXF 파일 종합 처리 완료: {file_path}")
self.logger.info(f"추출 요약: 텍스트 엔티티 {comprehensive_result.summary['total_text_entities']}개, "
f"블록 참조 {comprehensive_result.summary['total_block_references']}개, "
f"비어있지 않은 속성 {comprehensive_result.summary['non_empty_attributes']}")
except Exception as e:
self.logger.error(f"DXF 파일 처리 중 오류: {e}")
result['error'] = str(e)
return result
def save_analysis_result(self, result: Dict[str, Any], output_file: str) -> bool:
"""분석 결과를 JSON 파일로 저장"""
try:
os.makedirs(Config.RESULTS_FOLDER, exist_ok=True)
output_path = os.path.join(Config.RESULTS_FOLDER, output_file)
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2, default=str)
self.logger.info(f"분석 결과 저장 완료: {output_path}")
return True
except Exception as e:
self.logger.error(f"분석 결과 저장 실패: {e}")
return False
# 기존 클래스명과의 호환성을 위한 별칭
DXFProcessor = EnhancedDXFProcessor
def main():
"""테스트용 메인 함수"""
logging.basicConfig(level=logging.INFO)
if not EZDXF_AVAILABLE:
print("ezdxf 라이브러리가 설치되지 않았습니다.")
return
processor = EnhancedDXFProcessor()
# 테스트 파일 경로 (실제 파일 경로로 변경 필요)
test_file = "test_drawing.dxf"
if os.path.exists(test_file):
result = processor.process_dxf_file_comprehensive(test_file)
if result['success']:
print("DXF 파일 종합 처리 성공!")
summary = result['summary']
print(f"텍스트 엔티티: {summary['total_text_entities']}")
print(f"블록 참조: {summary['total_block_references']}")
print(f"도곽 발견: {summary['title_block_found']}")
print(f"비어있지 않은 속성: {summary['non_empty_attributes']}")
if summary['overall_bounding_box']:
bbox_info = summary['overall_bounding_box']
print(f"전체 바운딩 박스: ({bbox_info['min_x']:.2f}, {bbox_info['min_y']:.2f}) ~ "
f"({bbox_info['max_x']:.2f}, {bbox_info['max_y']:.2f})")
else:
print(f"처리 실패: {result['error']}")
else:
print(f"테스트 파일을 찾을 수 없습니다: {test_file}")
def process_dxf_file(self, file_path: str) -> Dict[str, Any]:
"""
기존 코드와의 호환성을 위한 메서드
process_dxf_file_comprehensive를 호출하고 기존 형식으로 변환
"""
try:
# 새로운 종합 처리 메서드 호출
comprehensive_result = self.process_dxf_file_comprehensive(file_path)
if not comprehensive_result['success']:
return comprehensive_result
# 기존 형식으로 변환
comp_data = comprehensive_result['comprehensive_result']
# 기존 형식으로 데이터 재구성
result = {
'success': True,
'error': None,
'file_path': file_path,
'title_block': comp_data.get('title_block'),
'block_references': comp_data.get('all_block_references', []),
'summary': comp_data.get('summary', {})
}
return result
except Exception as e:
self.logger.error(f"DXF 파일 처리 중 오류: {e}")
return {
'success': False,
'error': str(e),
'file_path': file_path,
'title_block': None,
'block_references': [],
'summary': {}
}