def mtext_handles(self) -> List[str]: """ Returns a list of all linked MTEXT handles. """ if self.linked_handles: return self.linked_handles handles = [] for column in self.linked_columns: if column.is_alive: handle = column.dxf.handle if handle is None: raise const.DXFStructureError( "Linked MTEXT column has no handle.") handles.append(handle) else: raise const.DXFStructureError("Linked MTEXT column deleted!") return handles
def bytes_loader(stream: BinaryIO) -> Iterable[DXFTag]: """ Yields :class:``DXFTag`` objects from a bytes `stream` (untrusted external source), skips all comment tags (group code == 999). ``DXFTag.code`` is always an ``int`` and ``DXFTag.value`` is always a raw bytes value without line endings. Works with file system streams and :class:`BytesIO` streams. Raises: DXFStructureError: Found invalid group code. """ line = 1 readline = stream.readline while True: code = readline() # ByteIO(): empty strings indicates EOF - does not raise an exception if code: try: code = int(code) except ValueError: code = code.decode(errors='ignore') raise const.DXFStructureError( f'Invalid group code "{code}" at line {line}.') else: return value = readline() # ByteIO(): empty strings indicates EOF if value: if code != 999: yield DXFTag(code, value.rstrip(b'\r\n')) line += 2 else: return
def load_dxf_attribs( self, processor: SubclassProcessor = None ) -> "DXFNamespace": dxf = super().load_dxf_attribs(processor) if processor: tags = processor.subclass_by_index(1) if tags is None: raise const.DXFStructureError( f"missing 'AcDbMLine' subclass in MLINE(#{dxf.handle})" ) try: # Find index of the count tag: index71 = tags.tag_index(71) except const.DXFValueError: # The count tag does not exist: DXF structure error? pass else: self.elements = MLineStyleElements(tags[index71 + 1 :]) # type: ignore # Remove processed tags: del tags[index71:] processor.fast_load_dxfattribs( dxf, acdb_mline_style_group_codes, tags ) return dxf
def load_edge(tags: Tags) -> 'EdgeTypes': edge_type = tags[0].value if 0 < edge_type < 5: return EDGE_CLASSES[edge_type].load_tags(tags[1:]) else: raise const.DXFStructureError( "HATCH: unknown edge type: {}".format(edge_type))
def export_linked_entities(self, tagwriter: 'TagWriter'): for mtext in self._columns.linked_columns: if mtext.dxf.handle is None: raise const.DXFStructureError( "Linked MTEXT column has no handle.") # Export linked columns as separated DXF entities: mtext.export_dxf(tagwriter)
def load_tags(self, tags: Iterator[DXFTag]) -> None: """ Constructor to generate header variables loaded from DXF files (untrusted environment). Args: tags: DXF tags as Tags() or ExtendedTags() """ tags = tags or 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
def load(self, entities: Iterator['DXFEntity']) -> None: """ Loading interface. (internal API)""" self._head = next(entities) if self._head.dxftype() != 'TABLE': raise const.DXFStructureError( "Critical structure error in TABLES section.") for table_entry in entities: self._append(table_entry)
def load_edge(tags: Tags) -> AbstractEdge: edge_type = tags[0].value if 0 < edge_type < 5: return EDGE_CLASSES[edge_type].load_tags(tags[1:]) else: raise const.DXFStructureError( f"HATCH: unknown edge type: {edge_type}" )
def set_data(self, entities: Iterable[DXFEntity]) -> None: """Set `entities` as new group content, entities should be an iterable :class:`DXFGraphic` or inherited (LINE, CIRCLE, ...). Raises :class:`DXFValueError` if not all entities be on the same layout (modelspace or any paperspace layout but not block) """ assert self.doc is not None entities = list(entities) valid_entities = filter_invalid_entities(entities, self.doc, str(self)) if len(valid_entities) != len(entities): raise const.DXFStructureError("invalid entities found") if not all_entities_on_same_layout(valid_entities): raise const.DXFStructureError( "All entities have to be in the same layout and are not allowed" " to be in a block layout.") self.clear() self._data = valid_entities
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 preprocess_export(self, tagwriter: "TagWriter") -> bool: # remove invalid entities assert self.doc is not None self.purge(self.doc) # export GROUP only if all entities reside on the same layout if not all_entities_on_same_layout(self._data): raise const.DXFStructureError( "All entities have to be in the same layout and are not allowed" " to be in a block layout.") return True
def associate(self, path: AbstractBoundaryPath, entities: Iterable["DXFEntity"]): """Set association from hatch boundary `path` to DXF geometry `entities`. A HATCH entity can be associative to a base geometry, this association is **not** maintained nor verified by `ezdxf`, so if you modify the base geometry the geometry of the boundary path is not updated and no verification is done to check if the associated geometry matches the boundary path, this opens many possibilities to create invalid DXF files: USE WITH CARE! """ # I don't see this as a time critical operation, do as much checks as # needed to avoid invalid DXF files. if not self.is_alive: raise const.DXFStructureError("HATCH entity is destroyed") doc = self.doc owner = self.dxf.owner handle = self.dxf.handle if doc is None or owner is None or handle is None: raise const.DXFStructureError( "virtual entity can not have associated entities") for entity in entities: if not entity.is_alive or entity.is_virtual: raise const.DXFStructureError( "associated entity is destroyed or a virtual entity") if doc is not entity.doc: raise const.DXFStructureError( "associated entity is from a different document") if owner != entity.dxf.owner: raise const.DXFStructureError( "associated entity is from a different layout") path.source_boundary_objects.append(entity.dxf.handle) entity.append_reactor_handle(handle) self.dxf.associative = 1 if len(path.source_boundary_objects) else 0
def load_paths(self, tags: Tags) -> Tags: # find first group code 91 = count of loops, Spline data also contains group code 91! try: start_index = tags.tag_index(91) except const.DXFValueError: raise const.DXFStructureError( "HATCH: Missing required DXF tag 'Number of boundary paths (loops)' (code=91).") path_tags = tags.collect_consecutive_tags(PATH_CODES, start=start_index + 1) if len(path_tags): self.paths = BoundaryPaths.load_tags(path_tags) end_index = start_index + len(path_tags) + 1 del tags[start_index: end_index] return tags
def import_entity(self, entity: "DXFEntity", target_layout: "BaseLayout" = None) -> None: """ Imports a single DXF `entity` into `target_layout` or the modelspace of the target document, if `target_layout` is ``None``. Args: entity: DXF entity to import target_layout: any layout (modelspace, paperspace or block) from the target document Raises: DXFStructureError: `target_layout` is not a layout of target document """ def set_dxf_attribs(e): e.doc = self.target # remove invalid resources e.dxf.discard("plotstyle_handle") e.dxf.discard("material_handle") e.dxf.discard("visualstyle_handle") if target_layout is None: target_layout = self.target.modelspace() elif target_layout.doc != self.target: raise const.DXFStructureError( "Target layout has to be a layout or block from the target " "document.") dxftype = entity.dxftype() if dxftype not in IMPORT_ENTITIES: logger.debug(f"Import of {str(entity)} not supported") return self._add_used_resources(entity) try: new_entity = cast("DXFGraphic", new_clean_entity(entity)) except const.DXFTypeError: logger.debug(f"Copying for DXF type {dxftype} not supported.") return set_dxf_attribs(new_entity) self.target.entitydb.add(new_entity) target_layout.add_entity(new_entity) try: # additional processing getattr(self, "_import_" + dxftype.lower())(new_entity) except AttributeError: pass
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 set_data(self, entities: Iterable[DXFEntity]) -> None: """ Set `entities` as new group content, entities should be an iterable :class:`DXFGraphic` or inherited (LINE, CIRCLE, ...). Raises :class:`DXFValueError` if not all entities be on the same layout (modelspace or any paperspace layout but not block) """ entities = list(entities) if not all_entities_on_same_layout(entities): raise const.DXFStructureError( "All entities have to be in the same layout and are not allowed" " to be in a block layout." ) self.clear() self._data = entities
def setup_columns(self, columns: MTextColumns, linked: bool = False) -> None: assert columns.column_type != ColumnType.NONE assert columns.count > 0, "one or more columns required" assert columns.width > 0, "column width has to be > 0" assert columns.gutter_width >= 0, "gutter width has to be >= 0" if self.has_columns: raise const.DXFStructureError('Column setup already exist.') self._columns = columns self.dxf.width = columns.width self.dxf.defined_height = columns.defined_height if columns.total_height < 1e-6: columns.total_height = columns.defined_height if columns.total_width < 1e-6: columns.update_total_width() if linked: self._create_linked_columns()
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 load_dxf_attribs(self, processor: SubclassProcessor = None ) -> "DXFNamespace": dxf = super().load_dxf_attribs(processor) if processor: tags = processor.subclass_by_index(2) if tags: tags = Tags(self.load_boundary_path(tags)) processor.fast_load_dxfattribs(dxf, acdb_underlay_group_codes, subclass=tags) if len(self.boundary_path) < 2: self.dxf = dxf self.reset_boundary_path() else: raise const.DXFStructureError( f"missing 'AcDbUnderlayReference' subclass in " f"{self.DXFTYPE}(#{dxf.handle})") return dxf
def load_dxf_attribs(self, processor: SubclassProcessor = None ) -> "DXFNamespace": dxf = super().load_dxf_attribs(processor) if processor: tags = processor.subclass_by_index(2) if tags: tags = Tags(self.load_mtext_content(tags)) processor.fast_load_dxfattribs(dxf, acdb_mtext_group_codes, subclass=tags, recover=True) if processor.embedded_objects: obj = processor.embedded_objects[0] self._columns = load_columns_from_embedded_object(dxf, obj) elif self.xdata: self._columns = load_columns_from_xdata(dxf, self.xdata) else: raise const.DXFStructureError( f"missing 'AcDbMText' subclass in MTEXT(#{dxf.handle})") return dxf
def bytes_loader(stream: BinaryIO) -> Iterable[DXFTag]: """ Yields :class:``DXFTag`` objects from a bytes `stream` (untrusted external source), skips all comment tags (group code == 999). ``DXFTag.code`` is always an ``int`` and ``DXFTag.value`` is always a raw bytes value without line endings. Works with file system streams and :class:`BytesIO` streams. Raises: DXFStructureError: Found invalid group code. """ line = 1 while True: try: code = stream.readline() value = stream.readline() except EOFError: # EOFError indicates a DXFStructureError, but should be handled # in top layers. return # ByteIO(): empty strings indicates EOF if code and value: try: code = int(code) except ValueError: code = code.decode(const.DEFAULT_ENCODING) raise const.DXFStructureError( f'Invalid group code "{code}" at line {line}.') else: if code != 999: yield DXFTag(code, value.rstrip(b'\r\n')) line += 2 else: return
def to_line_edges(spline_edge: SplineEdge): weights = spline_edge.weights if len(spline_edge.control_points): bspline = BSpline( control_points=spline_edge.control_points, order=spline_edge.degree + 1, knots=spline_edge.knot_values, weights=weights if len(weights) else None, ) elif len(spline_edge.fit_points): bspline = BSpline.from_fit_points( spline_edge.fit_points, spline_edge.degree ) else: raise const.DXFStructureError( "SplineEdge() without control points or fit points." ) segment_count = (max(len(bspline.control_points), 3) - 1) * factor vertices = list(bspline.approximate(segment_count)) for v1, v2 in zip(vertices[:-1], vertices[1:]): edge = LineEdge() edge.start = v1.vec2 edge.end = v2.vec2 yield edge
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 byte_tag_compiler( tags: Iterable[DXFTag], encoding=const.DEFAULT_ENCODING, messages: List = None, errors: str = 'surrogateescape', ) -> Iterable[DXFTag]: """ Compiles DXF tag values imported by bytes_loader() into Python types. Raises DXFStructureError() for invalid float values and invalid coordinate values. Expects DXF coordinates written in x, y[, z] order, see function :func:`safe_tag_loader` for usage with applied repair filters. Args: tags: DXF tag generator, yielding tag values as bytes like bytes_loader() encoding: text encoding messages: list to store error messages 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: Found invalid DXF tag or unexpected coordinate order. """ def error_msg(tag): code = tag.code value = tag.value.decode(encoding) return f'Invalid tag ({code}, "{value}") near line: {line}.' if messages is None: messages = [] tags = iter(tags) undo_tag = None line = 0 while True: try: if undo_tag is not None: x = undo_tag undo_tag = None else: x = next(tags) line += 2 code = x.code if code in POINT_CODES: y = next(tags) # y coordinate is mandatory line += 2 # e.g. y-code for x-code=10 is 20 if y.code != code + 10: raise const.DXFStructureError( f"Missing required y-coordinate near line: {line}.") # optional z coordinate z = next(tags) line += 2 try: # is it a z-coordinate like (30, 0.0) for base x-code=10 if z.code == code + 20: try: point = (float(x.value), float(y.value), float(z.value)) except ValueError: # search for any float values point = (_search_float(x.value), _search_float(y.value), _search_float(z.value)) else: try: point = (float(x.value), float(y.value)) except ValueError: # seach for any float values point = ( _search_float(x.value), _search_float(y.value), ) undo_tag = z except ValueError: raise const.DXFStructureError( f'Invalid floating point values near line: {line}.') yield DXFVertex(code, point) elif code in BINARY_DATA: # maybe pre compiled in low level tagger (binary DXF) if isinstance(x, DXFBinaryTag): tag = x else: try: tag = DXFBinaryTag.from_string(code, x.value) except ValueError: raise const.DXFStructureError( f'Invalid binary data near line: {line}.') yield tag else: # just a single tag type_ = TYPE_TABLE.get(code, str) value: bytes = x.value if type_ is str: if code == 0: # remove white space from structure tags value = x.value.strip().upper() try: # 2 stages to document decoding errors str_ = value.decode(encoding, errors='strict') except UnicodeDecodeError: str_ = value.decode(encoding, errors=errors) messages.append( (AuditError.DECODING_ERROR, f'Fixed unicode decoding error near line {line}')) # Convert DXF unicode notation "\U+xxxx" to unicode, # but exclude structure tags (code>0): if code and has_dxf_unicode(str_): str_ = decode_dxf_unicode(str_) yield DXFTag(code, str_) else: try: # fast path for int and float yield DXFTag(code, type_(value)) except ValueError: # slow path - e.g. ProE stores int values as floats :(( if type_ is int: try: yield DXFTag(code, _search_int(x.value)) except ValueError: raise const.DXFStructureError(error_msg(x)) elif type_ is float: try: yield DXFTag(code, _search_float(x.value)) except ValueError: raise const.DXFStructureError(error_msg(x)) else: raise const.DXFStructureError(error_msg(x)) except StopIteration: return
def generate_geometry(self, vertices: List[Vec3]) -> None: """Regenerate the MLINE geometry for new reference line defined by `vertices`. """ vertices = list(filter_close_vertices(vertices, abs_tol=1e-6)) if len(vertices) == 0: self.clear() return elif len(vertices) == 1: self.vertices = [MLineVertex.new(vertices[0], X_AXIS, Y_AXIS)] return style = self.style assert style is not None, "valid MLINE style required" if len(style.elements) == 0: raise const.DXFStructureError( f"No line elements defined in {str(style)}." ) def miter(dir1: Vec3, dir2: Vec3): return ((dir1 + dir2) * 0.5).normalize().orthogonal() ucs = UCS.from_z_axis_and_point_in_xz( origin=vertices[0], point=vertices[1], axis=self.dxf.extrusion, ) # Transform given vertices into UCS and project them into the # UCS-xy-plane by setting the z-axis to 0: vertices = [v.replace(z=0) for v in ucs.points_from_wcs(vertices)] start_angle = style.dxf.start_angle end_angle = style.dxf.end_angle line_directions = [ (v2 - v1).normalize() for v1, v2 in zip(vertices, vertices[1:]) ] if self.is_closed: line_directions.append((vertices[0] - vertices[-1]).normalize()) closing_miter = miter(line_directions[0], line_directions[-1]) miter_directions = [closing_miter] else: closing_miter = None line_directions.append(line_directions[-1]) miter_directions = [line_directions[0].rotate_deg(start_angle)] for d1, d2 in zip(line_directions, line_directions[1:]): miter_directions.append(miter(d1, d2)) if closing_miter is None: miter_directions.pop() miter_directions.append(line_directions[-1].rotate_deg(end_angle)) else: miter_directions.append(closing_miter) self.vertices = [ MLineVertex.new(v, d, m) for v, d, m in zip(vertices, line_directions, miter_directions) ] self._update_parametrization() # reverse transformation into WCS for v in self.vertices: v.transform(ucs.matrix)
def byte_tag_compiler(tags: Iterable[DXFTag], encoding=const.DEFAULT_ENCODING) -> Iterable[DXFTag]: """ Compiles DXF tag values imported by bytes_loader() into Python types. Raises DXFStructureError() for invalid float values and invalid coordinate values. Expects DXF coordinates written in x, y[, z] order, see function :func:`safe_tag_loader` for usage with applied repair filters. Args: tags: DXF tag generator, yielding tag values as bytes like bytes_loader() encoding: text encoding Raises: DXFStructureError: Found invalid DXF tag or unexpected coordinate order. """ def error_msg(tag): code = tag.code value = tag.value.decode(encoding) return f'Invalid tag ({code}, "{value}") near line: {line}.' tags = iter(tags) undo_tag = None line = 0 while True: try: if undo_tag is not None: x = undo_tag undo_tag = None else: x = next(tags) line += 2 code = x.code if code in POINT_CODES: y = next(tags) # y coordinate is mandatory line += 2 # e.g. y-code for x-code=10 is 20 if y.code != code + 10: raise const.DXFStructureError( f"Missing required y-coordinate near line: {line}.") # optional z coordinate z = next(tags) line += 2 try: # is it a z-coordinate like (30, 0.0) for base x-code=10 if z.code == code + 20: point = (float(x.value), float(y.value), float(z.value)) else: point = (float(x.value), float(y.value)) undo_tag = z except ValueError: raise const.DXFStructureError( f'Invalid floating point values near line: {line}.') yield DXFVertex(code, point) elif code in BINARY_DATA: # maybe pre compiled in low level tagger (binary DXF) if isinstance(x, DXFBinaryTag): tag = x else: try: tag = DXFBinaryTag.from_string(code, x.value) except ValueError: raise const.DXFStructureError( f'Invalid binary data near line: {line}.') yield tag else: # just a single tag type_ = TYPE_TABLE.get(code, str) value: bytes = x.value if type_ is str: if code == 0: # remove white space from structure tags value = x.value.strip() yield DXFTag(code, value.decode(encoding)) else: try: # fast path for int and float yield DXFTag(code, type_(value)) except ValueError: # slow path - ProE stores int values as floats :(( if type_ is int: try: yield DXFTag(code, int(float(x.value))) except ValueError: raise const.DXFStructureError(error_msg(x)) else: raise const.DXFStructureError(error_msg(x)) except StopIteration: return
def entity_linker_(entity: DXFEntity) -> bool: """Collect and link entities which are linked to a parent entity: - VERTEX -> POLYLINE - ATTRIB -> INSERT Args: entity: examined DXF entity Returns: True if `entity` is linked to a parent entity """ nonlocal main_entity, expected_dxftype dxftype: str = entity.dxftype() # INSERT & POLYLINE are not linked entities, they are stored in the # entity space. are_linked_entities = False if main_entity is not None: # VERTEX, ATTRIB & SEQEND are linked tags, they are NOT stored in # the entity space. are_linked_entities = True if dxftype == "SEQEND": main_entity.link_seqend(entity) # type: ignore # Marks also the end of the main entity main_entity = None # Check for valid DXF structure: # VERTEX follows POLYLINE # ATTRIB follows INSERT elif dxftype == expected_dxftype: main_entity.link_entity(entity) # type: ignore else: raise const.DXFStructureError( f"Expected DXF entity {dxftype} or SEQEND") elif dxftype in LINKED_ENTITIES: # Only INSERT and POLYLINE have a linked entities structure: if dxftype == "INSERT" and not entity.dxf.get("attribs_follow", 0): # INSERT must not have following ATTRIBS: # # INSERT with no ATTRIBS, attribs_follow == 0 # ATTRIB as a stand alone entity, which is a DXF structure # error, but this error should be handled in the audit # process. # .... # INSERT with ATTRIBS, attribs_follow == 1 # ATTRIB as connected entity # SEQEND # # Therefore a ATTRIB following an INSERT doesn't mean that # these entities are linked. pass else: main_entity = entity expected_dxftype = LINKED_ENTITIES[dxftype] # Attached MTEXT entity - this feature most likely does not exist! elif (dxftype == "MTEXT") and (entity.dxf.handle is None): logger.error( "Found attached MTEXT entity. Please open an issue at github: " "https://github.com/mozman/ezdxf/issues and provide a DXF " "example file.") return are_linked_entities
def entity_linker_(entity: DXFEntity) -> bool: """ Collect and link entities which are linked to a parent entity: - VERTEX -> POLYLINE - ATTRIB -> INSERT - attached MTEXT entity Args: entity: examined DXF entity Returns: True if `entity` is linked to a parent entity """ nonlocal main_entity, expected_dxftype, prev dxftype: str = entity.dxftype() # INSERT & POLYLINE are not linked entities, they are stored in the # entity space. are_linked_entities = False if main_entity is not None: # VERTEX, ATTRIB & SEQEND are linked tags, they are NOT stored in # the entity space. are_linked_entities = True if dxftype == 'SEQEND': main_entity.link_seqend(entity) # Marks also the end of the main entity main_entity = None # Check for valid DXF structure: # VERTEX follows POLYLINE # ATTRIB follows INSERT elif dxftype == expected_dxftype: main_entity.link_entity(entity) else: raise const.DXFStructureError( f"Expected DXF entity {dxftype} or SEQEND") elif dxftype in LINKED_ENTITIES: # Only INSERT and POLYLINE have a linked entities structure: if dxftype == 'INSERT' and not entity.dxf.get('attribs_follow', 0): # INSERT must not have following ATTRIBS, ATTRIB can be a stand # alone entity: # # INSERT with no ATTRIBS, attribs_follow == 0 # ATTRIB as stand alone entity # .... # INSERT with ATTRIBS, attribs_follow == 1 # ATTRIB as connected entity # SEQEND # # Therefore a ATTRIB following an INSERT doesn't mean that # these entities are linked. pass else: main_entity = entity expected_dxftype = LINKED_ENTITIES[dxftype] # Attached MTEXT entity: elif (dxftype == 'MTEXT') and (entity.dxf.handle is None): if prev: prev.link_entity(entity) are_linked_entities = True else: raise const.DXFStructureError( "Found attached MTEXT entity without a preceding entity.") prev = entity return are_linked_entities