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

481 lines
16 KiB
Python

# Copyright (c) 2020-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
Iterable,
Iterator,
cast,
BinaryIO,
Optional,
Union,
Any,
)
from io import StringIO
from pathlib import Path
from ezdxf.lldxf.const import DXFStructureError
from ezdxf.lldxf.extendedtags import ExtendedTags, DXFTag
from ezdxf.lldxf.tagwriter import TagWriter
from ezdxf.lldxf.tagger import tag_compiler, ascii_tags_loader
from ezdxf.filemanagement import dxf_file_info
from ezdxf.lldxf import fileindex
from ezdxf.entities import DXFGraphic, DXFEntity, Polyline, Insert
from ezdxf.entities import factory
from ezdxf.entities.subentity import entity_linker
from ezdxf.tools.codepage import toencoding
__all__ = ["opendxf", "single_pass_modelspace", "modelspace"]
SUPPORTED_TYPES = {
"ARC",
"LINE",
"CIRCLE",
"ELLIPSE",
"POINT",
"LWPOLYLINE",
"SPLINE",
"3DFACE",
"SOLID",
"TRACE",
"SHAPE",
"POLYLINE",
"VERTEX",
"SEQEND",
"MESH",
"TEXT",
"MTEXT",
"HATCH",
"INSERT",
"ATTRIB",
"ATTDEF",
"RAY",
"XLINE",
"DIMENSION",
"LEADER",
"IMAGE",
"WIPEOUT",
"HELIX",
"MLINE",
"MLEADER",
}
Filename = Union[Path, str]
class IterDXF:
"""Iterator for DXF entities stored in the modelspace.
Args:
name: filename, has to be a seekable file.
errors: specify decoding error handler
- "surrogateescape" to preserve possible binary data (default)
- "ignore" to use the replacement char U+FFFD "\ufffd" for invalid data
- "strict" to raise an :class:`UnicodeDecodeError`exception for invalid data
Raises:
DXFStructureError: invalid or incomplete DXF file
UnicodeDecodeError: if `errors` is "strict" and a decoding error occurs
"""
def __init__(self, name: Filename, errors: str = "surrogateescape"):
self.structure, self.sections = self._load_index(str(name))
self.errors = errors
self.file: BinaryIO = open(name, mode="rb")
if "ENTITIES" not in self.sections:
raise DXFStructureError("ENTITIES section not found.")
if self.structure.version > "AC1009" and "OBJECTS" not in self.sections:
raise DXFStructureError("OBJECTS section not found.")
def _load_index(
self, name: str
) -> tuple[fileindex.FileStructure, dict[str, int]]:
structure = fileindex.load(name)
sections: dict[str, int] = dict()
new_index = []
for e in structure.index:
if e.code == 0:
new_index.append(e)
elif e.code == 2:
sections[e.value] = len(new_index) - 1
# remove all other tags like handles (code == 5)
structure.index = new_index
return structure, sections
@property
def encoding(self):
return self.structure.encoding
@property
def dxfversion(self):
return self.structure.version
def export(self, name: Filename) -> IterDXFWriter:
"""Returns a companion object to export parts from the source DXF file
into another DXF file, the new file will have the same HEADER, CLASSES,
TABLES, BLOCKS and OBJECTS sections, which guarantees all necessary
dependencies are present in the new file.
Args:
name: filename, no special requirements
"""
doc = IterDXFWriter(name, self)
# Copy everything from start of source DXF until the first entity
# of the ENTITIES section to the new DXF.
location = self.structure.index[self.sections["ENTITIES"] + 1].location
self.file.seek(0)
data = self.file.read(location)
doc.write_data(data)
return doc
def copy_objects_section(self, f: BinaryIO) -> None:
start_index = self.sections["OBJECTS"]
try:
end_index = self.structure.get(0, "ENDSEC", start_index)
except ValueError:
raise DXFStructureError(f"ENDSEC of OBJECTS section not found.")
start_location = self.structure.index[start_index].location
end_location = self.structure.index[end_index + 1].location
count = end_location - start_location
self.file.seek(start_location)
data = self.file.read(count)
f.write(data)
def modelspace(
self, types: Optional[Iterable[str]] = None
) -> Iterable[DXFGraphic]:
"""Returns an iterator for all supported DXF entities in the
modelspace. These entities are regular :class:`~ezdxf.entities.DXFGraphic`
objects but without a valid document assigned. It is **not**
possible to add these entities to other `ezdxf` documents.
It is only possible to recreate the objects by factory functions base
on attributes of the source entity.
For MESH, POLYMESH and POLYFACE it is possible to use the
:class:`~ezdxf.render.MeshTransformer` class to render (recreate) this
objects as new entities in another document.
Args:
types: DXF types like ``['LINE', '3DFACE']`` which should be
returned, ``None`` returns all supported types.
"""
linked_entity = entity_linker()
queued = None
requested_types = _requested_types(types)
for entity in self.load_entities(
self.sections["ENTITIES"] + 1, requested_types
):
if not linked_entity(entity) and entity.dxf.paperspace == 0:
# queue one entity for collecting linked entities:
# VERTEX, ATTRIB
if queued:
yield queued
queued = entity
if queued:
yield queued
def load_entities(
self, start: int, requested_types: set[str]
) -> Iterable[DXFGraphic]:
def to_str(data: bytes) -> str:
return data.decode(self.encoding, errors=self.errors).replace(
"\r\n", "\n"
)
index = start
entry = self.structure.index[index]
self.file.seek(entry.location)
while entry.value != "ENDSEC":
index += 1
next_entry = self.structure.index[index]
size = next_entry.location - entry.location
data = self.file.read(size)
if entry.value in requested_types:
xtags = ExtendedTags.from_text(to_str(data))
yield factory.load(xtags) # type: ignore
entry = next_entry
def close(self):
"""Safe closing source DXF file."""
self.file.close()
class IterDXFWriter:
def __init__(self, name: Filename, loader: IterDXF):
self.name = str(name)
self.file: BinaryIO = open(name, mode="wb")
self.text = StringIO()
self.entity_writer = TagWriter(self.text, loader.dxfversion)
self.loader = loader
def write_data(self, data: bytes):
self.file.write(data)
def write(self, entity: DXFGraphic):
"""Write a DXF entity from the source DXF file to the export file.
Don't write entities from different documents than the source DXF file,
dependencies and resources will not match, maybe it will work once, but
not in a reliable way for different DXF documents.
"""
# Not necessary to remove this dependencies by copying
# them into the same document frame
# ---------------------------------
# remove all possible dependencies
# entity.xdata = None
# entity.appdata = None
# entity.extension_dict = None
# entity.reactors = None
# reset text stream
self.text.seek(0)
self.text.truncate()
if entity.dxf.handle is None: # DXF R12 without handles
self.entity_writer.write_handles = False
entity.export_dxf(self.entity_writer)
if entity.dxftype() == "POLYLINE":
polyline = cast(Polyline, entity)
for vertex in polyline.vertices:
vertex.export_dxf(self.entity_writer)
polyline.seqend.export_dxf(self.entity_writer) # type: ignore
elif entity.dxftype() == "INSERT":
insert = cast(Insert, entity)
if insert.attribs_follow:
for attrib in insert.attribs:
attrib.export_dxf(self.entity_writer)
insert.seqend.export_dxf(self.entity_writer) # type: ignore
data = self.text.getvalue().encode(self.loader.encoding)
self.file.write(data)
def close(self):
"""Safe closing of exported DXF file. Copying of OBJECTS section
happens only at closing the file, without closing the new DXF file is
invalid.
"""
self.file.write(b" 0\r\nENDSEC\r\n") # for ENTITIES section
if self.loader.dxfversion > "AC1009":
self.loader.copy_objects_section(self.file)
self.file.write(b" 0\r\nEOF\r\n")
self.file.close()
def opendxf(filename: Filename, errors: str = "surrogateescape") -> IterDXF:
"""Open DXF file for iterating, be sure to open valid DXF files, no DXF
structure checks will be applied.
Use this function to split up big DXF files as shown in the example above.
Args:
filename: DXF filename of a seekable DXF file.
errors: specify decoding error handler
- "surrogateescape" to preserve possible binary data (default)
- "ignore" to use the replacement char U+FFFD "\ufffd" for invalid data
- "strict" to raise an :class:`UnicodeDecodeError` exception for invalid data
Raises:
DXFStructureError: invalid or incomplete DXF file
UnicodeDecodeError: if `errors` is "strict" and a decoding error occurs
"""
return IterDXF(filename, errors=errors)
def modelspace(
filename: Filename,
types: Optional[Iterable[str]] = None,
errors: str = "surrogateescape",
) -> Iterable[DXFGraphic]:
"""Iterate over all modelspace entities as :class:`DXFGraphic` objects of
a seekable file.
Use this function to iterate "quick" over modelspace entities of a DXF file,
filtering DXF types may speed up things if many entity types will be skipped.
Args:
filename: filename of a seekable DXF file
types: DXF types like ``['LINE', '3DFACE']`` which should be returned,
``None`` returns all supported types.
errors: specify decoding error handler
- "surrogateescape" to preserve possible binary data (default)
- "ignore" to use the replacement char U+FFFD "\ufffd" for invalid data
- "strict" to raise an :class:`UnicodeDecodeError` exception for invalid data
Raises:
DXFStructureError: invalid or incomplete DXF file
UnicodeDecodeError: if `errors` is "strict" and a decoding error occurs
"""
info = dxf_file_info(str(filename))
prev_code: int = -1
prev_value: Any = ""
entities = False
requested_types = _requested_types(types)
with open(filename, mode="rt", encoding=info.encoding, errors=errors) as fp:
tagger = ascii_tags_loader(fp)
queued: Optional[DXFEntity] = None
tags: list[DXFTag] = []
linked_entity = entity_linker()
for tag in tag_compiler(tagger):
code = tag.code
value = tag.value
if entities:
if code == 0:
if len(tags) and tags[0].value in requested_types:
entity = factory.load(ExtendedTags(tags))
if (
not linked_entity(entity)
and entity.dxf.paperspace == 0
):
# queue one entity for collecting linked entities:
# VERTEX, ATTRIB
if queued:
yield queued # type: ignore
queued = entity
tags = [tag]
else:
tags.append(tag)
if code == 0 and value == "ENDSEC":
if queued:
yield queued # type: ignore
return
continue # if entities - nothing else matters
elif code == 2 and prev_code == 0 and prev_value == "SECTION":
entities = value == "ENTITIES"
prev_code = code
prev_value = value
def single_pass_modelspace(
stream: BinaryIO,
types: Optional[Iterable[str]] = None,
errors: str = "surrogateescape",
) -> Iterable[DXFGraphic]:
"""Iterate over all modelspace entities as :class:`DXFGraphic` objects in
a single pass.
Use this function to 'quick' iterate over modelspace entities of a **not**
seekable binary DXF stream, filtering DXF types may speed up things if many
entity types will be skipped.
Args:
stream: (not seekable) binary DXF stream
types: DXF types like ``['LINE', '3DFACE']`` which should be returned,
``None`` returns all supported types.
errors: specify decoding error handler
- "surrogateescape" to preserve possible binary data (default)
- "ignore" to use the replacement char U+FFFD "\ufffd" for invalid data
- "strict" to raise an :class:`UnicodeDecodeError` exception for invalid data
Raises:
DXFStructureError: Invalid or incomplete DXF file
UnicodeDecodeError: if `errors` is "strict" and a decoding error occurs
"""
fetch_header_var: Optional[str] = None
encoding = "cp1252"
version = "AC1009"
prev_code: int = -1
prev_value: str = ""
entities = False
requested_types = _requested_types(types)
for code, value in binary_tagger(stream):
if code == 0 and value == b"ENDSEC":
break
elif code == 2 and prev_code == 0 and value != b"HEADER":
# (0, SECTION), (2, name)
# First section is not the HEADER section
entities = value == b"ENTITIES"
break
elif code == 9 and value == b"$DWGCODEPAGE":
fetch_header_var = "ENCODING"
elif code == 9 and value == b"$ACADVER":
fetch_header_var = "VERSION"
elif fetch_header_var == "ENCODING":
encoding = toencoding(value.decode())
fetch_header_var = None
elif fetch_header_var == "VERSION":
version = value.decode()
fetch_header_var = None
prev_code = code
if version >= "AC1021":
encoding = "utf-8"
queued: Optional[DXFGraphic] = None
tags: list[DXFTag] = []
linked_entity = entity_linker()
for tag in tag_compiler(binary_tagger(stream, encoding, errors)):
code = tag.code
value = tag.value
if entities:
if code == 0 and value == "ENDSEC":
if queued:
yield queued
return
if code == 0:
if len(tags) and tags[0].value in requested_types:
entity = cast(DXFGraphic, factory.load(ExtendedTags(tags)))
if not linked_entity(entity) and entity.dxf.paperspace == 0:
# queue one entity for collecting linked entities:
# VERTEX, ATTRIB
if queued:
yield queued
queued = entity
tags = [tag]
else:
tags.append(tag)
continue # if entities - nothing else matters
elif code == 2 and prev_code == 0 and prev_value == "SECTION":
entities = value == "ENTITIES"
prev_code = code
prev_value = value
def binary_tagger(
file: BinaryIO,
encoding: Optional[str] = None,
errors: str = "surrogateescape",
) -> Iterator[DXFTag]:
while True:
try:
try:
code = int(file.readline())
except ValueError:
raise DXFStructureError(f"Invalid group code")
value = file.readline().rstrip(b"\r\n")
yield DXFTag(
code,
value.decode(encoding, errors=errors) if encoding else value,
)
except IOError:
return
def _requested_types(types: Optional[Iterable[str]]) -> set[str]:
if types:
requested = SUPPORTED_TYPES.intersection(set(types))
if "POLYLINE" in requested:
requested.add("SEQEND")
requested.add("VERTEX")
if "INSERT" in requested:
requested.add("SEQEND")
requested.add("ATTRIB")
else:
requested = SUPPORTED_TYPES
return requested