1# Copyright (c) 2011-2020, Manfred Moitzi
2# License: MIT License
3from typing import TYPE_CHECKING, Iterable, Union, Sequence, List, cast
4from ezdxf.lldxf.const import (
5    DXFStructureError, DXFBlockInUseError, DXFTableEntryError, DXFKeyError,
6)
7from ezdxf.lldxf import const
8from ezdxf.entities import factory, entity_linker
9from ezdxf.layouts.blocklayout import BlockLayout
10from ezdxf.render.arrows import ARROWS
11from .table import table_key
12import warnings
13import logging
14
15logger = logging.getLogger('ezdxf')
16
17if TYPE_CHECKING:
18    from ezdxf.eztypes import (
19        TagWriter, Drawing, EntityDB, DXFEntity, DXFTagStorage, Table,
20        BlockRecord,
21    )
22
23
24def is_special_block(name: str) -> bool:
25    name = name.upper()
26    # Anonymous dimension, groups and table blocks do not have explicit
27    # references by an INSERT entity:
28    if is_anonymous_block(name):
29        return True
30
31    # Arrow blocks maybe used in DIMENSION or LEADER override without an
32    # INSERT reference:
33    if ARROWS.is_ezdxf_arrow(name):
34        return True
35    if name.startswith('_'):
36        if ARROWS.is_acad_arrow(ARROWS.arrow_name(name)):
37            return True
38
39    return False
40
41
42def is_anonymous_block(name: str) -> bool:
43    # *U### = anonymous BLOCK, require an explicit INSERT to be in use
44    # *E### = anonymous non-uniformly scaled BLOCK, requires INSERT?
45    # *X### = anonymous HATCH graphic, requires INSERT?
46    # *D### = anonymous DIMENSION graphic, has no explicit INSERT
47    # *A### = anonymous GROUP, requires INSERT?
48    # *T### = anonymous block for ACAD_TABLE, has no explicit INSERT
49    return len(name) > 1 and name[0] == '*' and name[1] in 'UEXDAT'
50
51
52class BlocksSection:
53    """
54    Manages BLOCK definitions in a dict(), block names are case insensitive
55    e.g. 'Test' == 'TEST'.
56
57    """
58
59    def __init__(self, doc: 'Drawing' = None,
60                 entities: List['DXFEntity'] = None):
61        self.doc = doc
62        if entities is not None:
63            self.load(entities)
64        self._reconstruct_orphaned_block_records()
65        self._anonymous_block_counter = 0
66
67    def __len__(self):
68        return len(self.block_records)
69
70    @staticmethod
71    def key(entity: Union[str, 'BlockLayout']) -> str:
72        if not isinstance(entity, str):
73            entity = entity.name
74        return entity.lower()  # block key is lower case
75
76    @property
77    def block_records(self) -> 'Table':
78        return self.doc.block_records
79
80    @property
81    def entitydb(self) -> 'EntityDB':
82        return self.doc.entitydb
83
84    def load(self, entities: List['DXFEntity']) -> None:
85        """
86        Load DXF entities into BlockLayouts. `entities` is a list of
87        entity tags, separated by BLOCK and ENDBLK entities.
88
89        """
90
91        def load_block_record(
92                block_entities: Sequence['DXFEntity']) -> 'BlockRecord':
93            block = cast('Block', block_entities[0])
94            endblk = cast('EndBlk', block_entities[-1])
95
96            try:
97                block_record = cast(
98                    'BlockRecord',
99                    block_records.get(block.dxf.name)
100                )
101            # Special case DXF R12 - has no BLOCK_RECORD table
102            except DXFTableEntryError:
103                block_record = cast(
104                    'BlockRecord',
105                    block_records.new(block.dxf.name, dxfattribs={'scale': 0})
106                )
107
108            # The BLOCK_RECORD is the central object which stores all the
109            # information about a BLOCK and also owns all the entities of
110            # this block definition.
111            block_record.set_block(block, endblk)
112            for entity in block_entities[1:-1]:
113                block_record.add_entity(entity)
114            return block_record
115
116        def link_entities() -> Iterable['DXFEntity']:
117            linked = entity_linker()
118            for entity in entities:
119                # Do not store linked entities (VERTEX, ATTRIB, SEQEND) in
120                # the block layout, linked entities ares stored in their
121                # parent entity e.g. VERTEX -> POLYLINE:
122                if not linked(entity):
123                    yield entity
124
125        block_records = self.block_records
126        section_head: 'DXFTagStorage' = cast('DXFTagStorage', entities[0])
127        if section_head.dxftype() != 'SECTION' or \
128                section_head.base_class[1] != (2, 'BLOCKS'):
129            raise DXFStructureError(
130                "Critical structure error in BLOCKS section."
131            )
132        # Remove SECTION entity
133        del entities[0]
134        block_entities = []
135        for entity in link_entities():
136            block_entities.append(entity)
137            if entity.dxftype() == 'ENDBLK':
138                block_record = load_block_record(block_entities)
139                self.add(block_record)
140                block_entities = []
141
142    def _reconstruct_orphaned_block_records(self):
143        """ Find BLOCK_RECORD entries without block definition in the blocks
144        section and create block definitions for this orphaned block records.
145
146        """
147        for block_record in self.block_records:  # type: BlockRecord
148            if block_record.block is None:
149                block = factory.create_db_entry(
150                    'BLOCK',
151                    dxfattribs={
152                        'name': block_record.dxf.name,
153                        'base_point': (0, 0, 0),
154                    },
155                    doc=self.doc,
156                )
157                endblk = factory.create_db_entry(
158                    'ENDBLK',
159                    dxfattribs={},
160                    doc=self.doc,
161                )
162                block_record.set_block(block, endblk)
163                self.add(block_record)
164
165    def export_dxf(self, tagwriter: 'TagWriter') -> None:
166        tagwriter.write_str("  0\nSECTION\n  2\nBLOCKS\n")
167        for block_record in self.block_records:  # type: BlockRecord
168            block_record.export_block_definition(tagwriter)
169        tagwriter.write_tag2(0, "ENDSEC")
170
171    def add(self, block_record: 'BlockRecord') -> 'BlockLayout':
172        """ Add or replace a block layout object defined by its block record.
173        (internal API)
174        """
175        block_layout = BlockLayout(block_record)
176        block_record.block_layout = block_layout
177        assert self.block_records.has_entry(block_record.dxf.name)
178        return block_layout
179
180    def __iter__(self) -> Iterable['BlockLayout']:
181        """ Iterable of all :class:`~ezdxf.layouts.BlockLayout` objects. """
182        return (block_record.block_layout for block_record in
183                self.block_records)
184
185    def __contains__(self, name: str) -> bool:
186        """ Returns ``True`` if :class:`~ezdxf.layouts.BlockLayout` `name`
187        exist.
188        """
189        return self.block_records.has_entry(name)
190
191    def __getitem__(self, name: str) -> 'BlockLayout':
192        """ Returns :class:`~ezdxf.layouts.BlockLayout` `name`,
193        raises :class:`DXFKeyError` if `name` not exist.
194        """
195        try:
196            block_record = cast('BlockRecord', self.block_records.get(name))
197            return block_record.block_layout
198        except DXFTableEntryError:
199            raise DXFKeyError(name)
200
201    def __delitem__(self, name: str) -> None:
202        """ Deletes :class:`~ezdxf.layouts.BlockLayout` `name` and all of
203        its content, raises :class:`DXFKeyError` if `name` not exist.
204        """
205        if name in self:
206            self.block_records.remove(name)
207        else:
208            raise DXFKeyError(name)
209
210    def get(self, name: str, default=None) -> 'BlockLayout':
211        """ Returns :class:`~ezdxf.layouts.BlockLayout` `name`, returns
212        `default` if `name` not exist.
213        """
214        try:
215            return self.__getitem__(name)
216        except DXFKeyError:
217            return default
218
219    def get_block_layout_by_handle(self,
220                                   block_record_handle: str) -> 'BlockLayout':
221        """ Returns a block layout by block record handle. (internal API)
222        """
223        return self.doc.entitydb[block_record_handle].block_layout
224
225    def new(self, name: str, base_point: Sequence[float] = (0, 0),
226            dxfattribs: dict = None) -> 'BlockLayout':
227        """ Create and add a new :class:`~ezdxf.layouts.BlockLayout`, `name`
228        is the BLOCK name, `base_point` is the insertion point of the BLOCK.
229        """
230        block_record = self.doc.block_records.new(name)
231
232        dxfattribs = dxfattribs or {}
233        dxfattribs['owner'] = block_record.dxf.handle
234        dxfattribs['name'] = name
235        dxfattribs['base_point'] = base_point
236        head = factory.create_db_entry('BLOCK', dxfattribs, self.doc)
237        tail = factory.create_db_entry('ENDBLK', {
238            'owner': block_record.dxf.handle}, doc=self.doc)
239        block_record.set_block(head, tail)
240        return self.add(block_record)
241
242    def new_anonymous_block(self, type_char: str = 'U',
243                            base_point: Sequence[float] = (
244                                    0, 0)) -> 'BlockLayout':
245        """ Create and add a new anonymous :class:`~ezdxf.layouts.BlockLayout`,
246        `type_char` is the BLOCK type, `base_point` is the insertion point of
247        the BLOCK.
248
249            ========= ==========
250            type_char Anonymous Block Type
251            ========= ==========
252            ``'U'``   ``'*U###'`` anonymous BLOCK
253            ``'E'``   ``'*E###'`` anonymous non-uniformly scaled BLOCK
254            ``'X'``   ``'*X###'`` anonymous HATCH graphic
255            ``'D'``   ``'*D###'`` anonymous DIMENSION graphic
256            ``'A'``   ``'*A###'`` anonymous GROUP
257            ``'T'``   ``'*T###'`` anonymous block for ACAD_TABLE content
258            ========= ==========
259
260        """
261        blockname = self.anonymous_blockname(type_char)
262        block = self.new(blockname, base_point, {'flags': const.BLK_ANONYMOUS})
263        return block
264
265    def anonymous_blockname(self, type_char: str) -> str:
266        """ Create name for an anonymous block. (internal API)
267
268        Args:
269            type_char: letter
270
271                U = *U### anonymous blocks
272                E = *E### anonymous non-uniformly scaled blocks
273                X = *X### anonymous hatches
274                D = *D### anonymous dimensions
275                A = *A### anonymous groups
276                T = *T### anonymous ACAD_TABLE content
277
278        """
279        while True:
280            self._anonymous_block_counter += 1
281            blockname = f"*{type_char}{self._anonymous_block_counter}"
282            if not self.__contains__(blockname):
283                return blockname
284
285    def rename_block(self, old_name: str, new_name: str) -> None:
286        """ Rename :class:`~ezdxf.layouts.BlockLayout` `old_name` to `new_name`
287        """
288        block_record: 'BlockRecord' = self.block_records.get(old_name)
289        block_record.rename(new_name)
290        self.block_records.replace(old_name, block_record)
291        self.add(block_record)
292
293    def delete_block(self, name: str, safe: bool = True) -> None:
294        """
295        Delete block. If `save` is ``True``, check if block is still referenced.
296
297        Args:
298            name: block name (case insensitive)
299            safe: check if block is still referenced or special block without
300                  explicit references
301
302        Raises:
303            DXFKeyError: if block not exists
304            DXFBlockInUseError: if block is still referenced, and save is True
305
306        """
307        if safe:
308            if is_special_block(name):
309                raise DXFBlockInUseError(
310                    f'Special block "{name}" maybe used without explicit INSERT entity.'
311                )
312
313            block_refs = self.doc.query(
314                f"INSERT[name=='{name}']i")  # ignore case
315            if len(block_refs):
316                raise DXFBlockInUseError(
317                    f'Block "{name}" is still in use.'
318                )
319        self.__delitem__(name)
320
321    def delete_all_blocks(self) -> None:
322        """ Delete all blocks without references except modelspace- or
323        paperspace layout blocks, special arrow- and anonymous blocks
324        (DIMENSION, ACAD_TABLE).
325
326        .. warning::
327
328            There could exist undiscovered references to blocks which are
329            not documented in the DXF reference, hidden in extended data
330            sections or application defined data, which could produce invalid
331            DXF documents if such referenced blocks will be deleted.
332
333        .. versionchanged:: 0.14
334            removed unsafe mode
335
336        """
337        active_references = set(
338            table_key(entity.dxf.name) for entity in
339            self.doc.query('INSERT')
340        )
341
342        def is_safe(name: str) -> bool:
343            if is_special_block(name):
344                return False
345            return name not in active_references
346
347        trash = set()
348        for block in self:
349            name = table_key(block.name)
350            if not block.is_any_layout and is_safe(name):
351                trash.add(name)
352
353        for name in trash:
354            self.__delitem__(name)
355
356    def purge(self):
357        """ Purge functionality removed! - it was just too dangerous!
358        The method name suggests a functionality and quality similar
359        to that of a CAD application, which can not be delivered!
360        """
361        warnings.warn('Blocks.purge() deactivated, unsafe operation!',
362                      DeprecationWarning)
363