1# Copyright (c) 2019-2020 Manfred Moitzi
2# License: MIT License
3from typing import TYPE_CHECKING, Iterable, Dict, Tuple
4import logging
5import array
6from ezdxf.lldxf import validator
7from ezdxf.lldxf.const import DXF2000, DXFStructureError, SUBCLASS_MARKER
8from ezdxf.lldxf.tags import Tags
9from ezdxf.lldxf.types import dxftag, DXFTag, DXFBinaryTag
10from ezdxf.lldxf.attributes import (
11    DXFAttr, DXFAttributes, DefSubclass, RETURN_DEFAULT, group_code_mapping
12)
13from ezdxf.tools import take2
14from .dxfentity import DXFEntity, base_class, SubclassProcessor
15from .factory import register_entity
16
17logger = logging.getLogger('ezdxf')
18if TYPE_CHECKING:
19    from ezdxf.eztypes import Auditor, DXFNamespace, TagWriter
20
21__all__ = [
22    'DXFObject', 'Placeholder', 'XRecord', 'VBAProject', 'SortEntsTable',
23    'Field'
24]
25
26
27class DXFObject(DXFEntity):
28    """ Non graphical entities stored in the OBJECTS section. """
29    MIN_DXF_VERSION_FOR_EXPORT = DXF2000
30
31    def audit(self, auditor: 'Auditor') -> None:
32        """ Validity check. (internal API) """
33        super().audit(auditor)
34        auditor.check_owner_exist(self)
35
36
37@register_entity
38class Placeholder(DXFObject):
39    DXFTYPE = 'ACDBPLACEHOLDER'
40
41
42acdb_xrecord = DefSubclass('AcDbXrecord', {
43    # 0 = not applicable
44    # 1 = keep existing
45    # 2 = use clone
46    # 3 = <xref>$0$<name>
47    # 4 = $0$<name>
48    # 5 = Unmangle name
49    'cloning': DXFAttr(
50        280, default=1,
51        validator=validator.is_in_integer_range(0, 6),
52        fixer=RETURN_DEFAULT,
53    ),
54})
55
56
57def totags(tags: Iterable) -> Iterable[DXFTag]:
58    for tag in tags:
59        if isinstance(tag, DXFTag):
60            yield tag
61        else:
62            yield dxftag(tag[0], tag[1])
63
64
65@register_entity
66class XRecord(DXFObject):
67    """ DXF XRECORD entity """
68    DXFTYPE = 'XRECORD'
69    DXFATTRIBS = DXFAttributes(base_class, acdb_xrecord)
70
71    def __init__(self):
72        super().__init__()
73        self.tags = Tags()
74
75    def _copy_data(self, entity: 'XRecord') -> None:
76        entity.tags = Tags(entity.tags)
77
78    def load_dxf_attribs(
79            self, processor: SubclassProcessor = None) -> 'DXFNamespace':
80        dxf = super().load_dxf_attribs(processor)
81        if processor:
82            try:
83                tags = processor.subclasses[1]
84            except IndexError:
85                raise DXFStructureError(
86                    f'Missing subclass AcDbXrecord in XRecord (#{dxf.handle})')
87            start_index = 1
88            if len(tags) > 1:
89                # First tag is group code 280, but not for DXF R13/R14.
90                # SUT: doc may be None, but then doc also can not
91                # be R13/R14 - ezdxf does not create R13/R14
92                if self.doc is None or self.doc.dxfversion >= DXF2000:
93                    code, value = tags[1]
94                    if code == 280:
95                        dxf.cloning = value
96                        start_index = 2
97                    else:  # just log recoverable error
98                        logger.info(
99                            f'XRecord (#{dxf.handle}): expected group code 280 '
100                            f'as first tag in AcDbXrecord'
101                        )
102            self.tags = Tags(tags[start_index:])
103        return dxf
104
105    def export_entity(self, tagwriter: 'TagWriter') -> None:
106        super().export_entity(tagwriter)
107        tagwriter.write_tag2(SUBCLASS_MARKER, acdb_xrecord.name)
108        tagwriter.write_tag2(280, self.dxf.cloning)
109        tagwriter.write_tags(Tags(totags(self.tags)))
110
111
112acdb_vba_project = DefSubclass('AcDbVbaProject', {
113    # 90: Number of bytes of binary chunk data (contained in the group code
114    #   310 records that follow)
115    # 310: DXF: Binary object data (multiple entries containing VBA project
116    #   data)
117})
118
119
120@register_entity
121class VBAProject(DXFObject):
122    """ DXF VBA_PROJECT entity """
123    DXFTYPE = 'VBA_PROJECT'
124    DXFATTRIBS = DXFAttributes(base_class, acdb_vba_project)
125
126    def __init__(self):
127        super().__init__()
128        self.data = b''
129
130    def _copy_data(self, entity: 'VBAProject') -> None:
131        entity.tags = Tags(entity.tags)
132
133    def load_dxf_attribs(
134            self, processor: SubclassProcessor = None) -> 'DXFNamespace':
135        dxf = super().load_dxf_attribs(processor)
136        if processor:
137            self.load_byte_data(processor.subclasses[1])
138        return dxf
139
140    def load_byte_data(self, tags: 'Tags') -> None:
141        byte_array = array.array('B')
142        # Translation from String to binary data happens in tag_compiler():
143        for byte_data in (tag.value for tag in tags if tag.code == 310):
144            byte_array.extend(byte_data)
145        self.data = byte_array.tobytes()
146
147    def export_entity(self, tagwriter: 'TagWriter') -> None:
148        super().export_entity(tagwriter)
149        tagwriter.write_tag2(SUBCLASS_MARKER, acdb_vba_project.name)
150        tagwriter.write_tag2(90, len(self.data))
151        self.export_data(tagwriter)
152
153    def export_data(self, tagwriter: 'TagWriter'):
154        data = self.data
155        while data:
156            tagwriter.write_tag(DXFBinaryTag(310, data[:127]))
157            data = data[127:]
158
159    def clear(self) -> None:
160        self.data = b''
161
162
163acdb_sort_ents_table = DefSubclass('AcDbSortentsTable', {
164    # Soft-pointer ID/handle to owner (currently only the *MODEL_SPACE or
165    # *PAPER_SPACE blocks) in ezdxf the block_record handle for a layout is
166    # also called layout_key:
167    'block_record_handle': DXFAttr(330),
168    # 331: Soft-pointer ID/handle to an entity (zero or more entries may exist)
169    #   5: Sort handle (zero or more entries may exist)
170})
171acdb_sort_ents_table_group_codes = group_code_mapping(acdb_sort_ents_table)
172
173
174@register_entity
175class SortEntsTable(DXFObject):
176    """ DXF SORTENTSTABLE entity - sort entities table """
177    # should work with AC1015/R2000 but causes problems with TrueView/AutoCAD
178    # LT 2019: "expected was-a-zombie-flag"
179    # No problems with AC1018/R2004 and later
180    #
181    # If the header variable $SORTENTS Regen flag (bit-code value 16) is set,
182    # AutoCAD regenerates entities in ascending handle order.
183    #
184    # When the DRAWORDER command is used, a SORTENTSTABLE object is attached to
185    # the *Model_Space or *Paper_Space block's extension dictionary under the
186    # name ACAD_SORTENTS. The SORTENTSTABLE object related to this dictionary
187    # associates a different handle with each entity, which redefines the order
188    # in which the entities are regenerated.
189    #
190    # $SORTENTS (280): Controls the object sorting methods (bitcode):
191    # 0 = Disables SORTENTS
192    # 1 = Sorts for object selection
193    # 2 = Sorts for object snap
194    # 4 = Sorts for redraws; obsolete
195    # 8 = Sorts for MSLIDE command slide creation; obsolete
196    # 16 = Sorts for REGEN commands
197    # 32 = Sorts for plotting
198    # 64 = Sorts for PostScript output; obsolete
199
200    DXFTYPE = 'SORTENTSTABLE'
201    DXFATTRIBS = DXFAttributes(base_class, acdb_sort_ents_table)
202
203    def __init__(self):
204        super().__init__()
205        self.table: Dict[str, str] = dict()
206
207    def _copy_data(self, entity: 'SortEntsTable') -> None:
208        entity.tags = dict(entity.table)
209
210    def load_dxf_attribs(
211            self, processor: SubclassProcessor = None) -> 'DXFNamespace':
212        dxf = super().load_dxf_attribs(processor)
213        if processor:
214            tags = processor.fast_load_dxfattribs(
215                dxf, acdb_sort_ents_table_group_codes, 1, log=False)
216            self.load_table(tags)
217        return dxf
218
219    def load_table(self, tags: 'Tags') -> None:
220        for handle, sort_handle in take2(tags):
221            if handle.code != 331:
222                raise DXFStructureError(
223                    f'Invalid handle code {handle.code}, expected 331')
224            if sort_handle.code != 5:
225                raise DXFStructureError(
226                    f'Invalid sort handle code {handle.code}, expected 5')
227            self.table[handle.value] = sort_handle.value
228
229    def export_entity(self, tagwriter: 'TagWriter') -> None:
230        super().export_entity(tagwriter)
231        tagwriter.write_tag2(SUBCLASS_MARKER, acdb_sort_ents_table.name)
232        tagwriter.write_tag2(330, self.dxf.block_record_handle)
233        self.export_table(tagwriter)
234
235    def export_table(self, tagwriter: 'TagWriter'):
236        for handle, sort_handle in self.table.items():
237            tagwriter.write_tag2(331, handle)
238            tagwriter.write_tag2(5, sort_handle)
239
240    def __len__(self) -> int:
241        return len(self.table)
242
243    def __iter__(self) -> Iterable:
244        """ Yields all redraw associations as (object_handle, sort_handle)
245        tuples.
246
247        """
248        return iter(self.table.items())
249
250    def append(self, handle: str, sort_handle: str) -> None:
251        """ Append redraw association (handle, sort_handle).
252
253        Args:
254            handle: DXF entity handle (uppercase hex value without leading '0x')
255            sort_handle: sort handle (uppercase hex value without leading '0x')
256
257        """
258        self.table[handle] = sort_handle
259
260    def clear(self):
261        """ Remove all handles from redraw order table. """
262        self.table = dict()
263
264    def set_handles(self, handles: Iterable[Tuple[str, str]]) -> None:
265        """ Set all redraw associations from iterable `handles`, after removing
266        all existing associations.
267
268        Args:
269            handles: iterable yielding (object_handle, sort_handle) tuples
270
271        """
272        # The sort_handle doesn't have to be unique, same or all handles can
273        # share the same sort_handle and sort_handles can use existing handles
274        # too.
275        #
276        # The '0' handle can be used, but this sort_handle will be drawn as
277        # latest (on top of all other entities) and not as first as expected.
278        # Invalid entity handles will be ignored by AutoCAD.
279        self.table = dict(handles)
280
281    def remove_invalid_handles(self) -> None:
282        """ Remove all handles which do not exists in the drawing database. """
283        entitydb = self.doc.entitydb
284        self.table = {
285            handle: sort_handle for handle, sort_handle in self.table.items()
286            if handle in entitydb
287        }
288
289    def remove_handle(self, handle: str) -> None:
290        """ Remove handle of DXF entity from redraw order table.
291
292        Args:
293            handle: DXF entity handle (uppercase hex value without leading '0x')
294
295        """
296        try:
297            del self.table[handle]
298        except KeyError:
299            pass
300
301
302acdb_field = DefSubclass('AcDbField', {
303    'evaluator_id': DXFAttr(1),
304    'field_code': DXFAttr(2),
305
306    # Overflow of field code string
307    'field_code_overflow': DXFAttr(3),
308
309    # Number of child fields
310    'n_child_fields': DXFAttr(90),
311
312    # 360:  Child field ID (AcDbHardOwnershipId); repeats for number of children
313    #  97:  Number of object IDs used in the field code
314    # 331:  Object ID used in the field code (AcDbSoftPointerId); repeats for
315    #       the number of object IDs used in the field code
316    #  93:  Number of the data set in the field
317    #   6:  Key string for the field data; a key-field pair is repeated for the
318    #       number of data sets in the field
319    #   7:  Key string for the evaluated cache; this key is hard-coded
320    #       as ACFD_FIELD_VALUE
321    #  90:  Data type of field value
322    #  91:  Long value (if data type of field value is long)
323    # 140:  Double value (if data type of field value is double)
324    # 330:  ID value, AcDbSoftPointerId (if data type of field value is ID)
325    #  92:  Binary data buffer size (if data type of field value is binary)
326    # 310:  Binary data (if data type of field value is binary)
327    # 301:  Format string
328    #   9:  Overflow of Format string
329    #  98:  Length of format string
330
331})
332
333
334# todo: implement FIELD
335# register when done
336class Field(DXFObject):
337    """ DXF FIELD entity """
338    DXFTYPE = 'FIELD'
339    DXFATTRIBS = DXFAttributes(base_class, acdb_field)
340