# 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