Files
stepanalyser/.venv/lib/python3.12/site-packages/ezdxf/blkrefs.py
Christian Anetzberger a197de9456 initial
2026-01-22 20:23:51 +01:00

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