1# Copyright (c) 2019-2020, Manfred Moitzi 2# License: MIT-License 3from typing import ( 4 TYPE_CHECKING, KeysView, ItemsView, Any, Union, Dict, Optional, 5) 6import logging 7from ezdxf.lldxf import validator 8from ezdxf.lldxf.const import ( 9 SUBCLASS_MARKER, DXFKeyError, DXFValueError, 10) 11from ezdxf.lldxf.attributes import ( 12 DXFAttr, DXFAttributes, DefSubclass, RETURN_DEFAULT, group_code_mapping 13) 14from ezdxf.lldxf.types import is_valid_handle 15from ezdxf.audit import AuditError 16from ezdxf.entities import factory 17from .dxfentity import base_class, SubclassProcessor, DXFEntity 18from .dxfobj import DXFObject 19 20logger = logging.getLogger('ezdxf') 21 22if TYPE_CHECKING: 23 from ezdxf.eztypes import TagWriter, Drawing, DXFNamespace, Auditor 24 25__all__ = ['Dictionary', 'DictionaryWithDefault', 'DictionaryVar'] 26 27acdb_dictionary = DefSubclass('AcDbDictionary', { 28 # If set to 1, indicates that elements of the dictionary are to be treated 29 # as hard-owned: 30 'hard_owned': DXFAttr( 31 280, default=0, optional=True, 32 validator=validator.is_integer_bool, 33 fixer=RETURN_DEFAULT, 34 ), 35 36 # Duplicate record cloning flag (determines how to merge duplicate entries): 37 # 0 = not applicable 38 # 1 = keep existing 39 # 2 = use clone 40 # 3 = <xref>$0$<name> 41 # 4 = $0$<name> 42 # 5 = Unmangle name 43 'cloning': DXFAttr( 44 281, default=1, 45 validator=validator.is_in_integer_range(0, 6), 46 fixer=RETURN_DEFAULT, 47 ), 48 # 3: entry name 49 # 350: entry handle, some DICTIONARY objects have 360 as handle group code, 50 # this is accepted by AutoCAD but not documented by the DXF reference! 51 # ezdxf replaces group code 360 by 350. 52}) 53acdb_dictionary_group_codes = group_code_mapping(acdb_dictionary) 54KEY_CODE = 3 55VALUE_CODE = 350 56# Some DICTIONARY use group code 360: 57SEARCH_CODES = (VALUE_CODE, 360) 58 59 60@factory.register_entity 61class Dictionary(DXFObject): 62 """ AutoCAD maintains items such as mline styles and group definitions as 63 objects in dictionaries. Other applications are free to create and use 64 their own dictionaries as they see fit. The prefix "ACAD_" is reserved 65 for use by AutoCAD applications. 66 67 Dictionary entries are (key, DXFEntity) pairs. DXFEntity could be a string, 68 because at loading time not all objects are already stored in the EntityDB, 69 and have to acquired later. 70 71 """ 72 DXFTYPE = 'DICTIONARY' 73 DXFATTRIBS = DXFAttributes(base_class, acdb_dictionary) 74 75 def __init__(self): 76 super().__init__() 77 self._data: Dict[str, Union[str, DXFEntity]] = dict() 78 self._value_code = VALUE_CODE 79 80 def _copy_data(self, entity: 'Dictionary') -> None: 81 """ Copy hard owned entities but do not store the copies in the entity 82 database, this is a second step, this is just real copying. 83 """ 84 entity._value_code = self._value_code 85 if self.dxf.hard_owned: 86 # Reactors are removed from the cloned DXF objects. 87 entity._data = {key: entity.copy() for key, entity in self.items()} 88 else: 89 entity._data = {key: entity for key, entity in self.items()} 90 91 def post_bind_hook(self) -> None: 92 """ Called by binding a new or copied dictionary to the document, 93 bind hard owned sub-entities to the same document and add them to the 94 objects section. 95 """ 96 if not self.dxf.hard_owned: 97 return 98 # copied or new dictionary: 99 doc = self.doc 100 owner_handle = self.dxf.handle 101 for _, entity in self.items(): 102 entity.dxf.owner = owner_handle 103 factory.bind(entity, doc) 104 # For a correct DXF export add entities to the objects section: 105 doc.objects.add_object(entity) 106 107 def load_dxf_attribs( 108 self, processor: SubclassProcessor = None) -> 'DXFNamespace': 109 dxf = super().load_dxf_attribs(processor) 110 if processor: 111 tags = processor.fast_load_dxfattribs( 112 dxf, acdb_dictionary_group_codes, 1, log=False) 113 self.load_dict(tags) 114 return dxf 115 116 def load_dict(self, tags): 117 entry_handle = None 118 dict_key = None 119 value_code = VALUE_CODE 120 for code, value in tags: 121 if code in SEARCH_CODES: 122 # First store handles, because at this point, NOT all objects 123 # are stored in the EntityDB, at first access convert the handle 124 # to a DXFEntity object. 125 value_code = code 126 entry_handle = value 127 elif code == KEY_CODE: 128 dict_key = value 129 if dict_key and entry_handle: 130 # Store entity as handle string: 131 self._data[dict_key] = entry_handle 132 entry_handle = None 133 dict_key = None 134 # Use same value code as loaded: 135 self._value_code = value_code 136 137 def post_load_hook(self, doc: 'Drawing') -> None: 138 super().post_load_hook(doc) 139 db = doc.entitydb 140 141 def items(): 142 for key, handle in self.items(): 143 entity = db.get(handle) 144 if entity is not None and entity.is_alive: 145 yield key, entity 146 147 if len(self): 148 for k, v in list(items()): 149 self.__setitem__(k, v) 150 151 def export_entity(self, tagwriter: 'TagWriter') -> None: 152 """ Export entity specific data as DXF tags. """ 153 super().export_entity(tagwriter) 154 tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dictionary.name) 155 self.dxf.export_dxf_attribs(tagwriter, ['hard_owned', 'cloning']) 156 self.export_dict(tagwriter) 157 158 def export_dict(self, tagwriter: 'TagWriter'): 159 # key: dict key string 160 # value: DXFEntity or handle as string 161 # Ignore invalid handles at export, because removing can create an empty 162 # dictionary, which is more a problem for AutoCAD than invalid handles, 163 # and removing the whole dictionary is maybe also a problem. 164 for key, value in self._data.items(): 165 tagwriter.write_tag2(KEY_CODE, key) 166 # Value can be a handle string or a DXFEntity object: 167 if isinstance(value, DXFEntity): 168 if value.is_alive: 169 value = value.dxf.handle 170 else: 171 logger.debug( 172 f'Key "{key}" points to a destroyed entity ' 173 f'in {str(self)}, target replaced by "0" handle.' 174 ) 175 value = '0' 176 # Use same value code as loaded: 177 tagwriter.write_tag2(self._value_code, value) 178 179 @property 180 def is_hard_owner(self) -> bool: 181 """ Returns ``True`` if :class:`Dictionary` is hard owner of entities. 182 Hard owned entities will be destroyed by deleting the dictionary. 183 """ 184 return bool(self.dxf.hard_owned) 185 186 def keys(self) -> KeysView: 187 """ Returns :class:`KeysView` of all dictionary keys. """ 188 return self._data.keys() 189 190 def items(self) -> ItemsView: 191 """ Returns :class:`ItemsView` for all dictionary entries as 192 (:attr:`key`, :class:`DXFEntity`) pairs. 193 194 """ 195 for key in self.keys(): 196 yield key, self.get(key) # maybe handle -> DXFEntity 197 198 def __getitem__(self, key: str) -> 'DXFEntity': 199 """ Return the value for `key`, raises a :class:`DXFKeyError` if `key` 200 does not exist. 201 202 """ 203 return self.get(key) 204 205 def __setitem__(self, key: str, value: 'DXFEntity') -> None: 206 """ Add item as ``(key, value)`` pair to dictionary. """ 207 return self.add(key, value) 208 209 def __delitem__(self, key: str) -> None: 210 """ Delete entry `key` from the dictionary, raises :class:`DXFKeyError` 211 if key does not exist. 212 213 """ 214 return self.remove(key) 215 216 def __contains__(self, key: str) -> bool: 217 """ Returns ``True`` if `key` exist. """ 218 return key in self._data 219 220 def __len__(self) -> int: 221 """ Returns count of items. """ 222 return len(self._data) 223 224 count = __len__ 225 226 def get(self, key: str, default: Any = DXFKeyError) -> 'DXFEntity': 227 """ Returns :class:`DXFEntity` for `key`, if `key` exist, 228 else `default` or raises a :class:`DXFKeyError` for 229 `default` = :class:`DXFKeyError`. 230 """ 231 try: 232 return self._data[key] 233 except KeyError: 234 if default is DXFKeyError: 235 raise DXFKeyError(f"KeyError: '{key}'") 236 else: 237 return default 238 239 def add(self, key: str, value: 'DXFEntity') -> None: 240 """ Add entry ``(key, value)``. """ 241 if isinstance(value, str): 242 if not is_valid_handle(value): 243 raise DXFValueError( 244 f'Invalid entity handle #{value} for key {key}') 245 self._data[key] = value 246 247 def remove(self, key: str) -> None: 248 """ Delete entry `key`. Raises :class:`DXFKeyError`, if `key` does not 249 exist. Deletes also hard owned DXF objects from OBJECTS section. 250 """ 251 data = self._data 252 if key not in data: 253 raise DXFKeyError(key) 254 255 if self.is_hard_owner: 256 entity = self.get(key) 257 # Presumption: hard owned DXF objects always reside in the OBJECTS 258 # section. 259 self.doc.objects.delete_entity(entity) 260 del data[key] 261 262 def discard(self, key: str) -> None: 263 """ Delete entry `key` if exists. Does NOT raise an exception if `key` 264 not exist and does not delete hard owned DXF objects. 265 """ 266 try: 267 del self._data[key] 268 except KeyError: 269 pass 270 271 def clear(self) -> None: 272 """ Delete all entries from :class:`Dictionary`, deletes hard owned 273 DXF objects from OBJECTS section. 274 """ 275 if self.is_hard_owner: 276 self._delete_hard_owned_entries() 277 self._data.clear() 278 279 def _delete_hard_owned_entries(self) -> None: 280 # Presumption: hard owned DXF objects always reside in the OBJECTS section 281 objects = self.doc.objects 282 for key, entity in self.items(): 283 objects.delete_entity(entity) 284 285 def add_new_dict(self, key: str, hard_owned: bool = False) -> 'Dictionary': 286 """ Create a new sub :class:`Dictionary`. 287 288 Args: 289 key: name of the sub dictionary 290 hard_owned: entries of the new dictionary are hard owned 291 292 """ 293 dxf_dict = self.doc.objects.add_dictionary(owner=self.dxf.handle, 294 hard_owned=hard_owned) 295 self.add(key, dxf_dict) 296 return dxf_dict 297 298 def add_dict_var(self, key: str, value: str) -> 'DictionaryVar': 299 """ Add new :class:`DictionaryVar`. 300 301 Args: 302 key: entry name as string 303 value: entry value as string 304 305 """ 306 new_var = self.doc.objects.add_dictionary_var( 307 owner=self.dxf.handle, 308 value=value 309 ) 310 self.add(key, new_var) 311 return new_var 312 313 def set_or_add_dict_var(self, key: str, value: str) -> 'DictionaryVar': 314 """ Set or add new :class:`DictionaryVar`. 315 316 Args: 317 key: entry name as string 318 value: entry value as string 319 320 """ 321 if key not in self: 322 dict_var = self.doc.objects.add_dictionary_var( 323 owner=self.dxf.handle, 324 value=value 325 ) 326 self.add(key, dict_var) 327 else: 328 dict_var = self.get(key) 329 dict_var.dxf.value = str(value) 330 return dict_var 331 332 def get_required_dict(self, key: str) -> 'Dictionary': 333 """ Get entry `key` or create a new :class:`Dictionary`, 334 if `Key` not exist. 335 """ 336 try: 337 dxf_dict = self.get(key) 338 except DXFKeyError: 339 dxf_dict = self.add_new_dict(key) 340 return dxf_dict 341 342 def audit(self, auditor: 'Auditor') -> None: 343 super().audit(auditor) 344 self._check_invalid_entries(auditor) 345 346 def _check_invalid_entries(self, auditor: 'Auditor'): 347 trash = [] # do not delete content while iterating 348 append = trash.append 349 db = auditor.entitydb 350 for key, entry in self._data.items(): 351 if isinstance(entry, str): 352 if entry not in db: 353 append(key) 354 elif entry.is_alive: 355 if entry.dxf.handle not in db: 356 append(key) 357 else: # entry is destroyed 358 append(key) 359 for key in trash: 360 del self._data[key] 361 auditor.fixed_error( 362 code=AuditError.INVALID_DICTIONARY_ENTRY, 363 message=f'Removed entry "{key}" with invalid handle in {str(self)}', 364 dxf_entity=self, 365 data=key, 366 ) 367 368 def destroy(self) -> None: 369 if not self.is_alive: 370 return 371 372 if self.is_hard_owner: 373 self._delete_hard_owned_entries() 374 super().destroy() 375 376 377acdb_dict_with_default = DefSubclass('AcDbDictionaryWithDefault', { 378 'default': DXFAttr(340), 379}) 380acdb_dict_with_default_group_codes = group_code_mapping(acdb_dict_with_default) 381 382 383@factory.register_entity 384class DictionaryWithDefault(Dictionary): 385 DXFTYPE = 'ACDBDICTIONARYWDFLT' 386 DXFATTRIBS = DXFAttributes(base_class, acdb_dictionary, 387 acdb_dict_with_default) 388 389 def __init__(self): 390 super().__init__() 391 self._default: Optional[DXFEntity] = None 392 393 def _copy_data(self, entity: 'Dictionary') -> None: 394 entity._default = self._default 395 396 def post_load_hook(self, doc: 'Drawing') -> None: 397 # Set _default to None if default object not exist - audit() replaces 398 # a not existing default object by a place holder object. 399 # AutoCAD ignores not existing default objects! 400 self._default = doc.entitydb.get(self.dxf.default) 401 super().post_load_hook(doc) 402 403 def load_dxf_attribs( 404 self, processor: SubclassProcessor = None) -> 'DXFNamespace': 405 dxf = super().load_dxf_attribs(processor) 406 if processor: 407 processor.fast_load_dxfattribs( 408 dxf, acdb_dict_with_default_group_codes, 2) 409 return dxf 410 411 def export_entity(self, tagwriter: 'TagWriter') -> None: 412 super().export_entity(tagwriter) 413 tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dict_with_default.name) 414 self.dxf.export_dxf_attribs(tagwriter, 'default') 415 416 def get(self, key: str, default: Any = DXFKeyError) -> DXFEntity: 417 # `default` argument is ignored, exist only for API compatibility, 418 """ Returns :class:`DXFEntity` for `key` or the predefined dictionary 419 wide :attr:`dxf.default` entity if `key` does not exist or ``None`` 420 if default value also not exist. 421 422 """ 423 return super().get(key, default=self._default) 424 425 def set_default(self, default: DXFEntity) -> None: 426 """ Set dictionary wide default entry. 427 428 Args: 429 default: default entry as :class:`DXFEntity` 430 431 """ 432 self._default = default 433 self.dxf.default = self._default.dxf.handle 434 435 def audit(self, auditor: 'Auditor') -> None: 436 def create_missing_default_object(): 437 placeholder = self.doc.objects.add_placeholder( 438 owner=self.dxf.handle) 439 self.set_default(placeholder) 440 auditor.fixed_error( 441 code=AuditError.CREATED_MISSING_OBJECT, 442 message=f'Created missing default object in {str(self)}.' 443 ) 444 445 if self._default is None or not self._default.is_alive: 446 if auditor.entitydb.locked: 447 auditor.add_post_audit_job(create_missing_default_object) 448 else: 449 create_missing_default_object() 450 super().audit(auditor) 451 452 453acdb_dict_var = DefSubclass('DictionaryVariables', { 454 'schema': DXFAttr(280, default=0), 455 # Object schema number (currently set to 0) 456 'value': DXFAttr(1, default=''), 457}) 458acdb_dict_var_group_codes = group_code_mapping(acdb_dict_var) 459 460 461@factory.register_entity 462class DictionaryVar(DXFObject): 463 """ 464 DICTIONARYVAR objects are used by AutoCAD as a means to store named values 465 in the database for setvar / getvar purposes without the need to add entries 466 to the DXF HEADER section. System variables that are stored as 467 DICTIONARYVAR objects are the following: 468 469 - DEFAULTVIEWCATEGORY 470 - DIMADEC 471 - DIMASSOC 472 - DIMDSEP 473 - DRAWORDERCTL 474 - FIELDEVAL 475 - HALOGAP 476 - HIDETEXT 477 - INDEXCTL 478 - INDEXCTL 479 - INTERSECTIONCOLOR 480 - INTERSECTIONDISPLAY 481 - MSOLESCALE 482 - OBSCOLOR 483 - OBSLTYPE 484 - OLEFRAME 485 - PROJECTNAME 486 - SORTENTS 487 - UPDATETHUMBNAIL 488 - XCLIPFRAME 489 - XCLIPFRAME 490 491 """ 492 DXFTYPE = 'DICTIONARYVAR' 493 DXFATTRIBS = DXFAttributes(base_class, acdb_dict_var) 494 495 def load_dxf_attribs(self, 496 processor: SubclassProcessor = None) -> 'DXFNamespace': 497 dxf = super().load_dxf_attribs(processor) 498 if processor: 499 processor.fast_load_dxfattribs(dxf, acdb_dict_var_group_codes, 1) 500 return dxf 501 502 def export_entity(self, tagwriter: 'TagWriter') -> None: 503 """ Export entity specific data as DXF tags. """ 504 super().export_entity(tagwriter) 505 tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dict_var.name) 506 self.dxf.export_dxf_attribs(tagwriter, ['schema', 'value']) 507