1# Copyright (c) 2019-2020 Manfred Moitzi 2# License: MIT License 3""" :class:`DXFEntity` is the super class of all DXF entities. 4 5The current entity system uses the features of the latest supported DXF version. 6 7The stored DXF version of the document is used to warn users if they use 8unsupported DXF features of the current DXF version. 9 10The DXF version of the document can be changed at runtime or overridden by 11exporting, but unsupported DXF features are just ignored by exporting. 12 13Ezdxf does no conversion between different DXF versions, this package is 14still not a CAD application. 15 16""" 17from typing import ( 18 TYPE_CHECKING, List, Dict, Any, Iterable, Optional, Type, TypeVar, Set, 19 Callable, 20) 21import copy 22import logging 23import uuid 24from ezdxf import options 25from ezdxf.lldxf import const 26from ezdxf.lldxf.tags import Tags 27from ezdxf.lldxf.extendedtags import ExtendedTags 28from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass 29from ezdxf.tools import set_flag_state 30from . import factory 31from .appdata import AppData, Reactors 32from .dxfns import DXFNamespace, SubclassProcessor 33from .xdata import XData, EmbeddedObjects 34from .xdict import ExtensionDict 35 36logger = logging.getLogger('ezdxf') 37 38if TYPE_CHECKING: 39 from ezdxf.eztypes import Auditor, TagWriter, Drawing, DXFAttr 40 41__all__ = ['DXFEntity', 'DXFTagStorage', 'base_class', 'SubclassProcessor'] 42 43base_class = DefSubclass(None, { 44 'handle': DXFAttr(5), 45 46 # owner: Soft-pointer ID/handle to owner BLOCK_RECORD object 47 # This tag is not supported by DXF R12, but is used intern to unify entity 48 # handling between DXF R12 and DXF R2000+ 49 # Do not write this tag into DXF R12 files! 50 'owner': DXFAttr(330), 51 52 # Application defined data can only appear here: 53 # 102, {APPID ... multiple entries possible DXF R12? 54 # 102, {ACAD_REACTORS ... one entry DXF R2000+, optional 55 # 102, {ACAD_XDICTIONARY ... one entry DXF R2000+, optional 56}) 57 58T = TypeVar('T', bound='DXFEntity') 59 60 61class DXFEntity: 62 """ Common super class for all DXF entities. """ 63 DXFTYPE = 'DXFENTITY' # storing as class var needs less memory 64 DXFATTRIBS = DXFAttributes(base_class) # DXF attribute definitions 65 66 # Default DXF attributes are set at instantiating a new object, the the 67 # difference to attribute default values is, that this attributes are 68 # really set, this means there is an real object in the dxf namespace 69 # defined, where default attribute values get returned on access without 70 # an existing object in the dxf namespace. 71 DEFAULT_ATTRIBS: Dict = {} 72 MIN_DXF_VERSION_FOR_EXPORT = const.DXF12 73 74 def __init__(self): 75 """ Default constructor. (internal API)""" 76 # Public attributes for package users 77 self.doc: Optional[Drawing] = None 78 self.dxf: DXFNamespace = DXFNamespace(entity=self) 79 80 # None public attributes for package users 81 # create extended data only if needed: 82 self.appdata: Optional[AppData] = None 83 self.reactors: Optional[Reactors] = None 84 self.extension_dict: Optional[ExtensionDict] = None 85 self.xdata: Optional[XData] = None 86 # TODO: remove embedded_objects - no need to waste memory for every entity, 87 # this is a seldom used feature (ATTRIB, ATTDEF), and this entities have to 88 # manage the embedded objects by itself at loading stage and DXF export. 89 # Removing is possible if ATTRIB and ATTDEF have explicit 90 # support for embedded MTEXT objects 91 self.embedded_objects: Optional[EmbeddedObjects] = None 92 self.proxy_graphic: Optional[bytes] = None 93 # self._uuid # uuid generated at first request 94 95 @property 96 def uuid(self) -> uuid.UUID: 97 """ Returns an UUID, which allows to distinguish even 98 virtual entities without a handle. 99 100 This UUID will be created at the first request. 101 102 """ 103 uuid_ = getattr(self, '_uuid', None) 104 if uuid_ is None: 105 uuid_ = uuid.uuid4() 106 self._uuid = uuid_ 107 return uuid_ 108 109 @classmethod 110 def new(cls: Type[T], handle: str = None, owner: str = None, 111 dxfattribs: Dict = None, doc: 'Drawing' = None) -> T: 112 """ Constructor for building new entities from scratch by ezdxf. 113 114 NEW process: 115 116 This is a trusted environment where everything is under control of 117 ezdxf respectively the package-user, it is okay to raise exception 118 to show implementation errors in ezdxf or usage errors of the 119 package-user. 120 121 The :attr:`Drawing.is_loading` flag can be checked to distinguish the 122 NEW and the LOAD process. 123 124 Args: 125 handle: unique DXF entity handle or None 126 owner: owner handle if entity has an owner else None or '0' 127 dxfattribs: DXF attributes 128 doc: DXF document 129 130 (internal API) 131 """ 132 entity = cls() 133 entity.doc = doc 134 entity.dxf.handle = handle 135 entity.dxf.owner = owner 136 attribs = dict(cls.DEFAULT_ATTRIBS) 137 attribs.update(dxfattribs or {}) 138 entity.update_dxf_attribs(attribs) 139 # Only this method triggers the post_new_hook() 140 entity.post_new_hook() 141 return entity 142 143 def post_new_hook(self): 144 """ Post processing and integrity validation after entity creation. 145 146 Called only if created by ezdxf (see :meth:`DXFEntity.new`), 147 not if loaded from an external source. 148 149 (internal API) 150 """ 151 pass 152 153 def post_bind_hook(self): 154 """ Post processing and integrity validation after binding entity to a 155 DXF Document. This method is triggered by the :func:`factory.bind` 156 function only when the entity was created by ezdxf. 157 158 If the entity was loaded in the 1st loading stage, the 159 :func:`factory.load` functions also calls the :func:`factory.bind` 160 to bind entities to the loaded document, but not all entities are 161 loaded at this time. To avoid problems this method will not be called 162 when loading content from DXF file, but :meth:`post_load_hook` will be 163 triggered for loaded entities at a later and safer point in time. 164 165 (internal API) 166 """ 167 pass 168 169 @classmethod 170 def load(cls: Type[T], tags: ExtendedTags, doc: 'Drawing' = None) -> T: 171 """ Constructor to generate entities loaded from an external source. 172 173 LOAD process: 174 175 This is an untrusted environment where valid structure are not 176 guaranteed and errors should be fixed, because the package-user is not 177 responsible for the problems and also can't fix them, raising 178 exceptions should only be done for unrecoverable issues. 179 Log fixes for debugging! 180 181 Be more like BricsCAD and not as mean as AutoCAD! 182 183 The :attr:`Drawing.is_loading` flag can be checked to distinguish the 184 NEW and the LOAD process. 185 186 Args: 187 tags: DXF tags as :class:`ExtendedTags` 188 doc: DXF Document 189 190 (internal API) 191 """ 192 # This method does not trigger the post_new_hook() 193 entity = cls() 194 entity.doc = doc 195 dxfversion = doc.dxfversion if doc else None 196 entity.load_tags(tags, dxfversion=dxfversion) 197 return entity 198 199 def load_tags(self, tags: ExtendedTags, dxfversion: str = None) -> None: 200 """ Generic tag loading interface, called if DXF document is loaded 201 from external sources. 202 203 1. Loading stage which set the basic DXF attributes, additional 204 resources (DXF objects) are not loaded yet. References to these 205 resources have to be stored as handles and can be resolved in the 206 2. Loading stage: :meth:`post_load_hook`. 207 208 (internal API) 209 """ 210 if tags: 211 if len(tags.appdata): 212 self.setup_app_data(tags.appdata) 213 if len(tags.xdata): 214 self.xdata = XData(tags.xdata) 215 if tags.embedded_objects: # TODO: remove 216 self.embedded_objects = EmbeddedObjects( 217 tags.embedded_objects) 218 processor = SubclassProcessor(tags, dxfversion=dxfversion) 219 self.dxf = self.load_dxf_attribs(processor) 220 221 def load_dxf_attribs( 222 self, processor: SubclassProcessor = None) -> DXFNamespace: 223 """ Load DXF attributes into DXF namespace. """ 224 return DXFNamespace(processor, self) 225 226 def post_load_hook(self, doc: 'Drawing') -> Optional[Callable]: 227 """ The 2nd loading stage when loading DXF documents from an external 228 source, for the 1st loading stage see :meth:`load_tags`. 229 230 This stage is meant to convert resource handles into :class:`DXFEntity` 231 objects. This is an untrusted environment where valid structure are not 232 guaranteed, raise exceptions only for unrecoverable structure errors 233 and fix everything else. Log fixes for debugging! 234 235 Some fixes can not be applied at this stage, because some structures 236 like the OBJECTS section are not initialized, in this case return a 237 callable, which will be executed after the DXF document is fully 238 initialized, for an example see :class:`Image`. 239 240 Triggered in method: :meth:`Drawing._2nd_loading_stage` 241 242 Examples for two stage loading: 243 Image, Underlay, DXFGroup, Dictionary, Dimstyle, MText 244 245 """ 246 if self.extension_dict is not None: 247 self.extension_dict.load_resources(doc) 248 return None 249 250 @classmethod 251 def from_text(cls: Type[T], text: str, doc: 'Drawing' = None) -> T: 252 """ Load constructor from text for testing. (internal API)""" 253 return cls.load(ExtendedTags.from_text(text), doc) 254 255 @classmethod 256 def shallow_copy(cls: Type[T], other: 'DXFEntity') -> T: 257 """ Copy constructor for type casting e.g. Polyface and Polymesh. 258 (internal API) 259 """ 260 entity = cls() 261 entity.doc = other.doc 262 entity.dxf = other.dxf 263 entity.extension_dict = other.extension_dict 264 entity.reactors = other.reactors 265 entity.appdata = other.appdata 266 entity.xdata = other.xdata 267 entity.embedded_objects = other.embedded_objects # todo: remove 268 entity.proxy_graphic = other.proxy_graphic 269 entity.dxf.rewire(entity) 270 return entity 271 272 def copy(self: T) -> T: 273 """ Returns a copy of `self` but without handle, owner and reactors. 274 This copy is NOT stored in the entity database and does NOT reside 275 in any layout, block, table or objects section! Extension dictionary 276 and reactors are not copied. 277 278 Don't use this function to duplicate DXF entities in drawing, 279 use :meth:`EntityDB.duplicate_entity` instead for this task. 280 281 Copying is not trivial, because of linked resources and the lack of 282 documentation how to handle this linked resources: extension dictionary, 283 handles in appdata, xdata or embedded objects. 284 285 (internal API) 286 """ 287 entity = self.__class__() 288 entity.doc = self.doc 289 # copy and bind dxf namespace to new entity 290 entity.dxf = self.dxf.copy(entity) 291 entity.dxf.reset_handles() 292 293 # Do not copy extension dict: if the extension dict should be copied 294 # in the future - a deep copy is maybe required! 295 entity.extension_dict = None 296 # Do not copy reactors: 297 entity.reactors = None 298 299 entity.proxy_graphic = self.proxy_graphic # immutable bytes 300 301 # if appdata contains handles, they are treated as shared resources 302 entity.appdata = copy.deepcopy(self.appdata) 303 304 # if xdata contains handles, they are treated as shared resources 305 entity.xdata = copy.deepcopy(self.xdata) 306 307 # if embedded objects contains handles, they are treated as shared resources 308 entity.embedded_objects = copy.deepcopy(self.embedded_objects) # todo: remove 309 self._copy_data(entity) 310 return entity 311 312 def _copy_data(self, entity: 'DXFEntity') -> None: 313 """ Copy entity data like vertices or attribs and store the copies into 314 the entity database. 315 (internal API) 316 """ 317 pass 318 319 def __deepcopy__(self, memodict: Dict = None): 320 """ Some entities maybe linked by more than one entity, to be safe use 321 `memodict` for bookkeeping. 322 (internal API) 323 """ 324 memodict = memodict or {} 325 try: 326 return memodict[id(self)] 327 except KeyError: 328 copy = self.copy() 329 memodict[id(self)] = copy 330 return copy 331 332 def update_dxf_attribs(self, dxfattribs: Dict) -> None: 333 """ Set DXF attributes by a ``dict`` like :code:`{'layer': 'test', 334 'color': 4}`. 335 """ 336 setter = self.dxf.set 337 for key, value in dxfattribs.items(): 338 setter(key, value) 339 340 def setup_app_data(self, appdata: List[Tags]) -> None: 341 """ Setup data structures from APP data. (internal API) """ 342 for data in appdata: 343 code, appid = data[0] 344 if appid == const.ACAD_REACTORS: 345 self.reactors = Reactors.from_tags(data) 346 elif appid == const.ACAD_XDICTIONARY: 347 self.extension_dict = ExtensionDict.from_tags(data) 348 else: 349 self.set_app_data(appid, data) 350 351 def update_handle(self, handle: str) -> None: 352 """ Update entity handle. (internal API) """ 353 self.dxf.handle = handle 354 if self.extension_dict: 355 self.extension_dict.update_owner(handle) 356 357 @property 358 def is_alive(self): 359 """ Returns ``False`` if entity has been deleted. """ 360 return hasattr(self, 'dxf') 361 362 @property 363 def is_virtual(self): 364 """ Returns ``True`` if entity is a virtual entity. """ 365 return self.doc is None or self.dxf.handle is None 366 367 @property 368 def is_bound(self): 369 """ Returns ``True`` if entity is bound to DXF document. """ 370 if self.is_alive and not self.is_virtual: 371 return factory.is_bound(self, self.doc) 372 return False 373 374 def get_dxf_attrib(self, key: str, default: Any = None) -> Any: 375 """ Get DXF attribute `key`, returns `default` if key doesn't exist, or 376 raise :class:`DXFValueError` if `default` is :class:`DXFValueError` 377 and no DXF default value is defined:: 378 379 layer = entity.get_dxf_attrib("layer") 380 # same as 381 layer = entity.dxf.layer 382 383 Raises :class:`DXFAttributeError` if `key` is not an supported DXF 384 attribute. 385 386 """ 387 return self.dxf.get(key, default) 388 389 def set_dxf_attrib(self, key: str, value: Any) -> None: 390 """ Set new `value` for DXF attribute `key`:: 391 392 entity.set_dxf_attrib("layer", "MyLayer") 393 # same as 394 entity.dxf.layer = "MyLayer" 395 396 Raises :class:`DXFAttributeError` if `key` is not an supported DXF 397 attribute. 398 399 """ 400 self.dxf.set(key, value) 401 402 def del_dxf_attrib(self, key: str) -> None: 403 """ Delete DXF attribute `key`, does not raise an error if attribute is 404 supported but not present. 405 406 Raises :class:`DXFAttributeError` if `key` is not an supported DXF 407 attribute. 408 409 """ 410 self.dxf.discard(key) 411 412 def has_dxf_attrib(self, key: str) -> bool: 413 """ Returns ``True`` if DXF attribute `key` really exist. 414 415 Raises :class:`DXFAttributeError` if `key` is not an supported DXF 416 attribute. 417 418 """ 419 return self.dxf.hasattr(key) 420 421 dxf_attrib_exists = has_dxf_attrib 422 423 def is_supported_dxf_attrib(self, key: str) -> bool: 424 """ Returns ``True`` if DXF attrib `key` is supported by this entity. 425 Does not grant that attribute `key` really exist. 426 427 """ 428 if key in self.DXFATTRIBS: 429 if self.doc: 430 return self.doc.dxfversion >= self.DXFATTRIBS.get( 431 key).dxfversion 432 else: 433 return True 434 else: 435 return False 436 437 def dxftype(self) -> str: 438 """ Get DXF type as string, like ``LINE`` for the line entity. """ 439 return self.DXFTYPE 440 441 def __str__(self) -> str: 442 """ Returns a simple string representation. """ 443 return "{}(#{})".format(self.dxftype(), self.dxf.handle) 444 445 def __repr__(self) -> str: 446 """ Returns a simple string representation including the class. """ 447 return str(self.__class__) + " " + str(self) 448 449 def dxfattribs(self, drop: Set[str] = None) -> Dict: 450 """ Returns a ``dict`` with all existing DXF attributes and their 451 values and exclude all DXF attributes listed in set `drop`. 452 453 """ 454 all_attribs = self.dxf.all_existing_dxf_attribs() 455 if drop: 456 return {k: v for k, v in all_attribs.items() if k not in drop} 457 else: 458 return all_attribs 459 460 def set_flag_state(self, flag: int, state: bool = True, 461 name: str = 'flags') -> None: 462 """ Set binary coded `flag` of DXF attribute `name` to ``1`` (on) 463 if `state` is ``True``, set `flag` to ``0`` (off) 464 if `state` is ``False``. 465 """ 466 flags = self.dxf.get(name, 0) 467 self.dxf.set(name, set_flag_state(flags, flag, state=state)) 468 469 def get_flag_state(self, flag: int, name: str = 'flags') -> bool: 470 """ Returns ``True`` if any `flag` of DXF attribute is ``1`` (on), else 471 ``False``. Always check only one flag state at the time. 472 """ 473 return bool(self.dxf.get(name, 0) & flag) 474 475 def remove_dependencies(self, other: 'Drawing' = None): 476 """ Remove all dependencies from current document. 477 478 Intended usage is to remove dependencies from the current document to 479 move or copy the entity to `other` DXF document. 480 481 An error free call of this method does NOT guarantee that this entity 482 can be moved/copied to the `other` document, some entities like 483 DIMENSION have too much dependencies to a document to move or copy 484 them, but to check this is not the domain of this method! 485 486 (internal API) 487 """ 488 if self.is_alive: 489 self.dxf.owner = None 490 self.dxf.handle = None 491 self.reactors = None 492 self.extension_dict = None 493 self.appdata = None 494 self.xdata = None 495 self.embedded_objects = None # todo: remove 496 497 def destroy(self) -> None: 498 """ Delete all data and references. Does not delete entity from 499 structures like layouts or groups. 500 501 Starting with `ezdxf` v0.14 this method could be used to delete 502 entities. 503 504 (internal API) 505 506 """ 507 if not self.is_alive: 508 return 509 510 if self.extension_dict is not None: 511 self.extension_dict.destroy() 512 del self.extension_dict 513 del self.appdata 514 del self.reactors 515 del self.xdata 516 del self.embedded_objects # todo: remove 517 del self.doc 518 del self.dxf # check mark for is_alive 519 520 def preprocess_export(self, tagwriter: 'TagWriter') -> bool: 521 """ Pre requirement check and pre processing for export. 522 523 Returns False if entity should not be exported at all. 524 525 (internal API) 526 """ 527 return True 528 529 def export_dxf(self, tagwriter: 'TagWriter') -> None: 530 """ Export DXF entity by `tagwriter`. 531 532 This is the first key method for exporting DXF entities: 533 534 - has to know the group codes for each attribute 535 - has to add subclass tags in correct order 536 - has to integrate extended data: ExtensionDict, Reactors, AppData 537 - has to maintain the correct tag order (because sometimes order matters) 538 539 (internal API) 540 541 """ 542 if tagwriter.dxfversion < self.MIN_DXF_VERSION_FOR_EXPORT: 543 return 544 if not self.preprocess_export(tagwriter): 545 return 546 # ! first step ! 547 # write handle, AppData, Reactors, ExtensionDict, owner 548 self.export_base_class(tagwriter) 549 550 # this is the entity specific part 551 self.export_entity(tagwriter) 552 553 # ! Last step ! 554 # write xdata, embedded objects 555 self.export_embedded_objects(tagwriter) 556 self.export_xdata(tagwriter) 557 558 def export_base_class(self, tagwriter: 'TagWriter') -> None: 559 """ Export base class DXF attributes and structures. (internal API) """ 560 dxftype = self.DXFTYPE 561 _handle_code = 105 if dxftype == 'DIMSTYLE' else 5 562 # 1. tag: (0, DXFTYPE) 563 tagwriter.write_tag2(const.STRUCTURE_MARKER, dxftype) 564 565 if tagwriter.dxfversion >= const.DXF2000: 566 tagwriter.write_tag2(_handle_code, self.dxf.handle) 567 if self.appdata: 568 self.appdata.export_dxf(tagwriter) 569 if self.has_extension_dict: 570 self.extension_dict.export_dxf(tagwriter) 571 if self.reactors: 572 self.reactors.export_dxf(tagwriter) 573 tagwriter.write_tag2(const.OWNER_CODE, self.dxf.owner) 574 else: # DXF R12 575 if tagwriter.write_handles: 576 tagwriter.write_tag2(_handle_code, self.dxf.handle) 577 # do not write owner handle - not supported by DXF R12 578 579 def export_entity(self, tagwriter: 'TagWriter') -> None: 580 """ Export DXF entity specific data by `tagwriter`. 581 582 This is the second key method for exporting DXF entities: 583 584 - has to know the group codes for each attribute 585 - has to add subclass tags in correct order 586 - has to maintain the correct tag order (because sometimes order matters) 587 588 (internal API) 589 """ 590 # base class (handle, appid, reactors, xdict, owner) export is done by parent class 591 pass 592 # xdata and embedded objects export is also done by parent 593 594 def export_xdata(self, tagwriter: 'TagWriter') -> None: 595 """ Export DXF XDATA by `tagwriter`. (internal API)""" 596 if self.xdata: 597 self.xdata.export_dxf(tagwriter) 598 599 def export_embedded_objects(self, tagwriter: 'TagWriter') -> None: 600 """ Export embedded objects by `tagwriter`. (internal API)""" 601 if self.embedded_objects: # todo: remove 602 self.embedded_objects.export_dxf(tagwriter) # todo: remove 603 604 def audit(self, auditor: 'Auditor') -> None: 605 """ Validity check. (internal API) """ 606 # Important: do not check owner handle! -> DXFGraphic(), DXFObject() 607 # check app data 608 # check reactors 609 # check extension dict 610 # check XDATA 611 612 @property 613 def has_extension_dict(self) -> bool: 614 """ Returns ``True`` if entity has an attached 615 :class:`~ezdxf.entities.xdict.ExtensionDict`. 616 """ 617 xdict = self.extension_dict 618 # Don't use None check: bool(xdict) for an empty extension dict is False 619 if xdict is not None and xdict.is_alive: 620 # Check the associated Dictionary object 621 dictionary = xdict.dictionary 622 if isinstance(dictionary, str): 623 # just a handle string - SUT 624 return True 625 else: 626 return dictionary.is_alive 627 return False 628 629 def get_extension_dict(self) -> 'ExtensionDict': 630 """ Returns the existing :class:`~ezdxf.entities.xdict.ExtensionDict`. 631 632 Raises: 633 AttributeError: extension dict does not exist 634 635 """ 636 if self.has_extension_dict: 637 return self.extension_dict 638 else: 639 raise AttributeError('Entity has no extension dictionary.') 640 641 def new_extension_dict(self) -> 'ExtensionDict': 642 self.extension_dict = ExtensionDict.new(self.dxf.handle, self.doc) 643 return self.extension_dict 644 645 def has_app_data(self, appid: str) -> bool: 646 """ Returns ``True`` if application defined data for `appid` exist. """ 647 if self.appdata: 648 return appid in self.appdata 649 else: 650 return False 651 652 def get_app_data(self, appid: str) -> Tags: 653 """ Returns application defined data for `appid`. 654 655 Args: 656 appid: application name as defined in the APPID table. 657 658 Raises: 659 DXFValueError: no data for `appid` found 660 661 """ 662 if self.appdata: 663 return self.appdata.get(appid)[1:-1] 664 else: 665 raise const.DXFValueError(appid) 666 667 def set_app_data(self, appid: str, tags: Iterable) -> None: 668 """ Set application defined data for `appid` as iterable of tags. 669 670 Args: 671 appid: application name as defined in the APPID table. 672 tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag` 673 674 """ 675 if self.appdata is None: 676 self.appdata = AppData() 677 self.appdata.add(appid, tags) 678 679 def discard_app_data(self, appid: str): 680 """ Discard application defined data for `appid`. Does not raise an 681 exception if no data for `appid` exist. 682 """ 683 if self.appdata: 684 self.appdata.discard(appid) 685 686 def has_xdata(self, appid: str) -> bool: 687 """ Returns ``True`` if extended data for `appid` exist. """ 688 if self.xdata: 689 return appid in self.xdata 690 else: 691 return False 692 693 def get_xdata(self, appid: str) -> Tags: 694 """ Returns extended data for `appid`. 695 696 Args: 697 appid: application name as defined in the APPID table. 698 699 Raises: 700 DXFValueError: no extended data for `appid` found 701 702 """ 703 if self.xdata: 704 return Tags(self.xdata.get(appid)[1:]) 705 else: 706 raise const.DXFValueError(appid) 707 708 def set_xdata(self, appid: str, tags: Iterable) -> None: 709 """ Set extended data for `appid` as iterable of tags. 710 711 Args: 712 appid: application name as defined in the APPID table. 713 tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag` 714 715 """ 716 if self.xdata is None: 717 self.xdata = XData() 718 self.xdata.add(appid, tags) 719 720 def discard_xdata(self, appid: str) -> None: 721 """ Discard extended data for `appid`. Does not raise an exception if 722 no extended data for `appid` exist. 723 """ 724 if self.xdata: 725 self.xdata.discard(appid) 726 727 def has_xdata_list(self, appid: str, name: str) -> bool: 728 """ Returns ``True`` if a tag list `name` for extended data `appid` 729 exist. 730 """ 731 if self.has_xdata(appid): 732 return self.xdata.has_xlist(appid, name) 733 else: 734 return False 735 736 def get_xdata_list(self, appid: str, name: str) -> Tags: 737 """ Returns tag list `name` for extended data `appid`. 738 739 Args: 740 appid: application name as defined in the APPID table. 741 name: extended data list name 742 743 Raises: 744 DXFValueError: no extended data for `appid` found or no data list `name` not found 745 746 """ 747 if self.xdata: 748 return Tags(self.xdata.get_xlist(appid, name)) 749 else: 750 raise const.DXFValueError(appid) 751 752 def set_xdata_list(self, appid: str, name: str, tags: Iterable) -> None: 753 """ Set tag list `name` for extended data `appid` as iterable of tags. 754 755 Args: 756 appid: application name as defined in the APPID table. 757 name: extended data list name 758 tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag` 759 760 """ 761 if self.xdata is None: 762 self.xdata = XData() 763 self.xdata.set_xlist(appid, name, tags) 764 765 def discard_xdata_list(self, appid: str, name: str) -> None: 766 """ Discard tag list `name` for extended data `appid`. Does not raise 767 an exception if no extended data for `appid` or no tag list `name` 768 exist. 769 """ 770 if self.xdata: 771 self.xdata.discard_xlist(appid, name) 772 773 def replace_xdata_list(self, appid: str, name: str, tags: Iterable) -> None: 774 """ 775 Replaces tag list `name` for existing extended data `appid` by `tags`. 776 Appends new list if tag list `name` do not exist, but raises 777 :class:`DXFValueError` if extended data `appid` do not exist. 778 779 Args: 780 appid: application name as defined in the APPID table. 781 name: extended data list name 782 tags: iterable of (code, value) tuples or :class:`~ezdxf.lldxf.types.DXFTag` 783 784 Raises: 785 DXFValueError: no extended data for `appid` found 786 787 """ 788 self.xdata.replace_xlist(appid, name, tags) 789 790 def has_reactors(self) -> bool: 791 """ Returns ``True`` if entity has reactors. """ 792 return bool(self.reactors) 793 794 def get_reactors(self) -> List[str]: 795 """ Returns associated reactors as list of handles. """ 796 return self.reactors.get() if self.reactors else [] 797 798 def set_reactors(self, handles: Iterable[str]) -> None: 799 """ Set reactors as list of handles. """ 800 if self.reactors is None: 801 self.reactors = Reactors() 802 self.reactors.set(handles) 803 804 def append_reactor_handle(self, handle: str) -> None: 805 """ Append `handle` to reactors. """ 806 if self.reactors is None: 807 self.reactors = Reactors() 808 self.reactors.add(handle) 809 810 def discard_reactor_handle(self, handle: str) -> None: 811 """ Discard `handle` from reactors. Does not raise an exception if 812 `handle` does not exist. 813 """ 814 if self.reactors: 815 self.reactors.discard(handle) 816 817 818@factory.set_default_class 819class DXFTagStorage(DXFEntity): 820 """ Just store all the tags as they are. (internal class) """ 821 822 def __init__(self): 823 """ Default constructor """ 824 super().__init__() 825 self.xtags: Optional[ExtendedTags] = None 826 827 def copy(self) -> 'DXFEntity': 828 raise const.DXFTypeError( 829 f'Cloning of tag storage {self.dxftype()} not supported.' 830 ) 831 832 @property 833 def base_class(self): 834 return self.xtags.subclasses[0] 835 836 @classmethod 837 def load(cls, tags: ExtendedTags, doc: 'Drawing' = None) -> 'DXFTagStorage': 838 assert isinstance(tags, ExtendedTags) 839 entity = cls.new(doc=doc) 840 dxfversion = doc.dxfversion if doc else None 841 entity.load_tags(tags, dxfversion=dxfversion) 842 entity.store_tags(tags) 843 if options.load_proxy_graphics: 844 entity.load_proxy_graphic() 845 return entity 846 847 def load_proxy_graphic(self) -> Optional[bytes]: 848 try: 849 tags = self.xtags.get_subclass('AcDbEntity') 850 except const.DXFKeyError: 851 return 852 binary_data = [tag.value for tag in tags.find_all(310)] 853 if len(binary_data): 854 self.proxy_graphic = b''.join(binary_data) 855 856 def store_tags(self, tags: ExtendedTags) -> None: 857 # store DXFTYPE, overrides class member 858 # 1. tag of 1. subclass is the structure tag (0, DXFTYPE) 859 self.xtags = tags 860 self.DXFTYPE = self.base_class[0].value 861 try: 862 acdb_entity = tags.get_subclass('AcDbEntity') 863 self.dxf.__dict__['paperspace'] = acdb_entity.get_first_value(67, 0) 864 except const.DXFKeyError: 865 # just fake it 866 self.dxf.__dict__['paperspace'] = 0 867 868 def export_entity(self, tagwriter: 'TagWriter') -> None: 869 """ Write subclass tags as they are. """ 870 for subclass in self.xtags.subclasses[1:]: 871 tagwriter.write_tags(subclass) 872 873 def destroy(self) -> None: 874 if not self.is_alive: 875 return 876 877 del self.xtags 878 super().destroy() 879