1# Copyright (c) 2011-2020, Manfred Moitzi 2# License: MIT License 3from typing import TYPE_CHECKING, Iterable, Union, Sequence, List, cast 4from ezdxf.lldxf.const import ( 5 DXFStructureError, DXFBlockInUseError, DXFTableEntryError, DXFKeyError, 6) 7from ezdxf.lldxf import const 8from ezdxf.entities import factory, entity_linker 9from ezdxf.layouts.blocklayout import BlockLayout 10from ezdxf.render.arrows import ARROWS 11from .table import table_key 12import warnings 13import logging 14 15logger = logging.getLogger('ezdxf') 16 17if TYPE_CHECKING: 18 from ezdxf.eztypes import ( 19 TagWriter, Drawing, EntityDB, DXFEntity, DXFTagStorage, Table, 20 BlockRecord, 21 ) 22 23 24def is_special_block(name: str) -> bool: 25 name = name.upper() 26 # Anonymous dimension, groups and table blocks do not have explicit 27 # references by an INSERT entity: 28 if is_anonymous_block(name): 29 return True 30 31 # Arrow blocks maybe used in DIMENSION or LEADER override without an 32 # INSERT reference: 33 if ARROWS.is_ezdxf_arrow(name): 34 return True 35 if name.startswith('_'): 36 if ARROWS.is_acad_arrow(ARROWS.arrow_name(name)): 37 return True 38 39 return False 40 41 42def is_anonymous_block(name: str) -> bool: 43 # *U### = anonymous BLOCK, require an explicit INSERT to be in use 44 # *E### = anonymous non-uniformly scaled BLOCK, requires INSERT? 45 # *X### = anonymous HATCH graphic, requires INSERT? 46 # *D### = anonymous DIMENSION graphic, has no explicit INSERT 47 # *A### = anonymous GROUP, requires INSERT? 48 # *T### = anonymous block for ACAD_TABLE, has no explicit INSERT 49 return len(name) > 1 and name[0] == '*' and name[1] in 'UEXDAT' 50 51 52class BlocksSection: 53 """ 54 Manages BLOCK definitions in a dict(), block names are case insensitive 55 e.g. 'Test' == 'TEST'. 56 57 """ 58 59 def __init__(self, doc: 'Drawing' = None, 60 entities: List['DXFEntity'] = None): 61 self.doc = doc 62 if entities is not None: 63 self.load(entities) 64 self._reconstruct_orphaned_block_records() 65 self._anonymous_block_counter = 0 66 67 def __len__(self): 68 return len(self.block_records) 69 70 @staticmethod 71 def key(entity: Union[str, 'BlockLayout']) -> str: 72 if not isinstance(entity, str): 73 entity = entity.name 74 return entity.lower() # block key is lower case 75 76 @property 77 def block_records(self) -> 'Table': 78 return self.doc.block_records 79 80 @property 81 def entitydb(self) -> 'EntityDB': 82 return self.doc.entitydb 83 84 def load(self, entities: List['DXFEntity']) -> None: 85 """ 86 Load DXF entities into BlockLayouts. `entities` is a list of 87 entity tags, separated by BLOCK and ENDBLK entities. 88 89 """ 90 91 def load_block_record( 92 block_entities: Sequence['DXFEntity']) -> 'BlockRecord': 93 block = cast('Block', block_entities[0]) 94 endblk = cast('EndBlk', block_entities[-1]) 95 96 try: 97 block_record = cast( 98 'BlockRecord', 99 block_records.get(block.dxf.name) 100 ) 101 # Special case DXF R12 - has no BLOCK_RECORD table 102 except DXFTableEntryError: 103 block_record = cast( 104 'BlockRecord', 105 block_records.new(block.dxf.name, dxfattribs={'scale': 0}) 106 ) 107 108 # The BLOCK_RECORD is the central object which stores all the 109 # information about a BLOCK and also owns all the entities of 110 # this block definition. 111 block_record.set_block(block, endblk) 112 for entity in block_entities[1:-1]: 113 block_record.add_entity(entity) 114 return block_record 115 116 def link_entities() -> Iterable['DXFEntity']: 117 linked = entity_linker() 118 for entity in entities: 119 # Do not store linked entities (VERTEX, ATTRIB, SEQEND) in 120 # the block layout, linked entities ares stored in their 121 # parent entity e.g. VERTEX -> POLYLINE: 122 if not linked(entity): 123 yield entity 124 125 block_records = self.block_records 126 section_head: 'DXFTagStorage' = cast('DXFTagStorage', entities[0]) 127 if section_head.dxftype() != 'SECTION' or \ 128 section_head.base_class[1] != (2, 'BLOCKS'): 129 raise DXFStructureError( 130 "Critical structure error in BLOCKS section." 131 ) 132 # Remove SECTION entity 133 del entities[0] 134 block_entities = [] 135 for entity in link_entities(): 136 block_entities.append(entity) 137 if entity.dxftype() == 'ENDBLK': 138 block_record = load_block_record(block_entities) 139 self.add(block_record) 140 block_entities = [] 141 142 def _reconstruct_orphaned_block_records(self): 143 """ Find BLOCK_RECORD entries without block definition in the blocks 144 section and create block definitions for this orphaned block records. 145 146 """ 147 for block_record in self.block_records: # type: BlockRecord 148 if block_record.block is None: 149 block = factory.create_db_entry( 150 'BLOCK', 151 dxfattribs={ 152 'name': block_record.dxf.name, 153 'base_point': (0, 0, 0), 154 }, 155 doc=self.doc, 156 ) 157 endblk = factory.create_db_entry( 158 'ENDBLK', 159 dxfattribs={}, 160 doc=self.doc, 161 ) 162 block_record.set_block(block, endblk) 163 self.add(block_record) 164 165 def export_dxf(self, tagwriter: 'TagWriter') -> None: 166 tagwriter.write_str(" 0\nSECTION\n 2\nBLOCKS\n") 167 for block_record in self.block_records: # type: BlockRecord 168 block_record.export_block_definition(tagwriter) 169 tagwriter.write_tag2(0, "ENDSEC") 170 171 def add(self, block_record: 'BlockRecord') -> 'BlockLayout': 172 """ Add or replace a block layout object defined by its block record. 173 (internal API) 174 """ 175 block_layout = BlockLayout(block_record) 176 block_record.block_layout = block_layout 177 assert self.block_records.has_entry(block_record.dxf.name) 178 return block_layout 179 180 def __iter__(self) -> Iterable['BlockLayout']: 181 """ Iterable of all :class:`~ezdxf.layouts.BlockLayout` objects. """ 182 return (block_record.block_layout for block_record in 183 self.block_records) 184 185 def __contains__(self, name: str) -> bool: 186 """ Returns ``True`` if :class:`~ezdxf.layouts.BlockLayout` `name` 187 exist. 188 """ 189 return self.block_records.has_entry(name) 190 191 def __getitem__(self, name: str) -> 'BlockLayout': 192 """ Returns :class:`~ezdxf.layouts.BlockLayout` `name`, 193 raises :class:`DXFKeyError` if `name` not exist. 194 """ 195 try: 196 block_record = cast('BlockRecord', self.block_records.get(name)) 197 return block_record.block_layout 198 except DXFTableEntryError: 199 raise DXFKeyError(name) 200 201 def __delitem__(self, name: str) -> None: 202 """ Deletes :class:`~ezdxf.layouts.BlockLayout` `name` and all of 203 its content, raises :class:`DXFKeyError` if `name` not exist. 204 """ 205 if name in self: 206 self.block_records.remove(name) 207 else: 208 raise DXFKeyError(name) 209 210 def get(self, name: str, default=None) -> 'BlockLayout': 211 """ Returns :class:`~ezdxf.layouts.BlockLayout` `name`, returns 212 `default` if `name` not exist. 213 """ 214 try: 215 return self.__getitem__(name) 216 except DXFKeyError: 217 return default 218 219 def get_block_layout_by_handle(self, 220 block_record_handle: str) -> 'BlockLayout': 221 """ Returns a block layout by block record handle. (internal API) 222 """ 223 return self.doc.entitydb[block_record_handle].block_layout 224 225 def new(self, name: str, base_point: Sequence[float] = (0, 0), 226 dxfattribs: dict = None) -> 'BlockLayout': 227 """ Create and add a new :class:`~ezdxf.layouts.BlockLayout`, `name` 228 is the BLOCK name, `base_point` is the insertion point of the BLOCK. 229 """ 230 block_record = self.doc.block_records.new(name) 231 232 dxfattribs = dxfattribs or {} 233 dxfattribs['owner'] = block_record.dxf.handle 234 dxfattribs['name'] = name 235 dxfattribs['base_point'] = base_point 236 head = factory.create_db_entry('BLOCK', dxfattribs, self.doc) 237 tail = factory.create_db_entry('ENDBLK', { 238 'owner': block_record.dxf.handle}, doc=self.doc) 239 block_record.set_block(head, tail) 240 return self.add(block_record) 241 242 def new_anonymous_block(self, type_char: str = 'U', 243 base_point: Sequence[float] = ( 244 0, 0)) -> 'BlockLayout': 245 """ Create and add a new anonymous :class:`~ezdxf.layouts.BlockLayout`, 246 `type_char` is the BLOCK type, `base_point` is the insertion point of 247 the BLOCK. 248 249 ========= ========== 250 type_char Anonymous Block Type 251 ========= ========== 252 ``'U'`` ``'*U###'`` anonymous BLOCK 253 ``'E'`` ``'*E###'`` anonymous non-uniformly scaled BLOCK 254 ``'X'`` ``'*X###'`` anonymous HATCH graphic 255 ``'D'`` ``'*D###'`` anonymous DIMENSION graphic 256 ``'A'`` ``'*A###'`` anonymous GROUP 257 ``'T'`` ``'*T###'`` anonymous block for ACAD_TABLE content 258 ========= ========== 259 260 """ 261 blockname = self.anonymous_blockname(type_char) 262 block = self.new(blockname, base_point, {'flags': const.BLK_ANONYMOUS}) 263 return block 264 265 def anonymous_blockname(self, type_char: str) -> str: 266 """ Create name for an anonymous block. (internal API) 267 268 Args: 269 type_char: letter 270 271 U = *U### anonymous blocks 272 E = *E### anonymous non-uniformly scaled blocks 273 X = *X### anonymous hatches 274 D = *D### anonymous dimensions 275 A = *A### anonymous groups 276 T = *T### anonymous ACAD_TABLE content 277 278 """ 279 while True: 280 self._anonymous_block_counter += 1 281 blockname = f"*{type_char}{self._anonymous_block_counter}" 282 if not self.__contains__(blockname): 283 return blockname 284 285 def rename_block(self, old_name: str, new_name: str) -> None: 286 """ Rename :class:`~ezdxf.layouts.BlockLayout` `old_name` to `new_name` 287 """ 288 block_record: 'BlockRecord' = self.block_records.get(old_name) 289 block_record.rename(new_name) 290 self.block_records.replace(old_name, block_record) 291 self.add(block_record) 292 293 def delete_block(self, name: str, safe: bool = True) -> None: 294 """ 295 Delete block. If `save` is ``True``, check if block is still referenced. 296 297 Args: 298 name: block name (case insensitive) 299 safe: check if block is still referenced or special block without 300 explicit references 301 302 Raises: 303 DXFKeyError: if block not exists 304 DXFBlockInUseError: if block is still referenced, and save is True 305 306 """ 307 if safe: 308 if is_special_block(name): 309 raise DXFBlockInUseError( 310 f'Special block "{name}" maybe used without explicit INSERT entity.' 311 ) 312 313 block_refs = self.doc.query( 314 f"INSERT[name=='{name}']i") # ignore case 315 if len(block_refs): 316 raise DXFBlockInUseError( 317 f'Block "{name}" is still in use.' 318 ) 319 self.__delitem__(name) 320 321 def delete_all_blocks(self) -> None: 322 """ Delete all blocks without references except modelspace- or 323 paperspace layout blocks, special arrow- and anonymous blocks 324 (DIMENSION, ACAD_TABLE). 325 326 .. warning:: 327 328 There could exist undiscovered references to blocks which are 329 not documented in the DXF reference, hidden in extended data 330 sections or application defined data, which could produce invalid 331 DXF documents if such referenced blocks will be deleted. 332 333 .. versionchanged:: 0.14 334 removed unsafe mode 335 336 """ 337 active_references = set( 338 table_key(entity.dxf.name) for entity in 339 self.doc.query('INSERT') 340 ) 341 342 def is_safe(name: str) -> bool: 343 if is_special_block(name): 344 return False 345 return name not in active_references 346 347 trash = set() 348 for block in self: 349 name = table_key(block.name) 350 if not block.is_any_layout and is_safe(name): 351 trash.add(name) 352 353 for name in trash: 354 self.__delitem__(name) 355 356 def purge(self): 357 """ Purge functionality removed! - it was just too dangerous! 358 The method name suggests a functionality and quality similar 359 to that of a CAD application, which can not be delivered! 360 """ 361 warnings.warn('Blocks.purge() deactivated, unsafe operation!', 362 DeprecationWarning) 363