210 lines
7.5 KiB
Python
210 lines
7.5 KiB
Python
# Copyright (c) 2021-2022, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import TYPE_CHECKING, Iterable, Optional, Iterator
|
|
from collections import Counter
|
|
|
|
from ezdxf.lldxf.types import POINTER_CODES
|
|
from ezdxf.protocols import referenced_blocks
|
|
|
|
if TYPE_CHECKING:
|
|
from ezdxf.document import Drawing
|
|
from ezdxf.lldxf.tags import Tags
|
|
from ezdxf.entities import DXFEntity, BlockRecord
|
|
|
|
__all__ = ["BlockDefinitionIndex", "BlockReferenceCounter"]
|
|
|
|
"""
|
|
Where are block references located:
|
|
|
|
- HEADER SECTION: $DIMBLK, $DIMBLK1, $DIMBLK2, $DIMLDRBLK
|
|
- DIMENSION: arrows referenced in the associated anonymous BLOCK, covered by
|
|
the INSERT entities in that BLOCK
|
|
- ACAD_TABLE: has an anonymous BLOCK representation, covered by the
|
|
INSERT entities in that BLOCK
|
|
- LEADER: DIMSTYLE override "dimldrblk" is stored as handle in XDATA
|
|
|
|
Entity specific block references, returned by the "ReferencedBlocks" protocol:
|
|
- INSERT: "name"
|
|
- DIMSTYLE: "dimblk", "dimblk1", "dimblk2", "dimldrblk"
|
|
- MLEADER: arrows, blocks - has no anonymous BLOCK representation
|
|
- MLEADERSTYLE: arrows
|
|
- DIMENSION: "geometry", the associated anonymous BLOCK
|
|
- ACAD_TABLE: currently managed as generic DXFTagStorage, that should return
|
|
the group code 343 "block_record"
|
|
|
|
Possible unknown or undocumented block references:
|
|
- DXFTagStorage - all handle group codes
|
|
- XDATA - only group code 1005
|
|
- APPDATA - all handle group codes
|
|
- XRECORD - all handle group codes
|
|
|
|
Contains no block references as far as known:
|
|
- DICTIONARY: can only references DXF objects like XRECORD or DICTIONARYVAR
|
|
- Extension Dictionary is a DICTIONARY object
|
|
- REACTORS - used for object messaging, a reactor does not establish
|
|
a block reference
|
|
|
|
Block references are stored as handles to the BLOCK_RECORD entity!
|
|
|
|
Testing DXF documents with missing BLOCK definitions:
|
|
|
|
- INSERT without an existing BLOCK definition does NOT crash AutoCAD/BricsCAD
|
|
- HEADER variables $DIMBLK, $DIMBLK2, $DIMBLK2 and $DIMLDRBLK can reference
|
|
non existing blocks without crashing AutoCAD/BricsCAD
|
|
|
|
"""
|
|
|
|
|
|
class BlockDefinitionIndex:
|
|
"""Index of all :class:`~ezdxf.entities.BlockRecord` entities representing
|
|
real BLOCK definitions, excluding all :class:`~ezdxf.entities.BlockRecord`
|
|
entities defining model space or paper space layouts. External references
|
|
(XREF) and XREF overlays are included.
|
|
|
|
"""
|
|
def __init__(self, doc: Drawing):
|
|
self._doc = doc
|
|
# mapping: handle -> BlockRecord entity
|
|
self._handle_index: dict[str, BlockRecord] = dict()
|
|
# mapping: block name -> BlockRecord entity
|
|
self._name_index: dict[str, BlockRecord] = dict()
|
|
self.rebuild()
|
|
|
|
@property
|
|
def block_records(self) -> Iterator[BlockRecord]:
|
|
"""Returns an iterator of all :class:`~ezdxf.entities.BlockRecord`
|
|
entities representing BLOCK definitions.
|
|
"""
|
|
return iter(self._doc.tables.block_records)
|
|
|
|
def rebuild(self):
|
|
"""Rebuild index from scratch."""
|
|
handle_index = self._handle_index
|
|
name_index = self._name_index
|
|
handle_index.clear()
|
|
name_index.clear()
|
|
for block_record in self.block_records:
|
|
if block_record.is_block_layout:
|
|
handle_index[block_record.dxf.handle] = block_record
|
|
name_index[block_record.dxf.name] = block_record
|
|
|
|
def has_handle(self, handle: str) -> bool:
|
|
"""Returns ``True`` if a :class:`~ezdxf.entities.BlockRecord` for the
|
|
given block record handle exist.
|
|
"""
|
|
return handle in self._handle_index
|
|
|
|
def has_name(self, name: str) -> bool:
|
|
"""Returns ``True`` if a :class:`~ezdxf.entities.BlockRecord` for the
|
|
given block name exist.
|
|
"""
|
|
return name in self._name_index
|
|
|
|
def by_handle(self, handle: str) -> Optional[BlockRecord]:
|
|
"""Returns the :class:`~ezdxf.entities.BlockRecord` for the given block
|
|
record handle or ``None``.
|
|
"""
|
|
return self._handle_index.get(handle)
|
|
|
|
def by_name(self, name: str) -> Optional[BlockRecord]:
|
|
"""Returns :class:`~ezdxf.entities.BlockRecord` for the given block name
|
|
or ``None``.
|
|
"""
|
|
return self._name_index.get(name)
|
|
|
|
|
|
class BlockReferenceCounter:
|
|
"""
|
|
Counts all block references in a DXF document.
|
|
|
|
Check if a block is referenced by any entity or any resource (DIMSYTLE,
|
|
MLEADERSTYLE) in a DXF document::
|
|
|
|
import ezdxf
|
|
from ezdxf.blkrefs import BlockReferenceCounter
|
|
|
|
doc = ezdxf.readfile("your.dxf")
|
|
counter = BlockReferenceCounter(doc)
|
|
count = counter.by_name("XYZ")
|
|
print(f"Block 'XYZ' if referenced {count} times.")
|
|
|
|
"""
|
|
|
|
def __init__(self, doc: Drawing, index: Optional[BlockDefinitionIndex] = None):
|
|
# mapping: handle -> BlockRecord entity
|
|
self._block_record_index = (
|
|
index if index is not None else BlockDefinitionIndex(doc)
|
|
)
|
|
|
|
# mapping: handle -> reference count
|
|
self._counter = count_references(
|
|
doc.entitydb.values(), self._block_record_index
|
|
)
|
|
self._counter.update(header_section_handles(doc))
|
|
|
|
def by_handle(self, handle: str) -> int:
|
|
"""Returns the block reference count for a given
|
|
:class:`~ezdxf.entities.BlockRecord` handle.
|
|
"""
|
|
return self._counter[handle]
|
|
|
|
def by_name(self, block_name: str) -> int:
|
|
"""Returns the block reference count for a given block name."""
|
|
handle = ""
|
|
block_record = self._block_record_index.by_name(block_name)
|
|
if block_record is not None:
|
|
handle = block_record.dxf.handle
|
|
return self._counter[handle]
|
|
|
|
|
|
def count_references(
|
|
entities: Iterable[DXFEntity], index: BlockDefinitionIndex
|
|
) -> Counter:
|
|
from ezdxf.entities import XRecord, DXFTagStorage
|
|
|
|
def update(handles: Iterable[str]):
|
|
# only count references to existing blocks:
|
|
counter.update(h for h in handles if index.has_handle(h))
|
|
|
|
counter: Counter = Counter()
|
|
for entity in entities:
|
|
# add handles stored in XDATA and APP data
|
|
update(generic_handles(entity))
|
|
# add entity specific block references
|
|
update(referenced_blocks(entity))
|
|
# special entity types storing arbitrary raw DXF tags:
|
|
if isinstance(entity, XRecord):
|
|
update(all_pointer_handles(entity.tags))
|
|
elif isinstance(entity, DXFTagStorage):
|
|
# XDATA and APP data is already done!
|
|
for tags in entity.xtags.subclasses[1:]:
|
|
update(all_pointer_handles(tags))
|
|
# ignore embedded objects: special objects for MTEXT and ATTRIB
|
|
return counter
|
|
|
|
|
|
def generic_handles(entity: DXFEntity) -> Iterable[str]:
|
|
handles: list[str] = []
|
|
if entity.xdata is not None:
|
|
for tags in entity.xdata.data.values():
|
|
handles.extend(value for code, value in tags if code == 1005)
|
|
if entity.appdata is not None:
|
|
for tags in entity.appdata.data.values():
|
|
handles.extend(all_pointer_handles(tags))
|
|
return handles
|
|
|
|
|
|
def all_pointer_handles(tags: Tags) -> Iterable[str]:
|
|
return (value for code, value in tags if code in POINTER_CODES)
|
|
|
|
|
|
def header_section_handles(doc: "Drawing") -> Iterable[str]:
|
|
header = doc.header
|
|
for var_name in ("$DIMBLK", "$DIMBLK1", "$DIMBLK2", "$DIMLDRBLK"):
|
|
blk_name = header.get(var_name, None)
|
|
if blk_name is not None:
|
|
block = doc.blocks.get(blk_name, None)
|
|
if block is not None:
|
|
yield block.block_record.dxf.handle
|