1# Created: 13.03.2011
2# Copyright (c) 2011-2019, Manfred Moitzi
3# License: MIT License
4from typing import TYPE_CHECKING, Iterable, Tuple, cast, Iterator, Union
5import logging
6
7from ezdxf.entities.dictionary import Dictionary
8from ezdxf.entities import factory
9from ezdxf.lldxf.const import DXFStructureError, DXFValueError, RASTER_UNITS, DXFKeyError
10from ezdxf.entitydb import EntitySpace
11from ezdxf.query import EntityQuery
12from ezdxf.tools.handle import UnderlayKeyGenerator
13
14if TYPE_CHECKING:
15    from ezdxf.eztypes import GeoData, DictionaryVar
16    from ezdxf.eztypes import Drawing, TagWriter, EntityDB, DXFTagStorage, DXFObject
17    from ezdxf.eztypes import ImageDefReactor, ImageDef, UnderlayDef, DictionaryWithDefault, XRecord, Placeholder
18
19logger = logging.getLogger('ezdxf')
20
21
22class ObjectsSection:
23    def __init__(self, doc: 'Drawing', entities: Iterable['DXFObject'] = None):
24        self.doc = doc
25        self.underlay_key_generator = UnderlayKeyGenerator()
26        self._entity_space = EntitySpace()
27        if entities is not None:
28            self._build(iter(entities))
29
30    @property
31    def entitydb(self) -> 'EntityDB':
32        """ Returns drawing entity database. (internal API)"""
33        return self.doc.entitydb
34
35    def get_entity_space(self) -> 'EntitySpace':
36        """ Returns entity space. (internal API) """
37        return self._entity_space
38
39    def next_underlay_key(self, checkfunc=lambda k: True) -> str:
40        while True:
41            key = self.underlay_key_generator.next()
42            if checkfunc(key):
43                return key
44
45    def _build(self, entities: Iterator['DXFObject']) -> None:
46        section_head = next(entities)  # type: DXFTagStorage
47
48        if section_head.dxftype() != 'SECTION' or section_head.base_class[1] != (2, 'OBJECTS'):
49            raise DXFStructureError("Critical structure error in 'OBJECTS' section.")
50
51        for entity in entities:
52            self._entity_space.add(entity)
53
54    def export_dxf(self, tagwriter: 'TagWriter') -> None:
55        """ Export DXF entity by `tagwriter`. (internal API) """
56        tagwriter.write_str("  0\nSECTION\n  2\nOBJECTS\n")
57        self._entity_space.export_dxf(tagwriter)
58        tagwriter.write_tag2(0, "ENDSEC")
59
60    def new_entity(self, _type: str, dxfattribs: dict) -> 'DXFObject':
61        """ Create new DXF object, add it to the entity database and to the entity space.
62
63        Args:
64             _type: DXF type like `DICTIONARY`
65             dxfattribs: DXF attributes as dict
66
67        (internal API)
68        """
69        dxf_entity = factory.create_db_entry(_type, dxfattribs, self.doc)
70        self._entity_space.add(dxf_entity)
71        return dxf_entity
72
73    def delete_entity(self, entity: 'DXFObject') -> None:
74        """ Remove `entity` from entity space and destroy object. (internal API) """
75        self._entity_space.remove(entity)
76        self.entitydb.delete_entity(entity)
77
78    def delete_all_entities(self) -> None:
79        """ Delete all DXF objects. (internal API) """
80        db = self.entitydb
81        for entity in self._entity_space:
82            db.delete_entity(entity)
83        self._entity_space.clear()
84
85    def setup_rootdict(self) -> Dictionary:
86        """ Create a root dictionary. Has to be the first object in the objects section. (internal API) """
87        if len(self):
88            raise DXFStructureError("Can not create root dictionary in none empty objects section.")
89        logger.debug('Creating ROOT dictionary.')
90        # root directory has no owner
91        return self.add_dictionary(owner='0')
92
93    def setup_objects_management_tables(self, rootdict: Dictionary) -> None:
94        """ Setup required management tables. (internal API)"""
95
96        def setup_plot_style_name_table():
97            plot_style_name_dict = self.add_dictionary_with_default(owner=rootdict.dxf.handle)
98            placeholder = self.add_placeholder(owner=plot_style_name_dict.dxf.handle)
99            plot_style_name_dict.set_default(placeholder)
100            plot_style_name_dict['Normal'] = placeholder
101            rootdict['ACAD_PLOTSTYLENAME'] = plot_style_name_dict
102
103        for name in _OBJECT_TABLE_NAMES:
104            if name in rootdict:
105                continue  # just create not existing tables
106            logger.info('creating {} dictionary'.format(name))
107            if name == "ACAD_PLOTSTYLENAME":
108                setup_plot_style_name_table()
109            else:
110                rootdict.add_new_dict(name)
111
112    def add_object(self, entity: 'DXFObject') -> None:
113        """ Add `entity` to OBJECTS section. (internal API) """
114        self._entity_space.add(entity)
115
116    def add_dxf_object_with_reactor(self, dxftype: str, dxfattribs: dict) -> 'DXFObject':
117        """ Add DXF object with reactor. (internal API) """
118        dxfobject = self.new_entity(dxftype, dxfattribs)
119        dxfobject.set_reactors([dxfattribs['owner']])
120        return dxfobject
121
122    def purge(self):
123        self._entity_space.purge()
124
125    # start of public interface
126
127    @property
128    def rootdict(self) -> Dictionary:
129        """ Root dictionary. """
130        if len(self):
131            return self._entity_space[0]
132        else:
133            return self.setup_rootdict()
134
135    def __len__(self) -> int:
136        """ Returns count of DXF objects. """
137        return len(self._entity_space)
138
139    def __iter__(self) -> Iterable['DXFObject']:
140        """ Returns iterable of all DXF objects in the OBJECTS section. """
141        return iter(self._entity_space)
142
143    def __getitem__(self, index) -> 'DXFObject':
144        """ Get entity at `index`.
145
146        The underlying data structure for storing DXF objects is organized like a standard Python list, therefore `index`
147        can be any valid list indexing or slicing term, like a single index ``objects[-1]`` to get the last entity, or
148        an index slice ``objects[:10]`` to get the first 10 or less objects as ``List[DXFObject]``.
149
150        """
151        return self._entity_space[index]  # type: DXFObject
152
153    def __contains__(self, entity: Union['DXFObject', str]) -> bool:
154        """ Returns ``True`` if `entity` stored in OBJECTS section.
155
156        Args:
157             entity: :class:`DXFObject` or handle as hex string
158
159        """
160        if isinstance(entity, str):
161            try:
162                entity = self.entitydb[entity]
163            except KeyError:
164                return False
165        return entity in self._entity_space
166
167    def query(self, query: str = '*') -> EntityQuery:
168        """
169        Get all DXF objects matching the :ref:`entity query string`.
170
171        """
172        return EntityQuery(iter(self), query)
173
174    def add_dictionary(self, owner: str = '0', hard_owned: bool = False) -> Dictionary:
175        """ Add new :class:`~ezdxf.entities.Dictionary` object.
176
177        Args:
178            owner: handle to owner as hex string.
179            hard_owned: ``True`` to treat entries as hard owned.
180
181        """
182        entity = self.new_entity('DICTIONARY', dxfattribs={
183            'owner': owner,
184            'hard_owned': hard_owned,
185        })
186        return cast(Dictionary, entity)
187
188    def add_dictionary_with_default(self, owner='0', default='0', hard_owned: bool = False) -> 'DictionaryWithDefault':
189        """ Add new :class:`~ezdxf.entities.DictionaryWithDefault` object.
190
191        Args:
192            owner: handle to owner as hex string.
193            default: handle to default entry.
194            hard_owned: ``True`` to treat entries as hard owned.
195
196        """
197        entity = self.new_entity('ACDBDICTIONARYWDFLT', dxfattribs={
198            'owner': owner,
199            'default': default,
200            'hard_owned': hard_owned,
201        })
202        return cast('DictionaryWithDefault', entity)
203
204    def add_dictionary_var(self, owner: str = '0', value: str = '') -> 'DictionaryVar':
205        """
206        Add a new :class:`~ezdxf.entities.DictionaryVar` object.
207
208        Args:
209            owner: handle to owner as hex string.
210            value: value as string
211
212        """
213        return self.new_entity('DICTIONARYVAR', dxfattribs={'owner': owner, 'value': value})
214
215    def add_xrecord(self, owner: str = '0') -> 'XRecord':
216        """
217        Add a new :class:`~ezdxf.entities.XRecord` object.
218
219        Args:
220            owner: handle to owner as hex string.
221
222        """
223        return self.new_entity('XRECORD', dxfattribs={'owner': owner})
224
225    def add_placeholder(self, owner: str = '0') -> 'Placeholder':
226        """
227        Add a new :class:`~ezdxf.entities.Placeholder` object.
228
229        Args:
230            owner: handle to owner as hex string.
231
232        """
233        return self.new_entity('ACDBPLACEHOLDER', dxfattribs={'owner': owner})
234
235    # end of public interface
236
237    def set_raster_variables(self, frame: int = 0, quality: int = 1, units: str = 'm') -> None:
238        """
239        Set raster variables.
240
241        Args:
242            frame: ``0`` = do not show image frame; ``1`` = show image frame
243            quality: ``0`` = draft; ``1`` = high
244            units: units for inserting images. This defines the real world unit for one drawing unit for the purpose of
245                   inserting and scaling images with an associated resolution.
246
247                   ===== ===========================
248                   mm    Millimeter
249                   cm    Centimeter
250                   m     Meter (ezdxf default)
251                   km    Kilometer
252                   in    Inch
253                   ft    Foot
254                   yd    Yard
255                   mi    Mile
256                   ===== ===========================
257
258        (internal API), public interface :meth:`~ezdxf.drawing.Drawing.set_raster_variables`
259
260        """
261        units = RASTER_UNITS.get(units, 0)
262        try:
263            raster_vars = self.rootdict['ACAD_IMAGE_VARS']
264        except DXFKeyError:
265            raster_vars = self.add_dxf_object_with_reactor('RASTERVARIABLES', dxfattribs={
266                'owner': self.rootdict.dxf.handle,
267                'frame': frame,
268                'quality': quality,
269                'units': units,
270            })
271            self.rootdict['ACAD_IMAGE_VARS'] = raster_vars
272        else:
273            raster_vars.dxf.frame = frame
274            raster_vars.dxf.quality = quality
275            raster_vars.dxf.units = units
276
277    def set_wipeout_variables(self, frame: int = 0) -> None:
278        """
279        Set wipeout variables.
280
281        Args:
282            frame: ``0`` = do not show image frame; ``1`` = show image frame
283
284        (internal API), public interface :meth:`~ezdxf.drawing.Drawing.set_wipeout_variables`
285
286        """
287        try:
288            wipeout_vars = self.rootdict['ACAD_WIPEOUT_VARS']
289        except DXFKeyError:
290            wipeout_vars = self.add_dxf_object_with_reactor('WIPEOUTVARIABLES', dxfattribs={
291                'owner': self.rootdict.dxf.handle,
292                'frame': int(frame),
293            })
294            self.rootdict['ACAD_WIPEOUT_VARS'] = wipeout_vars
295        else:
296            wipeout_vars.dxf.frame = int(frame)
297
298    def add_image_def(self, filename: str, size_in_pixel: Tuple[int, int], name=None) -> 'ImageDef':
299        """
300        Add an image definition to the objects section.
301
302        Add an :class:`~ezdxf.entities.image.ImageDef` entity to the drawing (objects section). `filename` is the image
303        file name as relative or absolute path and `size_in_pixel` is the image size in pixel as (x, y) tuple. To avoid
304        dependencies to external packages, `ezdxf` can not determine the image size by itself. Returns a
305        :class:`~ezdxf.entities.image.ImageDef` entity which is needed to create an image reference. `name` is the
306        internal image name, if set to ``None``, name is auto-generated.
307
308        Absolute image paths works best for AutoCAD but not really good, you have to update external references manually
309        in AutoCAD, which is not possible in TrueView. If the drawing units differ from 1 meter, you also have to use:
310        :meth:`set_raster_variables`.
311
312        Args:
313            filename: image file name (absolute path works best for AutoCAD)
314            size_in_pixel: image size in pixel as (x, y) tuple
315            name: image name for internal use, None for using filename as name (best for AutoCAD)
316
317        """
318        # removed auto-generated name
319        # use absolute image paths for filename and AutoCAD loads images automatically
320        if name is None:
321            name = filename
322        image_dict = self.rootdict.get_required_dict('ACAD_IMAGE_DICT')
323        image_def = self.add_dxf_object_with_reactor('IMAGEDEF', dxfattribs={
324            'owner': image_dict.dxf.handle,
325            'filename': filename,
326            'image_size': size_in_pixel,
327        })
328        image_dict[name] = image_def.dxf.handle
329        return cast('ImageDef', image_def)
330
331    def add_image_def_reactor(self, image_handle: str) -> 'ImageDefReactor':
332        """ Add required IMAGEDEF_REACTOR object for IMAGEDEF object. (internal API) """
333        image_def_reactor = self.new_entity('IMAGEDEF_REACTOR', dxfattribs={
334            'owner': image_handle,
335            'image_handle': image_handle,
336        })
337        return cast('ImageDefReactor', image_def_reactor)
338
339    def add_underlay_def(self, filename: str, format: str = 'pdf', name: str = None) -> 'UnderlayDef':
340        """
341        Add an :class:`~ezdxf.entities.underlay.UnderlayDef` entity to the drawing (OBJECTS section).
342        `filename` is the underlay file name as relative or absolute path and `format` as string (pdf, dwf, dgn).
343        The underlay definition is required to create an underlay reference.
344
345        Args:
346            filename: underlay file name
347            format: file format as string ``'pdf'|'dwf'|'dgn'`` or ``'ext'`` for getting file format from filename extension
348            name: pdf format = page number to display; dgn format = ``'default'``; dwf: ????
349
350        """
351        fmt = format.upper()
352        if fmt in ('PDF', 'DWF', 'DGN'):
353            underlay_dict_name = 'ACAD_{}DEFINITIONS'.format(fmt)
354            underlay_def_entity = "{}DEFINITION".format(fmt)
355        else:
356            raise DXFValueError("Unsupported file format: '{}'".format(fmt))
357
358        if name is None:
359            if fmt == 'PDF':
360                name = '1'  # Display first page by default
361            elif fmt == 'DGN':
362                name = 'default'
363            else:
364                name = 'Model'  # Display model space for DWF ???
365
366        underlay_dict = self.rootdict.get_required_dict(underlay_dict_name)
367        underlay_def = self.new_entity(underlay_def_entity, dxfattribs={
368            'owner': underlay_dict.dxf.handle,
369            'filename': filename,
370            'name': name,
371        })
372
373        # auto-generated underlay key
374        key = self.next_underlay_key(lambda k: k not in underlay_dict)
375        underlay_dict[key] = underlay_def.dxf.handle
376        return cast('UnderlayDef', underlay_def)
377
378    def add_geodata(self, owner: str = '0', dxfattribs: dict = None) -> 'GeoData':
379        """
380        Creates a new :class:`GeoData` entity and replaces existing ones. The GEODATA entity resides in the OBJECTS section
381        and NOT in the layout entity space and it is linked to the layout by an extension dictionary located in BLOCK_RECORD
382        of the layout.
383
384        The GEODATA entity requires DXF version R2010+. The DXF Reference does not document if other layouts than model
385        space supports geo referencing, so getting/setting geo data may only make sense for the model space layout, but
386        it is also available in paper space layouts.
387
388        Args:
389            owner: handle to owner as hex string
390            dxfattribs: DXF attributes for :class:`~ezdxf.entities.GeoData` entity
391
392
393        """
394        if dxfattribs is None:
395            dxfattribs = {}
396        dxfattribs['owner'] = owner
397        return cast('GeoData', self.add_dxf_object_with_reactor('GEODATA', dxfattribs))
398
399
400_OBJECT_TABLE_NAMES = [
401    "ACAD_COLOR",
402    "ACAD_GROUP",
403    "ACAD_LAYOUT",
404    "ACAD_MATERIAL",
405    "ACAD_MLEADERSTYLE",
406    "ACAD_MLINESTYLE",
407    "ACAD_PLOTSETTINGS",
408    "ACAD_PLOTSTYLENAME",
409    "ACAD_SCALELIST",
410    "ACAD_TABLESTYLE",
411    "ACAD_VISUALSTYLE",
412]
413