# Copyright (c) 2019-2024 Manfred Moitzi # License: MIT License from __future__ import annotations from typing import TYPE_CHECKING, Iterable, Union, Any, Optional from typing_extensions import Self, TypeGuard import logging import array from ezdxf.lldxf import validator from ezdxf.lldxf.const import DXF2000, DXFStructureError, SUBCLASS_MARKER from ezdxf.lldxf.tags import Tags from ezdxf.lldxf.types import dxftag, DXFTag, DXFBinaryTag from ezdxf.lldxf.attributes import ( DXFAttr, DXFAttributes, DefSubclass, RETURN_DEFAULT, group_code_mapping, ) from ezdxf.tools import take2 from .dxfentity import DXFEntity, base_class, SubclassProcessor, DXFTagStorage from .factory import register_entity from .copy import default_copy if TYPE_CHECKING: from ezdxf.audit import Auditor from ezdxf.entities import DXFNamespace from ezdxf.lldxf.tagwriter import AbstractTagWriter __all__ = [ "DXFObject", "Placeholder", "XRecord", "VBAProject", "SortEntsTable", "Field", "is_dxf_object", ] logger = logging.getLogger("ezdxf") class DXFObject(DXFEntity): """Non-graphical entities stored in the OBJECTS section.""" MIN_DXF_VERSION_FOR_EXPORT = DXF2000 @register_entity class Placeholder(DXFObject): DXFTYPE = "ACDBPLACEHOLDER" acdb_xrecord = DefSubclass( "AcDbXrecord", { # 0 = not applicable # 1 = keep existing # 2 = use clone # 3 = $0$ # 4 = $0$ # 5 = Unmangle name "cloning": DXFAttr( 280, default=1, validator=validator.is_in_integer_range(0, 6), fixer=RETURN_DEFAULT, ), }, ) def totags(tags: Iterable) -> Iterable[DXFTag]: for tag in tags: if isinstance(tag, DXFTag): yield tag else: yield dxftag(tag[0], tag[1]) @register_entity class XRecord(DXFObject): """DXF XRECORD entity""" DXFTYPE = "XRECORD" DXFATTRIBS = DXFAttributes(base_class, acdb_xrecord) def __init__(self): super().__init__() self.tags = Tags() def copy_data(self, entity: Self, copy_strategy=default_copy) -> None: assert isinstance(entity, XRecord) entity.tags = Tags(self.tags) def load_dxf_attribs( self, processor: Optional[SubclassProcessor] = None ) -> DXFNamespace: dxf = super().load_dxf_attribs(processor) if processor: try: tags = processor.subclasses[1] except IndexError: raise DXFStructureError( f"Missing subclass AcDbXrecord in XRecord (#{dxf.handle})" ) start_index = 1 if len(tags) > 1: # First tag is group code 280, but not for DXF R13/R14. # SUT: doc may be None, but then doc also can not # be R13/R14 - ezdxf does not create R13/R14 if self.doc is None or self.doc.dxfversion >= DXF2000: code, value = tags[1] if code == 280: dxf.cloning = value start_index = 2 else: # just log recoverable error logger.info( f"XRecord (#{dxf.handle}): expected group code 280 " f"as first tag in AcDbXrecord" ) self.tags = Tags(tags[start_index:]) return dxf def export_entity(self, tagwriter: AbstractTagWriter) -> None: super().export_entity(tagwriter) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_xrecord.name) tagwriter.write_tag2(280, self.dxf.cloning) tagwriter.write_tags(Tags(totags(self.tags))) def reset(self, tags: Iterable[Union[DXFTag, tuple[int, Any]]]) -> None: """Reset DXF tags.""" self.tags.clear() self.tags.extend(totags(tags)) def extend(self, tags: Iterable[Union[DXFTag, tuple[int, Any]]]) -> None: """Extend DXF tags.""" self.tags.extend(totags(tags)) def clear(self) -> None: """Remove all DXF tags.""" self.tags.clear() acdb_vba_project = DefSubclass( "AcDbVbaProject", { # 90: Number of bytes of binary chunk data (contained in the group code # 310 records that follow) # 310: DXF: Binary object data (multiple entries containing VBA project # data) }, ) @register_entity class VBAProject(DXFObject): """DXF VBA_PROJECT entity""" DXFTYPE = "VBA_PROJECT" DXFATTRIBS = DXFAttributes(base_class, acdb_vba_project) def __init__(self): super().__init__() self.data = b"" def copy_data(self, entity: Self, copy_strategy=default_copy) -> None: assert isinstance(entity, VBAProject) entity.data = entity.data def load_dxf_attribs( self, processor: Optional[SubclassProcessor] = None ) -> DXFNamespace: dxf = super().load_dxf_attribs(processor) if processor: self.load_byte_data(processor.subclasses[1]) return dxf def load_byte_data(self, tags: Tags) -> None: byte_array = array.array("B") # Translation from String to binary data happens in tag_compiler(): for byte_data in (tag.value for tag in tags if tag.code == 310): byte_array.extend(byte_data) self.data = byte_array.tobytes() def export_entity(self, tagwriter: AbstractTagWriter) -> None: super().export_entity(tagwriter) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_vba_project.name) tagwriter.write_tag2(90, len(self.data)) self.export_data(tagwriter) def export_data(self, tagwriter: AbstractTagWriter): data = self.data while data: tagwriter.write_tag(DXFBinaryTag(310, data[:127])) data = data[127:] def clear(self) -> None: self.data = b"" acdb_sort_ents_table = DefSubclass( "AcDbSortentsTable", { # Soft-pointer ID/handle to owner (currently only the *MODEL_SPACE or # *PAPER_SPACE blocks) in ezdxf the block_record handle for a layout is # also called layout_key: "block_record_handle": DXFAttr(330), # 331: Soft-pointer ID/handle to an entity (zero or more entries may exist) # 5: Sort handle (zero or more entries may exist) }, ) acdb_sort_ents_table_group_codes = group_code_mapping(acdb_sort_ents_table) @register_entity class SortEntsTable(DXFObject): """DXF SORTENTSTABLE entity - sort entities table""" # should work with AC1015/R2000 but causes problems with TrueView/AutoCAD # LT 2019: "expected was-a-zombie-flag" # No problems with AC1018/R2004 and later # # If the header variable $SORTENTS Regen flag (bit-code value 16) is set, # AutoCAD regenerates entities in ascending handle order. # # When the DRAWORDER command is used, a SORTENTSTABLE object is attached to # the *Model_Space or *Paper_Space block's extension dictionary under the # name ACAD_SORTENTS. The SORTENTSTABLE object related to this dictionary # associates a different handle with each entity, which redefines the order # in which the entities are regenerated. # # $SORTENTS (280): Controls the object sorting methods (bitcode): # 0 = Disables SORTENTS # 1 = Sorts for object selection # 2 = Sorts for object snap # 4 = Sorts for redraws; obsolete # 8 = Sorts for MSLIDE command slide creation; obsolete # 16 = Sorts for REGEN commands # 32 = Sorts for plotting # 64 = Sorts for PostScript output; obsolete DXFTYPE = "SORTENTSTABLE" DXFATTRIBS = DXFAttributes(base_class, acdb_sort_ents_table) def __init__(self) -> None: super().__init__() self.table: dict[str, str] = dict() def copy_data(self, entity: Self, copy_strategy=default_copy) -> None: assert isinstance(entity, SortEntsTable) entity.table = dict(entity.table) def load_dxf_attribs( self, processor: Optional[SubclassProcessor] = None ) -> DXFNamespace: dxf = super().load_dxf_attribs(processor) if processor: tags = processor.fast_load_dxfattribs( dxf, acdb_sort_ents_table_group_codes, 1, log=False ) self.load_table(tags) return dxf def load_table(self, tags: Tags) -> None: for handle, sort_handle in take2(tags): if handle.code != 331: raise DXFStructureError( f"Invalid handle code {handle.code}, expected 331" ) if sort_handle.code != 5: raise DXFStructureError( f"Invalid sort handle code {handle.code}, expected 5" ) self.table[handle.value] = sort_handle.value def export_entity(self, tagwriter: AbstractTagWriter) -> None: super().export_entity(tagwriter) tagwriter.write_tag2(SUBCLASS_MARKER, acdb_sort_ents_table.name) tagwriter.write_tag2(330, self.dxf.block_record_handle) self.export_table(tagwriter) def export_table(self, tagwriter: AbstractTagWriter): for handle, sort_handle in self.table.items(): tagwriter.write_tag2(331, handle) tagwriter.write_tag2(5, sort_handle) def __len__(self) -> int: return len(self.table) def __iter__(self) -> Iterable: """Yields all redraw associations as (object_handle, sort_handle) tuples. """ return iter(self.table.items()) def append(self, handle: str, sort_handle: str) -> None: """Append redraw association (handle, sort_handle). Args: handle: DXF entity handle (uppercase hex value without leading '0x') sort_handle: sort handle (uppercase hex value without leading '0x') """ self.table[handle] = sort_handle def clear(self): """Remove all handles from redraw order table.""" self.table = dict() def set_handles(self, handles: Iterable[tuple[str, str]]) -> None: """Set all redraw associations from iterable `handles`, after removing all existing associations. Args: handles: iterable yielding (object_handle, sort_handle) tuples """ # The sort_handle doesn't have to be unique, same or all handles can # share the same sort_handle and sort_handles can use existing handles # too. # # The '0' handle can be used, but this sort_handle will be drawn as # latest (on top of all other entities) and not as first as expected. # Invalid entity handles will be ignored by AutoCAD. self.table = dict(handles) def remove_invalid_handles(self) -> None: """Remove all handles which do not exist in the drawing database.""" if self.doc is None: return entitydb = self.doc.entitydb self.table = { handle: sort_handle for handle, sort_handle in self.table.items() if handle in entitydb } def remove_handle(self, handle: str) -> None: """Remove handle of DXF entity from redraw order table. Args: handle: DXF entity handle (uppercase hex value without leading '0x') """ try: del self.table[handle] except KeyError: pass acdb_field = DefSubclass( "AcDbField", { "evaluator_id": DXFAttr(1), "field_code": DXFAttr(2), # Overflow of field code string "field_code_overflow": DXFAttr(3), # Number of child fields "n_child_fields": DXFAttr(90), # 360: Child field ID (AcDbHardOwnershipId); repeats for number of children # 97: Number of object IDs used in the field code # 331: Object ID used in the field code (AcDbSoftPointerId); repeats for # the number of object IDs used in the field code # 93: Number of the data set in the field # 6: Key string for the field data; a key-field pair is repeated for the # number of data sets in the field # 7: Key string for the evaluated cache; this key is hard-coded # as ACFD_FIELD_VALUE # 90: Data type of field value # 91: Long value (if data type of field value is long) # 140: Double value (if data type of field value is double) # 330: ID value, AcDbSoftPointerId (if data type of field value is ID) # 92: Binary data buffer size (if data type of field value is binary) # 310: Binary data (if data type of field value is binary) # 301: Format string # 9: Overflow of Format string # 98: Length of format string }, ) # todo: implement FIELD # register when done class Field(DXFObject): """DXF FIELD entity""" DXFTYPE = "FIELD" DXFATTRIBS = DXFAttributes(base_class, acdb_field) def is_dxf_object(entity: DXFEntity) -> TypeGuard[DXFObject]: """Returns ``True`` if the `entity` is a DXF object from the OBJECTS section, otherwise the entity is a table or class entry or a graphic entity which can not reside in the OBJECTS section. """ if isinstance(entity, DXFObject): return True if isinstance(entity, DXFTagStorage) and not entity.is_graphic_entity: return True return False