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

665 lines
24 KiB
Python

# Purpose: Import data from another DXF document
# Copyright (c) 2013-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, cast, Union, Optional
import logging
from ezdxf.lldxf import const
from ezdxf.render.arrows import ARROWS
from ezdxf.entities import (
DXFEntity,
DXFGraphic,
Hatch,
Polyline,
DimStyle,
Dimension,
Viewport,
Linetype,
Insert,
)
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.layouts import BaseLayout, Layout
logger = logging.getLogger("ezdxf")
IMPORT_TABLES = ["linetypes", "layers", "styles", "dimstyles"]
IMPORT_ENTITIES = {
"LINE",
"POINT",
"CIRCLE",
"ARC",
"TEXT",
"SOLID",
"TRACE",
"3DFACE",
"SHAPE",
"POLYLINE",
"ATTRIB",
"INSERT",
"ELLIPSE",
"MTEXT",
"LWPOLYLINE",
"SPLINE",
"HATCH",
"MESH",
"XLINE",
"RAY",
"ATTDEF",
"DIMENSION",
"LEADER", # dimension style override not supported!
"VIEWPORT",
}
class Importer:
"""
The :class:`Importer` class is central element for importing data from
other DXF documents.
Args:
source: source :class:`~ezdxf.drawing.Drawing`
target: target :class:`~ezdxf.drawing.Drawing`
Attributes:
source: source DXF document
target: target DXF document
used_layers: Set of used layer names as string, AutoCAD accepts layer
names without a LAYER table entry.
used_linetypes: Set of used linetype names as string, these linetypes
require a TABLE entry or AutoCAD will crash.
used_styles: Set of used text style names, these text styles require
a TABLE entry or AutoCAD will crash.
used_dimstyles: Set of used dimension style names, these dimension
styles require a TABLE entry or AutoCAD will crash.
"""
def __init__(self, source: Drawing, target: Drawing):
self.source: Drawing = source
self.target: Drawing = target
self.used_layers: set[str] = set()
self.used_linetypes: set[str] = set()
self.used_styles: set[str] = set()
self.used_shape_files: set[str] = set() # style entry without a name!
self.used_dimstyles: set[str] = set()
self.used_arrows: set[str] = set()
self.handle_mapping: dict[str, str] = dict() # old_handle: new_handle
# collects all imported INSERT entities, for later name resolving.
self.imported_inserts: list[DXFEntity] = list() # imported inserts
# collects all imported block names and their assigned new name
# imported_block[original_name] = new_name
self.imported_blocks: dict[str, str] = dict()
self._default_plotstyle_handle = target.plotstyles["Normal"].dxf.handle
self._default_material_handle = target.materials["Global"].dxf.handle
def _add_used_resources(self, entity: DXFEntity) -> None:
"""Register used resources."""
self.used_layers.add(entity.get_dxf_attrib("layer", "0"))
self.used_linetypes.add(entity.get_dxf_attrib("linetype", "BYLAYER"))
if entity.is_supported_dxf_attrib("style"):
self.used_styles.add(entity.get_dxf_attrib("style", "Standard"))
if entity.is_supported_dxf_attrib("dimstyle"):
self.used_dimstyles.add(entity.get_dxf_attrib("dimstyle", "Standard"))
def _add_dimstyle_resources(self, dimstyle: DimStyle) -> None:
self.used_styles.add(dimstyle.get_dxf_attrib("dimtxsty", "Standard"))
self.used_linetypes.add(dimstyle.get_dxf_attrib("dimltype", "BYLAYER"))
self.used_linetypes.add(dimstyle.get_dxf_attrib("dimltex1", "BYLAYER"))
self.used_linetypes.add(dimstyle.get_dxf_attrib("dimltex2", "BYLAYER"))
self.used_arrows.add(dimstyle.get_dxf_attrib("dimblk", ""))
self.used_arrows.add(dimstyle.get_dxf_attrib("dimblk1", ""))
self.used_arrows.add(dimstyle.get_dxf_attrib("dimblk2", ""))
self.used_arrows.add(dimstyle.get_dxf_attrib("dimldrblk", ""))
def _add_linetype_resources(self, linetype: Linetype) -> None:
if not linetype.pattern_tags.is_complex_type():
return
style_handle = linetype.pattern_tags.get_style_handle()
style = self.source.entitydb.get(style_handle)
if style is None:
return
if style.dxf.name == "":
# Shape file entries have no name!
self.used_shape_files.add(style.dxf.font)
else:
self.used_styles.add(style.dxf.name)
def import_tables(
self, table_names: Union[str, Iterable[str]] = "*", replace=False
) -> None:
"""Import DXF tables from the source document into the target document.
Args:
table_names: iterable of tables names as strings, or a single table
name as string or "*" for all supported tables
replace: ``True`` to replace already existing table entries else
ignore existing entries
Raises:
TypeError: unsupported table type
"""
if isinstance(table_names, str):
if table_names == "*": # import all supported tables
table_names = IMPORT_TABLES
else: # import one specific table
table_names = (table_names,)
for table_name in table_names:
self.import_table(table_name, entries="*", replace=replace)
def import_table(
self, name: str, entries: Union[str, Iterable[str]] = "*", replace=False
) -> None:
"""
Import specific table entries from the source document into the
target document.
Args:
name: valid table names are "layers", "linetypes" and "styles"
entries: Iterable of table names as strings, or a single table name
or "*" for all table entries
replace: ``True`` to replace the already existing table entry else
ignore existing entries
Raises:
TypeError: unsupported table type
"""
if name not in IMPORT_TABLES:
raise TypeError(f'Table "{name}" import not supported.')
source_table = getattr(self.source.tables, name)
target_table = getattr(self.target.tables, name)
if isinstance(entries, str):
if entries == "*": # import all table entries
entries = (entry.dxf.name for entry in source_table)
else: # import just one table entry
entries = (entries,)
for entry_name in entries:
try:
table_entry = source_table.get(entry_name)
except const.DXFTableEntryError:
logger.warning(
f'Required table entry "{entry_name}" in table f{name} '
f"not found."
)
continue
entry_name = table_entry.dxf.name
if entry_name in target_table:
if replace:
logger.debug(
f'Replacing already existing entry "{entry_name}" '
f"of {name} table."
)
target_table.remove(table_entry.dxf.name)
else:
logger.debug(
f'Discarding already existing entry "{entry_name}" '
f"of {name} table."
)
continue
if name == "layers":
self.used_linetypes.add(
table_entry.get_dxf_attrib("linetype", "Continuous")
)
elif name == "dimstyles":
self._add_dimstyle_resources(table_entry)
elif name == "linetypes":
self._add_linetype_resources(table_entry)
# Duplicate table entry:
new_table_entry = self._duplicate_table_entry(table_entry)
target_table.add_entry(new_table_entry)
# Register resource handles for mapping:
self.handle_mapping[table_entry.dxf.handle] = new_table_entry.dxf.handle
def import_shape_files(self, fonts: set[str]) -> None:
"""Import shape file table entries from the source document into the
target document.
Shape file entries are stored in the styles table but without a name.
"""
for font in fonts:
table_entry = self.source.styles.find_shx(font)
# copy is not necessary, just create a new entry:
new_table_entry = self.target.styles.get_shx(font)
if table_entry:
# Register resource handles for mapping:
self.handle_mapping[table_entry.dxf.handle] = new_table_entry.dxf.handle
else:
logger.warning(f'Required shape file entry "{font}" not found.')
def _set_table_entry_dxf_attribs(self, entity: DXFEntity) -> None:
entity.doc = self.target
if entity.dxf.hasattr("plotstyle_handle"):
entity.dxf.plotstyle_handle = self._default_plotstyle_handle
if entity.dxf.hasattr("material_handle"):
entity.dxf.material_handle = self._default_material_handle
def _duplicate_table_entry(self, entry: DXFEntity) -> DXFEntity:
# duplicate table entry
new_entry = new_clean_entity(entry)
self._set_table_entry_dxf_attribs(entry)
# create a new handle and add entity to target entity database
self.target.entitydb.add(new_entry)
return new_entry
def import_entity(
self, entity: DXFEntity, target_layout: Optional[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 _import_insert(self, insert: Insert):
self.imported_inserts.append(insert)
# remove all possible source document dependencies from sub entities
for attrib in insert.attribs:
remove_dependencies(attrib)
def _import_polyline(self, polyline: Polyline):
# remove all possible source document dependencies from sub entities
for vertex in polyline.vertices:
remove_dependencies(vertex)
def _import_hatch(self, hatch: Hatch):
hatch.dxf.discard("associative")
def _import_viewport(self, viewport: Viewport):
viewport.dxf.discard("sun_handle")
viewport.dxf.discard("clipping_boundary_handle")
viewport.dxf.discard("ucs_handle")
viewport.dxf.discard("ucs_base_handle")
viewport.dxf.discard("background_handle")
viewport.dxf.discard("shade_plot_handle")
viewport.dxf.discard("visual_style_handle")
viewport.dxf.discard("ref_vp_object_1")
viewport.dxf.discard("ref_vp_object_2")
viewport.dxf.discard("ref_vp_object_3")
viewport.dxf.discard("ref_vp_object_4")
def _import_dimension(self, dimension: Dimension):
if dimension.virtual_block_content:
for entity in dimension.virtual_block_content:
if isinstance(entity, Insert): # import arrow blocks
self.import_block(entity.dxf.name, rename=False)
self._add_used_resources(entity)
else:
logger.error("The required geometry block for DIMENSION is not defined.")
def import_entities(
self,
entities: Iterable[DXFEntity],
target_layout: Optional[BaseLayout] = None,
) -> None:
"""Import all `entities` into `target_layout` or the modelspace of the
target document, if `target_layout` is ``None``.
Args:
entities: Iterable of DXF entities
target_layout: any layout (modelspace, paperspace or block) from
the target document
Raises:
DXFStructureError: `target_layout` is not a layout of target document
"""
for entity in entities:
self.import_entity(entity, target_layout)
def import_modelspace(self, target_layout: Optional[BaseLayout] = None) -> None:
"""Import all entities from source modelspace into `target_layout` or
the modelspace of the target document, if `target_layout` is ``None``.
Args:
target_layout: any layout (modelspace, paperspace or block) from
the target document
Raises:
DXFStructureError: `target_layout` is not a layout of target document
"""
self.import_entities(self.source.modelspace(), target_layout=target_layout)
def recreate_source_layout(self, name: str) -> Layout:
"""Recreate source paperspace layout `name` in the target document.
The layout will be renamed if `name` already exist in the target
document. Returns target modelspace for layout name "Model".
Args:
name: layout name as string
Raises:
KeyError: if source layout `name` not exist
"""
def get_target_name():
tname = name
base_name = name
count = 1
while tname in self.target.layouts:
tname = base_name + str(count)
count += 1
return tname
def clear(dxfattribs: dict) -> dict:
def discard(name: str):
try:
del dxfattribs[name]
except KeyError:
pass
discard("handle")
discard("owner")
discard("taborder")
discard("shade_plot_handle")
discard("block_record_handle")
discard("viewport_handle")
discard("ucs_handle")
discard("base_ucs_handle")
return dxfattribs
if name.lower() == "model":
return self.target.modelspace()
source_layout = self.source.layouts.get(name) # raises KeyError
target_name = get_target_name()
dxfattribs = clear(source_layout.dxf_layout.dxfattribs())
target_layout = self.target.layouts.new(target_name, dxfattribs=dxfattribs)
return target_layout
def import_paperspace_layout(self, name: str) -> Layout:
"""Import paperspace layout `name` into the target document.
Recreates the source paperspace layout in the target document, renames
the target paperspace if a paperspace with same `name` already exist
and imports all entities from the source paperspace into the target
paperspace.
Args:
name: source paper space name as string
Returns: new created target paperspace :class:`Layout`
Raises:
KeyError: source paperspace does not exist
DXFTypeError: invalid modelspace import
"""
if name.lower() == "model":
raise const.DXFTypeError(
"Can not import modelspace, use method import_modelspace()."
)
source_layout = self.source.layouts.get(name)
target_layout = self.recreate_source_layout(name)
self.import_entities(source_layout, target_layout)
return target_layout
def import_paperspace_layouts(self) -> None:
"""Import all paperspace layouts and their content into the target
document.
Target layouts will be renamed if a layout with the same name already
exist. Layouts will be imported in original tab order.
"""
for name in self.source.layouts.names_in_taborder():
if name.lower() != "model": # do not import modelspace
self.import_paperspace_layout(name)
def import_blocks(self, block_names: Iterable[str], rename=False) -> None:
"""Import all BLOCK definitions from source document.
If a BLOCK already exist the BLOCK will be renamed if argument
`rename` is ``True``, otherwise the existing BLOCK in the target
document will be used instead of the BLOCK from the source document.
Required name resolving for imported BLOCK references (INSERT), will be
done in the :meth:`Importer.finalize` method.
Args:
block_names: names of BLOCK definitions to import
rename: rename BLOCK if a BLOCK with the same name already exist in
target document
Raises:
ValueError: BLOCK in source document not found (defined)
"""
for block_name in block_names:
self.import_block(block_name, rename=rename)
def import_block(self, block_name: str, rename=True) -> str:
"""Import one BLOCK definition from source document.
If the BLOCK already exist the BLOCK will be renamed if argument
`rename` is ``True``, otherwise the existing BLOCK in the target
document will be used instead of the BLOCK in the source document.
Required name resolving for imported block references (INSERT), will be
done in the :meth:`Importer.finalize` method.
To replace an existing BLOCK in the target document, just delete it
before importing data:
:code:`target.blocks.delete_block(block_name, safe=False)`
Args:
block_name: name of BLOCK to import
rename: rename BLOCK if a BLOCK with the same name already exist in
target document
Returns: (renamed) BLOCK name
Raises:
ValueError: BLOCK in source document not found (defined)
"""
def get_new_block_name() -> str:
num = 0
name = block_name
while name in target_blocks:
name = block_name + str(num)
num += 1
return name
try: # already imported block?
return self.imported_blocks[block_name]
except KeyError:
pass
try:
source_block = self.source.blocks[block_name]
except const.DXFKeyError:
raise ValueError(f'Source block "{block_name}" not found.')
target_blocks = self.target.blocks
if (block_name in target_blocks) and (rename is False):
self.imported_blocks[block_name] = block_name
return block_name
new_block_name = get_new_block_name()
block = source_block.block
assert block is not None
target_block = target_blocks.new(
new_block_name,
base_point=block.dxf.base_point,
dxfattribs={
"description": block.dxf.description,
"flags": block.dxf.flags,
"xref_path": block.dxf.xref_path,
},
)
self.import_entities(source_block, target_layout=target_block)
self.imported_blocks[block_name] = new_block_name
return new_block_name
def _create_missing_arrows(self):
"""Create or import required arrow blocks, used by the LEADER or the
DIMSTYLE entity, which are not imported automatically because they are
not used in an anonymous DIMENSION geometry BLOCK.
"""
self.used_arrows.discard(
""
) # standard default arrow '' needs no block definition
for arrow_name in self.used_arrows:
if ARROWS.is_acad_arrow(arrow_name):
self.target.acquire_arrow(arrow_name)
else:
self.import_block(arrow_name, rename=False)
def _resolve_inserts(self) -> None:
"""Resolve BLOCK names of imported BLOCK reference entities (INSERT).
This is required for the case the name of the imported BLOCK collides
with an already existing BLOCK in the target document and the conflict
resolving method is "rename".
"""
while len(self.imported_inserts):
inserts = list(self.imported_inserts)
# clear imported inserts, block import may append additional inserts
self.imported_inserts = []
for insert in inserts:
block_name = self.import_block(insert.dxf.name)
insert.dxf.name = block_name
def _import_required_table_entries(self) -> None:
"""Import required table entries collected while importing entities
into the target document.
"""
# 1. dimstyles import adds additional required linetype and style
# resources and required arrows
if len(self.used_dimstyles):
self.import_table("dimstyles", self.used_dimstyles)
# 2. layers import adds additional required linetype resources
if len(self.used_layers):
self.import_table("layers", self.used_layers)
# 3. complex linetypes adds additional required style resources
if len(self.used_linetypes):
self.import_table("linetypes", self.used_linetypes)
# 4. Text styles do not add additional required resources
if len(self.used_styles):
self.import_table("styles", self.used_styles)
# 5. Shape files are text style entries without a name
if len(self.used_shape_files):
self.import_shape_files(self.used_shape_files)
# 6. Update text style handles of imported complex linetypes:
self.update_complex_linetypes()
def _add_required_complex_linetype_resources(self):
for ltype_name in self.used_linetypes:
try:
ltype = self.source.linetypes.get(ltype_name)
except const.DXFTableEntryError:
continue
self._add_linetype_resources(ltype)
def update_complex_linetypes(self):
std_handle = self.target.styles.get("STANDARD").dxf.handle
for linetype in self.target.linetypes:
if linetype.pattern_tags.is_complex_type():
old_handle = linetype.pattern_tags.get_style_handle()
new_handle = self.handle_mapping.get(old_handle, std_handle)
linetype.pattern_tags.set_style_handle(new_handle)
def finalize(self) -> None:
"""Finalize the import by importing required table entries and BLOCK
definitions, without finalization the target document is maybe invalid
for AutoCAD. Call the :meth:`~Importer.finalize()` method as last step
of the import process.
"""
self._resolve_inserts()
self._add_required_complex_linetype_resources()
self._import_required_table_entries()
self._create_missing_arrows()
def new_clean_entity(entity: DXFEntity, keep_xdata: bool = False) -> DXFEntity:
"""Copy entity and remove all external dependencies.
Args:
entity: DXF entity
keep_xdata: keep xdata flag
"""
new_entity = entity.copy()
new_entity.doc = None
return remove_dependencies(new_entity, keep_xdata=keep_xdata)
def remove_dependencies(entity: DXFEntity, keep_xdata: bool = False) -> DXFEntity:
"""Remove all external dependencies.
Args:
entity: DXF entity
keep_xdata: keep xdata flag
"""
entity.appdata = None
entity.reactors = None
entity.extension_dict = None
if not keep_xdata:
entity.xdata = None
return entity