refactor: excel parse
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user