872 lines
36 KiB
Python
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': {}
|
|
}
|