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

689 lines
24 KiB
Python

# Copyright (c) 2019-2024, Manfred Moitzi
# License: MIT-License
from __future__ import annotations
from typing import TYPE_CHECKING, Union, Optional
from typing_extensions import Self
import logging
from ezdxf.lldxf import validator
from ezdxf.lldxf.const import (
SUBCLASS_MARKER,
DXFKeyError,
DXFValueError,
DXFTypeError,
DXFStructureError,
)
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.lldxf.types import is_valid_handle
from ezdxf.audit import AuditError
from ezdxf.entities import factory, DXFGraphic
from .dxfentity import base_class, SubclassProcessor, DXFEntity
from .dxfobj import DXFObject
from .copy import default_copy, CopyNotSupported
if TYPE_CHECKING:
from ezdxf.entities import DXFNamespace, XRecord
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.document import Drawing
from ezdxf.audit import Auditor
from ezdxf import xref
__all__ = ["Dictionary", "DictionaryWithDefault", "DictionaryVar"]
logger = logging.getLogger("ezdxf")
acdb_dictionary = DefSubclass(
"AcDbDictionary",
{
# If hard_owned is set to 1 the entries are owned by the DICTIONARY.
# The 1 state seems to be the default value, but is not documented by
# the DXF reference.
# BricsCAD creates the root DICTIONARY and the top level DICTIONARY entries
# without group code 280 tags, and they are all definitely hard owner of their
# entries, because their entries have the DICTIONARY handle as owner handle.
"hard_owned": DXFAttr(
280,
default=1,
optional=True,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
# Duplicate record cloning flag (determines how to merge duplicate entries):
# 0 = not applicable
# 1 = keep existing
# 2 = use clone
# 3 = <xref>$0$<name>
# 4 = $0$<name>
# 5 = Unmangle name
"cloning": DXFAttr(
281,
default=1,
validator=validator.is_in_integer_range(0, 6),
fixer=RETURN_DEFAULT,
),
# 3: entry name
# 350: entry handle, some DICTIONARY objects have 360 as handle group code,
# this is accepted by AutoCAD but not documented by the DXF reference!
# ezdxf replaces group code 360 by 350.
# - group code 350 is a soft-owner handle
# - group code 360 is a hard-owner handle
},
)
acdb_dictionary_group_codes = group_code_mapping(acdb_dictionary)
KEY_CODE = 3
VALUE_CODE = 350
# Some DICTIONARY use group code 360:
SEARCH_CODES = (VALUE_CODE, 360)
@factory.register_entity
class Dictionary(DXFObject):
"""AutoCAD maintains items such as mline styles and group definitions as
objects in dictionaries. Other applications are free to create and use
their own dictionaries as they see fit. The prefix "ACAD_" is reserved
for use by AutoCAD applications.
Dictionary entries are (key, DXFEntity) pairs. DXFEntity could be a string,
because at loading time not all objects are already stored in the EntityDB,
and have to be acquired later.
"""
DXFTYPE = "DICTIONARY"
DXFATTRIBS = DXFAttributes(base_class, acdb_dictionary)
def __init__(self) -> None:
super().__init__()
self._data: dict[str, Union[str, DXFObject]] = dict()
self._value_code = VALUE_CODE
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
"""Copy hard owned entities but do not store the copies in the entity
database, this is a second step (factory.bind), this is just real copying.
"""
assert isinstance(entity, Dictionary)
entity._value_code = self._value_code
if self.dxf.hard_owned:
# Reactors are removed from the cloned DXF objects.
data: dict[str, DXFEntity] = dict()
for key, ent in self.items():
# ignore strings and None - these entities do not exist
# in the entity database
if isinstance(ent, DXFEntity):
try: # todo: follow CopyStrategy.ignore_copy_errors_in_linked entities
data[key] = ent.copy(copy_strategy=copy_strategy)
except CopyNotSupported:
if copy_strategy.settings.ignore_copy_errors_in_linked_entities:
logger.warning(
f"copy process ignored {str(ent)} - this may cause problems in AutoCAD"
)
else:
raise
entity._data = data # type: ignore
else:
entity._data = dict(self._data)
def get_handle_mapping(self, clone: Dictionary) -> dict[str, str]:
"""Returns handle mapping for in-object copies."""
handle_mapping: dict[str, str] = dict()
if not self.is_hard_owner:
return handle_mapping
for key, entity in self.items():
if not isinstance(entity, DXFEntity):
continue
copied_entry = clone.get(key)
if copied_entry:
handle_mapping[entity.dxf.handle] = copied_entry.dxf.handle
return handle_mapping
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
assert isinstance(clone, Dictionary)
super().map_resources(clone, mapping)
if self.is_hard_owner:
return
data = dict()
for key, entity in self.items():
if not isinstance(entity, DXFEntity):
continue
entity_copy = mapping.get_reference_of_copy(entity.dxf.handle)
if entity_copy:
data[key] = entity
clone._data = data # type: ignore
def del_source_of_copy(self) -> None:
super().del_source_of_copy()
for _, entity in self.items():
if isinstance(entity, DXFEntity) and entity.is_alive:
entity.del_source_of_copy()
def post_bind_hook(self) -> None:
"""Called by binding a new or copied dictionary to the document,
bind hard owned sub-entities to the same document and add them to the
objects section.
"""
if not self.dxf.hard_owned:
return
# copied or new dictionary:
doc = self.doc
assert doc is not None
object_section = doc.objects
owner_handle = self.dxf.handle
for _, entity in self.items():
entity.dxf.owner = owner_handle
factory.bind(entity, doc)
# For a correct DXF export add entities to the objects section:
object_section.add_object(entity)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
tags = processor.fast_load_dxfattribs(
dxf, acdb_dictionary_group_codes, 1, log=False
)
self.load_dict(tags)
return dxf
def load_dict(self, tags):
entry_handle = None
dict_key = None
value_code = VALUE_CODE
for code, value in tags:
if code in SEARCH_CODES:
# First store handles, because at this point, NOT all objects
# are stored in the EntityDB, at first access convert the handle
# to a DXFEntity object.
value_code = code
entry_handle = value
elif code == KEY_CODE:
dict_key = value
if dict_key and entry_handle:
# Store entity as handle string:
self._data[dict_key] = entry_handle
entry_handle = None
dict_key = None
# Use same value code as loaded:
self._value_code = value_code
def post_load_hook(self, doc: Drawing) -> None:
super().post_load_hook(doc)
db = doc.entitydb
def items():
for key, handle in self.items():
entity = db.get(handle)
if entity is not None and entity.is_alive:
yield key, entity
if len(self):
for k, v in list(items()):
self.__setitem__(k, v)
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dictionary.name)
self.dxf.export_dxf_attribs(tagwriter, ["hard_owned", "cloning"])
self.export_dict(tagwriter)
def export_dict(self, tagwriter: AbstractTagWriter):
# key: dict key string
# value: DXFEntity or handle as string
# Ignore invalid handles at export, because removing can create an empty
# dictionary, which is more a problem for AutoCAD than invalid handles,
# and removing the whole dictionary is maybe also a problem.
for key, value in self._data.items():
tagwriter.write_tag2(KEY_CODE, key)
# Value can be a handle string or a DXFEntity object:
if isinstance(value, DXFEntity):
if value.is_alive:
value = value.dxf.handle
else:
logger.debug(
f'Key "{key}" points to a destroyed entity '
f'in {str(self)}, target replaced by "0" handle.'
)
value = "0"
# Use same value code as loaded:
tagwriter.write_tag2(self._value_code, value)
@property
def is_hard_owner(self) -> bool:
"""Returns ``True`` if the dictionary is hard owner of entities.
Hard owned entities will be destroyed by deleting the dictionary.
"""
return bool(self.dxf.hard_owned)
def keys(self):
"""Returns a :class:`KeysView` of all dictionary keys."""
return self._data.keys()
def items(self):
"""Returns an :class:`ItemsView` for all dictionary entries as
(key, entity) pairs. An entity can be a handle string if the entity
does not exist.
"""
for key in self.keys():
yield key, self.get(key) # maybe handle -> DXFEntity
def __getitem__(self, key: str) -> DXFEntity:
"""Return self[`key`].
The returned value can be a handle string if the entity does not exist.
Raises:
DXFKeyError: `key` does not exist
"""
if key in self._data:
return self._data[key] # type: ignore
else:
raise DXFKeyError(key)
def __setitem__(self, key: str, entity: DXFObject) -> None:
"""Set self[`key`] = `entity`.
Only DXF objects stored in the OBJECTS section are allowed as content
of :class:`Dictionary` objects. DXF entities stored in layouts are not
allowed.
Raises:
DXFTypeError: invalid DXF type
"""
return self.add(key, entity)
def __delitem__(self, key: str) -> None:
"""Delete self[`key`].
Raises:
DXFKeyError: `key` does not exist
"""
return self.remove(key)
def __contains__(self, key: str) -> bool:
"""Returns `key` ``in`` self."""
return key in self._data
def __len__(self) -> int:
"""Returns count of dictionary entries."""
return len(self._data)
count = __len__
def get(self, key: str, default: Optional[DXFObject] = None) -> Optional[DXFObject]:
"""Returns the :class:`DXFEntity` for `key`, if `key` exist else
`default`. An entity can be a handle string if the entity
does not exist.
"""
return self._data.get(key, default) # type: ignore
def find_key(self, entity: DXFEntity) -> str:
"""Returns the DICTIONARY key string for `entity` or an empty string if not
found.
"""
for key, entry in self._data.items():
if entry is entity:
return key
return ""
def add(self, key: str, entity: DXFObject) -> None:
"""Add entry (key, value).
If the DICTIONARY is hard owner of its entries, the :meth:`add` does NOT take
ownership of the entity automatically.
Raises:
DXFValueError: invalid entity handle
DXFTypeError: invalid DXF type
"""
if isinstance(entity, str):
if not is_valid_handle(entity):
raise DXFValueError(f"Invalid entity handle #{entity} for key {key}")
elif isinstance(entity, DXFGraphic):
if self.doc is not None and self.doc.is_loading: # type: ignore
# AutoCAD add-ons can store graphical entities in DICTIONARIES
# in the OBJECTS section and AutoCAD does not complain - so just
# preserve them!
# Example "ZJMC-288.dxf" in issue #585, add-on: "acdgnlsdraw.crx"?
logger.warning(f"Invalid entity {str(entity)} in {str(self)}")
else:
# Do not allow ezdxf users to add graphical entities to a
# DICTIONARY object!
raise DXFTypeError(f"Graphic entities not allowed: {entity.dxftype()}")
self._data[key] = entity
def take_ownership(self, key: str, entity: DXFObject):
"""Add entry (key, value) and take ownership."""
self.add(key, entity)
entity.dxf.owner = self.dxf.handle
def remove(self, key: str) -> None:
"""Delete entry `key`. Raises :class:`DXFKeyError`, if `key` does not
exist. Destroys hard owned DXF entities.
"""
data = self._data
if key not in data:
raise DXFKeyError(key)
if self.is_hard_owner:
assert self.doc is not None
entity = self.__getitem__(key)
# Presumption: hard owned DXF objects always reside in the OBJECTS
# section.
self.doc.objects.delete_entity(entity) # type: ignore
del data[key]
def discard(self, key: str) -> None:
"""Delete entry `key` if exists. Does not raise an exception if `key`
doesn't exist and does not destroy hard owned DXF entities.
"""
try:
del self._data[key]
except KeyError:
pass
def clear(self) -> None:
"""Delete all entries from the dictionary and destroys hard owned
DXF entities.
"""
if self.is_hard_owner:
self._delete_hard_owned_entries()
self._data.clear()
def _delete_hard_owned_entries(self) -> None:
# Presumption: hard owned DXF objects always reside in the OBJECTS section
objects = self.doc.objects # type: ignore
for key, entity in self.items():
if isinstance(entity, DXFEntity):
objects.delete_entity(entity) # type: ignore
def add_new_dict(self, key: str, hard_owned: bool = False) -> Dictionary:
"""Create a new sub-dictionary of type :class:`Dictionary`.
Args:
key: name of the sub-dictionary
hard_owned: entries of the new dictionary are hard owned
"""
dxf_dict = self.doc.objects.add_dictionary( # type: ignore
owner=self.dxf.handle, hard_owned=hard_owned
)
self.add(key, dxf_dict)
return dxf_dict
def add_dict_var(self, key: str, value: str) -> DictionaryVar:
"""Add a new :class:`DictionaryVar`.
Args:
key: entry name as string
value: entry value as string
"""
new_var = self.doc.objects.add_dictionary_var( # type: ignore
owner=self.dxf.handle, value=value
)
self.add(key, new_var)
return new_var
def add_xrecord(self, key: str) -> XRecord:
"""Add a new :class:`XRecord`.
Args:
key: entry name as string
"""
new_xrecord = self.doc.objects.add_xrecord( # type: ignore
owner=self.dxf.handle,
)
self.add(key, new_xrecord)
return new_xrecord
def set_or_add_dict_var(self, key: str, value: str) -> DictionaryVar:
"""Set or add new :class:`DictionaryVar`.
Args:
key: entry name as string
value: entry value as string
"""
if key not in self:
dict_var = self.doc.objects.add_dictionary_var( # type: ignore
owner=self.dxf.handle, value=value
)
self.add(key, dict_var)
else:
dict_var = self.get(key)
dict_var.dxf.value = str(value) # type: ignore
return dict_var
def link_dxf_object(self, name: str, obj: DXFObject) -> None:
"""Add `obj` and set owner of `obj` to this dictionary.
Graphical DXF entities have to reside in a layout and therefore can not
be owned by a :class:`Dictionary`.
Raises:
DXFTypeError: `obj` has invalid DXF type
"""
if not isinstance(obj, DXFObject):
raise DXFTypeError(f"invalid DXF type: {obj.dxftype()}")
self.add(name, obj)
obj.dxf.owner = self.dxf.handle
def get_required_dict(self, key: str, hard_owned=False) -> Dictionary:
"""Get entry `key` or create a new :class:`Dictionary`,
if `Key` not exist.
"""
dxf_dict = self.get(key)
if dxf_dict is None:
dxf_dict = self.add_new_dict(key, hard_owned=hard_owned)
elif not isinstance(dxf_dict, Dictionary):
raise DXFStructureError(
f"expected a DICTIONARY entity, got {str(dxf_dict)} for key: {key}"
)
return dxf_dict
def audit(self, auditor: Auditor) -> None:
if not self.is_alive:
return
super().audit(auditor)
self._remove_keys_to_missing_entities(auditor)
def _remove_keys_to_missing_entities(self, auditor: Auditor):
trash: list[str] = []
append = trash.append
db = auditor.entitydb
for key, entry in self._data.items():
if isinstance(entry, str):
if entry not in db:
append(key)
elif entry.is_alive:
if entry.dxf.handle not in db:
append(key)
continue
else: # entity is destroyed, remove key
append(key)
for key in trash:
del self._data[key]
auditor.fixed_error(
code=AuditError.INVALID_DICTIONARY_ENTRY,
message=f'Removed entry "{key}" with invalid handle in {str(self)}',
dxf_entity=self,
data=key,
)
def destroy(self) -> None:
if not self.is_alive:
return
if self.is_hard_owner:
self._delete_hard_owned_entries()
super().destroy()
acdb_dict_with_default = DefSubclass(
"AcDbDictionaryWithDefault",
{
"default": DXFAttr(340),
},
)
acdb_dict_with_default_group_codes = group_code_mapping(acdb_dict_with_default)
@factory.register_entity
class DictionaryWithDefault(Dictionary):
DXFTYPE = "ACDBDICTIONARYWDFLT"
DXFATTRIBS = DXFAttributes(base_class, acdb_dictionary, acdb_dict_with_default)
def __init__(self) -> None:
super().__init__()
self._default: Optional[DXFObject] = None
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
super().copy_data(entity, copy_strategy=copy_strategy)
assert isinstance(entity, DictionaryWithDefault)
entity._default = self._default
def post_load_hook(self, doc: Drawing) -> None:
# Set _default to None if default object not exist - audit() replaces
# a not existing default object by a placeholder object.
# AutoCAD ignores not existing default objects!
self._default = doc.entitydb.get(self.dxf.default) # type: ignore
super().post_load_hook(doc)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(dxf, acdb_dict_with_default_group_codes, 2)
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dict_with_default.name)
self.dxf.export_dxf_attribs(tagwriter, "default")
def __getitem__(self, key: str):
return self.get(key)
def get(self, key: str, default: Optional[DXFObject] = None) -> Optional[DXFObject]:
# `default` argument is ignored, exist only for API compatibility,
"""Returns :class:`DXFEntity` for `key` or the predefined dictionary
wide :attr:`dxf.default` entity if `key` does not exist or ``None``
if default value also not exist.
"""
return super().get(key, default=self._default)
def set_default(self, default: DXFObject) -> None:
"""Set dictionary wide default entry.
Args:
default: default entry as :class:`DXFEntity`
"""
self._default = default
self.dxf.default = self._default.dxf.handle
def audit(self, auditor: Auditor) -> None:
def create_missing_default_object():
placeholder = self.doc.objects.add_placeholder(owner=self.dxf.handle)
self.set_default(placeholder)
auditor.fixed_error(
code=AuditError.CREATED_MISSING_OBJECT,
message=f"Created missing default object in {str(self)}.",
)
if self._default is None or not self._default.is_alive:
if auditor.entitydb.locked:
auditor.add_post_audit_job(create_missing_default_object)
else:
create_missing_default_object()
super().audit(auditor)
acdb_dict_var = DefSubclass(
"DictionaryVariables",
{
"schema": DXFAttr(280, default=0),
# Object schema number (currently set to 0)
"value": DXFAttr(1, default=""),
},
)
acdb_dict_var_group_codes = group_code_mapping(acdb_dict_var)
@factory.register_entity
class DictionaryVar(DXFObject):
"""
DICTIONARYVAR objects are used by AutoCAD as a means to store named values
in the database for setvar / getvar purposes without the need to add entries
to the DXF HEADER section. System variables that are stored as
DICTIONARYVAR objects are the following:
- DEFAULTVIEWCATEGORY
- DIMADEC
- DIMASSOC
- DIMDSEP
- DRAWORDERCTL
- FIELDEVAL
- HALOGAP
- HIDETEXT
- INDEXCTL
- INDEXCTL
- INTERSECTIONCOLOR
- INTERSECTIONDISPLAY
- MSOLESCALE
- OBSCOLOR
- OBSLTYPE
- OLEFRAME
- PROJECTNAME
- SORTENTS
- UPDATETHUMBNAIL
- XCLIPFRAME
- XCLIPFRAME
"""
DXFTYPE = "DICTIONARYVAR"
DXFATTRIBS = DXFAttributes(base_class, acdb_dict_var)
@property
def value(self) -> str:
"""Get/set the value of the :class:`DictionaryVar` as string."""
return self.dxf.get("value", "")
@value.setter
def value(self, data: str) -> None:
self.dxf.set("value", str(data))
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(dxf, acdb_dict_var_group_codes, 1)
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dict_var.name)
self.dxf.export_dxf_attribs(tagwriter, ["schema", "value"])