refactor: excel parse

This commit is contained in:
Blizzard
2026-04-16 10:01:11 +08:00
parent 680ecc320f
commit f62f95ec02
7941 changed files with 2899112 additions and 0 deletions
@@ -0,0 +1,2 @@
# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License
@@ -0,0 +1,518 @@
# Purpose: acdsdata section manager
# Copyright (c) 2014-2022, Manfred Moitzi
# License: MIT License
"""
ACDSDATA entities have NO handles, therefore they can not be stored in the
drawing entity database.
section structure (work in progress):
0 <str> SECTION
2 <str> ACDSDATA
70 <int> 2 # flag?
71 <int> 6 # count of following ACDSSCHEMA entities ??? no, just another flag
0 <str> ACDSSCHEMA # dxftype: schema definition
90 <int> 0 # schema number 0, 1, 2, 3 ...
1 <str> AcDb3DSolid_ASM_Data # schema name
2 <str> AcDbDs::ID # subsection name
280 <int> 10 # subsection type 10 = ???
91 <int> 8 # data ???
2 <str> ASM_Data # subsection name
280 <int> 15 # subsection type
91 <int> 0 # data ???
101 <str> ACDSRECORD # data
95 <int> 0
90 <int> 2
...
0 <str> ACDSSCHEMA
90 <int> 1
1 <str> AcDb_Thumbnail_Schema
...
0 <str> ACDSSCHEMA
90 <int> 2
1 <str> AcDbDs::TreatedAsObjectDataSchema
...
0 <str> ACDSSCHEMA
90 <int> 3
1 <str> AcDbDs::LegacySchema
2 <str> AcDbDs::Legacy
280 <int> 1
91 <int> 0
0 <str> ACDSSCHEMA
90 <int> 4
1 <str> AcDbDs::IndexedPropertySchema
2 <str> AcDs:Indexable
280 <int> 1
91 <int> 0
0 <str> ACDSSCHEMA
90 <int> 5
1 <str> AcDbDs::HandleAttributeSchema
2 <str> AcDbDs::HandleAttribute
280 <int> 7
91 <int> 1
284 <int> 1
0 <str> ACDSRECORD # dxftype: data record
90 <int> 0 # ??? flag
2 <str> AcDbDs::ID # subsection name
280 <int> 10 # subsection type 10 = handle to owner entity, 3DSOLID/REGION
320 <str> 339 # handle
2 <str> ASM_Data # subsection name
280 <int> 15 # subsection type 15 = binary data
94 <int> 1088 # size of data
310 <binary encoded data> # data
310 <binary encoded data> # data
...
0 <str> ENDSEC
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Iterator, Iterable, Any, Optional
import abc
from itertools import islice
from ezdxf.lldxf.tags import group_tags, Tags
from ezdxf.lldxf.types import dxftag
from ezdxf.lldxf.const import DXFKeyError, DXFStructureError
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.lldxf.tagwriter import AbstractTagWriter
class AcDsEntity(abc.ABC):
@abc.abstractmethod
def export_dxf(self, tagwriter: AbstractTagWriter):
...
@abc.abstractmethod
def dxftype(self) -> str:
...
class AcDsDataSection:
name = "ACDSDATA"
def __init__(self, doc: Drawing, entities: Optional[Iterable[Tags]] = None):
self.doc = doc
self.entities: list[AcDsEntity] = []
self.section_info = Tags()
if entities is not None:
self.load_tags(iter(entities))
@property
def is_valid(self) -> bool:
return len(self.section_info) > 0
@property
def has_records(self) -> bool:
return any(
isinstance(entity, AcDsRecord) for entity in self.entities
)
def load_tags(self, entities: Iterator[Tags]) -> None:
section_head = next(entities)
if section_head[0] != (0, "SECTION") or section_head[1] != (
2,
"ACDSDATA",
):
raise DXFStructureError(
"Critical structure error in ACDSDATA section."
)
self.section_info = section_head
for entity in entities:
self.append(AcDsData(entity)) # tags have no subclasses
def append(self, entity: AcDsData) -> None:
cls = ACDSDATA_TYPES.get(entity.dxftype(), AcDsData)
data = cls(entity.tags)
self.entities.append(data)
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
if not self.is_valid or not self.has_records:
# Empty ACDSDATA section is not required!
return
tagwriter.write_tags(self.section_info)
for entity in self.entities:
entity.export_dxf(tagwriter)
tagwriter.write_tag2(0, "ENDSEC")
@property
def acdsrecords(self) -> Iterator[AcDsRecord]:
return (
entity for entity in self.entities if isinstance(entity, AcDsRecord)
)
def get_acis_data(self, handle: str) -> bytes:
asm_record = self.find_acis_record(handle)
if asm_record is not None:
return b"".join(get_acis_data(asm_record))
return b""
def set_acis_data(self, handle: str, sab_data: bytes) -> None:
asm_record = self.find_acis_record(handle)
if asm_record is not None:
set_acis_data(asm_record, sab_data)
else:
self.new_acis_data(handle, sab_data)
def new_acis_data(self, handle: str, sab_data: bytes) -> None:
self.entities.append(new_acis_record(handle, sab_data))
def del_acis_data(self, handle) -> None:
asm_record = self.find_acis_record(handle)
if asm_record is not None:
self.entities.remove(asm_record)
def find_acis_record(self, handle: str) -> Optional[AcDsRecord]:
for record in self.acdsrecords:
if is_acis_data(record) and acis_entity_handle(record) == handle:
return record
return None
class AcDsData(AcDsEntity):
def __init__(self, tags: Tags):
self.tags = tags
def export_dxf(self, tagwriter: AbstractTagWriter):
tagwriter.write_tags(self.tags)
def dxftype(self) -> str:
return self.tags[0].value
class Section(Tags):
@property
def name(self) -> str:
return self[0].value
@property
def type(self) -> str:
return self[1].value
@property
def data(self) -> Tags:
return Tags(self[2:])
class AcDsRecord(AcDsEntity):
def __init__(self, tags: Tags):
self._dxftype = tags[0]
self.flags = tags[1]
self.sections = [
Section(group)
for group in group_tags(islice(tags, 2, None), splitcode=2)
]
def dxftype(self) -> str:
return "ACDSRECORD"
def has_section(self, name: str) -> bool:
return self.get_section(name, default=None) is not None
def get_section(self, name: str, default: Any = DXFKeyError) -> Section:
for section in self.sections:
if section.name == name:
return section
if default is DXFKeyError:
raise DXFKeyError(name)
else:
return default
def index(self, name: str) -> int:
for i, section in enumerate(self.sections):
if section.name == name:
return i
return -1
def replace(self, section: Section) -> None:
index = self.index(section.name)
if index == -1:
self.sections.append(section)
else:
self.sections[index] = section
def append(self, section: Section):
self.sections.append(section)
def __len__(self):
return len(self.sections)
def __getitem__(self, item) -> Section:
return self.sections[item]
def _write_header(self, tagwriter: AbstractTagWriter) -> None:
tagwriter.write_tags(Tags([self._dxftype, self.flags]))
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
self._write_header(tagwriter)
for section in self.sections:
tagwriter.write_tags(section)
def get_acis_data(record: AcDsRecord) -> list[bytes]:
try:
asm_data = record.get_section("ASM_Data")
except DXFKeyError: # no data stored
return []
else:
return [tag.value for tag in asm_data if tag.code == 310]
def is_acis_data(record: AcDsRecord) -> bool:
return record.has_section("ASM_Data")
def acis_entity_handle(record: AcDsRecord) -> str:
try:
section = record.get_section("AcDbDs::ID")
except DXFKeyError: # not present
return ""
return section.get_first_value(320, "")
def set_acis_data(record: AcDsRecord, data: bytes) -> None:
chunk_size = 127
size = len(data)
tags = Tags(
[
dxftag(2, "ASM_Data"),
dxftag(280, 15),
dxftag(94, size),
]
)
index = 0
while index < size:
tags.append(dxftag(310, data[index : index + chunk_size]))
index += chunk_size
record.replace(Section(tags))
def new_acis_record(handle: str, sab_data: bytes) -> AcDsRecord:
tags = Tags(
[
dxftag(0, "ACDSRECORD"),
dxftag(90, 1),
dxftag(2, "AcDbDs::ID"),
dxftag(280, 10),
dxftag(320, handle),
]
)
record = AcDsRecord(tags)
set_acis_data(record, sab_data)
return record
ACDSDATA_TYPES = {
"ACDSRECORD": AcDsRecord,
}
DEFAULT_SETUP = """0
SECTION
2
ACDSDATA
70
2
71
2
0
ACDSSCHEMA
90
0
1
AcDb_Thumbnail_Schema
2
AcDbDs::ID
280
10
91
8
2
Thumbnail_Data
280
15
91
0
101
ACDSRECORD
95
0
90
2
2
AcDbDs::TreatedAsObjectData
280
1
291
1
101
ACDSRECORD
95
0
90
3
2
AcDbDs::Legacy
280
1
291
1
101
ACDSRECORD
1
AcDbDs::ID
90
4
2
AcDs:Indexable
280
1
291
1
101
ACDSRECORD
1
AcDbDs::ID
90
5
2
AcDbDs::HandleAttribute
280
7
282
1
0
ACDSSCHEMA
90
1
1
AcDb3DSolid_ASM_Data
2
AcDbDs::ID
280
10
91
8
2
ASM_Data
280
15
91
0
101
ACDSRECORD
95
1
90
2
2
AcDbDs::TreatedAsObjectData
280
1
291
1
101
ACDSRECORD
95
1
90
3
2
AcDbDs::Legacy
280
1
291
1
101
ACDSRECORD
1
AcDbDs::ID
90
4
2
AcDs:Indexable
280
1
291
1
101
ACDSRECORD
1
AcDbDs::ID
90
5
2
AcDbDs::HandleAttribute
280
7
282
1
0
ACDSSCHEMA
90
2
1
AcDbDs::TreatedAsObjectDataSchema
2
AcDbDs::TreatedAsObjectData
280
1
91
0
0
ACDSSCHEMA
90
3
1
AcDbDs::LegacySchema
2
AcDbDs::Legacy
280
1
91
0
0
ACDSSCHEMA
90
4
1
AcDbDs::IndexedPropertySchema
2
AcDs:Indexable
280
1
91
0
0
ACDSSCHEMA
90
5
1
AcDbDs::HandleAttributeSchema
2
AcDbDs::HandleAttribute
280
7
91
1
284
1
"""
# (0, ENDSEC) must be omitted!
def new_acds_data_section(doc: Drawing) -> AcDsDataSection:
if doc.dxfversion >= "AC1027":
return AcDsDataSection(doc, group_tags(Tags.from_text(DEFAULT_SETUP)))
else:
return AcDsDataSection(doc)
@@ -0,0 +1,518 @@
# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Iterable,
Iterator,
Union,
cast,
Optional,
)
import logging
from pyparsing import ParseException
from ezdxf.audit import Auditor, AuditError
from ezdxf.layouts.blocklayout import BlockLayout
from ezdxf.lldxf import const, validator
from ezdxf.lldxf.const import (
DXFBlockInUseError,
DXFKeyError,
DXFStructureError,
DXFTableEntryError,
DXFTypeError,
)
from ezdxf.entities import (
Attrib,
Block,
BlockRecord,
EndBlk,
entity_linker,
factory,
is_graphic_entity,
)
from ezdxf.math import UVec, NULLVEC, Vec3
from ezdxf.render.arrows import ARROWS
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.entities import DXFEntity, DXFTagStorage
from ezdxf.entitydb import EntityDB
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.sections.table import Table
logger = logging.getLogger("ezdxf")
def is_special_block(name: str) -> bool:
name = name.upper()
# Anonymous dimension, groups and table blocks do not have explicit
# references by an INSERT entity:
if is_anonymous_block(name):
return True
# Arrow blocks maybe used in DIMENSION or LEADER override without an
# INSERT reference:
if ARROWS.is_ezdxf_arrow(name):
return True
if name.startswith("_"):
if ARROWS.is_acad_arrow(ARROWS.arrow_name(name)):
return True
return False
def is_anonymous_block(name: str) -> bool:
# *U### = anonymous BLOCK, require an explicit INSERT to be in use
# *E### = anonymous non-uniformly scaled BLOCK, requires INSERT?
# *X### = anonymous HATCH graphic, requires INSERT?
# *D### = anonymous DIMENSION graphic, has no explicit INSERT
# *A### = anonymous GROUP, requires INSERT?
# *T### = anonymous block for ACAD_TABLE, has no explicit INSERT
return len(name) > 1 and name[0] == "*" and name[1] in "UEXDAT"
def recover_block_name(block: Block) -> str:
name = block.dxf.get("name", "")
if name:
return name
owner = block.dxf.get("owner", "")
if not owner:
return ""
doc = block.doc
# The owner of BLOCK is BLOCK_RECORD which also stores the block name
# as group code 2; DXF attribute name is "name"
if doc is not None and doc.entitydb is not None:
block_record = doc.entitydb.get(owner)
if isinstance(block_record, BlockRecord):
return block_record.dxf.get("name", "")
return ""
_MISSING_BLOCK_ = Block()
class BlocksSection:
"""
Manages BLOCK definitions in a dict(), block names are case insensitive
e.g. 'Test' == 'TEST'.
"""
def __init__(
self,
doc: Optional[Drawing] = None,
entities: Optional[list[DXFEntity]] = None,
):
self.doc = doc
if entities is not None:
self.load(entities)
self._reconstruct_orphaned_block_records()
self._anonymous_block_counter = 0
def __len__(self):
return len(self.block_records)
@staticmethod
def key(entity: Union[str, BlockLayout]) -> str:
if not isinstance(entity, str):
entity = entity.name
return entity.lower() # block key is lower case
@property
def block_records(self) -> Table:
return self.doc.block_records # type: ignore
@property
def entitydb(self) -> EntityDB:
return self.doc.entitydb # type: ignore
def load(self, entities: list[DXFEntity]) -> None:
"""
Load DXF entities into BlockLayouts. `entities` is a list of
entity tags, separated by BLOCK and ENDBLK entities.
"""
def load_block_record(
block: Block,
endblk: EndBlk,
block_entities: list[DXFEntity],
) -> BlockRecord | None:
try:
block_record = cast("BlockRecord", block_records.get(block.dxf.name))
# Special case DXF R12 - has no BLOCK_RECORD table
except DXFTableEntryError:
block_record = cast(
"BlockRecord",
block_records.new(block.dxf.name, dxfattribs={"scale": 0}),
)
except DXFTypeError:
raise DXFStructureError(
f"Invalid or missing name of BLOCK #{block.dxf.handle}"
)
# The BLOCK_RECORD is the central object which stores all the
# information about a BLOCK and also owns all the entities of
# this block definition.
block_record.set_block(block, endblk)
for entity in block_entities:
block_record.add_entity(entity) # type: ignore
return block_record
def link_entities() -> Iterable["DXFEntity"]:
linked = entity_linker()
for entity in entities:
# Do not store linked entities (VERTEX, ATTRIB, SEQEND) in
# the block layout, linked entities ares stored in their
# parent entity e.g. VERTEX -> POLYLINE:
if not linked(entity):
yield entity
block_records = self.block_records
section_head = cast("DXFTagStorage", entities[0])
if section_head.dxftype() != "SECTION" or section_head.base_class[1] != (
2,
"BLOCKS",
):
raise DXFStructureError("Critical structure error in BLOCKS section.")
# Remove SECTION entity
del entities[0]
content: list[DXFEntity] = []
block: Block = _MISSING_BLOCK_
for entity in link_entities():
if isinstance(entity, Block):
if block is not _MISSING_BLOCK_:
logger.warning("Missing required ENDBLK, ignoring content.")
block = entity
content.clear()
elif isinstance(entity, EndBlk):
if block is _MISSING_BLOCK_:
logger.warning(
"Found ENDBLK without a preceding BLOCK, ignoring content."
)
else:
block_name = block.dxf.get("name", "")
handle = block.dxf.get("handle", "<undefined>")
if not block_name:
block_name = recover_block_name(block)
if block_name:
logger.info(
f'Recovered block name "{block_name}" for block #{handle}.'
)
block.dxf.name = block_name
if block_name:
block_record = load_block_record(block, entity, content)
if isinstance(block_record, BlockRecord):
self.add(block_record)
else:
logger.warning(
f"Ignoring invalid BLOCK definition #{handle}."
)
else:
logger.warning(f"Ignoring BLOCK without name #{handle}.")
block = _MISSING_BLOCK_
content.clear()
else:
# No check for valid entities here:
# Use the audit or the recover module to fix invalid DXF files!
content.append(entity)
def _reconstruct_orphaned_block_records(self):
"""Find BLOCK_RECORD entries without block definition in the blocks
section and create block definitions for this orphaned block records.
"""
for block_record in self.block_records:
if block_record.block is None:
block = factory.create_db_entry(
"BLOCK",
dxfattribs={
"name": block_record.dxf.name,
"base_point": (0, 0, 0),
},
doc=self.doc,
)
endblk = factory.create_db_entry(
"ENDBLK",
dxfattribs={},
doc=self.doc,
)
block_record.set_block(block, endblk)
self.add(block_record)
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
tagwriter.write_str(" 0\nSECTION\n 2\nBLOCKS\n")
for block_record in self.block_records:
assert isinstance(block_record, BlockRecord)
block_record.export_block_definition(tagwriter)
tagwriter.write_tag2(0, "ENDSEC")
def add(self, block_record: BlockRecord) -> BlockLayout:
"""Add or replace a block layout object defined by its block record.
(internal API)
"""
block_layout = BlockLayout(block_record)
block_record.block_layout = block_layout
assert self.block_records.has_entry(block_record.dxf.name)
return block_layout
def __iter__(self) -> Iterator[BlockLayout]:
"""Iterable of all :class:`~ezdxf.layouts.BlockLayout` objects."""
return (block_record.block_layout for block_record in self.block_records)
def __contains__(self, name: str) -> bool:
"""Returns ``True`` if :class:`~ezdxf.layouts.BlockLayout` `name`
exist.
"""
return self.block_records.has_entry(name)
def __getitem__(self, name: str) -> BlockLayout:
"""Returns :class:`~ezdxf.layouts.BlockLayout` `name`,
raises :class:`DXFKeyError` if `name` not exist.
"""
try:
block_record = cast("BlockRecord", self.block_records.get(name))
return block_record.block_layout # type: ignore
except DXFTableEntryError:
raise DXFKeyError(name)
def __delitem__(self, name: str) -> None:
"""Deletes :class:`~ezdxf.layouts.BlockLayout` `name` and all of
its content, raises :class:`DXFKeyError` if `name` not exist.
"""
if name in self:
self.block_records.remove(name)
else:
raise DXFKeyError(f'Block "{name}" does not exist.')
def block_names(self) -> list[str]:
"""Returns a list of all block names."""
return list(self.doc.block_records.entries.keys()) # type: ignore
def get(self, name: str, default=None) -> BlockLayout:
"""Returns :class:`~ezdxf.layouts.BlockLayout` `name`, returns
`default` if `name` not exist.
"""
try:
return self.__getitem__(name)
except DXFKeyError:
return default
def get_block_layout_by_handle(self, block_record_handle: str) -> BlockLayout:
"""Returns a block layout by block record handle. (internal API)"""
return self.doc.entitydb[block_record_handle].block_layout # type: ignore
def new(
self,
name: str,
base_point: UVec = NULLVEC,
dxfattribs=None,
) -> BlockLayout:
"""Create and add a new :class:`~ezdxf.layouts.BlockLayout`, `name`
is the BLOCK name, `base_point` is the insertion point of the BLOCK.
"""
assert self.doc is not None
block_record = cast(BlockRecord, self.doc.block_records.new(name))
dxfattribs = dxfattribs or {}
dxfattribs["owner"] = block_record.dxf.handle
dxfattribs["name"] = name
dxfattribs["base_point"] = Vec3(base_point)
head = factory.create_db_entry("BLOCK", dxfattribs, self.doc)
tail = factory.create_db_entry(
"ENDBLK", {"owner": block_record.dxf.handle}, doc=self.doc
)
block_record.set_block(head, tail) # type: ignore
return self.add(block_record)
def new_anonymous_block(
self, type_char: str = "U", base_point: UVec = NULLVEC
) -> BlockLayout:
"""Create and add a new anonymous :class:`~ezdxf.layouts.BlockLayout`,
`type_char` is the BLOCK type, `base_point` is the insertion point of
the BLOCK.
========= ==========
type_char Anonymous Block Type
========= ==========
``'U'`` ``'*U###'`` anonymous BLOCK
``'E'`` ``'*E###'`` anonymous non-uniformly scaled BLOCK
``'X'`` ``'*X###'`` anonymous HATCH graphic
``'D'`` ``'*D###'`` anonymous DIMENSION graphic
``'A'`` ``'*A###'`` anonymous GROUP
``'T'`` ``'*T###'`` anonymous block for ACAD_TABLE content
========= ==========
"""
block_name = self.anonymous_block_name(type_char)
block = self.new(block_name, base_point, {"flags": const.BLK_ANONYMOUS})
return block
def anonymous_block_name(self, type_char: str) -> str:
"""Create name for an anonymous block. (internal API)
Args:
type_char: letter
U = *U### anonymous blocks
E = *E### anonymous non-uniformly scaled blocks
X = *X### anonymous hatches
D = *D### anonymous dimensions
A = *A### anonymous groups
T = *T### anonymous ACAD_TABLE content
"""
while True:
self._anonymous_block_counter += 1
block_name = f"*{type_char}{self._anonymous_block_counter}"
if not self.block_records.has_entry(block_name):
return block_name
def rename_block(self, old_name: str, new_name: str) -> None:
"""Rename :class:`~ezdxf.layouts.BlockLayout` `old_name` to `new_name`
.. warning::
This is a low-level tool and does not rename the block references,
so all block references to `old_name` are pointing to a non-existing
block definition!
"""
block_record = cast(BlockRecord, self.block_records.get(old_name))
block_record.rename(new_name)
self.block_records.replace(old_name, block_record)
self.add(block_record)
def delete_block(self, name: str, safe: bool = True) -> None:
"""Delete block.
Applies some safety checks when `safe` is ``True``.
A :class:`DXFBlockInUseError` will be raised for:
- blocks with active references
- blocks representing existing layouts
- special blocks used internally
Args:
name: block name (case-insensitive)
safe: apply safety checks
Raises:
DXFKeyError: if block not exists
DXFBlockInUseError: when safe is ``True`` and block is in use
"""
if safe:
assert self.doc is not None, "valid DXF document required"
block = self.doc.blocks.get(name)
if block is None:
raise DXFKeyError(f'Block "{name}" does not exist.')
if not block.is_alive:
return # block is already destroyed
if block.is_any_layout:
raise DXFBlockInUseError(
f'Block "{name}" represents an existing layout.'
)
if is_special_block(name):
raise DXFBlockInUseError(
f'Special block "{name}" maybe used without explicit INSERT entity.'
)
query_string = f'INSERT[name=="{name}"]i'
try:
block_refs = self.doc.query(query_string) # ignore case
except ParseException:
logger.error(f'Parsing error in query string: "{query_string}"')
return
if len(block_refs):
raise DXFBlockInUseError(f'Block "{name}" is still in use.')
self.__delitem__(name)
def delete_all_blocks(self) -> None:
"""Delete all blocks without references except modelspace- or
paperspace layout blocks, special arrow- and anonymous blocks
(DIMENSION, ACAD_TABLE).
.. warning::
There could exist references to blocks which are not documented in the DXF
reference, hidden in extended data sections or application defined data,
which could invalidate a DXF document if these blocks will be deleted.
"""
assert self.doc is not None
active_references = set(
validator.make_table_key(entity.dxf.name)
for entity in self.doc.query("INSERT")
)
def is_safe(name: str) -> bool:
if is_special_block(name):
return False
return name not in active_references
trash = set()
for block in self:
name = validator.make_table_key(block.name)
if not block.is_any_layout and is_safe(name):
trash.add(name)
for name in trash:
self.__delitem__(name)
def audit(self, auditor: Auditor) -> None:
"""Audit and repair BLOCKS section.
.. important::
Do not delete entities during the auditing process as this will alter
the entity database while iterating it, instead use::
auditor.trash(entity)
to delete invalid entities after auditing automatically.
"""
assert self.doc is auditor.doc, "Auditor for different DXF document."
for block_record in self.block_records:
assert isinstance(block_record, BlockRecord)
block_record_handle: str = block_record.dxf.handle
unlink_entities: list[DXFEntity] = []
es = block_record.entity_space
for entity in es:
if not is_graphic_entity(entity):
auditor.fixed_error(
code=AuditError.REMOVED_INVALID_GRAPHIC_ENTITY,
message=f"Removed invalid DXF entity {str(entity)} from"
f" BLOCK '{block_record.dxf.name}'.",
)
auditor.trash(entity)
elif isinstance(entity, Attrib):
# ATTRIB can only exist as an attached entity of the INSERT
# entity!
auditor.fixed_error(
code=AuditError.REMOVED_STANDALONE_ATTRIB_ENTITY,
message=f"Removed standalone {str(entity)} entity from"
f" BLOCK '{block_record.dxf.name}'.",
)
auditor.trash(entity)
if not entity.is_alive:
continue
if entity.dxf.owner != block_record_handle:
auditor.fixed_error(
code=AuditError.REMOVED_ENTITY_WITH_INVALID_OWNER_HANDLE,
message=f"Removed DXF entity {str(entity)} with invalid owner "
f"handle (#{entity.dxf.owner} != #{block_record_handle}) "
f"from BLOCK '{block_record.dxf.name}'.",
)
# do not destroy the entity, it's maybe owned to another block
unlink_entities.append(entity)
for entity in unlink_entities:
if entity.is_alive:
try:
es.remove(entity)
except ValueError:
pass
@@ -0,0 +1,324 @@
# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterator, Iterable, Union, cast, Optional
from collections import Counter, OrderedDict
import logging
from ezdxf.lldxf.const import DXFStructureError, DXF2004, DXF2000, DXFKeyError
from ezdxf.entities.dxfclass import DXFClass
from ezdxf.entities.dxfentity import DXFEntity, DXFTagStorage
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.lldxf.tagwriter import AbstractTagWriter
logger = logging.getLogger("ezdxf")
# name: cpp_class_name (2), app_name (3), flags(90), was_a_proxy (280),
# is_an_entity (281)
# Multiple entries for 'name' are possible and supported, ClassSection stores
# entries with key: (name, cpp_class_name).
# 0 <ctrl> CLASS
# 1 <str> MPOLYGON
# 2 <str> AcDbMPolygon
# 3 <str> "AcMPolygonObj15|Version(1.0.0.0) Product Desc: Object enabler for the AcDbMPolyg ... odesk.com"
# 90 <int> 3071, b101111111111
# 280 <int> 0
# 281 <int> 1
CLASS_DEFINITIONS = {
"ACDBDICTIONARYWDFLT": [
"AcDbDictionaryWithDefault",
"ObjectDBX Classes",
0,
0,
0,
],
"SUN": ["AcDbSun", "SCENEOE", 1153, 0, 0],
"DICTIONARYVAR": ["AcDbDictionaryVar", "ObjectDBX Classes", 0, 0, 0],
"TABLESTYLE": ["AcDbTableStyle", "ObjectDBX Classes", 4095, 0, 0],
"MATERIAL": ["AcDbMaterial", "ObjectDBX Classes", 1153, 0, 0],
"VISUALSTYLE": ["AcDbVisualStyle", "ObjectDBX Classes", 4095, 0, 0],
"SCALE": ["AcDbScale", "ObjectDBX Classes", 1153, 0, 0],
"MLEADERSTYLE": ["AcDbMLeaderStyle", "ACDB_MLEADERSTYLE_CLASS", 4095, 0, 0],
"MLEADER": ["AcDbMLeader", "ACDB_MLEADER_CLASS", 3071, 0, 1],
"MPOLYGON": ["AcDbMPolygon", "AcMPolygonObj15", 1025, 0, 1],
"CELLSTYLEMAP": ["AcDbCellStyleMap", "ObjectDBX Classes", 1152, 0, 0],
"EXACXREFPANELOBJECT": ["ExAcXREFPanelObject", "EXAC_ESW", 1025, 0, 0],
"NPOCOLLECTION": [
"AcDbImpNonPersistentObjectsCollection",
"ObjectDBX Classes",
1153,
0,
0,
],
"LAYER_INDEX": ["AcDbLayerIndex", "ObjectDBX Classes", 0, 0, 0],
"SPATIAL_INDEX": ["AcDbSpatialIndex", "ObjectDBX Classes", 0, 0, 0],
"IDBUFFER": ["AcDbIdBuffer", "ObjectDBX Classes", 0, 0, 0],
"DIMASSOC": ["AcDbDimAssoc", "AcDbDimAssoc", 0, 0, 0],
"ACDBSECTIONVIEWSTYLE": [
"AcDbSectionViewStyle",
"ObjectDBX Classes",
1025,
0,
0,
],
"ACDBDETAILVIEWSTYLE": [
"AcDbDetailViewStyle",
"ObjectDBX Classes",
1025,
0,
0,
],
"IMAGEDEF": ["AcDbRasterImageDef", "ISM", 0, 0, 0],
"RASTERVARIABLES": ["AcDbRasterVariables", "ISM", 0, 0, 0],
"IMAGEDEF_REACTOR": ["AcDbRasterImageDefReactor", "ISM", 1, 0, 0],
"IMAGE": ["AcDbRasterImage", "ISM", 2175, 0, 1],
"PDFDEFINITION": ["AcDbPdfDefinition", "ObjectDBX Classes", 1153, 0, 0],
"PDFUNDERLAY": ["AcDbPdfReference", "ObjectDBX Classes", 4095, 0, 1],
"DWFDEFINITION": ["AcDbDwfDefinition", "ObjectDBX Classes", 1153, 0, 0],
"DWFUNDERLAY": ["AcDbDwfReference", "ObjectDBX Classes", 1153, 0, 1],
"DGNDEFINITION": ["AcDbDgnDefinition", "ObjectDBX Classes", 1153, 0, 0],
"DGNUNDERLAY": ["AcDbDgnReference", "ObjectDBX Classes", 1153, 0, 1],
"MENTALRAYRENDERSETTINGS": [
"AcDbMentalRayRenderSettings",
"SCENEOE",
1024,
0,
0,
],
"ACDBPLACEHOLDER": ["AcDbPlaceHolder", "ObjectDBX Classes", 0, 0, 0],
"LAYOUT": ["AcDbLayout", "ObjectDBX Classes", 0, 0, 0],
"SURFACE": ["AcDbSurface", "ObjectDBX Classes", 4095, 0, 1],
"EXTRUDEDSURFACE": ["AcDbExtrudedSurface", "ObjectDBX Classes", 4095, 0, 1],
"LOFTEDSURFACE": ["AcDbLoftedSurface", "ObjectDBX Classes", 0, 0, 1],
"REVOLVEDSURFACE": ["AcDbRevolvedSurface", "ObjectDBX Classes", 0, 0, 1],
"SWEPTSURFACE": ["AcDbSweptSurface", "ObjectDBX Classes", 0, 0, 1],
"PLANESURFACE": ["AcDbPlaneSurface", "ObjectDBX Classes", 4095, 0, 1],
"NURBSSURFACE": ["AcDbNurbSurface", "ObjectDBX Classes", 4095, 0, 1],
"ACDBASSOCEXTRUDEDSURFACEACTIONBODY": [
"AcDbAssocExtrudedSurfaceActionBody",
"ObjectDBX Classes",
1025,
0,
0,
],
"ACDBASSOCLOFTEDSURFACEACTIONBODY": [
"AcDbAssocLoftedSurfaceActionBody",
"ObjectDBX Classes",
1025,
0,
0,
],
"ACDBASSOCREVOLVEDSURFACEACTIONBODY": [
"AcDbAssocRevolvedSurfaceActionBody",
"ObjectDBX Classes",
1025,
0,
0,
],
"ACDBASSOCSWEPTSURFACEACTIONBODY": [
"AcDbAssocSweptSurfaceActionBody",
"ObjectDBX Classes",
1025,
0,
0,
],
"HELIX": ["AcDbHelix", "ObjectDBX Classes", 4095, 0, 1],
"WIPEOUT": ["AcDbWipeout", "WipeOut", 127, 0, 1],
"WIPEOUTVARIABLES": ["AcDbWipeoutVariables", "WipeOut", 0, 0, 0],
"FIELDLIST": ["AcDbFieldList", "ObjectDBX Classes", 1152, 0, 0],
"GEODATA": ["AcDbGeoData", "ObjectDBX Classes", 4095, 0, 0],
"SORTENTSTABLE": ["AcDbSortentsTable", "ObjectDBX Classes", 0, 0, 0],
"ACAD_TABLE": ["AcDbTable", "ObjectDBX Classes", 1025, 0, 1],
"ARC_DIMENSION": ["AcDbArcDimension", "ObjectDBX Classes", 1025, 0, 1],
"LARGE_RADIAL_DIMENSION": [
"AcDbRadialDimensionLarge",
"ObjectDBX Classes",
1025,
0,
1,
],
}
REQ_R2000 = [
"ACDBDICTIONARYWDFLT",
"SUN",
"VISUALSTYLE",
"MATERIAL",
"SCALE",
"TABLESTYLE",
"MLEADERSTYLE",
"DICTIONARYVAR",
"CELLSTYLEMAP",
"MENTALRAYRENDERSETTINGS",
"ACDBDETAILVIEWSTYLE",
"ACDBSECTIONVIEWSTYLE",
"RASTERVARIABLES",
"ACDBPLACEHOLDER",
"LAYOUT",
]
REQ_R2004 = [
"ACDBDICTIONARYWDFLT",
"SUN",
"VISUALSTYLE",
"MATERIAL",
"SCALE",
"TABLESTYLE",
"MLEADERSTYLE",
"DICTIONARYVAR",
"CELLSTYLEMAP",
"MENTALRAYRENDERSETTINGS",
"ACDBDETAILVIEWSTYLE",
"ACDBSECTIONVIEWSTYLE",
"RASTERVARIABLES",
]
REQUIRED_CLASSES = {
DXF2000: REQ_R2000,
DXF2004: REQ_R2004,
}
class ClassesSection:
def __init__(
self,
doc: Optional[Drawing] = None,
entities: Optional[Iterable[DXFEntity]] = None,
):
# Multiple entries for 'name' possible -> key is (name, cpp_class_name)
# DXFClasses are not stored in the entities database, because CLASS has
# no handle.
self.classes: dict[tuple[str, str], DXFClass] = OrderedDict()
self.doc = doc
if entities is not None:
self.load(iter(entities))
def __iter__(self) -> Iterator[DXFClass]:
return (cls for cls in self.classes.values())
def load(self, entities: Iterator[DXFEntity]) -> None:
section_head = cast(DXFTagStorage, next(entities))
if section_head.dxftype() != "SECTION" or section_head.base_class[1] != (
2,
"CLASSES",
):
raise DXFStructureError("Critical structure error in CLASSES section.")
for cls_entity in entities:
if isinstance(cls_entity, DXFClass):
self.register(cls_entity)
else:
logger.warning(
f"Ignored invalid DXF entity type '{cls_entity.dxftype()}'"
f" in section CLASSES."
)
def register(
self, classes: Optional[Union[DXFClass, Iterable[DXFClass]]] = None
) -> None:
if classes is None:
return
if isinstance(classes, DXFClass):
classes = (classes,)
for dxfclass in classes:
key = dxfclass.key
if key not in self.classes:
self.classes[key] = dxfclass
def add_class(self, name: str):
"""Register a known class by `name`."""
if name not in CLASS_DEFINITIONS:
return
cls_data = CLASS_DEFINITIONS[name]
cls = DXFClass.new(doc=self.doc)
cpp, app, flags, proxy, entity = cls_data
cls.update_dxf_attribs(
{
"name": name,
"cpp_class_name": cpp,
"app_name": app,
"flags": flags,
"was_a_proxy": proxy,
"is_an_entity": entity,
}
)
self.register(cls)
def get(self, name: str) -> DXFClass:
"""Returns the first class matching `name`.
Storage key is the ``(name, cpp_class_name)`` tuple, because there are
some classes with the same :attr:`name` but different
:attr:`cpp_class_names`.
"""
for cls in self.classes.values():
if cls.dxf.name == name:
return cls
raise DXFKeyError(name)
def add_required_classes(self, dxfversion: str) -> None:
"""Add all required CLASS definitions for the specified DXF version."""
names = REQUIRED_CLASSES.get(dxfversion, REQ_R2004)
for name in names:
self.add_class(name)
if self.doc is None: # testing environment SUT
return
dxf_types_in_use = self.doc.entitydb.dxf_types_in_use()
if "IMAGE" in dxf_types_in_use:
self.add_class("IMAGE")
self.add_class("IMAGEDEF")
self.add_class("IMAGEDEF_REACTOR")
if "PDFUNDERLAY" in dxf_types_in_use:
self.add_class("PDFDEFINITION")
self.add_class("PDFUNDERLAY")
if "DWFUNDERLAY" in dxf_types_in_use:
self.add_class("DWFDEFINITION")
self.add_class("DWFUNDERLAY")
if "DGNUNDERLAY" in dxf_types_in_use:
self.add_class("DGNDEFINITION")
self.add_class("DGNUNDERLAY")
if "EXTRUDEDSURFACE" in dxf_types_in_use:
self.add_class("EXTRUDEDSURFACE")
self.add_class("ACDBASSOCEXTRUDEDSURFACEACTIONBODY")
if "LOFTEDSURFACE" in dxf_types_in_use:
self.add_class("LOFTEDSURFACE")
self.add_class("ACDBASSOCLOFTEDSURFACEACTIONBODY")
if "REVOLVEDSURFACE" in dxf_types_in_use:
self.add_class("REVOLVEDSURFACE")
self.add_class("ACDBASSOCREVOLVEDSURFACEACTIONBODY")
if "SWEPTSURFACE" in dxf_types_in_use:
self.add_class("SWEPTSURFACE")
self.add_class("ACDBASSOCSWEPTSURFACEACTIONBODY")
for dxftype in dxf_types_in_use:
self.add_class(dxftype)
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
"""Export DXF tags. (internal API)"""
tagwriter.write_str(" 0\nSECTION\n 2\nCLASSES\n")
for dxfclass in self.classes.values():
dxfclass.export_dxf(tagwriter)
tagwriter.write_str(" 0\nENDSEC\n")
def update_instance_counters(self) -> None:
"""Update CLASS instance counter for all registered classes, requires
DXF R2004+.
"""
assert self.doc is not None
if self.doc.dxfversion < DXF2004:
return # instance counter not supported
counter: dict[str, int] = Counter()
# count all entities in the entity database
for entity in self.doc.entitydb.values():
counter[entity.dxftype()] += 1
for dxfclass in self.classes.values():
dxfclass.dxf.instance_count = counter[dxfclass.dxf.name]
@@ -0,0 +1,113 @@
# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Iterator, cast, Optional
from itertools import chain
import logging
from ezdxf.lldxf import const
from ezdxf.entities import entity_linker
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.entities import DXFEntity, DXFTagStorage, BlockRecord, DXFGraphic
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.lldxf.tags import Tags
logger = logging.getLogger("ezdxf")
class StoredSection:
def __init__(self, entities: list[Tags]):
self.entities = entities
def export_dxf(self, tagwriter: AbstractTagWriter):
# (0, SECTION) (2, NAME) is stored in entities
for entity in self.entities:
tagwriter.write_tags(entity)
# ENDSEC not stored in entities !!!
tagwriter.write_str(" 0\nENDSEC\n")
class EntitySection:
""":class:`EntitiesSection` is just a proxy for :class:`Modelspace` and
active :class:`Paperspace` linked together.
"""
def __init__(
self,
doc: Optional[Drawing] = None,
entities: Optional[Iterable[DXFEntity]] = None,
):
self.doc = doc
if entities is not None:
self._build(iter(entities))
def __iter__(self) -> Iterator[DXFEntity]:
"""Returns an iterator for all entities of the modelspace and the active
paperspace.
"""
assert self.doc is not None
layouts = self.doc.layouts
for entity in chain(layouts.modelspace(), layouts.active_layout()):
yield entity
def __len__(self) -> int:
"""Returns the count of all entities in the modelspace and the active paperspace.
"""
assert self.doc is not None
layouts = self.doc.layouts
return len(layouts.modelspace()) + len(layouts.active_layout())
# none public interface
def _build(self, entities: Iterator[DXFEntity]) -> None:
assert self.doc is not None
section_head = cast("DXFTagStorage", next(entities))
if section_head.dxftype() != "SECTION" or section_head.base_class[
1
] != (2, "ENTITIES"):
raise const.DXFStructureError(
"Critical structure error in ENTITIES section."
)
def add(entity: DXFGraphic):
handle = entity.dxf.owner
# higher priority for owner handle
paperspace = 0
if handle == msp_layout_key:
paperspace = 0
elif handle == psp_layout_key:
paperspace = 1
elif entity.dxf.hasattr(
"paperspace"
): # paperspace flag as fallback
paperspace = entity.dxf.paperspace
if paperspace:
psp.add_entity(entity)
else:
msp.add_entity(entity)
msp = cast("BlockRecord", self.doc.block_records.get("*Model_Space"))
psp = cast("BlockRecord", self.doc.block_records.get("*Paper_Space"))
msp_layout_key: str = msp.dxf.handle
psp_layout_key: str = psp.dxf.handle
linked_entities = entity_linker()
# Don't store linked entities (VERTEX, ATTRIB, SEQEND) in entity space
for entity in entities:
# No check for valid entities here:
# Use the audit- or the recover module to fix invalid DXF files!
if not linked_entities(entity):
add(entity) # type: ignore
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
assert self.doc is not None
layouts = self.doc.layouts
tagwriter.write_str(" 0\nSECTION\n 2\nENTITIES\n")
# Just write *Model_Space and the active *Paper_Space into the
# ENTITIES section.
layouts.modelspace().entity_space.export_dxf(tagwriter)
layouts.active_layout().entity_space.export_dxf(tagwriter)
tagwriter.write_tag2(0, "ENDSEC")
@@ -0,0 +1,371 @@
# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
Any,
Iterable,
Iterator,
KeysView,
Optional,
Sequence,
TYPE_CHECKING,
Union,
)
import logging
from collections import OrderedDict
from ezdxf.lldxf import const
from ezdxf.lldxf.tags import group_tags, Tags, DXFTag
from ezdxf.lldxf.types import strtag
from ezdxf.lldxf.validator import header_validator
from ezdxf.sections.headervars import (
HEADER_VAR_MAP,
version_specific_group_code,
)
if TYPE_CHECKING:
from ezdxf.lldxf.tagwriter import AbstractTagWriter
logger = logging.getLogger("ezdxf")
MIN_HEADER_TEXT = """ 0
SECTION
2
HEADER
9
$ACADVER
1
AC1009
9
$DWGCODEPAGE
3
ANSI_1252
9
$HANDSEED
5
FF
"""
# Additional variables may be stored as DICTIONARYVAR in the OBJECTS
# section in the DICTIONARY "AcDbVariableDictionary" of the root dict.
# - CANNOSCALE
# - CENTEREXE
# - CENTERLTYPEFILE
# - CETRANSPARENCY
# - CMLEADERSTYLE
# - CTABLESTYLE
# - CVIEWDETAILSTYLE
# - CVIEWSECTIONSTYLE
# - LAYEREVAL
# - LAYERNOTIFY
# - LIGHTINGUNITS
# - MSLTSCALE
class CustomVars:
"""The :class:`CustomVars` class stores custom properties in the DXF header as
$CUSTOMPROPERTYTAG and $CUSTOMPROPERTY values. Custom properties require DXF R2004
or later, `ezdxf` can create custom properties for older DXF versions as well, but
AutoCAD will not show that properties.
"""
def __init__(self) -> None:
self.properties: list[tuple[str, str]] = list()
def __len__(self) -> int:
"""Count of custom properties."""
return len(self.properties)
def __iter__(self) -> Iterator[tuple[str, str]]:
"""Iterate over all custom properties as ``(tag, value)`` tuples."""
return iter(self.properties)
def clear(self) -> None:
"""Remove all custom properties."""
self.properties.clear()
def append(self, tag: str, value: str) -> None:
"""Add custom property as ``(tag, value)`` tuple."""
# custom properties always stored as strings
self.properties.append((tag, str(value)))
def get(self, tag: str, default: Optional[str] = None):
"""Returns the value of the first custom property `tag`."""
for key, value in self.properties:
if key == tag:
return value
else:
return default
def has_tag(self, tag: str) -> bool:
"""Returns ``True`` if custom property `tag` exist."""
return self.get(tag) is not None
def remove(self, tag: str, all: bool = False) -> None:
"""Removes the first occurrence of custom property `tag`, removes all
occurrences if `all` is ``True``.
Raises `:class:`DXFValueError` if `tag` does not exist.
"""
found_tag = False
for item in self.properties:
if item[0] == tag:
self.properties.remove(item)
found_tag = True
if not all:
return
if not found_tag:
raise const.DXFValueError(f"Tag '{tag}' does not exist")
def replace(self, tag: str, value: str) -> None:
"""Replaces the value of the first custom property `tag` by a new
`value`.
Raises :class:`DXFValueError` if `tag` does not exist.
"""
properties = self.properties
for index in range(len(properties)):
name = properties[index][0]
if name == tag:
properties[index] = (name, value)
return
raise const.DXFValueError(f"Tag '{tag}' does not exist")
def write(self, tagwriter: AbstractTagWriter) -> None:
"""Export custom properties as DXF tags. (internal API)"""
for tag, value in self.properties:
s = f" 9\n$CUSTOMPROPERTYTAG\n 1\n{tag}\n 9\n$CUSTOMPROPERTY\n 1\n{value}\n"
tagwriter.write_str(s)
def default_vars() -> OrderedDict:
vars = OrderedDict()
for vardef in HEADER_VAR_MAP.values():
vars[vardef.name] = HeaderVar(DXFTag(vardef.code, vardef.default))
return vars
class HeaderSection:
MIN_HEADER_TAGS = Tags.from_text(MIN_HEADER_TEXT)
name = "HEADER"
def __init__(self) -> None:
self.hdrvars: dict[str, HeaderVar] = OrderedDict()
self.custom_vars = CustomVars()
@classmethod
def load(cls, tags: Optional[Iterable[DXFTag]] = None) -> HeaderSection:
"""Constructor to generate header variables loaded from DXF files
(untrusted environment).
Args:
tags: DXF tags as Tags() or ExtendedTags()
(internal API)
"""
if tags is None: # create default header
# files without a header have the default version: R12
return cls.new(dxfversion=const.DXF12)
section = cls()
section.load_tags(iter(tags))
return section
@classmethod
def new(cls, dxfversion=const.LATEST_DXF_VERSION) -> HeaderSection:
section = HeaderSection()
section.hdrvars = default_vars()
section["$ACADVER"] = dxfversion
return section
def load_tags(self, tags: Iterable[DXFTag]) -> None:
"""Constructor to generate header variables loaded from DXF files
(untrusted environment).
Args:
tags: DXF tags as Tags() or ExtendedTags()
"""
_tags = iter(tags) or iter(self.MIN_HEADER_TAGS)
section_tag = next(_tags)
name_tag = next(_tags)
if section_tag != (0, "SECTION") or name_tag != (2, "HEADER"):
raise const.DXFStructureError("Critical structure error in HEADER section.")
groups = group_tags(header_validator(_tags), splitcode=9)
custom_property_stack = [] # collect $CUSTOMPROPERTY/TAG
for group in groups:
name = group[0].value
value = group[1]
if name in ("$CUSTOMPROPERTYTAG", "$CUSTOMPROPERTY"):
custom_property_stack.append(value.value)
else:
self.hdrvars[name] = HeaderVar(value)
custom_property_stack.reverse()
while len(custom_property_stack):
try:
self.custom_vars.append(
tag=custom_property_stack.pop(),
value=custom_property_stack.pop(),
)
except IndexError: # internal exception
break
@classmethod
def from_text(cls, text: str) -> HeaderSection:
"""Load constructor from text for testing"""
return cls.load(Tags.from_text(text))
def _headervar_factory(self, key: str, value: Any) -> DXFTag:
if key in HEADER_VAR_MAP:
factory = HEADER_VAR_MAP[key].factory
return factory(value)
else:
raise const.DXFKeyError(f"Invalid header variable {key}.")
def __len__(self) -> int:
"""Returns count of header variables."""
return len(self.hdrvars)
def __contains__(self, key) -> bool:
"""Returns ``True`` if header variable `key` exist."""
return key in self.hdrvars
def varnames(self) -> KeysView:
"""Returns an iterable of all header variable names."""
return self.hdrvars.keys()
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
"""Exports header section as DXF tags. (internal API)"""
def _write(name: str, value: Any) -> None:
if value.value is None:
logger.info(f"did not write header var {name}, value is None.")
return
tagwriter.write_tag2(9, name)
group_code = version_specific_group_code(name, dxfversion)
# fix invalid group codes
if group_code != value.code:
value = HeaderVar((group_code, value.value))
tagwriter.write_str(str(value))
dxfversion: str = tagwriter.dxfversion
write_handles = tagwriter.write_handles
tagwriter.write_str(" 0\nSECTION\n 2\nHEADER\n")
save = self["$ACADVER"]
self["$ACADVER"] = dxfversion
for name, value in header_vars_by_priority(self.hdrvars, dxfversion):
if not write_handles and name == "$HANDSEED":
continue # skip $HANDSEED
_write(name, value)
if name == "$LASTSAVEDBY": # ugly hack, but necessary for AutoCAD
self.custom_vars.write(tagwriter)
self["$ACADVER"] = save
tagwriter.write_str(" 0\nENDSEC\n")
def get(self, key: str, default: Any = None) -> Any:
"""Returns value of header variable `key` if exist, else the `default`
value.
"""
if key in self.hdrvars:
return self.__getitem__(key)
else:
return default
def __getitem__(self, key: str) -> Any:
"""Get header variable `key` by index operator like:
:code:`drawing.header['$ACADVER']`
"""
try:
return self.hdrvars[key].value
except KeyError: # map exception
raise const.DXFKeyError(str(key))
def __setitem__(self, key: str, value: Any) -> None:
"""Set header variable `key` to `value` by index operator like:
:code:`drawing.header['$ANGDIR'] = 1`
"""
try:
tags = self._headervar_factory(key, value)
except (IndexError, ValueError):
raise const.DXFValueError(str(value))
self.hdrvars[key] = HeaderVar(tags)
def __delitem__(self, key: str) -> None:
"""Delete header variable `key` by index operator like:
:code:`del drawing.header['$ANGDIR']`
"""
try:
del self.hdrvars[key]
except KeyError: # map exception
raise const.DXFKeyError(str(key))
def reset_wcs(self):
"""Reset the current UCS settings to the :ref:`WCS`."""
self["$UCSBASE"] = ""
self["$UCSNAME"] = ""
self["$UCSORG"] = (0, 0, 0)
self["$UCSXDIR"] = (1, 0, 0)
self["$UCSYDIR"] = (0, 1, 0)
self["$UCSORTHOREF"] = ""
self["$UCSORTHOVIEW"] = 0
self["$UCSORGTOP"] = (0, 0, 0)
self["$UCSORGBOTTOM"] = (0, 0, 0)
self["$UCSORGLEFT"] = (0, 0, 0)
self["$UCSORGRIGHT"] = (0, 0, 0)
self["$UCSORGFRONT"] = (0, 0, 0)
self["$UCSORGBACK"] = (0, 0, 0)
def header_vars_by_priority(
header_vars: dict[str, HeaderVar], dxfversion: str
) -> Iterable[tuple]:
order = []
for name, value in header_vars.items():
vardef = HEADER_VAR_MAP.get(name, None)
if vardef is None:
logger.info(f"Header variable {name} ignored, dxfversion={dxfversion}.")
continue
if vardef.mindxf <= dxfversion <= vardef.maxdxf:
order.append((vardef.priority, (name, value)))
order.sort()
for priority, tag in order:
yield tag
class HeaderVar:
def __init__(self, tag: Union[DXFTag, Sequence]):
self.tag = tag
@property
def code(self) -> int:
return self.tag[0]
@property
def value(self) -> Any:
return self.tag[1]
@property
def ispoint(self) -> bool:
return self.code == 10
def __str__(self) -> str:
if self.ispoint:
code, value = self.tag
s = []
for coord in value:
s.append(strtag((code, coord)))
code += 10
return "".join(s)
else:
return strtag(self.tag) # type: ignore
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,725 @@
# Copyright (c) 2011-2023, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
Iterable,
Iterator,
Optional,
TYPE_CHECKING,
Union,
cast,
)
import logging
from ezdxf.entities.dictionary import Dictionary
from ezdxf.entities import factory, is_dxf_object
from ezdxf.lldxf import const, validator
from ezdxf.entitydb import EntitySpace, EntityDB
from ezdxf.query import EntityQuery
from ezdxf.tools.handle import UnderlayKeyGenerator
from ezdxf.audit import Auditor, AuditError
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.entities import (
GeoData,
DictionaryVar,
DXFTagStorage,
DXFObject,
ImageDef,
UnderlayDefinition,
DictionaryWithDefault,
XRecord,
Placeholder,
)
from ezdxf.entities.image import ImageDefReactor
logger = logging.getLogger("ezdxf")
class ObjectsSection:
def __init__(self, doc: Drawing, entities: Optional[Iterable[DXFObject]] = None):
self.doc = doc
self.underlay_key_generator = UnderlayKeyGenerator()
self._entity_space = EntitySpace()
if entities is not None:
self._build(iter(entities))
@property
def entitydb(self) -> EntityDB:
"""Returns drawing entity database. (internal API)"""
return self.doc.entitydb
def get_entity_space(self) -> EntitySpace:
"""Returns entity space. (internal API)"""
return self._entity_space
def next_underlay_key(self, checkfunc=lambda k: True) -> str:
while True:
key = self.underlay_key_generator.next()
if checkfunc(key):
return key
def _build(self, entities: Iterator[DXFObject]) -> None:
section_head = cast("DXFTagStorage", next(entities))
if section_head.dxftype() != "SECTION" or section_head.base_class[1] != (
2,
"OBJECTS",
):
raise const.DXFStructureError(
"Critical structure error in the OBJECTS section."
)
for entity in entities:
# No check for valid entities here:
# Use the audit or the recover module to fix invalid DXF files!
self._entity_space.add(entity)
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
"""Export DXF entity by `tagwriter`. (internal API)"""
tagwriter.write_str(" 0\nSECTION\n 2\nOBJECTS\n")
self._entity_space.export_dxf(tagwriter)
tagwriter.write_tag2(0, "ENDSEC")
def new_entity(self, _type: str, dxfattribs: dict) -> DXFObject:
"""Create new DXF object, add it to the entity database and to the
entity space.
Args:
_type: DXF type like `DICTIONARY`
dxfattribs: DXF attributes as dict
(internal API)
"""
dxf_entity = factory.create_db_entry(_type, dxfattribs, self.doc)
self._entity_space.add(dxf_entity)
return dxf_entity # type: ignore
def delete_entity(self, entity: DXFObject) -> None:
"""Remove `entity` from entity space and destroy object. (internal API)"""
self._entity_space.remove(entity)
self.entitydb.delete_entity(entity)
def delete_all_entities(self) -> None:
"""Delete all DXF objects. (internal API)"""
db = self.entitydb
for entity in self._entity_space:
db.delete_entity(entity)
self._entity_space.clear()
def setup_rootdict(self) -> Dictionary:
"""Create a root dictionary. Has to be the first object in the objects
section. (internal API)"""
if len(self):
raise const.DXFStructureError(
"Can not create root dictionary in none empty objects section."
)
logger.debug("Creating ROOT dictionary.")
# root directory has no owner
return self.add_dictionary(owner="0", hard_owned=False)
def setup_object_management_tables(self, rootdict: Dictionary) -> None:
"""Setup required management tables. (internal API)"""
def setup_plot_style_name_table():
plot_style_name_dict = self.add_dictionary_with_default(
owner=rootdict.dxf.handle,
hard_owned=False,
)
placeholder = self.add_placeholder(owner=plot_style_name_dict.dxf.handle)
plot_style_name_dict.set_default(placeholder)
plot_style_name_dict["Normal"] = placeholder
rootdict["ACAD_PLOTSTYLENAME"] = plot_style_name_dict
def restore_table_handle(table_name: str, handle: str) -> None:
# The original object table does not exist, but the handle is maybe
# used by some DXF entities. Try to restore the original table
# handle.
new_table = rootdict.get(table_name)
if isinstance(new_table, Dictionary) and self.doc.entitydb.reset_handle(
new_table, handle
):
logger.debug(f"reset handle of table {table_name} to #{handle}")
# check required object tables:
for name in _OBJECT_TABLE_NAMES:
table: Union[Dictionary, str, None] = rootdict.get(name) # type: ignore
# Dictionary: existing table object
# str: handle of not existing table object
# None: no entry in the rootdict for "name"
if isinstance(table, Dictionary):
continue # skip existing tables
logger.info(f"creating {name} dictionary")
if name == "ACAD_PLOTSTYLENAME":
setup_plot_style_name_table()
else:
rootdict.add_new_dict(name, hard_owned=False)
if isinstance(table, str) and validator.is_handle(table):
restore_table_handle(name, handle=table)
def add_object(self, entity: DXFObject) -> None:
"""Add `entity` to OBJECTS section. (internal API)"""
if is_dxf_object(entity):
self._entity_space.add(entity)
else:
raise const.DXFTypeError(
f"invalid DXF type {entity.dxftype()} for OBJECTS section"
)
def add_dxf_object_with_reactor(self, dxftype: str, dxfattribs) -> DXFObject:
"""Add DXF object with reactor. (internal API)"""
dxfobject = self.new_entity(dxftype, dxfattribs)
dxfobject.set_reactors([dxfattribs["owner"]])
return dxfobject
def purge(self):
self._entity_space.purge()
# start of public interface
@property
def rootdict(self) -> Dictionary:
"""Returns the root DICTIONARY, or as AutoCAD calls it:
the named DICTIONARY.
"""
if len(self):
return self._entity_space[0] # type: ignore
else:
return self.setup_rootdict()
def __len__(self) -> int:
"""Returns the count of all DXF objects in the OBJECTS section."""
return len(self._entity_space)
def __iter__(self) -> Iterator[DXFObject]:
"""Returns an iterator of all DXF objects in the OBJECTS section."""
return iter(self._entity_space) # type: ignore
def __getitem__(self, index) -> DXFObject:
"""Get entity at `index`.
The underlying data structure for storing DXF objects is organized like
a standard Python list, therefore `index` can be any valid list indexing
or slicing term, like a single index ``objects[-1]`` to get the last
entity, or an index slice ``objects[:10]`` to get the first 10 or fewer
objects as ``list[DXFObject]``.
"""
return self._entity_space[index] # type: ignore
def __contains__(self, entity):
"""Returns ``True`` if `entity` stored in OBJECTS section.
Args:
entity: :class:`DXFObject` or handle as hex string
"""
if isinstance(entity, str):
try:
entity = self.entitydb[entity]
except KeyError:
return False
return entity in self._entity_space
def query(self, query: str = "*") -> EntityQuery:
"""Get all DXF objects matching the :ref:`entity query string`."""
return EntityQuery(iter(self), query)
def audit(self, auditor: Auditor) -> None:
"""Audit and repair OBJECTS section.
.. important::
Do not delete entities while auditing process, because this
would alter the entity database while iterating, instead use::
auditor.trash(entity)
to delete invalid entities after auditing automatically.
"""
assert self.doc is auditor.doc, "Auditor for different DXF document."
for entity in self._entity_space:
if not is_dxf_object(entity):
auditor.fixed_error(
code=AuditError.REMOVED_INVALID_DXF_OBJECT,
message=f"Removed invalid DXF entity {str(entity)} "
f"from OBJECTS section.",
)
auditor.trash(entity)
self.reorg(auditor)
self._entity_space.audit(auditor)
def add_dictionary(self, owner: str = "0", hard_owned: bool = True) -> Dictionary:
"""Add new :class:`~ezdxf.entities.Dictionary` object.
Args:
owner: handle to owner as hex string.
hard_owned: ``True`` to treat entries as hard owned.
"""
entity = self.new_entity(
"DICTIONARY",
dxfattribs={
"owner": owner,
"hard_owned": hard_owned,
},
)
return cast(Dictionary, entity)
def add_dictionary_with_default(
self, owner="0", default="0", hard_owned: bool = True
) -> DictionaryWithDefault:
"""Add new :class:`~ezdxf.entities.DictionaryWithDefault` object.
Args:
owner: handle to owner as hex string.
default: handle to default entry.
hard_owned: ``True`` to treat entries as hard owned.
"""
entity = self.new_entity(
"ACDBDICTIONARYWDFLT",
dxfattribs={
"owner": owner,
"default": default,
"hard_owned": hard_owned,
},
)
return cast("DictionaryWithDefault", entity)
def add_dictionary_var(self, owner: str = "0", value: str = "") -> DictionaryVar:
"""Add a new :class:`~ezdxf.entities.DictionaryVar` object.
Args:
owner: handle to owner as hex string.
value: value as string
"""
return self.new_entity( # type: ignore
"DICTIONARYVAR", dxfattribs={"owner": owner, "value": value}
)
def add_xrecord(self, owner: str = "0") -> XRecord:
"""Add a new :class:`~ezdxf.entities.XRecord` object.
Args:
owner: handle to owner as hex string.
"""
return self.new_entity("XRECORD", dxfattribs={"owner": owner}) # type: ignore
def add_placeholder(self, owner: str = "0") -> Placeholder:
"""Add a new :class:`~ezdxf.entities.Placeholder` object.
Args:
owner: handle to owner as hex string.
"""
return self.new_entity( # type: ignore
"ACDBPLACEHOLDER", dxfattribs={"owner": owner}
)
# end of public interface
def set_raster_variables(
self, frame: int = 0, quality: int = 1, units: str = "m"
) -> None:
"""Set raster variables.
Args:
frame: ``0`` = do not show image frame; ``1`` = show image frame
quality: ``0`` = draft; ``1`` = high
units: units for inserting images. This defines the real world unit for one
drawing unit for the purpose of inserting and scaling images with an
associated resolution.
===== ===========================
mm Millimeter
cm Centimeter
m Meter (ezdxf default)
km Kilometer
in Inch
ft Foot
yd Yard
mi Mile
none None
===== ===========================
(internal API), public interface :meth:`~ezdxf.drawing.Drawing.set_raster_variables`
"""
units_: int = const.RASTER_UNITS.get(units, 0)
try:
raster_vars = self.rootdict["ACAD_IMAGE_VARS"]
except const.DXFKeyError:
raster_vars = self.add_dxf_object_with_reactor(
"RASTERVARIABLES",
dxfattribs={
"owner": self.rootdict.dxf.handle,
"frame": frame,
"quality": quality,
"units": units_,
},
)
self.rootdict["ACAD_IMAGE_VARS"] = raster_vars
else:
raster_vars.dxf.frame = frame
raster_vars.dxf.quality = quality
raster_vars.dxf.units = units_
def get_raster_variables(self) -> tuple[int, int, str]:
try:
raster_vars = self.rootdict["ACAD_IMAGE_VARS"]
except const.DXFKeyError:
return 0, 1, "none"
return (
raster_vars.dxf.frame,
raster_vars.dxf.quality,
const.REVERSE_RASTER_UNITS.get(raster_vars.dxf.units, "none"),
)
def set_wipeout_variables(self, frame: int = 0) -> None:
"""Set wipeout variables.
Args:
frame: ``0`` = do not show image frame; ``1`` = show image frame
(internal API)
"""
try:
wipeout_vars = self.rootdict["ACAD_WIPEOUT_VARS"]
except const.DXFKeyError:
wipeout_vars = self.add_dxf_object_with_reactor(
"WIPEOUTVARIABLES",
dxfattribs={
"owner": self.rootdict.dxf.handle,
"frame": int(frame),
},
)
self.rootdict["ACAD_WIPEOUT_VARS"] = wipeout_vars
else:
wipeout_vars.dxf.frame = int(frame)
def get_wipeout_frame_setting(self) -> int:
try:
wipeout_vars = self.rootdict["ACAD_WIPEOUT_VARS"]
except const.DXFKeyError:
return 0
return wipeout_vars.dxf.frame
def add_image_def(
self,
filename: str,
size_in_pixel: tuple[int, int],
name: Optional[str] = None,
) -> ImageDef:
"""Add an image definition to the objects section.
Add an :class:`~ezdxf.entities.image.ImageDef` entity to the drawing
(objects section). `filename` is the image file name as relative or
absolute path and `size_in_pixel` is the image size in pixel as (x, y)
tuple. To avoid dependencies to external packages, `ezdxf` can not
determine the image size by itself. Returns a :class:`~ezdxf.entities.image.ImageDef`
entity which is needed to create an image reference. `name` is the
internal image name, if set to ``None``, name is auto-generated.
Absolute image paths works best for AutoCAD but not really good, you
have to update external references manually in AutoCAD, which is not
possible in TrueView. If the drawing units differ from 1 meter, you also
have to use: :meth:`set_raster_variables`.
Args:
filename: image file name (absolute path works best for AutoCAD)
size_in_pixel: image size in pixel as (x, y) tuple
name: image name for internal use, None for using filename as name
(best for AutoCAD)
"""
if name is None:
name = filename
image_dict = self.rootdict.get_required_dict("ACAD_IMAGE_DICT")
image_def = self.add_dxf_object_with_reactor(
"IMAGEDEF",
dxfattribs={
"owner": image_dict.dxf.handle,
"filename": filename,
"image_size": size_in_pixel,
},
)
image_dict[name] = image_def
return cast("ImageDef", image_def)
def add_image_def_reactor(self, image_handle: str) -> ImageDefReactor:
"""Add required IMAGEDEF_REACTOR object for IMAGEDEF object.
(internal API)
"""
image_def_reactor = self.new_entity(
"IMAGEDEF_REACTOR",
dxfattribs={
"owner": image_handle,
"image_handle": image_handle,
},
)
return cast("ImageDefReactor", image_def_reactor)
def add_underlay_def(
self, filename: str, fmt: str = "pdf", name: Optional[str] = None
) -> UnderlayDefinition:
"""Add an :class:`~ezdxf.entities.underlay.UnderlayDefinition` entity
to the drawing (OBJECTS section). `filename` is the underlay file name
as relative or absolute path and `fmt` as string (pdf, dwf, dgn).
The underlay definition is required to create an underlay reference.
Args:
filename: underlay file name
fmt: file format as string ``'pdf'|'dwf'|'dgn'``
name: pdf format = page number to display; dgn format = ``'default'``; dwf: ????
"""
fmt = fmt.upper()
if fmt in ("PDF", "DWF", "DGN"):
underlay_dict_name = f"ACAD_{fmt}DEFINITIONS"
underlay_def_entity = f"{fmt}DEFINITION"
else:
raise const.DXFValueError(f"Unsupported file format: '{fmt}'")
if name is None:
if fmt == "PDF":
name = "1" # Display first page by default
elif fmt == "DGN":
name = "default"
else:
name = "Model" # Display model space for DWF ???
underlay_dict = self.rootdict.get_required_dict(underlay_dict_name)
underlay_def = self.new_entity(
underlay_def_entity,
dxfattribs={
"owner": underlay_dict.dxf.handle,
"filename": filename,
"name": name,
},
)
# auto-generated underlay key
key = self.next_underlay_key(lambda k: k not in underlay_dict)
underlay_dict[key] = underlay_def
return cast("UnderlayDefinition", underlay_def)
def add_geodata(self, owner: str = "0", dxfattribs=None) -> GeoData:
"""Creates a new :class:`GeoData` entity and replaces existing ones.
The GEODATA entity resides in the OBJECTS section and NOT in the layout
entity space, and it is linked to the layout by an extension dictionary
located in BLOCK_RECORD of the layout.
The GEODATA entity requires DXF version R2010+. The DXF Reference does
not document if other layouts than model space supports geo referencing,
so getting/setting geo data may only make sense for the model space
layout, but it is also available in paper space layouts.
Args:
owner: handle to owner as hex string
dxfattribs: DXF attributes for :class:`~ezdxf.entities.GeoData` entity
"""
if dxfattribs is None:
dxfattribs = {}
dxfattribs["owner"] = owner
return cast("GeoData", self.add_dxf_object_with_reactor("GEODATA", dxfattribs))
def reorg(self, auditor: Optional[Auditor] = None) -> Auditor:
"""Validate and recreate the integrity of the OBJECTS section."""
if auditor is None:
assert self.doc is not None, "valid document required"
auditor = Auditor(self.doc)
sanitizer = _Sanitizer(auditor, self)
sanitizer.execute()
return auditor
_OBJECT_TABLE_NAMES = [
"ACAD_COLOR",
"ACAD_GROUP",
"ACAD_LAYOUT",
"ACAD_MATERIAL",
"ACAD_MLEADERSTYLE",
"ACAD_MLINESTYLE",
"ACAD_PLOTSETTINGS",
"ACAD_PLOTSTYLENAME",
"ACAD_SCALELIST",
"ACAD_TABLESTYLE",
"ACAD_VISUALSTYLE",
]
KNOWN_DICT_CONTENT: dict[str, str] = {
"ACAD_COLOR": "DBCOLOR",
"ACAD_GROUP": "GROUP",
"ACAD_IMAGE_DICT": "IMAGEDEF",
"ACAD_DETAILVIEWSTYLE": "ACDBDETAILVIEWSTYLE",
"ACAD_LAYOUT": "LAYOUT",
"ACAD_MATERIAL": "MATERIAL",
"ACAD_MLEADERSTYLE": "MLEADERSTYLE",
"ACAD_MLINESTYLE": "MLINESTYLE",
"ACAD_PLOTSETTINGS": "PLOTSETTINGS",
"ACAD_RENDER_ACTIVE_SETTINGS": "MENTALRAYRENDERSETTINGS",
"ACAD_SCALELIST": "SCALE",
"ACAD_SECTIONVIEWSTYLE": "ACDBSECTIONVIEWSTYLE",
"ACAD_TABLESTYLE": "TABLESTYLE",
"ACAD_PDFDEFINITIONS": "PDFDEFINITION",
"ACAD_DWFDEFINITIONS": "DWFDEFINITION",
"ACAD_DGNDEFINITIONS": "DGNDEFINITION",
"ACAD_VISUALSTYLE": "VISUALSTYLE",
"AcDbVariableDictionary": "DICTIONARYVAR",
}
class _Sanitizer:
def __init__(self, auditor: Auditor, objects: ObjectsSection) -> None:
self.objects = objects
self.auditor = auditor
self.rootdict = objects.rootdict
self.removed_entity = True
def dictionaries(self) -> Iterator[Dictionary]:
for d in self.objects:
if isinstance(d, Dictionary):
yield d
def execute(self, max_loops=100) -> None:
self.restore_owner_handles_of_dictionary_entries()
self.validate_known_dictionaries()
loops = 0
self.removed_entity = True
while self.removed_entity and loops < max_loops:
loops += 1
self.removed_entity = False
self.remove_orphaned_dictionaries()
# Run audit on all entities of the OBJECTS section to take the removed
# dictionaries into account.
self.audit_objects()
self.create_required_structures()
def restore_owner_handles_of_dictionary_entries(self) -> None:
def reclaim_entity() -> None:
entity.dxf.owner = dict_handle
self.auditor.fixed_error(
AuditError.INVALID_OWNER_HANDLE,
f"Fixed invalid owner handle of {entity}.",
)
def purge_key():
purge.append(key)
self.auditor.fixed_error(
AuditError.INVALID_OWNER_HANDLE,
f"Removed invalid key {key} in {str(dictionary)}.",
)
entitydb = self.auditor.entitydb
for dictionary in self.dictionaries():
purge: list[str] = [] # list of keys to discard
dict_handle = dictionary.dxf.handle
for key, entity in dictionary.items():
if isinstance(entity, str):
# handle is not resolved -> entity does not exist
purge_key()
continue
owner_handle = entity.dxf.get("owner")
if owner_handle == dict_handle:
continue
parent_dict = entitydb.get(owner_handle)
if isinstance(parent_dict, Dictionary) and parent_dict.find_key(entity):
# entity belongs to parent_dict, discard key
purge_key()
else:
reclaim_entity()
for key in purge:
dictionary.discard(key)
def remove_orphaned_dictionaries(self) -> None:
def kill_dictionary():
entitydb.discard(dictionary)
dictionary._silent_kill()
entitydb = self.auditor.entitydb
rootdict = self.rootdict
for dictionary in self.dictionaries():
if dictionary is rootdict:
continue
owner = entitydb.get(dictionary.dxf.get("owner"))
if owner is None:
# owner does not exist:
# A DICTIONARY without an owner has no purpose and the owner can not be
# determined, except for searching all dictionaries for an entry that
# references this DICTIONARY, this is done in the method
# restore_owner_handles_of_dictionary_entries().
kill_dictionary()
continue
if not isinstance(owner, Dictionary):
continue
key = owner.find_key(dictionary)
if not key: # owner dictionary has no entry for this dict
kill_dictionary()
def validate_known_dictionaries(self) -> None:
from ezdxf.entities import DXFEntity
auditor = self.auditor
for dict_name, expected_type in KNOWN_DICT_CONTENT.items():
object_dict = self.rootdict.get(dict_name)
if not isinstance(object_dict, Dictionary):
continue
purge_keys: list[str] = []
for key, entry in object_dict.items():
if isinstance(entry, DXFEntity) and entry.dxftype() != expected_type:
auditor.fixed_error(
AuditError.REMOVED_INVALID_DXF_OBJECT,
f"Removed invalid type {entry} from {object_dict}<{dict_name}>, "
f"expected type {expected_type}",
)
purge_keys.append(key)
auditor.trash(entry)
for key in purge_keys:
object_dict.discard(key)
def create_required_structures(self):
self.objects.setup_object_management_tables(self.rootdict)
doc = self.objects.doc
# update ObjectCollections:
doc.materials.update_object_dict()
doc.materials.create_required_entries()
doc.mline_styles.update_object_dict()
doc.mline_styles.create_required_entries()
doc.mleader_styles.update_object_dict()
doc.mleader_styles.create_required_entries()
doc.groups.update_object_dict()
def cleanup_entitydb(self):
self.auditor.empty_trashcan()
self.auditor.entitydb.purge()
def audit_objects(self):
self.cleanup_entitydb()
auditor = self.auditor
current_db_size = len(auditor.entitydb)
for entity in self.objects:
if not entity.dxf.hasattr("owner"):
# check_owner_exist() ignores entities without owner handle
auditor.fixed_error(
code=AuditError.INVALID_OWNER_HANDLE,
message=f"Deleted {str(entity)} entity without owner handle.",
)
auditor.trash(entity)
continue
auditor.check_owner_exist(entity)
entity.audit(auditor)
auditor.empty_trashcan()
if current_db_size != len(auditor.entitydb):
self.removed_entity = True
@@ -0,0 +1,780 @@
# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
Generic,
Iterator,
List,
Optional,
TYPE_CHECKING,
TypeVar,
Union,
cast,
Sequence,
)
from collections import OrderedDict
import logging
from ezdxf.audit import Auditor, AuditError
from ezdxf.lldxf import const, validator
from ezdxf.entities.table import TableHead
from ezdxf.entities import (
factory,
DXFEntity,
Layer,
Linetype,
Textstyle,
VPort,
View,
AppID,
UCSTableEntry,
BlockRecord,
DimStyle,
is_graphic_entity,
)
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.entitydb import EntityDB
logger = logging.getLogger("ezdxf")
T = TypeVar("T", bound="DXFEntity")
class Table(Generic[T]):
TABLE_TYPE = "UNKNOWN"
def __init__(self) -> None:
self.doc: Optional[Drawing] = None
self.entries: dict[str, T] = OrderedDict()
self._head = TableHead()
def load(self, doc: Drawing, entities: Iterator[DXFEntity]) -> None:
"""Loading interface. (internal API)"""
self.doc = doc
table_head = next(entities)
if isinstance(table_head, TableHead):
self._head = table_head
else:
raise const.DXFStructureError("Critical structure error in TABLES section.")
expected_entry_dxftype = self.TABLE_TYPE
for table_entry in entities:
if table_entry.dxftype() == expected_entry_dxftype:
self._append(cast(T, table_entry))
else:
logger.warning(
f"Ignored invalid DXF entity type '{table_entry.dxftype()}'"
f" in {self.TABLE_TYPE} table."
)
def reset(self, doc: Drawing, handle: str) -> None:
"""Reset table. (internal API)"""
self.doc = doc
self._set_head(self.TABLE_TYPE, handle)
self.entries.clear()
def _set_head(self, name: str, handle: Optional[str] = None) -> None:
self._head = TableHead.new(
handle, owner="0", dxfattribs={"name": name}, doc=self.doc
)
@property
def head(self):
"""Returns table head entry."""
return self._head
@property
def name(self) -> str:
return self.TABLE_TYPE
@staticmethod
def key(name: str) -> str:
"""Unified table entry key."""
return validator.make_table_key(name)
def has_entry(self, name: str) -> bool:
"""Returns ``True`` if a table entry `name` exist."""
return self.key(name) in self.entries
__contains__ = has_entry
def __len__(self) -> int:
"""Count of table entries."""
return len(self.entries)
def __iter__(self) -> Iterator[T]:
"""Iterable of all table entries."""
for e in self.entries.values():
if e.is_alive:
yield e
def new(self, name: str, dxfattribs=None) -> T:
"""Create a new table entry `name`.
Args:
name: name of table entry
dxfattribs: additional DXF attributes for table entry
"""
if self.has_entry(name):
raise const.DXFTableEntryError(
f"{self.TABLE_TYPE} '{name}' already exists!"
)
dxfattribs = dxfattribs or {}
dxfattribs["name"] = name
dxfattribs["owner"] = self._head.dxf.handle
return self.new_entry(dxfattribs)
def get(self, name: str) -> T:
"""Returns table entry `name`.
Args:
name: name of table entry, case-insensitive
Raises:
DXFTableEntryError: table entry does not exist
"""
entry = self.entries.get(self.key(name))
if entry:
return entry
else:
raise const.DXFTableEntryError(name)
def get_entry_by_handle(self, handle: str) -> Optional[T]:
"""Returns table entry by handle or ``None`` if entry does not exist.
(internal API)
"""
entry = self.doc.entitydb.get(handle) # type: ignore
if entry and entry.dxftype() == self.TABLE_TYPE:
return entry # type: ignore
return None
def get_handle_of_entry(self, name: str) -> str:
"""Returns the handle of table entry by `name`, returns an empty string if no
entry for the given name exist.
Args:
name: name of table entry, case-insensitive
(internal API)
"""
entry = self.entries.get(self.key(name))
if entry is not None:
return entry.dxf.handle
return ""
def remove(self, name: str) -> None:
"""Removes table entry `name`.
Args:
name: name of table entry, case-insensitive
Raises:
DXFTableEntryError: table entry does not exist
"""
key = self.key(name)
entry = self.get(name)
self.entitydb.delete_entity(entry)
self.discard(key)
def duplicate_entry(self, name: str, new_name: str) -> T:
"""Returns a new table entry `new_name` as copy of `name`,
replaces entry `new_name` if already exist.
Args:
name: name of table entry, case-insensitive
new_name: name of duplicated table entry
Raises:
DXFTableEntryError: table entry does not exist
"""
entry = self.get(name)
entitydb = self.entitydb
if entitydb:
new_entry = entitydb.duplicate_entity(entry)
else: # only for testing!
new_entry = entry.copy()
new_entry.dxf.name = new_name
entry = cast(T, new_entry)
self._append(entry)
return entry
def discard(self, name: str) -> None:
"""Remove table entry without destroying object.
Args:
name: name of table entry, case-insensitive
(internal API)
"""
del self.entries[self.key(name)]
def replace(self, name: str, entry: T) -> None:
"""Replace table entry `name` by new `entry`. (internal API)"""
self.discard(name)
self._append(entry)
@property
def entitydb(self) -> EntityDB:
return self.doc.entitydb # type: ignore
def new_entry(self, dxfattribs) -> T:
"""Create and add new table-entry of type 'self.entry_dxftype'.
Does not check if an entry dxfattribs['name'] already exists!
Duplicate entries are possible for Viewports.
"""
assert self.doc is not None, "valid DXF document required"
entry = cast(T, factory.create_db_entry(self.TABLE_TYPE, dxfattribs, self.doc))
self._append(entry)
return entry
def _append(self, entry: T) -> None:
"""Add a table entry, replaces existing entries with same name.
(internal API).
"""
assert entry.dxftype() == self.TABLE_TYPE
self.entries[self.key(entry.dxf.name)] = entry
def add_entry(self, entry: T) -> None:
"""Add a table `entry`, created by other object than this table.
(internal API)
"""
if entry.dxftype() != self.TABLE_TYPE:
raise const.DXFTypeError(
f"Invalid table entry type {entry.dxftype()} "
f"for table {self.TABLE_TYPE}"
)
name = entry.dxf.name
if self.has_entry(name):
raise const.DXFTableEntryError(
f"{self._head.dxf.name} {name} already exists!"
)
if self.doc:
factory.bind(entry, self.doc)
entry.dxf.owner = self._head.dxf.handle
self._append(entry)
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
"""Export DXF representation. (internal API)"""
self.update_owner_handles()
# The table head itself has no owner and is therefore always '0':
self._head.dxf.owner = "0"
self._head.dxf.count = len(self)
self._head.export_dxf(tagwriter)
self.export_table_entries(tagwriter)
tagwriter.write_tag2(0, "ENDTAB")
def export_table_entries(self, tagwriter: AbstractTagWriter) -> None:
for entry in self.entries.values():
entry.export_dxf(tagwriter)
def update_owner_handles(self) -> None:
owner_handle = self._head.dxf.handle
for entry in self.entries.values():
if entry.is_alive:
entry.dxf.owner = owner_handle
def set_handle(self, handle: str):
"""Set new `handle` for table, updates also :attr:`owner` tag of table
entries. (internal API)
"""
if self._head.dxf.handle is None:
self._head.dxf.handle = handle
self.update_owner_handles()
def audit(self, auditor: Auditor):
# The table entries are stored in the entity database and are already
# audited!
self._fix_table_head(auditor)
self._fix_entry_handles(auditor)
def _fix_entry_handles(self, auditor: Auditor):
# Why: see duplicate handle issue #604
entitydb = self.entitydb
for entry in self:
entity = entitydb.get(entry.dxf.handle)
if entity is not entry: # duplicate handle usage
# This can break entities referring to this entity, but at
# least the DXF readable
entry.dxf.handle = entitydb.next_handle()
self.entitydb.add(entry)
auditor.fixed_error(
code=AuditError.INVALID_TABLE_HANDLE,
message=f"Fixed invalid table entry handle in {entry}",
)
def _fix_table_head(self, auditor: Auditor):
def fix_head():
head.dxf.handle = entitydb.next_handle()
entitydb.add(head)
if log:
auditor.fixed_error(
code=AuditError.INVALID_TABLE_HANDLE,
message=f"Fixed invalid table head handle in table {self.name}",
)
# fix silently for older DXF versions
log = auditor.doc.dxfversion > const.DXF12
head = self.head
# Another exception for an invalid owner tag, but this usage is
# covered in Auditor.check_owner_exist():
head.dxf.owner = "0"
handle = head.dxf.handle
entitydb = self.entitydb
if handle is None or handle == "0":
# Entity database does not assign new handle:
fix_head()
else:
# Why: see duplicate handle issue #604
entry = self.entitydb.get(handle)
if entry is not head: # another entity has the same handle!
fix_head()
# Just to be sure owner handle is valid in every circumstance:
self.update_owner_handles()
class LayerTable(Table[Layer]):
TABLE_TYPE = "LAYER"
def new_entry(self, dxfattribs) -> Layer:
layer = cast(Layer, super().new_entry(dxfattribs))
if self.doc:
layer.set_required_attributes()
return layer
def add(
self,
name: str,
*,
color: int = const.BYLAYER,
true_color: Optional[int] = None,
linetype: str = "Continuous",
lineweight: int = const.LINEWEIGHT_BYLAYER,
plot: bool = True,
transparency: Optional[float] = None,
dxfattribs=None,
) -> Layer:
"""Add a new :class:`~ezdxf.entities.Layer`.
Args:
name (str): layer name
color (int): :ref:`ACI` value, default is BYLAYER
true_color (int): true color value, use :func:`ezdxf.rgb2int` to
create ``int`` values from RGB values
linetype (str): line type name, default is "Continuous"
lineweight (int): line weight, default is BYLAYER
plot (bool): plot layer as bool, default is ``True``
transparency: transparency value in the range [0, 1], where 1 is
100% transparent and 0 is opaque
dxfattribs (dict): additional DXF attributes
"""
dxfattribs = dict(dxfattribs or {})
if validator.is_valid_aci_color(color):
dxfattribs["color"] = color
else:
raise const.DXFValueError(f"invalid color: {color}")
dxfattribs["linetype"] = linetype
if validator.is_valid_lineweight(lineweight):
dxfattribs["lineweight"] = lineweight
else:
raise const.DXFValueError(f"invalid lineweight: {lineweight}")
if true_color is not None:
dxfattribs["true_color"] = int(true_color)
dxfattribs["plot"] = int(plot)
layer = cast("Layer", self.new(name, dxfattribs))
if transparency is not None:
layer.transparency = transparency
return layer
def create_referenced_layers(self) -> None:
"""Create for all referenced layers table entries if not exist."""
if self.doc is None:
return
for e in self.doc.entitydb.values():
if not is_graphic_entity(e):
continue
layer_name = e.dxf.get("layer", "")
if layer_name and not self.has_entry(layer_name):
# create layer table entry with default settings
self.add(layer_name)
class LinetypeTable(Table[Linetype]):
TABLE_TYPE = "LTYPE"
def new_entry(self, dxfattribs) -> Linetype:
pattern = dxfattribs.pop("pattern", [0.0])
length = dxfattribs.pop("length", 0) # required for complex types
ltype = cast(Linetype, super().new_entry(dxfattribs))
ltype.setup_pattern(pattern, length)
return ltype
def add(
self,
name: str,
pattern: Union[Sequence[float], str],
*,
description: str = "",
length: float = 0.0,
dxfattribs=None,
) -> Linetype:
"""Add a new line type entry. The simple line type pattern is a list of
floats :code:`[total_pattern_length, elem1, elem2, ...]`
where an element > 0 is a line, an element < 0 is a gap and an
element == 0.0 is a dot. The definition for complex line types are
strings, like: ``'A,.5,-.2,["GAS",STANDARD,S=.1,U=0.0,X=-0.1,Y=-.05],-.25'``
similar to the line type definitions stored in the line definition
`.lin` files, for more information see the tutorial about complex line
types. Be aware that not many CAD applications and DXF viewers support
complex linetypes.
.. seealso::
- `Tutorial for simple line types <https://ezdxf.mozman.at/docs/tutorials/linetypes.html>`_
- `Tutorial for complex line types <https://ezdxf.mozman.at/docs/tutorials/linetypes.html#tutorial-for-complex-linetypes>`_
Args:
name (str): line type name
pattern: line type pattern as list of floats or as a string
description (str): line type description, optional
length (float): total pattern length, only for complex line types required
dxfattribs (dict): additional DXF attributes
"""
dxfattribs = dict(dxfattribs or {})
dxfattribs.update(
{
"name": name,
"description": str(description),
"pattern": pattern,
"length": float(length),
}
)
return self.new_entry(dxfattribs)
class TextstyleTable(Table[Textstyle]):
TABLE_TYPE = "STYLE"
def __init__(self) -> None:
super().__init__()
self.shx_files: dict[str, Textstyle] = dict()
def export_table_entries(self, tagwriter: AbstractTagWriter) -> None:
super().export_table_entries(tagwriter)
for shx_file in self.shx_files.values():
shx_file.export_dxf(tagwriter)
def _append(self, entry: Textstyle) -> None:
"""Add a table entry, replaces existing entries with same name.
(internal API).
"""
if entry.dxf.name == "" and (entry.dxf.flags & 1): # shx shape file
self.shx_files[self.key(entry.dxf.font)] = entry
else:
self.entries[self.key(entry.dxf.name)] = entry
def update_owner_handles(self) -> None:
super().update_owner_handles()
owner_handle = self._head.dxf.handle
for entry in self.shx_files.values():
entry.dxf.owner = owner_handle
def add(self, name: str, *, font: str, dxfattribs=None) -> Textstyle:
"""Add a new text style entry for TTF fonts. The entry must not yet
exist, otherwise an :class:`DXFTableEntryError` exception will be
raised.
Finding the TTF font files is the task of the DXF viewer and each
viewer is different (hint: support files).
Args:
name (str): text style name
font (str): TTF font file name like "Arial.ttf", the real font file
name from the file system is required and only the Windows filesystem
is case-insensitive.
dxfattribs (dict): additional DXF attributes
"""
dxfattribs = dict(dxfattribs or {})
dxfattribs.update(
{
"name": name,
"font": str(font),
"last_height": 2.5, # maybe required by AutoCAD
}
)
return self.new_entry(dxfattribs)
def add_shx(self, shx_file_name: str, *, dxfattribs=None) -> Textstyle:
"""Add a new shape font (SHX file) entry. These are special text style
entries and have no name. The entry must not yet exist, otherwise an
:class:`DXFTableEntryError` exception will be raised.
Locating the SHX files in the filesystem is the task of the DXF viewer and each
viewer is different (hint: support files).
Args:
shx_file_name (str): shape file name like "gdt.shx"
dxfattribs (dict): additional DXF attributes
"""
if self.find_shx(shx_file_name) is not None:
raise const.DXFTableEntryError(
f"{self._head.dxf.name} shape file entry for "
f"'{shx_file_name}' already exists!"
)
dxfattribs = dict(dxfattribs or {})
dxfattribs.update(
{
"name": "", # shape file entry has no name
"flags": 1, # shape file flag
"font": shx_file_name,
"last_height": 2.5, # maybe required by AutoCAD
}
)
return self.new_entry(dxfattribs)
def get_shx(self, shx_file_name: str) -> Textstyle:
"""Get existing entry for a shape file (SHX file), or create a new
entry.
Locating the SHX files in the filesystem is the task of the DXF viewer and each
viewer is different (hint: support files).
Args:
shx_file_name (str): shape file name like "gdt.shx"
"""
shape_file = self.find_shx(shx_file_name)
if shape_file is None:
return self.add_shx(shx_file_name)
return shape_file
def find_shx(self, shx_file_name: str) -> Optional[Textstyle]:
"""Find the shape file (SHX file) text style table entry, by a
case-insensitive search.
A shape file table entry has no name, so you have to search by the
font attribute.
Args:
shx_file_name (str): shape file name like "gdt.shx"
"""
return self.shx_files.get(self.key(shx_file_name))
def discard_shx(self, shx_file_name: str) -> None:
"""Discard the shape file (SHX file) text style table entry. Does not raise an
exception if the entry does not exist.
Args:
shx_file_name (str): shape file name like "gdt.shx"
"""
try:
del self.shx_files[self.key(shx_file_name)]
except KeyError:
pass
class ViewportTable(Table[VPort]):
TABLE_TYPE = "VPORT"
# Viewport-Table can have multiple entries with same name
# each table entry is a list of VPORT entries
def export_table_entries(self, tagwriter: AbstractTagWriter) -> None:
for entry in self.entries.values():
assert isinstance(entry, list)
for e in entry:
e.export_dxf(tagwriter)
def new(self, name: str, dxfattribs=None) -> VPort:
"""Create a new table entry."""
dxfattribs = dxfattribs or {}
dxfattribs["name"] = name
return self.new_entry(dxfattribs)
def add(self, name: str, *, dxfattribs=None) -> VPort:
"""Add a new modelspace viewport entry. A modelspace viewport
configuration can consist of multiple viewport entries with the same
name.
Args:
name (str): viewport name, multiple entries possible
dxfattribs (dict): additional DXF attributes
"""
dxfattribs = dict(dxfattribs or {})
dxfattribs["name"] = name
return self.new_entry(dxfattribs)
def remove(self, name: str) -> None:
"""Remove table-entry from table and entitydb by name."""
key = self.key(name)
entries = cast(List[DXFEntity], self.get(name))
for entry in entries:
self.entitydb.delete_entity(entry)
del self.entries[key]
def __iter__(self) -> Iterator[VPort]:
for entries in self.entries.values():
yield from iter(entries) # type: ignore
def _flatten(self) -> Iterator[VPort]:
for entries in self.entries.values():
yield from iter(entries) # type: ignore
def __len__(self) -> int:
# calling __iter__() invokes recursion!
return len(list(self._flatten()))
def new_entry(self, dxfattribs) -> VPort:
"""Create and add new table-entry of type 'self.entry_dxftype'.
Does not check if an entry dxfattribs['name'] already exists!
Duplicate entries are possible for Viewports.
"""
assert self.doc is not None, "valid DXF document expected"
entry = cast(
VPort,
factory.create_db_entry(self.TABLE_TYPE, dxfattribs, self.doc),
)
self._append(entry)
return entry
def duplicate_entry(self, name: str, new_name: str) -> VPort:
raise NotImplementedError()
def _append(self, entry: T) -> None:
key = self.key(entry.dxf.name)
if key in self.entries:
self.entries[key].append(entry) # type: ignore
else:
self.entries[key] = [entry] # type: ignore # store list of VPORT
def replace(self, name: str, entry: T) -> None:
self.discard(name)
config: list[T]
if isinstance(entry, list):
config = entry
else:
config = [entry]
if not config:
return
key = self.key(config[0].dxf.name)
self.entries[key] = config # type: ignore
def update_owner_handles(self) -> None:
owner_handle = self._head.dxf.handle
for entries in self.entries.values():
for entry in entries: # type: ignore
entry.dxf.owner = owner_handle
def get_config(self, name: str) -> list[VPort]:
"""Returns a list of :class:`~ezdxf.entities.VPort` objects, for
the multi-viewport configuration `name`.
"""
try:
return self.entries[self.key(name)] # type: ignore
except KeyError:
raise const.DXFTableEntryError(name)
def delete_config(self, name: str) -> None:
"""Delete all :class:`~ezdxf.entities.VPort` objects of the
multi-viewport configuration `name`.
"""
self.remove(name)
class AppIDTable(Table[AppID]):
TABLE_TYPE = "APPID"
def add(self, name: str, *, dxfattribs=None) -> AppID:
"""Add a new appid table entry.
Args:
name (str): appid name
dxfattribs (dict): DXF attributes
"""
dxfattribs = dict(dxfattribs or {})
dxfattribs["name"] = name
return self.new_entry(dxfattribs)
class ViewTable(Table[View]):
TABLE_TYPE = "VIEW"
def add(self, name: str, *, dxfattribs=None) -> View:
"""Add a new view table entry.
Args:
name (str): view name
dxfattribs (dict): DXF attributes
"""
dxfattribs = dict(dxfattribs or {})
dxfattribs["name"] = name
return self.new_entry(dxfattribs)
class BlockRecordTable(Table[BlockRecord]):
TABLE_TYPE = "BLOCK_RECORD"
def add(self, name: str, *, dxfattribs=None) -> BlockRecord:
"""Add a new block record table entry.
Args:
name (str): block record name
dxfattribs (dict): DXF attributes
"""
dxfattribs = dict(dxfattribs or {})
dxfattribs["name"] = name
return self.new_entry(dxfattribs)
class DimStyleTable(Table[DimStyle]):
TABLE_TYPE = "DIMSTYLE"
def add(self, name: str, *, dxfattribs=None) -> DimStyle:
"""Add a new dimension style table entry.
Args:
name (str): dimension style name
dxfattribs (dict): DXF attributes
"""
dxfattribs = dict(dxfattribs or {})
dxfattribs["name"] = name
return self.new_entry(dxfattribs)
class UCSTable(Table[UCSTableEntry]):
TABLE_TYPE = "UCS"
def add(self, name: str, *, dxfattribs=None) -> UCSTableEntry:
"""Add a new UCS table entry.
Args:
name (str): UCS name
dxfattribs (dict): DXF attributes
"""
dxfattribs = dict(dxfattribs or {})
dxfattribs["name"] = name
return self.new_entry(dxfattribs)
@@ -0,0 +1,152 @@
# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Sequence, Optional
import logging
from ezdxf.lldxf.const import DXFStructureError, DXF12
from .table import (
Table,
ViewportTable,
TextstyleTable,
LayerTable,
LinetypeTable,
AppIDTable,
ViewTable,
BlockRecordTable,
DimStyleTable,
UCSTable,
)
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.entities import DXFEntity, DXFTagStorage
from ezdxf.lldxf.tagwriter import AbstractTagWriter
logger = logging.getLogger("ezdxf")
TABLENAMES = {
"LAYER": "layers",
"LTYPE": "linetypes",
"APPID": "appids",
"DIMSTYLE": "dimstyles",
"STYLE": "styles",
"UCS": "ucs",
"VIEW": "views",
"VPORT": "viewports",
"BLOCK_RECORD": "block_records",
}
class TablesSection:
def __init__(self, doc: Drawing, entities: Optional[list[DXFEntity]] = None):
assert doc is not None
self.doc = doc
# not loaded tables: table.doc is None
self.layers = LayerTable()
self.linetypes = LinetypeTable()
self.appids = AppIDTable()
self.dimstyles = DimStyleTable()
self.styles = TextstyleTable()
self.ucs = UCSTable()
self.views = ViewTable()
self.viewports = ViewportTable()
self.block_records = BlockRecordTable()
if entities is not None:
self._load(entities)
self._reset_not_loaded_tables()
def tables(self) -> Sequence[Table]:
return (
self.layers,
self.linetypes,
self.appids,
self.dimstyles,
self.styles,
self.ucs,
self.views,
self.viewports,
self.block_records,
)
def _load(self, entities: list[DXFEntity]) -> None:
section_head: "DXFTagStorage" = entities[0] # type: ignore
if section_head.dxftype() != "SECTION" or section_head.base_class[
1
] != (2, "TABLES"):
raise DXFStructureError(
"Critical structure error in TABLES section."
)
del entities[0] # delete first entity (0, SECTION)
table_records: list[DXFEntity] = []
table_name = None
for entity in entities:
if entity.dxftype() == "TABLE":
if len(table_records):
# TABLE entity without preceding ENDTAB entity, should we care?
logger.debug(
f'Ignore missing ENDTAB entity in table "{table_name}".'
)
self._load_table(table_name, table_records) # type: ignore
table_name = entity.dxf.name
table_records = [entity] # collect table head
elif entity.dxftype() == "ENDTAB": # do not collect (0, 'ENDTAB')
self._load_table(table_name, table_records) # type: ignore
table_records = (
[]
) # collect entities outside of tables, but ignore it
else: # collect table entries
table_records.append(entity)
if len(table_records):
# last ENDTAB entity is missing, should we care?
logger.debug(
'Ignore missing ENDTAB entity in table "{}".'.format(table_name)
)
self._load_table(table_name, table_records) # type: ignore
def _load_table(
self, name: str, table_entities: Iterable[DXFEntity]
) -> None:
"""
Load table from tags.
Args:
name: table name e.g. VPORT
table_entities: iterable of table records
"""
table = getattr(self, TABLENAMES[name])
if isinstance(table, Table):
table.load(self.doc, iter(table_entities))
def _reset_not_loaded_tables(self) -> None:
entitydb = self.doc.entitydb
for table in self.tables():
if table.doc is None:
handle = entitydb.next_handle()
table.reset(self.doc, handle)
entitydb.add(table.head)
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
tagwriter.write_str(" 0\nSECTION\n 2\nTABLES\n")
version = tagwriter.dxfversion
self.viewports.export_dxf(tagwriter)
self.linetypes.export_dxf(tagwriter)
self.layers.export_dxf(tagwriter)
self.styles.export_dxf(tagwriter)
self.views.export_dxf(tagwriter)
self.ucs.export_dxf(tagwriter)
self.appids.export_dxf(tagwriter)
self.dimstyles.export_dxf(tagwriter)
if version > DXF12:
self.block_records.export_dxf(tagwriter)
tagwriter.write_tag2(0, "ENDSEC")
def create_table_handles(self):
# DXF R12: TABLE does not require a handle and owner tag
# DXF R2000+: TABLE requires a handle and an owner tag
for table in self.tables():
handle = self.doc.entitydb.next_handle()
table.set_handle(handle)