refactor: excel parse
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Low Level DXF modules
|
||||
# Copyright (c) 2011-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
@@ -0,0 +1,255 @@
|
||||
# Copyright (c) 2011-2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Callable,
|
||||
Any,
|
||||
Union,
|
||||
NewType,
|
||||
cast,
|
||||
NamedTuple,
|
||||
Mapping,
|
||||
)
|
||||
from enum import Enum
|
||||
from .const import DXFAttributeError, DXF12
|
||||
import copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFEntity
|
||||
|
||||
|
||||
class DefSubclass(NamedTuple):
|
||||
name: Optional[str]
|
||||
attribs: dict[str, DXFAttr]
|
||||
|
||||
|
||||
VIRTUAL_TAG = -666
|
||||
|
||||
|
||||
class XType(Enum):
|
||||
"""Extended Attribute Types"""
|
||||
|
||||
point2d = 1 # 2D points only
|
||||
point3d = 2 # 3D points only
|
||||
any_point = 3 # 2D or 3D points
|
||||
callback = 4 # callback attribute
|
||||
|
||||
|
||||
def group_code_mapping(
|
||||
subclass: DefSubclass, *, ignore: Optional[Iterable[int]] = None
|
||||
) -> dict[int, Union[str, list[str]]]:
|
||||
# Unique group codes are stored as group_code <int>: name <str>
|
||||
# Duplicate group codes are stored as group_code <int>: [name1, name2, ...] <list>
|
||||
# The order of appearance is important, therefore also callback attributes
|
||||
# have to be included, but they should not be loaded into the DXF namespace.
|
||||
mapping: dict[int, Union[str, list[str]]] = dict()
|
||||
for name, dxfattrib in subclass.attribs.items():
|
||||
if dxfattrib.xtype == XType.callback:
|
||||
# Mark callback attributes for special treatment as invalid
|
||||
# Python name:
|
||||
name = "*" + name
|
||||
code = dxfattrib.code
|
||||
existing_data: Union[None, str, list[str]] = mapping.get(code)
|
||||
if existing_data is None:
|
||||
mapping[code] = name
|
||||
else:
|
||||
if isinstance(existing_data, str):
|
||||
existing_data = [existing_data]
|
||||
mapping[code] = existing_data
|
||||
assert isinstance(existing_data, list)
|
||||
existing_data.append(name)
|
||||
|
||||
if ignore:
|
||||
# Mark these tags as "*IGNORE" to be ignored,
|
||||
# but they are not real callbacks! See POLYLINE for example!
|
||||
for code in ignore:
|
||||
mapping[code] = "*IGNORE"
|
||||
return mapping
|
||||
|
||||
|
||||
def merge_group_code_mappings(*mappings: Mapping) -> dict[int, str]:
|
||||
merge_group_code_mapping: dict[int, str] = {}
|
||||
for index, mapping in enumerate(mappings):
|
||||
msg = f"{index}. mapping contains none unique group codes"
|
||||
if not all(isinstance(e, str) for e in mapping.values()):
|
||||
raise TypeError(msg)
|
||||
if any(k in merge_group_code_mapping for k in mapping.keys()):
|
||||
raise TypeError(msg)
|
||||
merge_group_code_mapping.update(mapping)
|
||||
return merge_group_code_mapping
|
||||
|
||||
|
||||
# Unique object as marker
|
||||
ReturnDefault = NewType("ReturnDefault", object)
|
||||
RETURN_DEFAULT = cast(ReturnDefault, object())
|
||||
|
||||
|
||||
class DXFAttr:
|
||||
"""Represents a DXF attribute for an DXF entity, accessible by the
|
||||
DXF namespace :attr:`DXFEntity.dxf` like ``entity.dxf.color = 7``.
|
||||
This definitions are immutable by design not by implementation.
|
||||
|
||||
Extended Attribute Types
|
||||
------------------------
|
||||
|
||||
- XType.point2d: 2D points only
|
||||
- XType.point3d: 3D point only
|
||||
- XType.any_point: mixed 2D/3D point
|
||||
- XType.callback: Calls get_value(entity) to get the value of DXF
|
||||
attribute 'name', and calls set_value(entity, value) to set value
|
||||
of DXF attribute 'name'.
|
||||
|
||||
See example definition: ezdxf.entities.dxfgfx.acdb_entity.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: int,
|
||||
xtype: Optional[XType] = None,
|
||||
default=None,
|
||||
optional: bool = False,
|
||||
dxfversion: str = DXF12,
|
||||
getter: str = "",
|
||||
setter: str = "",
|
||||
alias: str = "",
|
||||
validator: Optional[Callable[[Any], bool]] = None,
|
||||
fixer: Optional[Union[Callable[[Any], Any], None, ReturnDefault]] = None,
|
||||
):
|
||||
|
||||
# Attribute name set by DXFAttributes.__init__()
|
||||
self.name: str = ""
|
||||
|
||||
# DXF group code
|
||||
self.code: int = code
|
||||
|
||||
# Extended attribute type:
|
||||
self.xtype: Optional[XType] = xtype
|
||||
|
||||
# DXF default value
|
||||
self.default: Any = default
|
||||
|
||||
# If optional is True, this attribute will be exported to DXF files
|
||||
# only if the given value differs from default value.
|
||||
self.optional: bool = optional
|
||||
|
||||
# This attribute is valid for all DXF versions starting from the
|
||||
# specified DXF version, default is DXF12 = 'AC1009'
|
||||
self.dxfversion: str = dxfversion
|
||||
|
||||
# DXF entity getter method name for callback attributes
|
||||
self.getter: str = getter
|
||||
|
||||
# DXF entity setter method name for callback attributes
|
||||
self.setter: str = setter
|
||||
|
||||
# Alternative name for this attribute
|
||||
self.alias: str = alias
|
||||
|
||||
# Returns True if given value is valid - the validator should be as
|
||||
# fast as possible!
|
||||
self.validator: Optional[Callable[[Any], bool]] = validator
|
||||
|
||||
# Returns a fixed value for invalid attributes, the fixer is called
|
||||
# only if the validator returns False.
|
||||
if fixer is RETURN_DEFAULT:
|
||||
fixer = self._return_default
|
||||
# excluding ReturnDefault type
|
||||
self.fixer = cast(Optional[Callable[[Any], Any]], fixer)
|
||||
|
||||
def _return_default(self, x: Any) -> Any:
|
||||
return self.default
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"({self.name}, {self.code})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "DXFAttr" + self.__str__()
|
||||
|
||||
def get_callback_value(self, entity: DXFEntity) -> Any:
|
||||
"""
|
||||
Executes a callback function in 'entity' to get a DXF value.
|
||||
|
||||
Callback function is defined by self.getter as string.
|
||||
|
||||
Args:
|
||||
entity: DXF entity
|
||||
|
||||
Raises:
|
||||
AttributeError: getter method does not exist
|
||||
TypeError: getter is None
|
||||
|
||||
Returns:
|
||||
DXF attribute value
|
||||
|
||||
"""
|
||||
try:
|
||||
return getattr(entity, self.getter)()
|
||||
except AttributeError:
|
||||
raise DXFAttributeError(
|
||||
f"DXF attribute {self.name}: invalid getter {self.getter}."
|
||||
)
|
||||
except TypeError:
|
||||
raise DXFAttributeError(f"DXF attribute {self.name} has no getter.")
|
||||
|
||||
def set_callback_value(self, entity: DXFEntity, value: Any) -> None:
|
||||
"""Executes a callback function in 'entity' to set a DXF value.
|
||||
|
||||
Callback function is defined by self.setter as string.
|
||||
|
||||
Args:
|
||||
entity: DXF entity
|
||||
value: DXF attribute value
|
||||
|
||||
Raises:
|
||||
AttributeError: setter method does not exist
|
||||
TypeError: setter is None
|
||||
|
||||
"""
|
||||
try:
|
||||
getattr(entity, self.setter)(value)
|
||||
except AttributeError:
|
||||
raise DXFAttributeError(
|
||||
f"DXF attribute {self.name}: invalid setter {self.setter}."
|
||||
)
|
||||
except TypeError:
|
||||
raise DXFAttributeError(f"DXF attribute {self.name} has no setter.")
|
||||
|
||||
def is_valid_value(self, value: Any) -> bool:
|
||||
if self.validator:
|
||||
return self.validator(value)
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class DXFAttributes:
|
||||
__slots__ = ("_attribs",)
|
||||
|
||||
def __init__(self, *subclassdefs: DefSubclass):
|
||||
self._attribs: dict[str, DXFAttr] = dict()
|
||||
for subclass in subclassdefs:
|
||||
for name, dxfattrib in subclass.attribs.items():
|
||||
dxfattrib.name = name
|
||||
self._attribs[name] = dxfattrib
|
||||
if dxfattrib.alias:
|
||||
alias = copy.copy(dxfattrib)
|
||||
alias.name = dxfattrib.alias
|
||||
alias.alias = dxfattrib.name
|
||||
self._attribs[dxfattrib.alias] = alias
|
||||
|
||||
def __contains__(self, name: str) -> bool:
|
||||
return name in self._attribs
|
||||
|
||||
def get(self, key: str) -> Optional[DXFAttr]:
|
||||
return self._attribs.get(key)
|
||||
|
||||
def build_group_code_items(self, func=lambda x: True) -> Iterator[tuple[int, str]]:
|
||||
return (
|
||||
(attrib.code, name)
|
||||
for name, attrib in self._attribs.items()
|
||||
if attrib.code > 0 and func(name)
|
||||
)
|
||||
@@ -0,0 +1,706 @@
|
||||
# Copyright (c) 2011-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
DXF9 = "AC1004"
|
||||
DXF10 = "AC1006"
|
||||
DXF12 = "AC1009"
|
||||
DXF13 = "AC1012"
|
||||
DXF14 = "AC1014"
|
||||
DXF2000 = "AC1015"
|
||||
DXF2004 = "AC1018"
|
||||
DXF2007 = "AC1021"
|
||||
DXF2010 = "AC1024"
|
||||
DXF2013 = "AC1027"
|
||||
DXF2018 = "AC1032"
|
||||
|
||||
acad_release = {
|
||||
DXF9: "R9",
|
||||
DXF10: "R10",
|
||||
DXF12: "R12",
|
||||
DXF13: "R13",
|
||||
DXF14: "R14",
|
||||
DXF2000: "R2000",
|
||||
DXF2004: "R2004",
|
||||
DXF2007: "R2007",
|
||||
DXF2010: "R2010",
|
||||
DXF2013: "R2013",
|
||||
DXF2018: "R2018",
|
||||
}
|
||||
|
||||
acad_maint_ver = {
|
||||
DXF12: 0,
|
||||
DXF2000: 6,
|
||||
DXF2004: 0,
|
||||
DXF2007: 25,
|
||||
DXF2010: 6,
|
||||
DXF2013: 105,
|
||||
DXF2018: 4,
|
||||
}
|
||||
|
||||
versions_supported_by_new = [
|
||||
DXF12,
|
||||
DXF2000,
|
||||
DXF2004,
|
||||
DXF2007,
|
||||
DXF2010,
|
||||
DXF2013,
|
||||
DXF2018,
|
||||
]
|
||||
versions_supported_by_save = versions_supported_by_new
|
||||
LATEST_DXF_VERSION = versions_supported_by_new[-1]
|
||||
|
||||
acad_release_to_dxf_version = {acad: dxf for dxf, acad in acad_release.items()}
|
||||
|
||||
|
||||
class DXFError(Exception):
|
||||
"""Base exception for all `ezdxf` exceptions. """
|
||||
pass
|
||||
|
||||
|
||||
class InvalidGeoDataException(DXFError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFStructureError(DXFError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFLoadError(DXFError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFAppDataError(DXFStructureError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFXDataError(DXFStructureError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFVersionError(DXFError):
|
||||
"""Errors related to features not supported by the chosen DXF Version"""
|
||||
pass
|
||||
|
||||
|
||||
class DXFInternalEzdxfError(DXFError):
|
||||
"""Indicates internal errors - should be fixed by mozman"""
|
||||
pass
|
||||
|
||||
|
||||
class DXFUnsupportedFeature(DXFError):
|
||||
"""Indicates unsupported features for DXFEntities e.g. translation for
|
||||
ACIS data
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class DXFValueError(DXFError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFKeyError(DXFError, KeyError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFAttributeError(DXFError, AttributeError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFIndexError(DXFError, IndexError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFTypeError(DXFError, TypeError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFTableEntryError(DXFValueError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFEncodingError(DXFError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFDecodingError(DXFError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFInvalidLineType(DXFValueError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFBlockInUseError(DXFValueError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFUndefinedBlockError(DXFKeyError):
|
||||
pass
|
||||
|
||||
|
||||
class DXFRenderError(DXFError):
|
||||
"""Errors related to DXF "rendering" tasks.
|
||||
|
||||
In this context "rendering" means creating the graphical representation of
|
||||
complex DXF entities by DXF primitives (LINE, TEXT, ...)
|
||||
e.g. for DIMENSION or LEADER entities.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class DXFMissingDefinitionPoint(DXFRenderError):
|
||||
"""Missing required definition points in the DIMENSION entity."""
|
||||
|
||||
|
||||
def normalize_dxfversion(dxfversion: str, check_save=True) -> str:
|
||||
"""Normalizes the DXF version string to "AC10xx"."""
|
||||
dxfversion = dxfversion.upper()
|
||||
dxfversion = acad_release_to_dxf_version.get(dxfversion, dxfversion)
|
||||
if check_save and dxfversion not in versions_supported_by_save:
|
||||
raise DXFVersionError(f"Invalid DXF version: {dxfversion}")
|
||||
return dxfversion
|
||||
|
||||
|
||||
MANAGED_SECTIONS = {
|
||||
"HEADER",
|
||||
"CLASSES",
|
||||
"TABLES",
|
||||
"BLOCKS",
|
||||
"ENTITIES",
|
||||
"OBJECTS",
|
||||
"ACDSDATA",
|
||||
}
|
||||
|
||||
TABLE_NAMES_ACAD_ORDER = [
|
||||
"VPORT",
|
||||
"LTYPE",
|
||||
"LAYER",
|
||||
"STYLE",
|
||||
"VIEW",
|
||||
"UCS",
|
||||
"APPID",
|
||||
"DIMSTYLE",
|
||||
"BLOCK_RECORD",
|
||||
]
|
||||
|
||||
DEFAULT_TEXT_STYLE = "Standard"
|
||||
DEFAULT_TEXT_FONT = "txt"
|
||||
APP_DATA_MARKER = 102
|
||||
SUBCLASS_MARKER = 100
|
||||
XDATA_MARKER = 1001
|
||||
COMMENT_MARKER = 999
|
||||
STRUCTURE_MARKER = 0
|
||||
HEADER_VAR_MARKER = 9
|
||||
ACAD_REACTORS = "{ACAD_REACTORS"
|
||||
ACAD_XDICTIONARY = "{ACAD_XDICTIONARY"
|
||||
XDICT_HANDLE_CODE = 360
|
||||
REACTOR_HANDLE_CODE = 330
|
||||
OWNER_CODE = 330
|
||||
|
||||
# https://help.autodesk.com/view/OARX/2018/ENU/?guid=GUID-2553CF98-44F6-4828-82DD-FE3BC7448113
|
||||
MAX_STR_LEN = 255 # DXF R12 without line endings
|
||||
EXT_MAX_STR_LEN = 2049 # DXF R2000+ without line endings
|
||||
|
||||
# Special tag codes for internal purpose:
|
||||
# -1 to -5 id reserved by AutoCAD for internal use, but this tags will never be
|
||||
# saved to file.
|
||||
# Same approach here, the following tags have to be converted/transformed into
|
||||
# normal tags before saved to file.
|
||||
COMPRESSED_TAGS = -10
|
||||
|
||||
# All color related constants are located in colors.py
|
||||
BYBLOCK = 0
|
||||
BYLAYER = 256
|
||||
BYOBJECT = 257
|
||||
RED = 1
|
||||
YELLOW = 2
|
||||
GREEN = 3
|
||||
CYAN = 4
|
||||
BLUE = 5
|
||||
MAGENTA = 6
|
||||
BLACK = 7
|
||||
WHITE = 7
|
||||
|
||||
# All transparency related constants are located in colors.py
|
||||
TRANSPARENCY_BYBLOCK = 0x01000000
|
||||
|
||||
|
||||
LINEWEIGHT_BYLAYER = -1
|
||||
LINEWEIGHT_BYBLOCK = -2
|
||||
LINEWEIGHT_DEFAULT = -3
|
||||
|
||||
VALID_DXF_LINEWEIGHTS = (
|
||||
0,
|
||||
5,
|
||||
9,
|
||||
13,
|
||||
15,
|
||||
18,
|
||||
20,
|
||||
25,
|
||||
30,
|
||||
35,
|
||||
40,
|
||||
50,
|
||||
53,
|
||||
60,
|
||||
70,
|
||||
80,
|
||||
90,
|
||||
100,
|
||||
106,
|
||||
120,
|
||||
140,
|
||||
158,
|
||||
200,
|
||||
211,
|
||||
)
|
||||
MAX_VALID_LINEWEIGHT = VALID_DXF_LINEWEIGHTS[-1]
|
||||
VALID_DXF_LINEWEIGHT_VALUES = set(VALID_DXF_LINEWEIGHTS) | {
|
||||
LINEWEIGHT_DEFAULT,
|
||||
LINEWEIGHT_BYLAYER,
|
||||
LINEWEIGHT_BYBLOCK,
|
||||
}
|
||||
|
||||
# Entity: Polyline, Polymesh
|
||||
# 70 flags
|
||||
POLYLINE_CLOSED = 1
|
||||
POLYLINE_MESH_CLOSED_M_DIRECTION = POLYLINE_CLOSED
|
||||
POLYLINE_CURVE_FIT_VERTICES_ADDED = 2
|
||||
POLYLINE_SPLINE_FIT_VERTICES_ADDED = 4
|
||||
POLYLINE_3D_POLYLINE = 8
|
||||
POLYLINE_3D_POLYMESH = 16
|
||||
POLYLINE_MESH_CLOSED_N_DIRECTION = 32
|
||||
POLYLINE_POLYFACE = 64
|
||||
POLYLINE_GENERATE_LINETYPE_PATTERN = 128
|
||||
|
||||
# Entity: Polymesh
|
||||
# 75 surface smooth type
|
||||
POLYMESH_NO_SMOOTH = 0
|
||||
POLYMESH_QUADRATIC_BSPLINE = 5
|
||||
POLYMESH_CUBIC_BSPLINE = 6
|
||||
POLYMESH_BEZIER_SURFACE = 8
|
||||
|
||||
# Entity: Vertex
|
||||
# 70 flags
|
||||
VERTEXNAMES = ("vtx0", "vtx1", "vtx2", "vtx3")
|
||||
VTX_EXTRA_VERTEX_CREATED = 1 # Extra vertex created by curve-fitting
|
||||
VTX_CURVE_FIT_TANGENT = 2 # Curve-fit tangent defined for this vertex.
|
||||
# A curve-fit tangent direction of 0 may be omitted from the DXF output, but is
|
||||
# significant if this bit is set.
|
||||
# 4 = unused, never set in dxf files
|
||||
VTX_SPLINE_VERTEX_CREATED = 8 # Spline vertex created by spline-fitting
|
||||
VTX_SPLINE_FRAME_CONTROL_POINT = 16
|
||||
VTX_3D_POLYLINE_VERTEX = 32
|
||||
VTX_3D_POLYGON_MESH_VERTEX = 64
|
||||
VTX_3D_POLYFACE_MESH_VERTEX = 128
|
||||
|
||||
VERTEX_FLAGS = {
|
||||
"AcDb2dPolyline": 0,
|
||||
"AcDb3dPolyline": VTX_3D_POLYLINE_VERTEX,
|
||||
"AcDbPolygonMesh": VTX_3D_POLYGON_MESH_VERTEX,
|
||||
"AcDbPolyFaceMesh": VTX_3D_POLYGON_MESH_VERTEX
|
||||
| VTX_3D_POLYFACE_MESH_VERTEX,
|
||||
}
|
||||
POLYLINE_FLAGS = {
|
||||
"AcDb2dPolyline": 0,
|
||||
"AcDb3dPolyline": POLYLINE_3D_POLYLINE,
|
||||
"AcDbPolygonMesh": POLYLINE_3D_POLYMESH,
|
||||
"AcDbPolyFaceMesh": POLYLINE_POLYFACE,
|
||||
}
|
||||
|
||||
# block-type flags (bit coded values, may be combined):
|
||||
# Entity: BLOCK
|
||||
# 70 flags
|
||||
|
||||
# This is an anonymous block generated by hatching, associative dimensioning,
|
||||
# other internal operations, or an application:
|
||||
BLK_ANONYMOUS = 1
|
||||
|
||||
# This block has non-constant attribute definitions (this bit is not set if the
|
||||
# block has any attribute definitions that are constant, or has no attribute
|
||||
# definitions at all)
|
||||
BLK_NON_CONSTANT_ATTRIBUTES = 2
|
||||
|
||||
# This block is an external reference (xref):
|
||||
BLK_XREF = 4
|
||||
|
||||
# This block is an xref overlay:
|
||||
BLK_XREF_OVERLAY = 8
|
||||
|
||||
# This block is externally dependent:
|
||||
BLK_EXTERNAL = 16
|
||||
|
||||
# This is a resolved external reference, or dependent of an external reference
|
||||
# (ignored on input):
|
||||
BLK_RESOLVED = 32
|
||||
|
||||
# This definition is a referenced external reference (ignored on input):
|
||||
BLK_REFERENCED = 64
|
||||
|
||||
LWPOLYLINE_CLOSED = 1
|
||||
LWPOLYLINE_PLINEGEN = 128
|
||||
|
||||
DEFAULT_TTF = "DejaVuSans.ttf"
|
||||
|
||||
# TextHAlign enum
|
||||
LEFT = 0
|
||||
CENTER = 1
|
||||
RIGHT = 2
|
||||
ALIGNED = 3
|
||||
FIT = 5
|
||||
|
||||
BASELINE = 0
|
||||
BOTTOM = 1
|
||||
MIDDLE = 2
|
||||
TOP = 3
|
||||
MIRROR_X = 2
|
||||
BACKWARD = MIRROR_X
|
||||
MIRROR_Y = 4
|
||||
UPSIDE_DOWN = MIRROR_Y
|
||||
|
||||
VERTICAL_STACKED = 4 # only stored in TextStyle.dxf.flags!
|
||||
|
||||
# Special char and encodings used in TEXT, ATTRIB and ATTDEF:
|
||||
# "%%d" -> "°"
|
||||
SPECIAL_CHAR_ENCODING = {
|
||||
"c": "Ø", # alt-0216
|
||||
"d": "°", # alt-0176
|
||||
"p": "±", # alt-0177
|
||||
}
|
||||
# Inline codes for strokes in TEXT, ATTRIB and ATTDEF
|
||||
# %%u underline
|
||||
# %%o overline
|
||||
# %%k strike through
|
||||
# Formatting will be applied until the same code appears again or the end
|
||||
# of line.
|
||||
# Special codes and formatting is case insensitive: d=D, u=U
|
||||
|
||||
# MTextEntityAlignment enum
|
||||
MTEXT_TOP_LEFT = 1
|
||||
MTEXT_TOP_CENTER = 2
|
||||
MTEXT_TOP_RIGHT = 3
|
||||
MTEXT_MIDDLE_LEFT = 4
|
||||
MTEXT_MIDDLE_CENTER = 5
|
||||
MTEXT_MIDDLE_RIGHT = 6
|
||||
MTEXT_BOTTOM_LEFT = 7
|
||||
MTEXT_BOTTOM_CENTER = 8
|
||||
MTEXT_BOTTOM_RIGHT = 9
|
||||
|
||||
# MTextFlowDirection enum
|
||||
MTEXT_LEFT_TO_RIGHT = 1
|
||||
MTEXT_TOP_TO_BOTTOM = 3
|
||||
MTEXT_BY_STYLE = 5
|
||||
|
||||
# MTextLineSpacing enum
|
||||
MTEXT_AT_LEAST = 1
|
||||
MTEXT_EXACT = 2
|
||||
|
||||
MTEXT_COLOR_INDEX = {
|
||||
"red": RED,
|
||||
"yellow": YELLOW,
|
||||
"green": GREEN,
|
||||
"cyan": CYAN,
|
||||
"blue": BLUE,
|
||||
"magenta": MAGENTA,
|
||||
"white": WHITE,
|
||||
}
|
||||
|
||||
# MTextBackgroundColor enum
|
||||
MTEXT_BG_OFF = 0
|
||||
MTEXT_BG_COLOR = 1
|
||||
MTEXT_BG_WINDOW_COLOR = 2
|
||||
MTEXT_BG_CANVAS_COLOR = 3
|
||||
MTEXT_TEXT_FRAME = 16
|
||||
|
||||
|
||||
CLOSED_SPLINE = 1
|
||||
PERIODIC_SPLINE = 2
|
||||
RATIONAL_SPLINE = 4
|
||||
PLANAR_SPLINE = 8
|
||||
LINEAR_SPLINE = 16
|
||||
|
||||
# Hatch constants
|
||||
HATCH_TYPE_USER_DEFINED = 0
|
||||
HATCH_TYPE_PREDEFINED = 1
|
||||
HATCH_TYPE_CUSTOM = 2
|
||||
HATCH_PATTERN_TYPE = ["user-defined", "predefined", "custom"]
|
||||
|
||||
ISLAND_DETECTION = ["nested", "outermost", "ignore"]
|
||||
HATCH_STYLE_NORMAL = 0
|
||||
HATCH_STYLE_NESTED = 0
|
||||
HATCH_STYLE_OUTERMOST = 1
|
||||
HATCH_STYLE_IGNORE = 2
|
||||
|
||||
BOUNDARY_PATH_DEFAULT = 0
|
||||
BOUNDARY_PATH_EXTERNAL = 1
|
||||
BOUNDARY_PATH_POLYLINE = 2
|
||||
BOUNDARY_PATH_DERIVED = 4
|
||||
BOUNDARY_PATH_TEXTBOX = 8
|
||||
BOUNDARY_PATH_OUTERMOST = 16
|
||||
|
||||
|
||||
def boundary_path_flag_names(flags: int) -> list[str]:
|
||||
if flags == 0:
|
||||
return ["default"]
|
||||
types: list[str] = []
|
||||
if flags & BOUNDARY_PATH_EXTERNAL:
|
||||
types.append("external")
|
||||
if flags & BOUNDARY_PATH_POLYLINE:
|
||||
types.append("polyline")
|
||||
if flags & BOUNDARY_PATH_DERIVED:
|
||||
types.append("derived")
|
||||
if flags & BOUNDARY_PATH_TEXTBOX:
|
||||
types.append("textbox")
|
||||
if flags & BOUNDARY_PATH_OUTERMOST:
|
||||
types.append("outermost")
|
||||
return types
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BoundaryPathState:
|
||||
external: bool = False
|
||||
derived: bool = False
|
||||
textbox: bool = False
|
||||
outermost: bool = False
|
||||
|
||||
@staticmethod
|
||||
def from_flags(flag: int) -> "BoundaryPathState":
|
||||
return BoundaryPathState(
|
||||
external=bool(flag & BOUNDARY_PATH_EXTERNAL),
|
||||
derived=bool(flag & BOUNDARY_PATH_DERIVED),
|
||||
textbox=bool(flag & BOUNDARY_PATH_TEXTBOX),
|
||||
outermost=bool(flag & BOUNDARY_PATH_OUTERMOST),
|
||||
)
|
||||
|
||||
@property
|
||||
def default(self) -> bool:
|
||||
return not (
|
||||
self.external or self.derived or self.outermost or self.textbox
|
||||
)
|
||||
|
||||
|
||||
GRADIENT_TYPES = frozenset(
|
||||
[
|
||||
"LINEAR",
|
||||
"CYLINDER",
|
||||
"INVCYLINDER",
|
||||
"SPHERICAL",
|
||||
"INVSPHERICAL",
|
||||
"HEMISPHERICAL",
|
||||
"INVHEMISPHERICAL",
|
||||
"CURVED",
|
||||
"INVCURVED",
|
||||
]
|
||||
)
|
||||
|
||||
# Viewport Status Flags (VSF) group code=90
|
||||
VSF_PERSPECTIVE_MODE = 0x1 # enabled if set
|
||||
VSF_FRONT_CLIPPING = 0x2 # enabled if set
|
||||
VSF_BACK_CLIPPING = 0x4 # enabled if set
|
||||
VSF_USC_FOLLOW = 0x8 # enabled if set
|
||||
VSF_FRONT_CLIPPING_NOT_AT_EYE = 0x10 # enabled if set
|
||||
VSF_UCS_ICON_VISIBILITY = 0x20 # enabled if set
|
||||
VSF_UCS_ICON_AT_ORIGIN = 0x40 # enabled if set
|
||||
VSF_FAST_ZOOM = 0x80 # enabled if set
|
||||
VSF_SNAP_MODE = 0x100 # enabled if set
|
||||
VSF_GRID_MODE = 0x200 # enabled if set
|
||||
VSF_ISOMETRIC_SNAP_STYLE = 0x400 # enabled if set
|
||||
VSF_HIDE_PLOT_MODE = 0x800 # enabled if set
|
||||
|
||||
# If set and kIsoPairRight is not set, then isopair top is enabled.
|
||||
# If both kIsoPairTop and kIsoPairRight are set, then isopair left is enabled:
|
||||
VSF_KISOPAIR_TOP = 0x1000
|
||||
|
||||
# If set and kIsoPairTop is not set, then isopair right is enabled:
|
||||
VSF_KISOPAIR_RIGHT = 0x2000
|
||||
VSF_VIEWPORT_ZOOM_LOCKING = 0x4000 # enabled if set
|
||||
VSF_LOCK_ZOOM = 0x4000 # enabled if set
|
||||
VSF_CURRENTLY_ALWAYS_ENABLED = 0x8000 # always set without a meaning :)
|
||||
VSF_NON_RECTANGULAR_CLIPPING = 0x10000 # enabled if set
|
||||
VSF_TURN_VIEWPORT_OFF = 0x20000
|
||||
VSF_NO_GRID_LIMITS = 0x40000
|
||||
VSF_ADAPTIVE_GRID_DISPLAY = 0x80000
|
||||
VSF_SUBDIVIDE_GRID = 0x100000
|
||||
VSF_GRID_FOLLOW_WORKPLANE = 0x200000
|
||||
|
||||
# Viewport Render Mode (VRM) group code=281
|
||||
VRM_2D_OPTIMIZED = 0
|
||||
VRM_WIREFRAME = 1
|
||||
VRM_HIDDEN_LINE = 2
|
||||
VRM_FLAT_SHADED = 3
|
||||
VRM_GOURAUD_SHADED = 4
|
||||
VRM_FLAT_SHADED_WITH_WIREFRAME = 5
|
||||
VRM_GOURAUD_SHADED_WITH_WIREFRAME = 6
|
||||
|
||||
IMAGE_SHOW = 1
|
||||
IMAGE_SHOW_WHEN_NOT_ALIGNED = 2
|
||||
IMAGE_USE_CLIPPING_BOUNDARY = 4
|
||||
IMAGE_TRANSPARENCY_IS_ON = 8
|
||||
|
||||
UNDERLAY_CLIPPING = 1
|
||||
UNDERLAY_ON = 2
|
||||
UNDERLAY_MONOCHROME = 4
|
||||
UNDERLAY_ADJUST_FOR_BG = 8
|
||||
|
||||
DIM_LINEAR = 0
|
||||
DIM_ALIGNED = 1
|
||||
DIM_ANGULAR = 2
|
||||
DIM_DIAMETER = 3
|
||||
DIM_RADIUS = 4
|
||||
DIM_ANGULAR_3P = 5
|
||||
DIM_ORDINATE = 6
|
||||
DIM_ARC = 8
|
||||
DIM_BLOCK_EXCLUSIVE = 32
|
||||
DIM_ORDINATE_TYPE = 64 # unset for x-type, set for y-type
|
||||
DIM_USER_LOCATION_OVERRIDE = 128
|
||||
|
||||
DIMZIN_SUPPRESS_ZERO_FEET_AND_PRECISELY_ZERO_INCHES = 0
|
||||
DIMZIN_INCLUDES_ZERO_FEET_AND_PRECISELY_ZERO_INCHES = 1
|
||||
DIMZIN_INCLUDES_ZERO_FEET_AND_SUPPRESSES_ZERO_INCHES = 2
|
||||
DIMZIN_INCLUDES_ZERO_INCHES_AND_SUPPRESSES_ZERO_FEET = 3
|
||||
DIMZIN_SUPPRESSES_LEADING_ZEROS = 4 # only decimal dimensions
|
||||
DIMZIN_SUPPRESSES_TRAILING_ZEROS = 8 # only decimal dimensions
|
||||
|
||||
# ATTRIB & ATTDEF flags
|
||||
ATTRIB_INVISIBLE = 1 # Attribute is invisible (does not appear)
|
||||
ATTRIB_CONST = 2 # This is a constant attribute
|
||||
ATTRIB_VERIFY = 4 # Verification is required on input of this attribute
|
||||
ATTRIB_IS_PRESET = 8 # no prompt during insertion
|
||||
|
||||
ATTRIB_TYPE_SINGLE_LINE = 1
|
||||
ATTRIB_TYPE_MULTI_LINE = 2
|
||||
ATTDEF_TYPE_MULTI_LINE = 4
|
||||
|
||||
# '|' is allowed in layer name, as ltype name ...
|
||||
INVALID_NAME_CHARACTERS = '<>/\\":;?*=`'
|
||||
INVALID_LAYER_NAME_CHARACTERS = set(INVALID_NAME_CHARACTERS)
|
||||
|
||||
STD_SCALES = {
|
||||
1: (1.0 / 128.0, 12.0),
|
||||
2: (1.0 / 64.0, 12.0),
|
||||
3: (1.0 / 32.0, 12.0),
|
||||
4: (1.0 / 16.0, 12.0),
|
||||
5: (3.0 / 32.0, 12.0),
|
||||
6: (1.0 / 8.0, 12.0),
|
||||
7: (3.0 / 16.0, 12.0),
|
||||
8: (1.0 / 4.0, 12.0),
|
||||
9: (3.0 / 8.0, 12.0),
|
||||
10: (1.0 / 2.0, 12.0),
|
||||
11: (3.0 / 4.0, 12.0),
|
||||
12: (1.0, 12.0),
|
||||
13: (3.0, 12.0),
|
||||
14: (6.0, 12.0),
|
||||
15: (12.0, 12.0),
|
||||
16: (1.0, 1.0),
|
||||
17: (1.0, 2.0),
|
||||
18: (1.0, 4.0),
|
||||
19: (1.0, 8.0),
|
||||
20: (1.0, 10.0),
|
||||
21: (1.0, 16.0),
|
||||
22: (1.0, 20.0),
|
||||
23: (1.0, 30.0),
|
||||
24: (1.0, 40.0),
|
||||
25: (1.0, 50.0),
|
||||
26: (1.0, 100.0),
|
||||
27: (2.0, 1.0),
|
||||
28: (4.0, 1.0),
|
||||
29: (8.0, 1.0),
|
||||
30: (10.0, 1.0),
|
||||
31: (100.0, 1.0),
|
||||
32: (1000.0, 1.0),
|
||||
}
|
||||
|
||||
RASTER_UNITS = {
|
||||
"none": 0,
|
||||
"mm": 1,
|
||||
"cm": 2,
|
||||
"m": 3,
|
||||
"km": 4,
|
||||
"in": 5,
|
||||
"ft": 6,
|
||||
"yd": 7,
|
||||
"mi": 8,
|
||||
}
|
||||
REVERSE_RASTER_UNITS = {value: name for name, value in RASTER_UNITS.items()}
|
||||
|
||||
MODEL_SPACE_R2000 = "*Model_Space"
|
||||
MODEL_SPACE_R12 = "$Model_Space"
|
||||
PAPER_SPACE_R2000 = "*Paper_Space"
|
||||
PAPER_SPACE_R12 = "$Paper_Space"
|
||||
TMP_PAPER_SPACE_NAME = "*Paper_Space999999"
|
||||
|
||||
MODEL_SPACE = {
|
||||
MODEL_SPACE_R2000.lower(),
|
||||
MODEL_SPACE_R12.lower(),
|
||||
}
|
||||
|
||||
PAPER_SPACE = {
|
||||
PAPER_SPACE_R2000.lower(),
|
||||
PAPER_SPACE_R12.lower(),
|
||||
}
|
||||
|
||||
LAYOUT_NAMES = {
|
||||
PAPER_SPACE_R2000.lower(),
|
||||
PAPER_SPACE_R12.lower(),
|
||||
MODEL_SPACE_R2000.lower(),
|
||||
MODEL_SPACE_R12.lower(),
|
||||
}
|
||||
|
||||
|
||||
# TODO: make enum
|
||||
DIMJUST = {
|
||||
"center": 0,
|
||||
"left": 1,
|
||||
"right": 2,
|
||||
"above1": 3,
|
||||
"above2": 4,
|
||||
}
|
||||
|
||||
|
||||
# TODO: make enum
|
||||
DIMTAD = {
|
||||
"above": 1,
|
||||
"center": 0,
|
||||
"below": 4,
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_ENCODING = "cp1252"
|
||||
|
||||
MLINE_TOP = 0
|
||||
MLINE_ZERO = 1
|
||||
MLINE_BOTTOM = 2
|
||||
MLINE_HAS_VERTICES = 1
|
||||
MLINE_CLOSED = 2
|
||||
MLINE_SUPPRESS_START_CAPS = 4
|
||||
MLINE_SUPPRESS_END_CAPS = 8
|
||||
|
||||
MLINESTYLE_FILL = 1
|
||||
MLINESTYLE_MITER = 2
|
||||
MLINESTYLE_START_SQUARE = 16
|
||||
MLINESTYLE_START_INNER_ARC = 32
|
||||
MLINESTYLE_START_ROUND = 64
|
||||
MLINESTYLE_END_SQUARE = 256
|
||||
MLINESTYLE_END_INNER_ARC = 512
|
||||
MLINESTYLE_END_ROUND = 1024
|
||||
|
||||
# VP Layer Overrides
|
||||
|
||||
OVR_ALPHA_KEY = "ADSK_XREC_LAYER_ALPHA_OVR"
|
||||
OVR_COLOR_KEY = "ADSK_XREC_LAYER_COLOR_OVR"
|
||||
OVR_LTYPE_KEY = "ADSK_XREC_LAYER_LINETYPE_OVR"
|
||||
OVR_LW_KEY = "ADSK_XREC_LAYER_LINEWT_OVR"
|
||||
|
||||
OVR_ALPHA_CODE = 440
|
||||
OVR_COLOR_CODE = 420
|
||||
OVR_LTYPE_CODE = 343
|
||||
OVR_LW_CODE = 91
|
||||
OVR_VP_HANDLE_CODE = 335
|
||||
|
||||
OVR_APP_ALPHA = "{ADSK_LYR_ALPHA_OVERRIDE"
|
||||
OVR_APP_COLOR = "{ADSK_LYR_COLOR_OVERRIDE"
|
||||
OVR_APP_LTYPE = "{ADSK_LYR_LINETYPE_OVERRIDE"
|
||||
OVR_APP_LW = "{ADSK_LYR_LINEWT_OVERRIDE"
|
||||
@@ -0,0 +1,85 @@
|
||||
# Copyright (c) 2016-2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
import re
|
||||
import codecs
|
||||
import binascii
|
||||
|
||||
surrogate_escape = codecs.lookup_error("surrogateescape")
|
||||
BACKSLASH_UNICODE = re.compile(r"(\\U\+[A-F0-9]{4})")
|
||||
MIF_ENCODED = re.compile(r"(\\M\+[1-5][A-F0-9]{4})")
|
||||
|
||||
|
||||
def dxf_backslash_replace(exc: Exception):
|
||||
if isinstance(exc, (UnicodeEncodeError, UnicodeTranslateError)):
|
||||
s = ""
|
||||
# mypy does not recognize properties: exc.start, exc.end, exc.object
|
||||
for c in exc.object[exc.start : exc.end]:
|
||||
x = ord(c)
|
||||
if x <= 0xFF:
|
||||
s += "\\x%02x" % x
|
||||
elif 0xDC80 <= x <= 0xDCFF:
|
||||
# Delegate surrogate handling:
|
||||
return surrogate_escape(exc)
|
||||
elif x <= 0xFFFF:
|
||||
s += "\\U+%04x" % x
|
||||
else:
|
||||
s += "\\U+%08x" % x
|
||||
return s, exc.end
|
||||
else:
|
||||
raise TypeError(f"Can't handle {exc.__class__.__name__}")
|
||||
|
||||
|
||||
def encode(s: str, encoding="utf8") -> bytes:
|
||||
"""Shortcut to use the correct error handler"""
|
||||
return s.encode(encoding, errors="dxfreplace")
|
||||
|
||||
|
||||
def _decode(s: str) -> str:
|
||||
if s.startswith(r"\U+"):
|
||||
return chr(int(s[3:], 16))
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
def has_dxf_unicode(s: str) -> bool:
|
||||
"""Returns ``True`` if string `s` contains ``\\U+xxxx`` encoded characters."""
|
||||
return bool(re.search(BACKSLASH_UNICODE, s))
|
||||
|
||||
|
||||
def decode_dxf_unicode(s: str) -> str:
|
||||
"""Decode ``\\U+xxxx`` encoded characters."""
|
||||
|
||||
return "".join(_decode(part) for part in re.split(BACKSLASH_UNICODE, s))
|
||||
|
||||
|
||||
def has_mif_encoding(s: str) -> bool:
|
||||
"""Returns ``True`` if string `s` contains MIF encoded (``\\M+cxxxx``) characters.
|
||||
"""
|
||||
return bool(re.search(MIF_ENCODED, s))
|
||||
|
||||
|
||||
def decode_mif_to_unicode(s: str) -> str:
|
||||
"""Decode MIF encoded characters ``\\M+cxxxx``."""
|
||||
return "".join(_decode_mif(part) for part in re.split(MIF_ENCODED, s))
|
||||
|
||||
|
||||
MIF_CODE_PAGE = {
|
||||
# See https://docs.intellicad.org/files/oda/2021_11/oda_drawings_docs/frames.html?frmname=topic&frmfile=FontHandling.html
|
||||
"1": "cp932", # Japanese (Shift-JIS)
|
||||
"2": "cp950", # Traditional Chinese (Big 5)
|
||||
"3": "cp949", # Wansung (KS C-5601-1987)
|
||||
"4": "cp1391", # Johab (KS C-5601-1992)
|
||||
"5": "cp936", # Simplified Chinese (GB 2312-80)
|
||||
}
|
||||
|
||||
|
||||
def _decode_mif(s: str) -> str:
|
||||
if s.startswith(r"\M+"):
|
||||
try:
|
||||
code_page = MIF_CODE_PAGE[s[3]]
|
||||
codec = codecs.lookup(code_page)
|
||||
byte_data = binascii.unhexlify(s[4:])
|
||||
return codec.decode(byte_data)[0]
|
||||
except Exception:
|
||||
pass
|
||||
return s
|
||||
@@ -0,0 +1,463 @@
|
||||
# Copyright (c) 2011-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, Iterator
|
||||
from itertools import chain
|
||||
import logging
|
||||
from .types import tuples_to_tags, NONE_TAG
|
||||
from .tags import Tags, DXFTag
|
||||
from .const import DXFStructureError, DXFValueError, DXFKeyError
|
||||
from .types import (
|
||||
APP_DATA_MARKER,
|
||||
SUBCLASS_MARKER,
|
||||
XDATA_MARKER,
|
||||
EMBEDDED_OBJ_MARKER,
|
||||
EMBEDDED_OBJ_STR,
|
||||
)
|
||||
from .types import is_app_data_marker, is_embedded_object_marker
|
||||
from .tagger import internal_tag_compiler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.eztypes import IterableTags
|
||||
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
class ExtendedTags:
|
||||
"""Manages DXF tags located in sub structures:
|
||||
|
||||
- Subclasses
|
||||
- AppData
|
||||
- Extended Data (XDATA)
|
||||
- Embedded objects
|
||||
|
||||
Args:
|
||||
tags: iterable of type DXFTag()
|
||||
legacy: flag for DXF R12 tags
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("subclasses", "appdata", "xdata", "embedded_objects")
|
||||
|
||||
def __init__(self, tags: Optional[Iterable[DXFTag]] = None, legacy=False):
|
||||
if isinstance(tags, str):
|
||||
raise DXFValueError(
|
||||
"use ExtendedTags.from_text() to create tags from a string."
|
||||
)
|
||||
|
||||
# code == 102, keys are "{<arbitrary name>", values are Tags()
|
||||
self.appdata: list[Tags] = list()
|
||||
|
||||
# code == 100, keys are "subclass-name", values are Tags()
|
||||
self.subclasses: list[Tags] = list()
|
||||
|
||||
# code >= 1000, keys are "APPID", values are Tags()
|
||||
self.xdata: list[Tags] = list()
|
||||
|
||||
# Store embedded objects as list, but embedded objects are rare, so
|
||||
# storing an empty list for every DXF entity is waste of memory.
|
||||
# Support for multiple embedded objects is maybe future proof, but
|
||||
# for now only one embedded object per entity is used.
|
||||
self.embedded_objects: Optional[list[Tags]] = None
|
||||
|
||||
if tags is not None:
|
||||
self._setup(iter(tags))
|
||||
if legacy:
|
||||
self.legacy_repair()
|
||||
|
||||
def legacy_repair(self):
|
||||
"""Legacy (DXF R12) tags handling and repair."""
|
||||
self.flatten_subclasses()
|
||||
# ... and we can do some checks:
|
||||
# DXF R12 does not support (102, '{APPID') ... structures
|
||||
if len(self.appdata):
|
||||
# Just a debug message, do not delete appdata, this would corrupt
|
||||
# the data structure.
|
||||
self.debug("Found application defined entity data in DXF R12.")
|
||||
|
||||
# That is really unlikely, but...
|
||||
if self.embedded_objects is not None:
|
||||
# Removing embedded objects from DXF R12 does not corrupt the
|
||||
# data structure:
|
||||
self.embedded_objects = None
|
||||
self.debug("Found embedded object in DXF R12.")
|
||||
|
||||
def flatten_subclasses(self):
|
||||
"""Flatten subclasses in legacy mode (DXF R12).
|
||||
|
||||
There exists DXF R12 with subclass markers, technical incorrect but
|
||||
works if the reader ignore subclass marker tags, unfortunately ezdxf
|
||||
tries to use this subclass markers and therefore R12 parsing by ezdxf
|
||||
does not work without removing these subclass markers.
|
||||
|
||||
This method removes all subclass markers and flattens all subclasses
|
||||
into ExtendedTags.noclass.
|
||||
|
||||
"""
|
||||
if len(self.subclasses) < 2:
|
||||
return
|
||||
noclass = self.noclass
|
||||
for subclass in self.subclasses[1:]:
|
||||
# Exclude first tag (100, subclass marker):
|
||||
noclass.extend(subclass[1:])
|
||||
self.subclasses = [noclass]
|
||||
self.debug("Removed subclass marker from entity for DXF R12.")
|
||||
|
||||
def debug(self, msg: str) -> None:
|
||||
msg += f" <{self.entity_name()}>"
|
||||
logger.debug(msg)
|
||||
|
||||
def entity_name(self) -> str:
|
||||
try:
|
||||
handle = f"(#{self.get_handle()})"
|
||||
except DXFValueError:
|
||||
handle = ""
|
||||
return self.dxftype() + handle
|
||||
|
||||
def __copy__(self) -> ExtendedTags:
|
||||
"""Shallow copy."""
|
||||
|
||||
def copy(tag_lists):
|
||||
return [tags.clone() for tags in tag_lists]
|
||||
|
||||
clone = self.__class__()
|
||||
clone.appdata = copy(self.appdata)
|
||||
clone.subclasses = copy(self.subclasses)
|
||||
clone.xdata = copy(self.xdata)
|
||||
if self.embedded_objects is not None:
|
||||
clone.embedded_objects = copy(self.embedded_objects)
|
||||
return clone
|
||||
|
||||
clone = __copy__
|
||||
|
||||
def __getitem__(self, index) -> Tags:
|
||||
return self.noclass[index]
|
||||
|
||||
@property
|
||||
def noclass(self) -> Tags:
|
||||
"""Short cut to access first subclass."""
|
||||
return self.subclasses[0]
|
||||
|
||||
def get_handle(self) -> str:
|
||||
"""Returns handle as hex string."""
|
||||
return self.noclass.get_handle()
|
||||
|
||||
def dxftype(self) -> str:
|
||||
"""Returns DXF type as string like "LINE"."""
|
||||
return self.noclass[0].value
|
||||
|
||||
def replace_handle(self, handle: str) -> None:
|
||||
"""Replace the existing entity handle by a new value."""
|
||||
self.noclass.replace_handle(handle)
|
||||
|
||||
def _setup(self, tags: Iterator[DXFTag]) -> None:
|
||||
def is_end_of_class(tag):
|
||||
# fast path
|
||||
if tag.code not in {
|
||||
SUBCLASS_MARKER,
|
||||
EMBEDDED_OBJ_MARKER,
|
||||
XDATA_MARKER,
|
||||
}:
|
||||
return False
|
||||
else:
|
||||
# really an embedded object
|
||||
if (
|
||||
tag.code == EMBEDDED_OBJ_MARKER
|
||||
and tag.value != EMBEDDED_OBJ_STR
|
||||
):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def collect_base_class() -> DXFTag:
|
||||
"""The base class contains AppData, but not XDATA and ends with
|
||||
SUBCLASS_MARKER, XDATA_MARKER or EMBEDDED_OBJ_MARKER.
|
||||
"""
|
||||
# All subclasses begin with (100, subclass name) EXCEPT DIMASSOC
|
||||
# has a subclass starting with: (1, AcDbOsnapPointRef)
|
||||
# This special subclass is ignored by ezdxf, content is included in
|
||||
# the preceding subclass: (100, AcDbDimAssoc)
|
||||
# TEXT contains 2x the (100, AcDbText).
|
||||
#
|
||||
# Therefore it is not possible to use an (ordered) dict with
|
||||
# the subclass name as key, but usual use case is access by
|
||||
# numerical index.
|
||||
|
||||
data = Tags()
|
||||
try:
|
||||
while True:
|
||||
tag = next(tags)
|
||||
if is_app_data_marker(tag):
|
||||
app_data_pos = len(self.appdata)
|
||||
data.append(DXFTag(tag.code, app_data_pos))
|
||||
collect_app_data(tag)
|
||||
elif is_end_of_class(tag):
|
||||
self.subclasses.append(data)
|
||||
return tag
|
||||
else:
|
||||
data.append(tag)
|
||||
except StopIteration:
|
||||
pass
|
||||
self.subclasses.append(data)
|
||||
return NONE_TAG
|
||||
|
||||
def collect_subclass(starttag: DXFTag) -> DXFTag:
|
||||
"""A subclass does NOT contain AppData or XDATA, and ends with
|
||||
SUBCLASS_MARKER, XDATA_MARKER or EMBEDDED_OBJ_MARKER.
|
||||
"""
|
||||
# All subclasses begin with (100, subclass name)
|
||||
# for exceptions and rant see: collect_base_class()
|
||||
|
||||
data = Tags([starttag])
|
||||
try:
|
||||
while True:
|
||||
tag = next(tags)
|
||||
# removed app data collection in subclasses
|
||||
if is_end_of_class(tag):
|
||||
self.subclasses.append(data)
|
||||
return tag
|
||||
else:
|
||||
data.append(tag)
|
||||
except StopIteration:
|
||||
pass
|
||||
self.subclasses.append(data)
|
||||
return NONE_TAG
|
||||
|
||||
def collect_app_data(starttag: DXFTag) -> None:
|
||||
"""AppData can't contain XDATA or subclasses.
|
||||
|
||||
AppData can only appear in the first unnamed subclass
|
||||
"""
|
||||
data = Tags([starttag])
|
||||
# Alternative closing tag 'APPID}':
|
||||
closing_strings = ("}", starttag.value[1:] + "}")
|
||||
while True:
|
||||
try:
|
||||
tag = next(tags)
|
||||
except StopIteration:
|
||||
raise DXFStructureError(
|
||||
"Missing closing (102, '}') tag in appdata structure."
|
||||
)
|
||||
data.append(tag)
|
||||
if (tag.code == APP_DATA_MARKER) and (
|
||||
tag.value in closing_strings
|
||||
):
|
||||
break
|
||||
# Other (102, ) tags are treated as usual DXF tags.
|
||||
self.appdata.append(data)
|
||||
|
||||
def collect_xdata(starttag: DXFTag) -> DXFTag:
|
||||
"""XDATA is always at the end of the entity even if an embedded
|
||||
object is present and can not contain AppData or subclasses.
|
||||
|
||||
"""
|
||||
data = Tags([starttag])
|
||||
try:
|
||||
while True:
|
||||
tag = next(tags)
|
||||
if tag.code == XDATA_MARKER:
|
||||
self.xdata.append(data)
|
||||
return tag
|
||||
else:
|
||||
data.append(tag)
|
||||
except StopIteration:
|
||||
pass
|
||||
self.xdata.append(data)
|
||||
return NONE_TAG
|
||||
|
||||
def collect_embedded_object(starttag: DXFTag) -> DXFTag:
|
||||
"""Since AutoCAD 2018, DXF entities can contain embedded objects
|
||||
starting with a (101, 'Embedded Object') tag.
|
||||
|
||||
All embedded object data is collected in a simple Tags() object,
|
||||
no subclass app data or XDATA processing is done.
|
||||
ezdxf does not use or modify the embedded object data, the data is
|
||||
just stored and written out as it is.
|
||||
|
||||
self.embedded_objects = [
|
||||
1. embedded object as Tags(),
|
||||
2. embedded object as Tags(),
|
||||
...
|
||||
]
|
||||
|
||||
Support for multiple embedded objects is maybe future proof, but
|
||||
for now only one embedded object per entity is used.
|
||||
|
||||
"""
|
||||
if self.embedded_objects is None:
|
||||
self.embedded_objects = list()
|
||||
data = Tags([starttag])
|
||||
try:
|
||||
while True:
|
||||
tag = next(tags)
|
||||
if (
|
||||
is_embedded_object_marker(tag)
|
||||
or tag.code == XDATA_MARKER
|
||||
):
|
||||
# Another embedded object found: maybe in the future
|
||||
# DXF entities can contain more than one embedded
|
||||
# object.
|
||||
self.embedded_objects.append(data)
|
||||
return tag
|
||||
else:
|
||||
data.append(tag)
|
||||
except StopIteration:
|
||||
pass
|
||||
self.embedded_objects.append(data)
|
||||
return NONE_TAG
|
||||
|
||||
# Preceding tags without a subclass
|
||||
tag = collect_base_class()
|
||||
while tag.code == SUBCLASS_MARKER:
|
||||
tag = collect_subclass(tag)
|
||||
|
||||
while is_embedded_object_marker(tag):
|
||||
tag = collect_embedded_object(tag)
|
||||
|
||||
# XDATA appear after an embedded object
|
||||
while tag.code == XDATA_MARKER:
|
||||
tag = collect_xdata(tag)
|
||||
|
||||
if tag is not NONE_TAG:
|
||||
raise DXFStructureError(
|
||||
"Unexpected tag '%r' at end of entity." % tag
|
||||
)
|
||||
|
||||
def __iter__(self) -> Iterator[DXFTag]:
|
||||
for subclass in self.subclasses:
|
||||
for tag in subclass:
|
||||
if tag.code == APP_DATA_MARKER and isinstance(tag.value, int):
|
||||
yield from self.appdata[tag.value]
|
||||
else:
|
||||
yield tag
|
||||
yield from chain.from_iterable(self.xdata)
|
||||
if self.embedded_objects is not None:
|
||||
yield from chain.from_iterable(self.embedded_objects)
|
||||
|
||||
def get_subclass(self, name: str, pos: int = 0) -> Tags:
|
||||
"""Get subclass `name`.
|
||||
|
||||
Args:
|
||||
name: subclass name as string like "AcDbEntity"
|
||||
pos: start searching at subclass `pos`.
|
||||
|
||||
"""
|
||||
for index, subclass in enumerate(self.subclasses):
|
||||
try:
|
||||
if (index >= pos) and (subclass[0].value == name):
|
||||
return subclass
|
||||
except IndexError:
|
||||
pass # subclass[0]: ignore empty subclasses
|
||||
|
||||
raise DXFKeyError(f'Subclass "{name}" does not exist.')
|
||||
|
||||
def has_subclass(self, name: str) -> bool:
|
||||
for subclass in self.subclasses:
|
||||
try:
|
||||
if subclass[0].value == name:
|
||||
return True
|
||||
except IndexError:
|
||||
pass # ignore empty subclasses
|
||||
return False
|
||||
|
||||
def has_xdata(self, appid: str) -> bool:
|
||||
"""``True`` if has XDATA for `appid`."""
|
||||
return any(xdata[0].value == appid for xdata in self.xdata)
|
||||
|
||||
def get_xdata(self, appid: str) -> Tags:
|
||||
"""Returns XDATA for `appid` as :class:`Tags`."""
|
||||
for xdata in self.xdata:
|
||||
if xdata[0].value == appid:
|
||||
return xdata
|
||||
raise DXFValueError(f'No extended data for APPID "{appid}".')
|
||||
|
||||
def set_xdata(self, appid: str, tags: IterableTags) -> None:
|
||||
"""Set `tags` as XDATA for `appid`."""
|
||||
xdata = self.get_xdata(appid)
|
||||
xdata[1:] = tuples_to_tags(tags)
|
||||
|
||||
def new_xdata(
|
||||
self, appid: str, tags: Optional[IterableTags] = None
|
||||
) -> Tags:
|
||||
"""Append a new XDATA block.
|
||||
|
||||
Assumes that no XDATA block with the same `appid` already exist::
|
||||
|
||||
try:
|
||||
xdata = tags.get_xdata('EZDXF')
|
||||
except ValueError:
|
||||
xdata = tags.new_xdata('EZDXF')
|
||||
"""
|
||||
xtags = Tags([DXFTag(XDATA_MARKER, appid)])
|
||||
if tags is not None:
|
||||
xtags.extend(tuples_to_tags(tags))
|
||||
self.xdata.append(xtags)
|
||||
return xtags
|
||||
|
||||
def has_app_data(self, appid: str) -> bool:
|
||||
"""``True`` if has application defined data for `appid`."""
|
||||
return any(appdata[0].value == appid for appdata in self.appdata)
|
||||
|
||||
def get_app_data(self, appid: str) -> Tags:
|
||||
"""Returns application defined data for `appid` as :class:`Tags`
|
||||
including marker tags."""
|
||||
for appdata in self.appdata:
|
||||
if appdata[0].value == appid:
|
||||
return appdata
|
||||
raise DXFValueError(
|
||||
f'Application defined group "{appid}" does not exist.'
|
||||
)
|
||||
|
||||
def get_app_data_content(self, appid: str) -> Tags:
|
||||
"""Returns application defined data for `appid` as :class:`Tags`
|
||||
without first and last marker tag.
|
||||
"""
|
||||
return Tags(self.get_app_data(appid)[1:-1])
|
||||
|
||||
def set_app_data_content(self, appid: str, tags: IterableTags) -> None:
|
||||
"""Set application defined data for `appid` for already exiting data."""
|
||||
app_data = self.get_app_data(appid)
|
||||
app_data[1:-1] = tuples_to_tags(tags)
|
||||
|
||||
def new_app_data(
|
||||
self,
|
||||
appid: str,
|
||||
tags: Optional[IterableTags] = None,
|
||||
subclass_name: Optional[str] = None,
|
||||
) -> Tags:
|
||||
"""Append a new application defined data to subclass `subclass_name`.
|
||||
|
||||
Assumes that no app data block with the same `appid` already exist::
|
||||
|
||||
try:
|
||||
app_data = tags.get_app_data('{ACAD_REACTORS', tags)
|
||||
except ValueError:
|
||||
app_data = tags.new_app_data('{ACAD_REACTORS', tags)
|
||||
|
||||
"""
|
||||
if not appid.startswith("{"):
|
||||
raise DXFValueError("Appid has to start with '{'.")
|
||||
|
||||
app_tags = Tags(
|
||||
[
|
||||
DXFTag(APP_DATA_MARKER, appid),
|
||||
DXFTag(APP_DATA_MARKER, "}"),
|
||||
]
|
||||
)
|
||||
if tags is not None:
|
||||
app_tags[1:1] = tuples_to_tags(tags)
|
||||
|
||||
if subclass_name is None:
|
||||
subclass = self.noclass
|
||||
else:
|
||||
# raises KeyError, if not exist
|
||||
subclass = self.get_subclass(subclass_name, 1)
|
||||
app_data_pos = len(self.appdata)
|
||||
subclass.append(DXFTag(APP_DATA_MARKER, app_data_pos))
|
||||
self.appdata.append(app_tags)
|
||||
return app_tags
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str, legacy=False) -> ExtendedTags:
|
||||
"""Create :class:`ExtendedTags` from DXF text."""
|
||||
return cls(internal_tag_compiler(text), legacy=legacy)
|
||||
@@ -0,0 +1,158 @@
|
||||
# Copyright (c) 2020-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Iterable, NamedTuple, BinaryIO
|
||||
|
||||
from .const import DXFStructureError
|
||||
from ezdxf.tools.codepage import toencoding
|
||||
|
||||
|
||||
class IndexEntry(NamedTuple):
|
||||
code: int
|
||||
value: str
|
||||
location: int
|
||||
line: int
|
||||
|
||||
|
||||
class FileStructure:
|
||||
"""DXF file structure representation stored as file locations.
|
||||
|
||||
Store all DXF structure tags and some other tags as :class:`IndexEntry`
|
||||
tuples:
|
||||
|
||||
- code: group code
|
||||
- value: tag value as string
|
||||
- location: file location as int
|
||||
- line: line number as int
|
||||
|
||||
Indexed tags:
|
||||
|
||||
- structure tags, every tag with group code 0
|
||||
- section names, (2, name) tag following a (0, SECTION) tag
|
||||
- entity handle tags with group code 5, the DIMSTYLE handle group code
|
||||
105 is also stored as group code 5
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, filename: str):
|
||||
# stores the file system name of the DXF document.
|
||||
self.filename: str = filename
|
||||
# DXF version if header variable $ACADVER is present, default is DXFR12
|
||||
self.version: str = "AC1009"
|
||||
# Python encoding required to read the DXF document as text file.
|
||||
self.encoding: str = "cp1252"
|
||||
self.index: list[IndexEntry] = []
|
||||
|
||||
def print(self) -> None:
|
||||
print(f"Filename: {self.filename}")
|
||||
print(f"DXF Version: {self.version}")
|
||||
print(f"encoding: {self.encoding}")
|
||||
for entry in self.index:
|
||||
print(f"Line: {entry.line} - ({entry.code}, {entry.value})")
|
||||
|
||||
def get(self, code: int, value: str, start: int = 0) -> int:
|
||||
"""Returns index of first entry matching `code` and `value`."""
|
||||
self_index = self.index
|
||||
index: int = start
|
||||
count: int = len(self_index)
|
||||
while index < count:
|
||||
entry = self_index[index]
|
||||
if entry.code == code and entry.value == value:
|
||||
return index
|
||||
index += 1
|
||||
raise ValueError(f"No entry for tag ({code}, {value}) found.")
|
||||
|
||||
def fetchall(
|
||||
self, code: int, value: str, start: int = 0
|
||||
) -> Iterable[IndexEntry]:
|
||||
"""Iterate over all specified entities.
|
||||
|
||||
e.g. fetchall(0, 'LINE') returns an iterator for all LINE entities.
|
||||
|
||||
"""
|
||||
for entry in self.index[start:]:
|
||||
if entry.code == code and entry.value == value:
|
||||
yield entry
|
||||
|
||||
|
||||
def load(filename: str) -> FileStructure:
|
||||
"""Load DXF file structure for file `filename`, the file has to be seekable.
|
||||
|
||||
Args:
|
||||
filename: file system file name
|
||||
|
||||
Raises:
|
||||
DXFStructureError: Invalid or incomplete DXF file.
|
||||
|
||||
"""
|
||||
file_structure = FileStructure(filename)
|
||||
file: BinaryIO = open(filename, mode="rb")
|
||||
line: int = 1
|
||||
eof: bool = False
|
||||
header: bool = False
|
||||
index: list[IndexEntry] = []
|
||||
prev_code: int = -1
|
||||
prev_value: bytes = b""
|
||||
structure = None # the current structure tag: 'SECTION', 'LINE', ...
|
||||
|
||||
def load_tag() -> tuple[int, bytes]:
|
||||
nonlocal line
|
||||
try:
|
||||
code = int(file.readline())
|
||||
except ValueError:
|
||||
raise DXFStructureError(f"Invalid group code in line {line}")
|
||||
|
||||
if code < 0 or code > 1071:
|
||||
raise DXFStructureError(f"Invalid group code {code} in line {line}")
|
||||
value = file.readline().rstrip(b"\r\n")
|
||||
line += 2
|
||||
return code, value
|
||||
|
||||
def load_header_var() -> str:
|
||||
_, value = load_tag()
|
||||
return value.decode()
|
||||
|
||||
while not eof:
|
||||
location = file.tell()
|
||||
tag_line = line
|
||||
try:
|
||||
code, value = load_tag()
|
||||
if header and code == 9:
|
||||
if value == b"$ACADVER":
|
||||
file_structure.version = load_header_var()
|
||||
elif value == b"$DWGCODEPAGE":
|
||||
file_structure.encoding = toencoding(load_header_var())
|
||||
continue
|
||||
except IOError:
|
||||
break
|
||||
|
||||
if code == 0:
|
||||
# All structure tags have group code == 0, store file location
|
||||
structure = value
|
||||
index.append(IndexEntry(0, value.decode(), location, tag_line))
|
||||
eof = value == b"EOF"
|
||||
|
||||
elif code == 2 and prev_code == 0 and prev_value == b"SECTION":
|
||||
# Section name is the tag (2, name) following the (0, SECTION) tag.
|
||||
header = value == b"HEADER"
|
||||
index.append(IndexEntry(2, value.decode(), location, tag_line))
|
||||
|
||||
elif code == 5 and structure != b"DIMSTYLE":
|
||||
# Entity handles have always group code 5.
|
||||
index.append(IndexEntry(5, value.decode(), location, tag_line))
|
||||
|
||||
elif code == 105 and structure == b"DIMSTYLE":
|
||||
# Except the DIMSTYLE table entry has group code 105.
|
||||
index.append(IndexEntry(5, value.decode(), location, tag_line))
|
||||
|
||||
prev_code = code
|
||||
prev_value = value
|
||||
|
||||
file.close()
|
||||
if not eof:
|
||||
raise DXFStructureError(f"Unexpected end of file.")
|
||||
|
||||
if file_structure.version >= "AC1021": # R2007 and later
|
||||
file_structure.encoding = "utf-8"
|
||||
file_structure.index = index
|
||||
return file_structure
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright (c) 2010-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from typing import Sequence, Union, Callable, Any, NamedTuple, Optional
|
||||
from .types import DXFVertex, DXFTag, cast_tag_value
|
||||
|
||||
|
||||
def SingleValue(value: Union[str, float], code: int = 1) -> DXFTag:
|
||||
return DXFTag(code, cast_tag_value(code, value))
|
||||
|
||||
|
||||
def Point2D(value: Sequence[float]) -> DXFVertex:
|
||||
return DXFVertex(10, (value[0], value[1]))
|
||||
|
||||
|
||||
def Point3D(value: Sequence[float]) -> DXFVertex:
|
||||
return DXFVertex(10, (value[0], value[1], value[2]))
|
||||
|
||||
|
||||
class HeaderVarDef(NamedTuple):
|
||||
name: str
|
||||
code: int
|
||||
factory: Callable[[Any], Any]
|
||||
mindxf: str
|
||||
maxdxf: str
|
||||
priority: int
|
||||
default: Optional[Any] = None
|
||||
@@ -0,0 +1,156 @@
|
||||
# Copyright (c) 2018-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from typing import Iterable, TYPE_CHECKING, Optional
|
||||
from collections import OrderedDict
|
||||
|
||||
from .const import DXFStructureError
|
||||
from .tags import group_tags, DXFTag, Tags
|
||||
from .extendedtags import ExtendedTags
|
||||
from ezdxf.entities import factory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.entities import DXFEntity
|
||||
from ezdxf.eztypes import SectionDict
|
||||
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
def load_dxf_structure(
|
||||
tagger: Iterable[DXFTag], ignore_missing_eof: bool = False
|
||||
) -> SectionDict:
|
||||
"""Divide input tag stream from tagger into DXF structure entities.
|
||||
Each DXF structure entity starts with a DXF structure (0, ...) tag,
|
||||
and ends before the next DXF structure tag.
|
||||
|
||||
Generated structure:
|
||||
|
||||
each entity is a Tags() object
|
||||
|
||||
{
|
||||
# 1. section, HEADER section consist only of one entity
|
||||
'HEADER': [entity],
|
||||
'CLASSES': [entity, entity, ...], # 2. section
|
||||
'TABLES': [entity, entity, ...], # 3. section
|
||||
...
|
||||
'OBJECTS': [entity, entity, ...],
|
||||
}
|
||||
|
||||
{
|
||||
# HEADER section consist only of one entity
|
||||
'HEADER': [(0, 'SECTION'), (2, 'HEADER'), .... ],
|
||||
'CLASSES': [
|
||||
[(0, 'SECTION'), (2, 'CLASSES')],
|
||||
[(0, 'CLASS'), ...],
|
||||
[(0, 'CLASS'), ...]
|
||||
],
|
||||
'TABLES': [
|
||||
[(0, 'SECTION'), (2, 'TABLES')],
|
||||
[(0, 'TABLE'), (2, 'VPORT')],
|
||||
[(0, 'VPORT'), ...],
|
||||
... ,
|
||||
[(0, 'ENDTAB')]
|
||||
],
|
||||
...
|
||||
'OBJECTS': [
|
||||
[(0, 'SECTION'), (2, 'OBJECTS')],
|
||||
... ,
|
||||
]
|
||||
}
|
||||
|
||||
Args:
|
||||
tagger: generates DXFTag() entities from input data
|
||||
ignore_missing_eof: raises DXFStructureError() if False and EOF tag is
|
||||
not present, set to True only in tests
|
||||
|
||||
Returns:
|
||||
dict of sections, each section is a list of DXF structure entities
|
||||
as Tags() objects
|
||||
|
||||
"""
|
||||
|
||||
def inside_section() -> bool:
|
||||
if len(section):
|
||||
return section[0][0] == (0, "SECTION") # first entity, first tag
|
||||
return False
|
||||
|
||||
def outside_section() -> bool:
|
||||
if len(section):
|
||||
return section[0][0] != (0, "SECTION") # first entity, first tag
|
||||
return True
|
||||
|
||||
sections: SectionDict = OrderedDict()
|
||||
section: list[Tags] = []
|
||||
eof = False
|
||||
# The structure checking here should not be changed, ezdxf expect a valid
|
||||
# DXF file, to load messy DXF files exist an (future) add-on
|
||||
# called 'recover'.
|
||||
|
||||
for entity in group_tags(tagger):
|
||||
tag = entity[0]
|
||||
if tag == (0, "SECTION"):
|
||||
if inside_section():
|
||||
raise DXFStructureError("DXFStructureError: missing ENDSEC tag.")
|
||||
if len(section):
|
||||
logger.warning(
|
||||
"DXF Structure Warning: found tags outside a SECTION, "
|
||||
"ignored by ezdxf."
|
||||
)
|
||||
section = [entity]
|
||||
elif tag == (0, "ENDSEC"):
|
||||
# ENDSEC tag is not collected.
|
||||
if outside_section():
|
||||
raise DXFStructureError(
|
||||
"DXFStructureError: found ENDSEC tag without previous "
|
||||
"SECTION tag."
|
||||
)
|
||||
section_header = section[0]
|
||||
|
||||
if len(section_header) < 2 or section_header[1].code != 2:
|
||||
raise DXFStructureError(
|
||||
"DXFStructureError: missing required section NAME tag "
|
||||
"(2, name) at start of section."
|
||||
)
|
||||
name_tag = section_header[1]
|
||||
sections[name_tag.value] = section # type: ignore
|
||||
# Collect tags outside of sections, but ignore it.
|
||||
section = []
|
||||
elif tag == (0, "EOF"):
|
||||
# EOF tag is not collected.
|
||||
if eof:
|
||||
logger.warning("DXF Structure Warning: found more than one EOF tags.")
|
||||
eof = True
|
||||
else:
|
||||
section.append(entity)
|
||||
if inside_section():
|
||||
raise DXFStructureError("DXFStructureError: missing ENDSEC tag.")
|
||||
if not eof and not ignore_missing_eof:
|
||||
raise DXFStructureError("DXFStructureError: missing EOF tag.")
|
||||
return sections
|
||||
|
||||
|
||||
def load_dxf_entities(
|
||||
entities: Iterable[Tags], doc: Optional[Drawing] = None
|
||||
) -> Iterable[DXFEntity]:
|
||||
for entity in entities:
|
||||
yield factory.load(ExtendedTags(entity), doc)
|
||||
|
||||
|
||||
def load_and_bind_dxf_content(sections: dict, doc: Drawing) -> None:
|
||||
# HEADER has no database entries.
|
||||
db = doc.entitydb
|
||||
for name in ["TABLES", "CLASSES", "ENTITIES", "BLOCKS", "OBJECTS"]:
|
||||
if name in sections:
|
||||
section = sections[name]
|
||||
for index, entity in enumerate(load_dxf_entities(section, doc)):
|
||||
handle = entity.dxf.get("handle")
|
||||
if handle and handle in db:
|
||||
logger.warning(
|
||||
f"Found non-unique entity handle #{handle}, data validation is required."
|
||||
)
|
||||
# Replace Tags() by DXFEntity() objects
|
||||
section[index] = entity
|
||||
# Bind entities to the DXF document:
|
||||
factory.bind(entity, doc)
|
||||
@@ -0,0 +1,208 @@
|
||||
# Copyright (c) 2018-2024 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Iterable, MutableSequence, Sequence, Iterator, Optional
|
||||
from typing_extensions import overload
|
||||
from array import array
|
||||
import numpy as np
|
||||
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.math import Matrix44
|
||||
from ezdxf.tools.indexing import Index
|
||||
|
||||
from .tags import Tags
|
||||
from .types import DXFTag
|
||||
|
||||
|
||||
class TagList:
|
||||
"""Store data in a standard Python ``list``."""
|
||||
|
||||
__slots__ = ("values",)
|
||||
|
||||
def __init__(self, data: Optional[Iterable] = None):
|
||||
self.values: MutableSequence = list(data or [])
|
||||
|
||||
def clone(self) -> TagList:
|
||||
"""Returns a deep copy."""
|
||||
return self.__class__(data=self.values)
|
||||
|
||||
@classmethod
|
||||
def from_tags(cls, tags: Tags, code: int) -> TagList:
|
||||
"""
|
||||
Setup list from iterable tags.
|
||||
|
||||
Args:
|
||||
tags: tag collection as :class:`~ezdxf.lldxf.tags.Tags`
|
||||
code: group code to collect
|
||||
|
||||
"""
|
||||
return cls(data=(tag.value for tag in tags if tag.code == code))
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Delete all data values."""
|
||||
del self.values[:]
|
||||
|
||||
|
||||
class TagArray(TagList):
|
||||
"""Store data in an :class:`array.array`. Array type is defined by class
|
||||
variable ``DTYPE``.
|
||||
"""
|
||||
|
||||
__slots__ = ("values",)
|
||||
# Defines the data type of array.array()
|
||||
DTYPE = "i"
|
||||
|
||||
def __init__(self, data: Optional[Iterable] = None):
|
||||
self.values: array = array(self.DTYPE, data or [])
|
||||
|
||||
def set_values(self, values: Iterable) -> None:
|
||||
"""Replace data by `values`."""
|
||||
self.values[:] = array(self.DTYPE, values)
|
||||
|
||||
|
||||
class VertexArray:
|
||||
"""Store vertices in a ``numpy.ndarray``. Vertex size is defined by class variable
|
||||
``VERTEX_SIZE``.
|
||||
"""
|
||||
|
||||
VERTEX_SIZE = 3
|
||||
__slots__ = ("values",)
|
||||
|
||||
def __init__(self, data: Iterable[Sequence[float]] | None = None):
|
||||
size = self.VERTEX_SIZE
|
||||
if data:
|
||||
values = np.array(data, dtype=np.float64)
|
||||
if values.shape[1] != size:
|
||||
raise TypeError(
|
||||
f"invalid data shape, expected (n, {size}), got {values.shape}"
|
||||
)
|
||||
else:
|
||||
values = np.ndarray((0, size), dtype=np.float64)
|
||||
self.values = values
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Count of vertices."""
|
||||
return len(self.values)
|
||||
|
||||
@overload
|
||||
def __getitem__(self, index: int) -> Sequence[float]: ...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, index: slice) -> Sequence[Sequence[float]]: ...
|
||||
|
||||
def __getitem__(self, index: int | slice):
|
||||
"""Get vertex at `index`, extended slicing supported."""
|
||||
return self.values[index]
|
||||
|
||||
def __setitem__(self, index: int, point: Sequence[float]) -> None:
|
||||
"""Set vertex `point` at `index`, extended slicing not supported."""
|
||||
if isinstance(index, slice):
|
||||
raise TypeError("slicing not supported")
|
||||
self._set_point(self._index(index), point)
|
||||
|
||||
def __delitem__(self, index: int | slice) -> None:
|
||||
"""Delete vertex at `index`, extended slicing supported."""
|
||||
if isinstance(index, slice):
|
||||
self._del_points(self._slicing(index))
|
||||
else:
|
||||
self._del_points((index,))
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation."""
|
||||
return str(self.values)
|
||||
|
||||
def __iter__(self) -> Iterator[Sequence[float]]:
|
||||
"""Returns iterable of vertices."""
|
||||
return iter(self.values)
|
||||
|
||||
def insert(self, pos: int, point: Sequence[float]):
|
||||
"""Insert `point` in front of vertex at index `pos`.
|
||||
|
||||
Args:
|
||||
pos: insert position
|
||||
point: point as tuple
|
||||
|
||||
"""
|
||||
size = self.VERTEX_SIZE
|
||||
if len(point) != size:
|
||||
raise ValueError(f"point requires exact {size} components.")
|
||||
|
||||
values = self.values
|
||||
if len(values) == 0:
|
||||
self.extend((point,))
|
||||
ins_point = np.array((point,), dtype=np.float64)
|
||||
self.values = np.concatenate((values[0:pos], ins_point, values[pos:]))
|
||||
|
||||
def clone(self) -> VertexArray:
|
||||
"""Returns a deep copy."""
|
||||
return self.__class__(data=self.values)
|
||||
|
||||
@classmethod
|
||||
def from_tags(cls, tags: Iterable[DXFTag], code: int = 10) -> VertexArray:
|
||||
"""Setup point array from iterable tags.
|
||||
|
||||
Args:
|
||||
tags: iterable of :class:`~ezdxf.lldxf.types.DXFVertex`
|
||||
code: group code to collect
|
||||
|
||||
"""
|
||||
vertices = [tag.value for tag in tags if tag.code == code]
|
||||
return cls(data=vertices)
|
||||
|
||||
def _index(self, item) -> int:
|
||||
return Index(self).index(item, error=IndexError)
|
||||
|
||||
def _slicing(self, index) -> Iterable[int]:
|
||||
return Index(self).slicing(index)
|
||||
|
||||
def _set_point(self, index: int, point: Sequence[float]):
|
||||
size = self.VERTEX_SIZE
|
||||
if len(point) != size:
|
||||
raise ValueError(f"point requires exact {size} components.")
|
||||
self.values[index] = point # type: ignore
|
||||
|
||||
def _del_points(self, indices: Iterable[int]) -> None:
|
||||
del_flags = set(indices)
|
||||
survivors = np.array(
|
||||
[v for i, v in enumerate(self.values) if i not in del_flags], np.float64
|
||||
)
|
||||
self.values = survivors
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter, code=10):
|
||||
for vertex in self.values:
|
||||
tagwriter.write_tag2(code, vertex[0])
|
||||
tagwriter.write_tag2(code + 10, vertex[1])
|
||||
if len(vertex) > 2:
|
||||
tagwriter.write_tag2(code + 20, vertex[2])
|
||||
|
||||
def append(self, point: Sequence[float]) -> None:
|
||||
"""Append `point`."""
|
||||
if len(point) != self.VERTEX_SIZE:
|
||||
raise ValueError(f"point requires exact {self.VERTEX_SIZE} components.")
|
||||
self.extend((point,))
|
||||
|
||||
def extend(self, points: Iterable[Sequence[float]]) -> None:
|
||||
"""Extend array by `points`."""
|
||||
vertices = np.array(points, np.float64)
|
||||
if vertices.shape[1] != self.VERTEX_SIZE:
|
||||
raise ValueError(f"points require exact {self.VERTEX_SIZE} components.")
|
||||
if len(self.values) == 0:
|
||||
self.values = vertices
|
||||
else:
|
||||
self.values = np.concatenate((self.values, vertices))
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Delete all vertices."""
|
||||
self.values = np.ndarray((0, self.VERTEX_SIZE), dtype=np.float64)
|
||||
|
||||
def set(self, points: Iterable[Sequence[float]]) -> None:
|
||||
"""Replace all vertices by `points`."""
|
||||
vertices = np.array(points, np.float64)
|
||||
if vertices.shape[1] != self.VERTEX_SIZE:
|
||||
raise ValueError(f"points require exact {self.VERTEX_SIZE} components.")
|
||||
self.values = vertices
|
||||
|
||||
def transform(self, m: Matrix44) -> None:
|
||||
"""Transform vertices inplace by transformation matrix `m`."""
|
||||
if self.VERTEX_SIZE in (2, 3):
|
||||
m.transform_array_inplace(self.values, self.VERTEX_SIZE)
|
||||
@@ -0,0 +1,212 @@
|
||||
# Copyright (c) 2016-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Iterable,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Sequence,
|
||||
Any,
|
||||
Iterator,
|
||||
)
|
||||
from functools import partial
|
||||
import logging
|
||||
from .tags import DXFTag
|
||||
from .types import POINT_CODES, NONE_TAG, VALID_XDATA_GROUP_CODES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.eztypes import Tags
|
||||
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
def tag_reorder_layer(tagger: Iterable[DXFTag]) -> Iterator[DXFTag]:
|
||||
"""Reorder coordinates of legacy DXF Entities, for now only LINE.
|
||||
|
||||
Input Raw tag filter.
|
||||
|
||||
Args:
|
||||
tagger: low level tagger
|
||||
|
||||
"""
|
||||
|
||||
collector: Optional[list] = None
|
||||
for tag in tagger:
|
||||
if tag.code == 0:
|
||||
if collector is not None:
|
||||
# stop collecting if inside a supported entity
|
||||
entity = _s(collector[0].value)
|
||||
yield from COORDINATE_FIXING_TOOLBOX[entity](collector) # type: ignore
|
||||
collector = None
|
||||
|
||||
if _s(tag.value) in COORDINATE_FIXING_TOOLBOX:
|
||||
collector = [tag]
|
||||
# do not yield collected tag yet
|
||||
tag = NONE_TAG
|
||||
else: # tag.code != 0
|
||||
if collector is not None:
|
||||
collector.append(tag)
|
||||
# do not yield collected tag yet
|
||||
tag = NONE_TAG
|
||||
if tag is not NONE_TAG:
|
||||
yield tag
|
||||
|
||||
|
||||
# invalid point codes if not part of a point started with 1010, 1011, 1012, 1013
|
||||
INVALID_Y_CODES = {code + 10 for code in POINT_CODES}
|
||||
INVALID_Z_CODES = {code + 20 for code in POINT_CODES}
|
||||
# A single group code 38 is an elevation tag (e.g. LWPOLYLINE)
|
||||
# Is (18, 28, 38?) is a valid point code?
|
||||
INVALID_Z_CODES.remove(38)
|
||||
INVALID_CODES = INVALID_Y_CODES | INVALID_Z_CODES
|
||||
X_CODES = POINT_CODES
|
||||
|
||||
|
||||
def filter_invalid_point_codes(tagger: Iterable[DXFTag]) -> Iterable[DXFTag]:
|
||||
"""Filter invalid and misplaced point group codes.
|
||||
|
||||
- removes x-axis without following y-axis
|
||||
- removes y- and z-axis without leading x-axis
|
||||
|
||||
Args:
|
||||
tagger: low level tagger
|
||||
|
||||
"""
|
||||
|
||||
def entity() -> str:
|
||||
if handle_tag:
|
||||
return f"in entity #{_s(handle_tag[1])}"
|
||||
else:
|
||||
return ""
|
||||
|
||||
expected_code = -1
|
||||
z_code = 0
|
||||
point: list[Any] = []
|
||||
handle_tag = None
|
||||
for tag in tagger:
|
||||
code = tag[0]
|
||||
if code == 5: # ignore DIMSTYLE entity
|
||||
handle_tag = tag
|
||||
if point and code != expected_code:
|
||||
# at least x, y axis is required else ignore point
|
||||
if len(point) > 1:
|
||||
yield from point
|
||||
else:
|
||||
logger.info(
|
||||
f"remove misplaced x-axis tag: {str(point[0])}" + entity()
|
||||
)
|
||||
point.clear()
|
||||
|
||||
if code in X_CODES:
|
||||
expected_code = code + 10
|
||||
z_code = code + 20
|
||||
point.append(tag)
|
||||
elif code == expected_code:
|
||||
point.append(tag)
|
||||
expected_code += 10
|
||||
if expected_code > z_code:
|
||||
expected_code = -1
|
||||
else:
|
||||
# ignore point group codes without leading x-axis
|
||||
if code not in INVALID_CODES:
|
||||
yield tag
|
||||
else:
|
||||
axis = "y-axis" if code in INVALID_Y_CODES else "z-axis"
|
||||
logger.info(
|
||||
f"remove misplaced {axis} tag: {str(tag)}" + entity()
|
||||
)
|
||||
|
||||
if len(point) == 1:
|
||||
logger.info(f"remove misplaced x-axis tag: {str(point[0])}" + entity())
|
||||
elif len(point) > 1:
|
||||
yield from point
|
||||
|
||||
|
||||
def fix_coordinate_order(tags: Tags, codes: Sequence[int] = (10, 11)):
|
||||
def extend_codes():
|
||||
for code in codes:
|
||||
yield code # x tag
|
||||
yield code + 10 # y tag
|
||||
yield code + 20 # z tag
|
||||
|
||||
def get_coords(code: int):
|
||||
# if x or y coordinate is missing, it is a DXFStructureError
|
||||
# but here is not the location to validate the DXF structure
|
||||
try:
|
||||
yield coordinates[code]
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
yield coordinates[code + 10]
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
yield coordinates[code + 20]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
coordinate_codes = frozenset(extend_codes())
|
||||
coordinates = {}
|
||||
remaining_tags = []
|
||||
insert_pos = None
|
||||
for tag in tags:
|
||||
# separate tags
|
||||
if tag.code in coordinate_codes:
|
||||
coordinates[tag.code] = tag
|
||||
if insert_pos is None:
|
||||
insert_pos = tags.index(tag)
|
||||
else:
|
||||
remaining_tags.append(tag)
|
||||
|
||||
if len(coordinates) == 0:
|
||||
# no coordinates found, this is probably a DXFStructureError,
|
||||
# but here is not the location to validate the DXF structure,
|
||||
# just do nothing.
|
||||
return tags
|
||||
|
||||
ordered_coords = []
|
||||
for code in codes:
|
||||
ordered_coords.extend(get_coords(code))
|
||||
remaining_tags[insert_pos:insert_pos] = ordered_coords
|
||||
return remaining_tags
|
||||
|
||||
|
||||
COORDINATE_FIXING_TOOLBOX = {
|
||||
"LINE": partial(fix_coordinate_order, codes=(10, 11)),
|
||||
}
|
||||
|
||||
|
||||
def filter_invalid_xdata_group_codes(
|
||||
tags: Iterable[DXFTag],
|
||||
) -> Iterator[DXFTag]:
|
||||
return (tag for tag in tags if tag.code in VALID_XDATA_GROUP_CODES)
|
||||
|
||||
|
||||
def filter_invalid_handles(tags: Iterable[DXFTag]) -> Iterator[DXFTag]:
|
||||
line = -1
|
||||
handle_code = 5
|
||||
structure_tag = ""
|
||||
for tag in tags:
|
||||
line += 2
|
||||
if tag.code == 0:
|
||||
structure_tag = tag.value
|
||||
if _s(tag.value) == "DIMSTYLE":
|
||||
handle_code = 105
|
||||
else:
|
||||
handle_code = 5
|
||||
elif tag.code == handle_code:
|
||||
try:
|
||||
int(tag.value, 16)
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
f'skipped invalid handle "{_s(tag.value)}" in '
|
||||
f'DXF entity "{_s(structure_tag)}" near line {line}'
|
||||
)
|
||||
continue
|
||||
yield tag
|
||||
|
||||
|
||||
def _s(b) -> str:
|
||||
if isinstance(b, bytes):
|
||||
return b.decode(encoding="ascii", errors="ignore")
|
||||
return b
|
||||
@@ -0,0 +1,384 @@
|
||||
# Copyright (c) 2016-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Iterable, TextIO, Iterator, Any, Optional, Sequence
|
||||
import struct
|
||||
from .types import (
|
||||
DXFTag,
|
||||
DXFVertex,
|
||||
DXFBinaryTag,
|
||||
BYTES,
|
||||
INT16,
|
||||
INT32,
|
||||
INT64,
|
||||
DOUBLE,
|
||||
POINT_CODES,
|
||||
TYPE_TABLE,
|
||||
BINARY_DATA,
|
||||
is_point_code,
|
||||
)
|
||||
from .const import DXFStructureError
|
||||
from ezdxf.tools.codepage import toencoding
|
||||
|
||||
|
||||
def internal_tag_compiler(s: str) -> Iterable[DXFTag]:
|
||||
"""Yields DXFTag() from trusted (internal) source - relies on
|
||||
well-formed and error free DXF format. Does not skip comment
|
||||
tags (group code == 999).
|
||||
|
||||
Args:
|
||||
s: DXF unicode string, lines separated by universal line endings '\n'
|
||||
|
||||
"""
|
||||
assert isinstance(s, str)
|
||||
lines: list[str] = s.split("\n")
|
||||
# split() creates an extra item, if s ends with '\n',
|
||||
# but lines[-1] can be an empty string!!!
|
||||
if s.endswith("\n"):
|
||||
lines.pop()
|
||||
pos: int = 0
|
||||
count: int = len(lines)
|
||||
point: tuple[float, ...]
|
||||
while pos < count:
|
||||
code = int(lines[pos])
|
||||
value = lines[pos + 1]
|
||||
pos += 2
|
||||
if code in POINT_CODES:
|
||||
# next tag; y-axis is mandatory - internal_tag_compiler relies on
|
||||
# well formed DXF strings:
|
||||
y = lines[pos + 1]
|
||||
pos += 2
|
||||
if pos < count:
|
||||
# next tag; z coordinate just for 3d points
|
||||
z_code = int(lines[pos])
|
||||
z = lines[pos + 1]
|
||||
else: # if string s ends with a 2d point
|
||||
z_code, z = -1, ""
|
||||
if z_code == code + 20: # 3d point
|
||||
pos += 2
|
||||
point = (float(value), float(y), float(z))
|
||||
else: # 2d point
|
||||
point = (float(value), float(y))
|
||||
yield DXFVertex(code, point) # 2d/3d point
|
||||
elif code in BINARY_DATA:
|
||||
yield DXFBinaryTag.from_string(code, value)
|
||||
else: # single value tag: int, float or string
|
||||
yield DXFTag(code, TYPE_TABLE.get(code, str)(value))
|
||||
|
||||
|
||||
# No performance advantage by processing binary data!
|
||||
#
|
||||
# Profiling result for just reading DXF data (profiling/raw_data_reading.py):
|
||||
# Loading the example file "torso_uniform.dxf" (50MB) by readline() from a
|
||||
# text stream with decoding takes ~0.65 seconds longer than loading the same
|
||||
# file as binary data.
|
||||
#
|
||||
# Text :1.30s vs Binary data: 0.65s)
|
||||
# This is twice the time, but without any processing, ascii_tags_loader() takes
|
||||
# ~5.3 seconds to process this file.
|
||||
#
|
||||
# And this performance advantage is more than lost by the necessary decoding
|
||||
# of the binary data afterwards, even much fewer strings have to be decoded,
|
||||
# because numeric data like group codes and vertices doesn't need to be
|
||||
# decoded.
|
||||
#
|
||||
# I assume the runtime overhead for calling Python functions is the reason.
|
||||
|
||||
|
||||
def ascii_tags_loader(stream: TextIO, skip_comments: bool = True) -> Iterator[DXFTag]:
|
||||
"""Yields :class:``DXFTag`` objects from a text `stream` (untrusted
|
||||
external source) and does not optimize coordinates. Comment tags (group
|
||||
code == 999) will be skipped if argument `skip_comments` is `True`.
|
||||
``DXFTag.code`` is always an ``int`` and ``DXFTag.value`` is always an
|
||||
unicode string without a trailing '\n'.
|
||||
Works with file system streams and :class:`StringIO` streams, only required
|
||||
feature is the :meth:`readline` method.
|
||||
|
||||
Args:
|
||||
stream: text stream
|
||||
skip_comments: skip comment tags (group code == 999) if `True`
|
||||
|
||||
Raises:
|
||||
DXFStructureError: Found invalid group code.
|
||||
|
||||
"""
|
||||
line: int = 1
|
||||
eof = False
|
||||
yield_comments = not skip_comments
|
||||
# localize attributes
|
||||
readline = stream.readline
|
||||
_DXFTag = DXFTag
|
||||
# readline() returns an empty string at EOF, not exception will be raised!
|
||||
while not eof:
|
||||
code: str = readline()
|
||||
if code: # empty string indicates EOF
|
||||
try:
|
||||
group_code = int(code)
|
||||
except ValueError:
|
||||
raise DXFStructureError(f'Invalid group code "{code}" at line {line}.')
|
||||
else:
|
||||
return
|
||||
|
||||
value: str = readline()
|
||||
if value: # empty string indicates EOF
|
||||
value = value.rstrip("\n")
|
||||
if group_code == 0 and value == "EOF":
|
||||
eof = True # yield EOF tag but ignore any data beyond EOF
|
||||
if group_code != 999 or yield_comments:
|
||||
yield _DXFTag(group_code, value)
|
||||
line += 2
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
def binary_tags_loader(
|
||||
data: bytes, errors: str = "surrogateescape"
|
||||
) -> Iterator[DXFTag]:
|
||||
"""Yields :class:`DXFTag` or :class:`DXFBinaryTag` objects from binary DXF
|
||||
`data` (untrusted external source) and does not optimize coordinates.
|
||||
``DXFTag.code`` is always an ``int`` and ``DXFTag.value`` is either an
|
||||
unicode string,``float``, ``int`` or ``bytes`` for binary chunks.
|
||||
|
||||
Args:
|
||||
data: binary DXF data
|
||||
errors: specify decoding error handler
|
||||
|
||||
- "surrogateescape" to preserve possible binary data (default)
|
||||
- "ignore" to use the replacement char U+FFFD: "\ufffd"
|
||||
- "strict" to raise an :class:`UnicodeDecodeError`
|
||||
|
||||
Raises:
|
||||
DXFStructureError: Not a binary DXF file
|
||||
DXFVersionError: Unsupported DXF version
|
||||
UnicodeDecodeError: if `errors` is "strict" and a decoding error occurs
|
||||
|
||||
"""
|
||||
if data[:22] != b"AutoCAD Binary DXF\r\n\x1a\x00":
|
||||
raise DXFStructureError("Not a binary DXF data structure.")
|
||||
|
||||
def scan_params():
|
||||
dxfversion = "AC1009"
|
||||
encoding = "cp1252"
|
||||
try:
|
||||
# Limit search to first 1024 bytes - an arbitrary number
|
||||
# start index for 1-byte group code
|
||||
start = data.index(b"$ACADVER", 22, 1024) + 10
|
||||
except ValueError:
|
||||
pass # HEADER var $ACADVER not present
|
||||
else:
|
||||
if data[start] != 65: # not 'A' = 2-byte group code
|
||||
start += 1
|
||||
dxfversion = data[start : start + 6].decode()
|
||||
|
||||
if dxfversion >= "AC1021":
|
||||
encoding = "utf8"
|
||||
else:
|
||||
try:
|
||||
# Limit search to first 1024 bytes - an arbitrary number
|
||||
# start index for 1-byte group code
|
||||
start = data.index(b"$DWGCODEPAGE", 22, 1024) + 14
|
||||
except ValueError:
|
||||
pass # HEADER var $DWGCODEPAGE not present
|
||||
else: # name schema is 'ANSI_xxxx'
|
||||
if data[start] != 65: # not 'A' = 2-byte group code
|
||||
start += 1
|
||||
end = start + 5
|
||||
while data[end] != 0:
|
||||
end += 1
|
||||
codepage = data[start:end].decode()
|
||||
encoding = toencoding(codepage)
|
||||
|
||||
return encoding, dxfversion
|
||||
|
||||
encoding, dxfversion = scan_params()
|
||||
r12 = dxfversion <= "AC1009"
|
||||
index: int = 22
|
||||
data_length: int = len(data)
|
||||
unpack = struct.unpack_from
|
||||
value: Any
|
||||
|
||||
while index < data_length:
|
||||
# decode next group code
|
||||
code = data[index]
|
||||
if r12:
|
||||
if code == 255: # extended data
|
||||
code = (data[index + 2] << 8) | data[index + 1]
|
||||
index += 3
|
||||
else:
|
||||
index += 1
|
||||
else: # 2-byte group code
|
||||
code = (data[index + 1] << 8) | code
|
||||
index += 2
|
||||
|
||||
# decode next value
|
||||
if code in BINARY_DATA:
|
||||
length = data[index]
|
||||
index += 1
|
||||
value = data[index : index + length]
|
||||
index += length
|
||||
yield DXFBinaryTag(code, value)
|
||||
else:
|
||||
if code in INT16:
|
||||
value = unpack("<h", data, offset=index)[0]
|
||||
index += 2
|
||||
elif code in DOUBLE:
|
||||
value = unpack("<d", data, offset=index)[0]
|
||||
index += 8
|
||||
elif code in INT32:
|
||||
value = unpack("<i", data, offset=index)[0]
|
||||
index += 4
|
||||
elif code in INT64:
|
||||
value = unpack("<q", data, offset=index)[0]
|
||||
index += 8
|
||||
elif code in BYTES:
|
||||
value = data[index]
|
||||
index += 1
|
||||
else: # zero terminated string
|
||||
start_index = index
|
||||
end_index = data.index(b"\x00", start_index)
|
||||
s = data[start_index:end_index]
|
||||
index = end_index + 1
|
||||
value = s.decode(encoding, errors=errors)
|
||||
yield DXFTag(code, value)
|
||||
|
||||
|
||||
# invalid point codes if not part of a point started with 1010, 1011, 1012, 1013
|
||||
INVALID_POINT_CODES = {1020, 1021, 1022, 1023, 1030, 1031, 1032, 1033}
|
||||
|
||||
|
||||
def tag_compiler(tags: Iterator[DXFTag]) -> Iterator[DXFTag]:
|
||||
"""Compiles DXF tag values imported by ascii_tags_loader() into Python
|
||||
types.
|
||||
|
||||
Raises DXFStructureError() for invalid float values and invalid coordinate
|
||||
values.
|
||||
|
||||
Expects DXF coordinates written in x, y[, z] order, this is not required by
|
||||
the DXF standard, but nearly all CAD applications write DXF coordinates that
|
||||
(sane) way, there are older CAD applications (namely an older QCAD version)
|
||||
that write LINE coordinates in x1, x2, y1, y2 order, which does not work
|
||||
with tag_compiler(). For this cases use tag_reorder_layer() from the repair
|
||||
module to reorder the LINE coordinates::
|
||||
|
||||
tag_compiler(tag_reorder_layer(ascii_tags_loader(stream)))
|
||||
|
||||
Args:
|
||||
tags: DXF tag generator e.g. ascii_tags_loader()
|
||||
|
||||
Raises:
|
||||
DXFStructureError: Found invalid DXF tag or unexpected coordinate order.
|
||||
|
||||
"""
|
||||
|
||||
def error_msg(tag):
|
||||
return (
|
||||
f'Invalid tag (code={tag.code}, value="{tag.value}") ' f"near line: {line}."
|
||||
)
|
||||
|
||||
undo_tag: Optional[DXFTag] = None
|
||||
line: int = 0
|
||||
point: tuple[float, ...]
|
||||
# Silencing mypy by "type: ignore", because this is a work horse function
|
||||
# and should not be slowed down by isinstance(...) checks or unnecessary
|
||||
# cast() calls
|
||||
while True:
|
||||
try:
|
||||
if undo_tag is not None:
|
||||
x = undo_tag
|
||||
undo_tag = None
|
||||
else:
|
||||
x = next(tags)
|
||||
line += 2
|
||||
code: int = x.code
|
||||
if code in POINT_CODES:
|
||||
# y-axis is mandatory
|
||||
y = next(tags)
|
||||
line += 2
|
||||
if y.code != code + 10: # like 20 for base x-code 10
|
||||
raise DXFStructureError(
|
||||
f"Missing required y coordinate near line: {line}."
|
||||
)
|
||||
# z-axis just for 3d points
|
||||
z = next(tags)
|
||||
line += 2
|
||||
try:
|
||||
# z-axis 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 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 DXFStructureError(
|
||||
f"Invalid binary data near line: {line}."
|
||||
)
|
||||
yield tag
|
||||
else: # Just a single tag
|
||||
try:
|
||||
# Fast path!
|
||||
if code == 0:
|
||||
value = x.value.strip()
|
||||
else:
|
||||
value = x.value
|
||||
yield DXFTag(code, TYPE_TABLE.get(code, str)(value))
|
||||
except ValueError:
|
||||
# ProE stores int values as floats :((
|
||||
if TYPE_TABLE.get(code, str) is int:
|
||||
try:
|
||||
yield DXFTag(code, int(float(x.value)))
|
||||
except ValueError:
|
||||
raise DXFStructureError(error_msg(x))
|
||||
else:
|
||||
raise DXFStructureError(error_msg(x))
|
||||
except StopIteration:
|
||||
return
|
||||
|
||||
|
||||
def json_tag_loader(
|
||||
data: Sequence[Any], skip_comments: bool = True
|
||||
) -> Iterator[DXFTag]:
|
||||
"""Yields :class:``DXFTag`` objects from a JSON data structure (untrusted
|
||||
external source) and does not optimize coordinates. Comment tags (group
|
||||
code == 999) will be skipped if argument `skip_comments` is `True`.
|
||||
``DXFTag.code`` is always an ``int`` and ``DXFTag.value`` is always an
|
||||
unicode string without a trailing ``\n``.
|
||||
|
||||
The expected JSON format is a list of [group-code, value] pairs where each pair is
|
||||
a DXF tag. The `compact` and the `verbose` format is supported.
|
||||
|
||||
Args:
|
||||
data: JSON data structure as a sequence of [group-code, value] pairs
|
||||
skip_comments: skip comment tags (group code == 999) if `True`
|
||||
|
||||
Raises:
|
||||
DXFStructureError: Found invalid group code or value type.
|
||||
|
||||
"""
|
||||
yield_comments = not skip_comments
|
||||
_DXFTag = DXFTag
|
||||
for tag_number, (code, value) in enumerate(data):
|
||||
if not isinstance(code, int):
|
||||
raise DXFStructureError(
|
||||
f'Invalid group code "{code}" in tag number {tag_number}.'
|
||||
)
|
||||
if is_point_code(code) and isinstance(value, (list, tuple)):
|
||||
# yield coordinates as single tags
|
||||
for index, coordinate in enumerate(value):
|
||||
yield _DXFTag(code + index * 10, coordinate)
|
||||
continue
|
||||
if code != 999 or yield_comments:
|
||||
yield _DXFTag(code, value)
|
||||
if code == 0 and value == "EOF":
|
||||
return
|
||||
@@ -0,0 +1,458 @@
|
||||
# Copyright (c) 2011-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
"""
|
||||
Tags
|
||||
----
|
||||
|
||||
A list of :class:`~ezdxf.lldxf.types.DXFTag`, inherits from Python standard list.
|
||||
Unlike the statement in the DXF Reference "Do not write programs that rely on
|
||||
the order given here", tag order is sometimes essential and some group codes
|
||||
may appear multiples times in one entity. At the worst case
|
||||
(:class:`~ezdxf.entities.material.Material`: normal map shares group codes with
|
||||
diffuse map) using same group codes with different meanings.
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import Iterable, Iterator, Any, Optional
|
||||
|
||||
from .const import DXFStructureError, DXFValueError, STRUCTURE_MARKER
|
||||
from .types import DXFTag, EMBEDDED_OBJ_MARKER, EMBEDDED_OBJ_STR, dxftag
|
||||
from .tagger import internal_tag_compiler
|
||||
from . import types
|
||||
|
||||
COMMENT_CODE = 999
|
||||
|
||||
|
||||
class Tags(list):
|
||||
"""Collection of :class:`~ezdxf.lldxf.types.DXFTag` as flat list.
|
||||
Low level tag container, only required for advanced stuff.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Tags:
|
||||
"""Constructor from DXF string."""
|
||||
return cls(internal_tag_compiler(text))
|
||||
|
||||
@classmethod
|
||||
def from_tuples(cls, tags: Iterable[tuple[int, Any]]) -> Tags:
|
||||
return cls(DXFTag(code, value) for code, value in tags)
|
||||
|
||||
def __copy__(self) -> Tags:
|
||||
return self.__class__(tag.clone() for tag in self)
|
||||
|
||||
clone = __copy__
|
||||
|
||||
def get_handle(self) -> str:
|
||||
"""Get DXF handle. Raises :class:`DXFValueError` if handle not exist.
|
||||
|
||||
Returns:
|
||||
handle as plain hex string like ``'FF00'``
|
||||
|
||||
Raises:
|
||||
DXFValueError: no handle found
|
||||
|
||||
"""
|
||||
try:
|
||||
code, handle = self[1] # fast path for most common cases
|
||||
except IndexError:
|
||||
raise DXFValueError("No handle found.")
|
||||
|
||||
if code == 5 or code == 105:
|
||||
return handle
|
||||
|
||||
for code, handle in self:
|
||||
if code == 5 or code == 105:
|
||||
return handle
|
||||
raise DXFValueError("No handle found.")
|
||||
|
||||
def replace_handle(self, new_handle: str) -> None:
|
||||
"""Replace existing handle.
|
||||
|
||||
Args:
|
||||
new_handle: new handle as plain hex string e.g. ``'FF00'``
|
||||
|
||||
"""
|
||||
for index, tag in enumerate(self):
|
||||
if tag.code in (5, 105):
|
||||
self[index] = DXFTag(tag.code, new_handle)
|
||||
return
|
||||
|
||||
def dxftype(self) -> str:
|
||||
"""Returns DXF type of entity, e.g. ``'LINE'``."""
|
||||
return self[0].value
|
||||
|
||||
def has_tag(self, code: int) -> bool:
|
||||
"""Returns ``True`` if a :class:`~ezdxf.lldxf.types.DXFTag` with given
|
||||
group `code` is present.
|
||||
|
||||
Args:
|
||||
code: group code as int
|
||||
|
||||
"""
|
||||
return any(tag.code == code for tag in self)
|
||||
|
||||
def get_first_value(self, code: int, default: Any=DXFValueError) -> Any:
|
||||
"""Returns value of first :class:`~ezdxf.lldxf.types.DXFTag` with given
|
||||
group code or default if `default` != :class:`DXFValueError`, else
|
||||
raises :class:`DXFValueError`.
|
||||
|
||||
Args:
|
||||
code: group code as int
|
||||
default: return value for default case or raises :class:`DXFValueError`
|
||||
|
||||
"""
|
||||
for tag in self:
|
||||
if tag.code == code:
|
||||
return tag.value
|
||||
if default is DXFValueError:
|
||||
raise DXFValueError(code)
|
||||
else:
|
||||
return default
|
||||
|
||||
def get_first_tag(self, code: int, default=DXFValueError) -> DXFTag:
|
||||
"""Returns first :class:`~ezdxf.lldxf.types.DXFTag` with given group
|
||||
code or `default`, if `default` != :class:`DXFValueError`, else raises
|
||||
:class:`DXFValueError`.
|
||||
|
||||
Args:
|
||||
code: group code as int
|
||||
default: return value for default case or raises :class:`DXFValueError`
|
||||
|
||||
"""
|
||||
for tag in self:
|
||||
if tag.code == code:
|
||||
return tag
|
||||
if default is DXFValueError:
|
||||
raise DXFValueError(code)
|
||||
else:
|
||||
return default
|
||||
|
||||
def find_all(self, code: int) -> Tags:
|
||||
"""Returns a list of :class:`~ezdxf.lldxf.types.DXFTag` with given
|
||||
group code.
|
||||
|
||||
Args:
|
||||
code: group code as int
|
||||
|
||||
"""
|
||||
return self.__class__(tag for tag in self if tag.code == code)
|
||||
|
||||
def tag_index(self, code: int, start: int = 0, end: Optional[int] = None) -> int:
|
||||
"""Return index of first :class:`~ezdxf.lldxf.types.DXFTag` with given
|
||||
group code.
|
||||
|
||||
Args:
|
||||
code: group code as int
|
||||
start: start index as int
|
||||
end: end index as int, ``None`` for end index = ``len(self)``
|
||||
|
||||
"""
|
||||
if end is None:
|
||||
end = len(self)
|
||||
index = start
|
||||
while index < end:
|
||||
if self[index].code == code:
|
||||
return index
|
||||
index += 1
|
||||
raise DXFValueError(code)
|
||||
|
||||
def update(self, tag: DXFTag) -> None:
|
||||
"""Update first existing tag with same group code as `tag`, raises
|
||||
:class:`DXFValueError` if tag not exist.
|
||||
|
||||
"""
|
||||
index = self.tag_index(tag.code)
|
||||
self[index] = tag
|
||||
|
||||
def set_first(self, tag: DXFTag) -> None:
|
||||
"""Update first existing tag with group code ``tag.code`` or append tag."""
|
||||
try:
|
||||
self.update(tag)
|
||||
except DXFValueError:
|
||||
self.append(tag)
|
||||
|
||||
def remove_tags(self, codes: Iterable[int]) -> None:
|
||||
"""Remove all tags inplace with group codes specified in `codes`.
|
||||
|
||||
Args:
|
||||
codes: iterable of group codes as int
|
||||
|
||||
"""
|
||||
self[:] = [tag for tag in self if tag.code not in set(codes)]
|
||||
|
||||
def pop_tags(self, codes: Iterable[int]) -> Iterator[DXFTag]:
|
||||
"""Pop tags with group codes specified in `codes`.
|
||||
|
||||
Args:
|
||||
codes: iterable of group codes
|
||||
|
||||
"""
|
||||
remaining = []
|
||||
codes = set(codes)
|
||||
for tag in self:
|
||||
if tag.code in codes:
|
||||
yield tag
|
||||
else:
|
||||
remaining.append(tag)
|
||||
self[:] = remaining
|
||||
|
||||
def remove_tags_except(self, codes: Iterable[int]) -> None:
|
||||
"""Remove all tags inplace except those with group codes specified in
|
||||
`codes`.
|
||||
|
||||
Args:
|
||||
codes: iterable of group codes
|
||||
|
||||
"""
|
||||
self[:] = [tag for tag in self if tag.code in set(codes)]
|
||||
|
||||
def filter(self, codes: Iterable[int]) -> Iterator[DXFTag]:
|
||||
"""Iterate and filter tags by group `codes`.
|
||||
|
||||
Args:
|
||||
codes: group codes to filter
|
||||
|
||||
"""
|
||||
return (tag for tag in self if tag.code not in set(codes))
|
||||
|
||||
def collect_consecutive_tags(
|
||||
self, codes: Iterable[int], start: int = 0, end: Optional[int] = None
|
||||
) -> Tags:
|
||||
"""Collect all consecutive tags with group code in `codes`, `start` and
|
||||
`end` delimits the search range. A tag code not in codes ends the
|
||||
process.
|
||||
|
||||
Args:
|
||||
codes: iterable of group codes
|
||||
start: start index as int
|
||||
end: end index as int, ``None`` for end index = ``len(self)``
|
||||
|
||||
Returns:
|
||||
collected tags as :class:`Tags`
|
||||
|
||||
"""
|
||||
codes = frozenset(codes)
|
||||
index = int(start)
|
||||
if end is None:
|
||||
end = len(self)
|
||||
bag = self.__class__()
|
||||
|
||||
while index < end:
|
||||
tag = self[index]
|
||||
if tag.code in codes:
|
||||
bag.append(tag)
|
||||
index += 1
|
||||
else:
|
||||
break
|
||||
return bag
|
||||
|
||||
def has_embedded_objects(self) -> bool:
|
||||
for tag in self:
|
||||
if tag.code == EMBEDDED_OBJ_MARKER and tag.value == EMBEDDED_OBJ_STR:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def strip(cls, tags: Tags, codes: Iterable[int]) -> Tags:
|
||||
"""Constructor from `tags`, strips all tags with group codes in `codes`
|
||||
from tags.
|
||||
|
||||
Args:
|
||||
tags: iterable of :class:`~ezdxf.lldxf.types.DXFTag`
|
||||
codes: iterable of group codes as int
|
||||
|
||||
"""
|
||||
return cls((tag for tag in tags if tag.code not in frozenset(codes)))
|
||||
|
||||
def get_soft_pointers(self) -> Tags:
|
||||
"""Returns all soft-pointer handles in group code range 330-339."""
|
||||
return Tags(tag for tag in self if types.is_soft_pointer(tag))
|
||||
|
||||
def get_hard_pointers(self) -> Tags:
|
||||
"""Returns all hard-pointer handles in group code range 340-349, 390-399 and
|
||||
480-481. Hard pointers protect an object from being purged.
|
||||
"""
|
||||
return Tags(tag for tag in self if types.is_hard_pointer(tag))
|
||||
|
||||
def get_soft_owner_handles(self) -> Tags:
|
||||
"""Returns all soft-owner handles in group code range 350-359."""
|
||||
return Tags(tag for tag in self if types.is_soft_owner(tag))
|
||||
|
||||
def get_hard_owner_handles(self) -> Tags:
|
||||
"""Returns all hard-owner handles in group code range 360-369."""
|
||||
return Tags(tag for tag in self if types.is_hard_owner(tag))
|
||||
|
||||
def has_translatable_pointers(self) -> bool:
|
||||
"""Returns ``True`` if any pointer handle has to be translated during INSERT
|
||||
and XREF operations.
|
||||
"""
|
||||
return any(types.is_translatable_pointer(tag) for tag in self)
|
||||
|
||||
def get_translatable_pointers(self) -> Tags:
|
||||
"""Returns all pointer handles which should be translated during INSERT and XREF
|
||||
operations.
|
||||
"""
|
||||
return Tags(tag for tag in self if types.is_translatable_pointer(tag))
|
||||
|
||||
|
||||
def text2tags(text: str) -> Tags:
|
||||
return Tags.from_text(text)
|
||||
|
||||
|
||||
def group_tags(
|
||||
tags: Iterable[DXFTag], splitcode: int = STRUCTURE_MARKER
|
||||
) -> Iterable[Tags]:
|
||||
"""Group of tags starts with a SplitTag and ends before the next SplitTag.
|
||||
A SplitTag is a tag with code == splitcode, like (0, 'SECTION') for
|
||||
splitcode == 0.
|
||||
|
||||
Args:
|
||||
tags: iterable of :class:`DXFTag`
|
||||
splitcode: group code of split tag
|
||||
|
||||
"""
|
||||
|
||||
# first do nothing, skip tags in front of the first split tag
|
||||
def append(tag):
|
||||
pass
|
||||
|
||||
group = None
|
||||
for tag in tags:
|
||||
if tag.code == splitcode:
|
||||
if group is not None:
|
||||
yield group
|
||||
group = Tags([tag])
|
||||
append = group.append # redefine append: add tags to this group
|
||||
else:
|
||||
append(tag)
|
||||
if group is not None:
|
||||
yield group
|
||||
|
||||
|
||||
def text_to_multi_tags(
|
||||
text: str, code: int = 303, size: int = 255, line_ending: str = "^J"
|
||||
) -> Tags:
|
||||
text = "".join(text).replace("\n", line_ending)
|
||||
|
||||
def chop():
|
||||
start = 0
|
||||
end = size
|
||||
while start < len(text):
|
||||
yield text[start:end]
|
||||
start = end
|
||||
end += size
|
||||
|
||||
return Tags(DXFTag(code, part) for part in chop())
|
||||
|
||||
|
||||
def multi_tags_to_text(tags: Tags, line_ending: str = "^J") -> str:
|
||||
return "".join(tag.value for tag in tags).replace(line_ending, "\n")
|
||||
|
||||
|
||||
OPEN_LIST = (1002, "{")
|
||||
CLOSE_LIST = (1002, "}")
|
||||
|
||||
|
||||
def xdata_list(name: str, xdata_tags: Iterable) -> Tags:
|
||||
tags = Tags()
|
||||
if name:
|
||||
tags.append((1000, name))
|
||||
tags.append(OPEN_LIST)
|
||||
tags.extend(xdata_tags)
|
||||
tags.append(CLOSE_LIST)
|
||||
return tags
|
||||
|
||||
|
||||
def remove_named_list_from_xdata(name: str, tags: Tags) -> Tags:
|
||||
start, end = get_start_and_end_of_named_list_in_xdata(name, tags)
|
||||
del tags[start:end]
|
||||
return tags
|
||||
|
||||
|
||||
def get_named_list_from_xdata(name: str, tags: Tags) -> Tags:
|
||||
start, end = get_start_and_end_of_named_list_in_xdata(name, tags)
|
||||
return Tags(tags[start:end])
|
||||
|
||||
|
||||
class NotFoundException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_start_and_end_of_named_list_in_xdata(name: str, tags: Tags) -> tuple[int, int]:
|
||||
start = None
|
||||
end = None
|
||||
level = 0
|
||||
for index in range(len(tags)):
|
||||
tag = tags[index]
|
||||
|
||||
if start is None and tag == (1000, name):
|
||||
next_tag = tags[index + 1]
|
||||
if next_tag == OPEN_LIST:
|
||||
start = index
|
||||
continue
|
||||
if start is not None:
|
||||
if tag == OPEN_LIST:
|
||||
level += 1
|
||||
elif tag == CLOSE_LIST:
|
||||
level -= 1
|
||||
if level == 0:
|
||||
end = index
|
||||
break
|
||||
|
||||
if start is None:
|
||||
raise NotFoundException
|
||||
if end is None:
|
||||
raise DXFStructureError('Invalid XDATA structure: missing (1002, "}").')
|
||||
return start, end + 1
|
||||
|
||||
|
||||
def find_begin_and_end_of_encoded_xdata_tags(name: str, tags: Tags) -> tuple[int, int]:
|
||||
"""Find encoded XDATA tags, surrounded by group code 1000 tags
|
||||
name_BEGIN and name_END (e.g. MTEXT column specification).
|
||||
|
||||
Raises:
|
||||
NotFoundError: tag group not found
|
||||
DXFStructureError: missing begin- or end tag
|
||||
|
||||
"""
|
||||
begin_name = name + "_BEGIN"
|
||||
end_name = name + "_END"
|
||||
start = None
|
||||
end = None
|
||||
for index, (code, value) in enumerate(tags):
|
||||
if code == 1000:
|
||||
if value == begin_name:
|
||||
start = index
|
||||
elif value == end_name:
|
||||
end = index + 1
|
||||
break
|
||||
if start is None:
|
||||
if end is not None: # end tag without begin tag!
|
||||
raise DXFStructureError(
|
||||
f"Invalid XDATA structure: missing begin tag (1000, {begin_name})."
|
||||
)
|
||||
raise NotFoundException
|
||||
if end is None:
|
||||
raise DXFStructureError(
|
||||
f"Invalid XDATA structure: missing end tag (1000, {end_name})."
|
||||
)
|
||||
return start, end
|
||||
|
||||
|
||||
def binary_data_to_dxf_tags(
|
||||
data: bytes,
|
||||
length_group_code: int = 160,
|
||||
value_group_code: int = 310,
|
||||
value_size=127,
|
||||
) -> Tags:
|
||||
"""Convert binary data to DXF tags."""
|
||||
tags = Tags()
|
||||
length = len(data)
|
||||
tags.append(dxftag(length_group_code, length))
|
||||
index = 0
|
||||
while index < length:
|
||||
chunk = data[index : index + value_size]
|
||||
tags.append(dxftag(value_group_code, chunk))
|
||||
index += value_size
|
||||
return tags
|
||||
@@ -0,0 +1,316 @@
|
||||
# Copyright (c) 2018-2024, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Any, TextIO, TYPE_CHECKING, Union, Iterable, BinaryIO
|
||||
import abc
|
||||
|
||||
from .types import TAG_STRING_FORMAT, cast_tag_value, DXFVertex
|
||||
from .types import BYTES, INT16, INT32, INT64, DOUBLE, BINARY_DATA
|
||||
from .tags import DXFTag, Tags
|
||||
from .const import LATEST_DXF_VERSION
|
||||
from ezdxf.tools import take2
|
||||
import struct
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.lldxf.extendedtags import ExtendedTags
|
||||
from ezdxf.entities import DXFEntity
|
||||
|
||||
__all__ = [
|
||||
"TagWriter",
|
||||
"BinaryTagWriter",
|
||||
"TagCollector",
|
||||
"basic_tags_from_text",
|
||||
"AbstractTagWriter",
|
||||
]
|
||||
CRLF = b"\r\n"
|
||||
|
||||
|
||||
class AbstractTagWriter:
|
||||
# Options for functions using an inherited class for DXF export:
|
||||
dxfversion = LATEST_DXF_VERSION
|
||||
write_handles = True
|
||||
# Force writing optional values if equal to default value when True.
|
||||
# True is only used for testing scenarios!
|
||||
force_optional = False
|
||||
|
||||
# Start of low level interface:
|
||||
@abc.abstractmethod
|
||||
def write_tag(self, tag: DXFTag) -> None: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def write_tag2(self, code: int, value: Any) -> None: ...
|
||||
|
||||
@abc.abstractmethod
|
||||
def write_str(self, s: str) -> None: ...
|
||||
|
||||
# End of low level interface
|
||||
|
||||
# Tag export based on low level tag export:
|
||||
def write_tags(self, tags: Union[Tags, ExtendedTags]) -> None:
|
||||
for tag in tags:
|
||||
self.write_tag(tag)
|
||||
|
||||
def write_vertex(self, code: int, vertex: Iterable[float]) -> None:
|
||||
for index, value in enumerate(vertex):
|
||||
self.write_tag2(code + index * 10, value)
|
||||
|
||||
|
||||
class TagWriter(AbstractTagWriter):
|
||||
"""Writes DXF tags into a text stream."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream: TextIO,
|
||||
dxfversion: str = LATEST_DXF_VERSION,
|
||||
write_handles: bool = True,
|
||||
):
|
||||
self._stream: TextIO = stream
|
||||
self.dxfversion: str = dxfversion
|
||||
self.write_handles: bool = write_handles
|
||||
self.force_optional: bool = False
|
||||
|
||||
# Start of low level interface:
|
||||
def write_tag(self, tag: DXFTag) -> None:
|
||||
self._stream.write(tag.dxfstr())
|
||||
|
||||
def write_tag2(self, code: int, value: Any) -> None:
|
||||
self._stream.write(TAG_STRING_FORMAT % (code, value))
|
||||
|
||||
def write_str(self, s: str) -> None:
|
||||
self._stream.write(s)
|
||||
|
||||
# End of low level interface
|
||||
|
||||
def write_vertex(self, code: int, vertex: Iterable[float]) -> None:
|
||||
"""Optimized vertex export."""
|
||||
write = self._stream.write
|
||||
for index, value in enumerate(vertex):
|
||||
write(TAG_STRING_FORMAT % (code + index * 10, value))
|
||||
|
||||
|
||||
class BinaryTagWriter(AbstractTagWriter):
|
||||
"""Write binary encoded DXF tags into a binary stream.
|
||||
|
||||
.. warning::
|
||||
|
||||
DXF files containing ``ACSH_SWEEP_CLASS`` entities and saved as Binary
|
||||
DXF by `ezdxf` can not be opened with AutoCAD, this is maybe also true
|
||||
for other 3rd party entities. BricsCAD opens this binary DXF files
|
||||
without complaining, but saves the ``ACSH_SWEEP_CLASS`` entities as
|
||||
``ACAD_PROXY_OBJECT`` when writing back, so error analyzing is not
|
||||
possible without the full version of AutoCAD.
|
||||
|
||||
I have no clue why, because converting this DXF files from binary
|
||||
format back to ASCII format by `ezdxf` produces a valid DXF for
|
||||
AutoCAD - so all required information is preserved.
|
||||
|
||||
Two examples available:
|
||||
|
||||
- AutodeskSamples\visualization_-_condominium_with_skylight.dxf
|
||||
- AutodeskSamples\visualization_-_conference_room.dxf
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream: BinaryIO,
|
||||
dxfversion=LATEST_DXF_VERSION,
|
||||
write_handles: bool = True,
|
||||
encoding="utf8",
|
||||
):
|
||||
self._stream = stream
|
||||
self.dxfversion = dxfversion
|
||||
self.write_handles = write_handles
|
||||
self._encoding = encoding # output encoding
|
||||
self._r12 = self.dxfversion <= "AC1009"
|
||||
|
||||
def write_signature(self) -> None:
|
||||
self._stream.write(b"AutoCAD Binary DXF\r\n\x1a\x00")
|
||||
|
||||
# Start of low level interface:
|
||||
def write_tag(self, tag: DXFTag) -> None:
|
||||
if isinstance(tag, DXFVertex):
|
||||
for code, value in tag.dxftags():
|
||||
self.write_tag2(code, value)
|
||||
else:
|
||||
self.write_tag2(tag.code, tag.value)
|
||||
|
||||
def write_str(self, s: str) -> None:
|
||||
data = s.split("\n")
|
||||
for code, value in take2(data):
|
||||
self.write_tag2(int(code), value)
|
||||
|
||||
def write_tag2(self, code: int, value: Any) -> None:
|
||||
# Binary DXF files do not support comments!
|
||||
assert code != 999
|
||||
if code in BINARY_DATA:
|
||||
self._write_binary_chunks(code, value)
|
||||
return
|
||||
stream = self._stream
|
||||
|
||||
# write group code
|
||||
if self._r12:
|
||||
# Special group code handling if DXF R12 and older
|
||||
if code >= 1000: # extended data
|
||||
stream.write(b"\xff")
|
||||
# always 2-byte group code for extended data
|
||||
stream.write(code.to_bytes(2, "little"))
|
||||
else:
|
||||
stream.write(code.to_bytes(1, "little"))
|
||||
else: # for R2000+ do not need a leading 0xff in front of extended data
|
||||
stream.write(code.to_bytes(2, "little"))
|
||||
# write tag content
|
||||
if code in BYTES:
|
||||
stream.write(int(value).to_bytes(1, "little"))
|
||||
elif code in INT16:
|
||||
stream.write(int(value).to_bytes(2, "little", signed=True))
|
||||
elif code in INT32:
|
||||
stream.write(int(value).to_bytes(4, "little", signed=True))
|
||||
elif code in INT64:
|
||||
stream.write(int(value).to_bytes(8, "little", signed=True))
|
||||
elif code in DOUBLE:
|
||||
stream.write(struct.pack("<d", float(value)))
|
||||
else: # write zero terminated string
|
||||
stream.write(str(value).encode(self._encoding, errors="dxfreplace"))
|
||||
stream.write(b"\x00")
|
||||
|
||||
# End of low level interface
|
||||
|
||||
def _write_binary_chunks(self, code: int, data: bytes) -> None:
|
||||
# Split binary data into small chunks, 127 bytes is the
|
||||
# regular size of binary data in ASCII DXF files.
|
||||
CHUNK_SIZE = 127
|
||||
index = 0
|
||||
size = len(data)
|
||||
stream = self._stream
|
||||
|
||||
while index < size:
|
||||
# write group code
|
||||
if self._r12 and code >= 1000: # extended data, just 1004?
|
||||
stream.write(b"\xff") # extended data marker
|
||||
# binary data does not exist in regular R12 entities,
|
||||
# only 2-byte group codes required
|
||||
stream.write(code.to_bytes(2, "little"))
|
||||
|
||||
# write max CHUNK_SIZE bytes of binary data in one tag
|
||||
chunk = data[index : index + CHUNK_SIZE]
|
||||
# write actual chunk size
|
||||
stream.write(len(chunk).to_bytes(1, "little"))
|
||||
stream.write(chunk)
|
||||
index += CHUNK_SIZE
|
||||
|
||||
|
||||
class TagCollector(AbstractTagWriter):
|
||||
"""Collect DXF tags as DXFTag() entities for testing."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dxfversion: str = LATEST_DXF_VERSION,
|
||||
write_handles: bool = True,
|
||||
optional: bool = True,
|
||||
):
|
||||
self.tags: list[DXFTag] = []
|
||||
self.dxfversion: str = dxfversion
|
||||
self.write_handles: bool = write_handles
|
||||
self.force_optional: bool = optional
|
||||
|
||||
# Start of low level interface:
|
||||
def write_tag(self, tag: DXFTag) -> None:
|
||||
if hasattr(tag, "dxftags"):
|
||||
self.tags.extend(tag.dxftags())
|
||||
else:
|
||||
self.tags.append(tag)
|
||||
|
||||
def write_tag2(self, code: int, value: Any) -> None:
|
||||
self.tags.append(DXFTag(code, cast_tag_value(int(code), value)))
|
||||
|
||||
def write_str(self, s: str) -> None:
|
||||
self.write_tags(Tags.from_text(s))
|
||||
|
||||
# End of low level interface
|
||||
|
||||
def has_all_tags(self, other: TagCollector):
|
||||
return all(tag in self.tags for tag in other.tags)
|
||||
|
||||
def reset(self):
|
||||
self.tags = []
|
||||
|
||||
@classmethod
|
||||
def dxftags(cls, entity: DXFEntity, dxfversion=LATEST_DXF_VERSION):
|
||||
collector = cls(dxfversion=dxfversion)
|
||||
entity.export_dxf(collector)
|
||||
return Tags(collector.tags)
|
||||
|
||||
|
||||
def basic_tags_from_text(text: str) -> list[DXFTag]:
|
||||
"""Returns all tags from `text` as basic DXFTags(). All complex tags are
|
||||
resolved into basic (code, value) tags (e.g. DXFVertex(10, (1, 2, 3)) ->
|
||||
DXFTag(10, 1), DXFTag(20, 2), DXFTag(30, 3).
|
||||
|
||||
Args:
|
||||
text: DXF data as string
|
||||
|
||||
Returns: List of basic DXF tags (code, value)
|
||||
|
||||
"""
|
||||
collector = TagCollector()
|
||||
collector.write_tags(Tags.from_text(text))
|
||||
return collector.tags
|
||||
|
||||
|
||||
class JSONTagWriter(AbstractTagWriter):
|
||||
"""Writes DXF tags in JSON format into a text stream.
|
||||
|
||||
The `compact` format is a list of ``[group-code, value]`` pairs where each pair is
|
||||
a DXF tag. The group-code has to be an integer and the value has to be a string,
|
||||
integer, float or list of floats for vertices.
|
||||
|
||||
The `verbose` format (`compact` is ``False``) is a list of ``[group-code, value]``
|
||||
pairs where each pair is a 1:1 representation of a DXF tag. The group-code has to be
|
||||
an integer and the value has to be a string.
|
||||
|
||||
"""
|
||||
|
||||
JSON_HEADER = "[\n"
|
||||
JSON_STRING = '[{0}, "{1}"],\n'
|
||||
JSON_NUMBER = '[{0}, {1}],\n'
|
||||
VERTEX_TAG_FORMAT = "[{0}, {1}],\n"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream: TextIO,
|
||||
dxfversion: str = LATEST_DXF_VERSION,
|
||||
write_handles=True,
|
||||
compact=True,
|
||||
):
|
||||
self._stream = stream
|
||||
self.dxfversion = str(dxfversion)
|
||||
self.write_handles = bool(write_handles)
|
||||
self.force_optional = False
|
||||
self.compact = bool(compact)
|
||||
self._stream.write(self.JSON_HEADER)
|
||||
|
||||
def write_tag(self, tag: DXFTag) -> None:
|
||||
if isinstance(tag, DXFVertex):
|
||||
if self.compact:
|
||||
vertex = ",".join(str(value) for _, value in tag.dxftags())
|
||||
self._stream.write(
|
||||
self.VERTEX_TAG_FORMAT.format(tag.code, f"[{vertex}]")
|
||||
)
|
||||
else:
|
||||
for code, value in tag.dxftags():
|
||||
self.write_tag2(code, value)
|
||||
else:
|
||||
self.write_tag2(tag.code, tag.value)
|
||||
|
||||
def write_tag2(self, code: int, value: Any) -> None:
|
||||
if code == 0 and value == "EOF":
|
||||
self._stream.write('[0, "EOF"]\n]\n') # no trailing comma!
|
||||
return
|
||||
if self.compact and isinstance(value, (float, int)):
|
||||
self._stream.write(self.JSON_NUMBER.format(code, value))
|
||||
return
|
||||
self._stream.write(self.JSON_STRING.format(code, value))
|
||||
|
||||
def write_str(self, s: str) -> None:
|
||||
self.write_tags(Tags.from_text(s))
|
||||
@@ -0,0 +1,472 @@
|
||||
# Copyright (c) 2014-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
"""
|
||||
DXF Types
|
||||
=========
|
||||
|
||||
Required DXF tag interface:
|
||||
|
||||
- property :attr:`code`: group code as int
|
||||
- property :attr:`value`: tag value of unspecific type
|
||||
- :meth:`dxfstr`: returns the DXF string
|
||||
- :meth:`clone`: returns a deep copy of tag
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Union,
|
||||
Iterable,
|
||||
Sequence,
|
||||
Type,
|
||||
Any,
|
||||
)
|
||||
from array import array
|
||||
from itertools import chain
|
||||
from binascii import unhexlify, hexlify
|
||||
import reprlib
|
||||
from ezdxf.math import Vec3
|
||||
|
||||
|
||||
TAG_STRING_FORMAT = "%3d\n%s\n"
|
||||
POINT_CODES = {
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
110,
|
||||
111,
|
||||
112,
|
||||
210,
|
||||
211,
|
||||
212,
|
||||
213,
|
||||
1010,
|
||||
1011,
|
||||
1012,
|
||||
1013,
|
||||
}
|
||||
|
||||
MAX_GROUP_CODE = 1071
|
||||
GENERAL_MARKER = 0
|
||||
SUBCLASS_MARKER = 100
|
||||
XDATA_MARKER = 1001
|
||||
EMBEDDED_OBJ_MARKER = 101
|
||||
APP_DATA_MARKER = 102
|
||||
EXT_DATA_MARKER = 1001
|
||||
GROUP_MARKERS = {
|
||||
GENERAL_MARKER,
|
||||
SUBCLASS_MARKER,
|
||||
EMBEDDED_OBJ_MARKER,
|
||||
APP_DATA_MARKER,
|
||||
EXT_DATA_MARKER,
|
||||
}
|
||||
BINARY_FLAGS = {70, 90}
|
||||
HANDLE_CODES = {5, 105}
|
||||
POINTER_CODES = set(chain(range(320, 370), range(390, 400), (480, 481, 1005)))
|
||||
|
||||
# pointer group codes 320-329 are not translated during INSERT and XREF operations
|
||||
TRANSLATABLE_POINTER_CODES = set(
|
||||
chain(range(330, 370), range(390, 400), (480, 481, 1005))
|
||||
)
|
||||
HEX_HANDLE_CODES = set(chain(HANDLE_CODES, POINTER_CODES))
|
||||
BINARY_DATA = {310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 1004}
|
||||
EMBEDDED_OBJ_STR = "Embedded Object"
|
||||
|
||||
BYTES = set(range(290, 300)) # bool
|
||||
|
||||
INT16 = set(
|
||||
chain(
|
||||
range(60, 80),
|
||||
range(170, 180),
|
||||
range(270, 290),
|
||||
range(370, 390),
|
||||
range(400, 410),
|
||||
range(1060, 1071),
|
||||
)
|
||||
)
|
||||
|
||||
INT32 = set(
|
||||
chain(
|
||||
range(90, 100),
|
||||
range(420, 430),
|
||||
range(440, 450),
|
||||
range(450, 460), # Long in DXF reference, ->signed<- or unsigned?
|
||||
[1071],
|
||||
)
|
||||
)
|
||||
|
||||
INT64 = set(range(160, 170))
|
||||
|
||||
DOUBLE = set(
|
||||
chain(
|
||||
range(10, 60),
|
||||
range(110, 150),
|
||||
range(210, 240),
|
||||
range(460, 470),
|
||||
range(1010, 1060),
|
||||
)
|
||||
)
|
||||
|
||||
VALID_XDATA_GROUP_CODES = {
|
||||
1000,
|
||||
1001,
|
||||
1002,
|
||||
1003,
|
||||
1004,
|
||||
1005,
|
||||
1010,
|
||||
1011,
|
||||
1012,
|
||||
1013,
|
||||
1040,
|
||||
1041,
|
||||
1042,
|
||||
1070,
|
||||
1071,
|
||||
}
|
||||
|
||||
|
||||
def _build_type_table(types):
|
||||
table = {}
|
||||
for caster, codes in types:
|
||||
for code in codes:
|
||||
table[code] = caster
|
||||
return table
|
||||
|
||||
|
||||
TYPE_TABLE = _build_type_table(
|
||||
[
|
||||
# all group code < 0 are spacial tags for internal use
|
||||
(float, DOUBLE),
|
||||
(int, BYTES),
|
||||
(int, INT16),
|
||||
(int, INT32),
|
||||
(int, INT64),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class DXFTag:
|
||||
"""Immutable DXFTag class.
|
||||
|
||||
Args:
|
||||
code: group code as int
|
||||
value: tag value, type depends on group code
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("_code", "_value")
|
||||
|
||||
def __init__(self, code: int, value: Any):
|
||||
self._code: int = int(code)
|
||||
# Do not use _value, always use property value - overwritten in subclasses
|
||||
self._value = value
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Returns content string ``'(code, value)'``."""
|
||||
return str((self._code, self.value))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Returns representation string ``'DXFTag(code, value)'``."""
|
||||
return f"DXFTag{str(self)}"
|
||||
|
||||
@property
|
||||
def code(self) -> int:
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def value(self) -> Any:
|
||||
return self._value
|
||||
|
||||
def __getitem__(self, index: int):
|
||||
"""Returns :attr:`code` for index 0 and :attr:`value` for index 1,
|
||||
emulates a tuple.
|
||||
"""
|
||||
return (self._code, self.value)[index]
|
||||
|
||||
def __iter__(self):
|
||||
"""Returns (code, value) tuples."""
|
||||
yield self._code
|
||||
yield self.value
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
"""``True`` if `other` and `self` has same content for :attr:`code`
|
||||
and :attr:`value`.
|
||||
"""
|
||||
return (self._code, self.value) == other
|
||||
|
||||
def __hash__(self):
|
||||
"""Hash support, :class:`DXFTag` can be used in sets and as dict key."""
|
||||
return hash((self._code, self._value))
|
||||
|
||||
def dxfstr(self) -> str:
|
||||
"""Returns the DXF string e.g. ``' 0\\nLINE\\n'``"""
|
||||
return TAG_STRING_FORMAT % (self.code, self._value)
|
||||
|
||||
def clone(self) -> "DXFTag":
|
||||
"""Returns a clone of itself, this method is necessary for the more
|
||||
complex (and not immutable) DXF tag types.
|
||||
"""
|
||||
return self # immutable tags
|
||||
|
||||
|
||||
# Special marker tag
|
||||
NONE_TAG = DXFTag(0, 0)
|
||||
|
||||
|
||||
def uniform_appid(appid: str) -> str:
|
||||
if appid[0] == "{":
|
||||
return appid
|
||||
else:
|
||||
return "{" + appid
|
||||
|
||||
|
||||
def is_app_data_marker(tag: DXFTag) -> bool:
|
||||
return tag.code == APP_DATA_MARKER and tag.value.startswith("{")
|
||||
|
||||
|
||||
def is_embedded_object_marker(tag: DXFTag) -> bool:
|
||||
return tag.code == EMBEDDED_OBJ_MARKER and tag.value == EMBEDDED_OBJ_STR
|
||||
|
||||
|
||||
def is_arbitrary_pointer(tag: DXFTag) -> bool:
|
||||
"""Arbitrary object handles; handle values that are taken "as is".
|
||||
They are not translated during INSERT and XREF operations.
|
||||
"""
|
||||
return 319 < tag.code < 330
|
||||
|
||||
|
||||
def is_soft_pointer(tag: DXFTag) -> bool:
|
||||
"""Soft-pointer handle; arbitrary soft pointers to other objects within same DXF
|
||||
file or drawing. Translated during INSERT and XREF operations.
|
||||
"""
|
||||
return 329 < tag.code < 340 or tag.code == 1005
|
||||
|
||||
|
||||
def is_hard_pointer(tag: DXFTag) -> bool:
|
||||
"""Hard-pointer handle; arbitrary hard pointers to other objects within same DXF
|
||||
file or drawing. Translated during INSERT and XREF operations. Hard pointers
|
||||
protect an object from being purged.
|
||||
"""
|
||||
code = tag.code
|
||||
return 339 < code < 350 or 389 < code < 400 or 479 < code < 482
|
||||
|
||||
|
||||
def is_soft_owner(tag: DXFTag) -> bool:
|
||||
"""Soft-owner handle; arbitrary soft ownership links to other objects within same
|
||||
DXF file or drawing. Translated during INSERT and XREF operations.
|
||||
"""
|
||||
return 349 < tag.code < 360
|
||||
|
||||
|
||||
def is_hard_owner(tag: DXFTag) -> bool:
|
||||
"""Hard-owner handle; arbitrary hard ownership links to other objects within same
|
||||
DXF file or drawing. Translated during INSERT and XREF operations. Hard owner handle
|
||||
protect an object from being purged.
|
||||
"""
|
||||
return 359 < tag.code < 370
|
||||
|
||||
|
||||
def is_translatable_pointer(tag: DXFTag) -> bool:
|
||||
# pointer group codes 320-329 are not translated during INSERT and XREF operations
|
||||
return tag.code in TRANSLATABLE_POINTER_CODES
|
||||
|
||||
|
||||
class DXFVertex(DXFTag):
|
||||
"""Represents a 2D or 3D vertex, stores only the group code of the
|
||||
x-component of the vertex, because the y-group-code is x-group-code + 10
|
||||
and z-group-code id x-group-code+20, this is a rule that ALWAYS applies.
|
||||
This tag is `immutable` by design, not by implementation.
|
||||
|
||||
Args:
|
||||
code: group code of x-component
|
||||
value: sequence of x, y and optional z values
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self, code: int, value: Iterable[float]):
|
||||
super(DXFVertex, self).__init__(code, array("d", value))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"DXFVertex({self.code}, {str(self)})"
|
||||
|
||||
def __hash__(self):
|
||||
x, y, *z = self._value
|
||||
z = 0.0 if len(z) == 0 else z[0]
|
||||
return hash((self.code, x, y, z))
|
||||
|
||||
@property
|
||||
def value(self) -> tuple[float, ...]:
|
||||
return tuple(self._value)
|
||||
|
||||
def dxftags(self) -> Iterable[DXFTag]:
|
||||
"""Returns all vertex components as single :class:`DXFTag` objects."""
|
||||
c = self.code
|
||||
return (
|
||||
DXFTag(code, value) for code, value in zip((c, c + 10, c + 20), self.value)
|
||||
)
|
||||
|
||||
def dxfstr(self) -> str:
|
||||
"""Returns the DXF string for all vertex components."""
|
||||
return "".join(tag.dxfstr() for tag in self.dxftags())
|
||||
|
||||
|
||||
class DXFBinaryTag(DXFTag):
|
||||
"""Immutable BinaryTags class - immutable by design, not by implementation."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"({self.code}, {self.tostring()})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"DXFBinaryTag({self.code}, {reprlib.repr(self.tostring())})"
|
||||
|
||||
def tostring(self) -> str:
|
||||
"""Returns binary value as single hex-string."""
|
||||
assert isinstance(self.value, bytes)
|
||||
return hexlify(self.value).upper().decode()
|
||||
|
||||
def dxfstr(self) -> str:
|
||||
"""Returns the DXF string for all vertex components."""
|
||||
return TAG_STRING_FORMAT % (self.code, self.tostring())
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, code: int, value: Union[str, bytes]):
|
||||
return cls(code, unhexlify(value))
|
||||
|
||||
|
||||
def dxftag(code: int, value: Any) -> DXFTag:
|
||||
"""DXF tag factory function.
|
||||
|
||||
Args:
|
||||
code: group code
|
||||
value: tag value
|
||||
|
||||
Returns: :class:`DXFTag` or inherited
|
||||
|
||||
"""
|
||||
if code in BINARY_DATA:
|
||||
return DXFBinaryTag(code, value)
|
||||
elif code in POINT_CODES:
|
||||
return DXFVertex(code, value)
|
||||
else:
|
||||
return DXFTag(code, cast_tag_value(code, value))
|
||||
|
||||
|
||||
def tuples_to_tags(iterable: Iterable[tuple[int, Any]]) -> Iterable[DXFTag]:
|
||||
"""Returns an iterable if :class:`DXFTag` or inherited, accepts an
|
||||
iterable of (code, value) tuples as input.
|
||||
"""
|
||||
for code, value in iterable:
|
||||
if code in POINT_CODES:
|
||||
yield DXFVertex(code, value)
|
||||
elif code in BINARY_DATA:
|
||||
assert isinstance(value, (str, bytes))
|
||||
yield DXFBinaryTag.from_string(code, value)
|
||||
else:
|
||||
yield DXFTag(code, value)
|
||||
|
||||
|
||||
def is_valid_handle(handle) -> bool:
|
||||
if isinstance(handle, str):
|
||||
try:
|
||||
int(handle, 16)
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def is_binary_data(code: int) -> bool:
|
||||
return code in BINARY_DATA
|
||||
|
||||
|
||||
def is_pointer_code(code: int) -> bool:
|
||||
return code in POINTER_CODES
|
||||
|
||||
|
||||
def is_point_code(code: int) -> bool:
|
||||
return code in POINT_CODES
|
||||
|
||||
|
||||
def is_point_tag(tag: Sequence) -> bool:
|
||||
return tag[0] in POINT_CODES
|
||||
|
||||
|
||||
def cast_tag_value(code: int, value: Any) -> Any:
|
||||
return TYPE_TABLE.get(code, str)(value)
|
||||
|
||||
|
||||
def tag_type(code: int) -> Type:
|
||||
return TYPE_TABLE.get(code, str)
|
||||
|
||||
|
||||
def strtag(tag: Union[DXFTag, tuple[int, Any]]) -> str:
|
||||
return TAG_STRING_FORMAT % tuple(tag)
|
||||
|
||||
|
||||
def get_xcode_for(code) -> int:
|
||||
if code in HEX_HANDLE_CODES:
|
||||
return 1005
|
||||
if code in BINARY_DATA:
|
||||
return 1004
|
||||
type_ = TYPE_TABLE.get(code, str)
|
||||
if type_ is int:
|
||||
return 1070
|
||||
if type_ is float:
|
||||
return 1040
|
||||
return 1000
|
||||
|
||||
|
||||
def cast_value(code: int, value):
|
||||
if value is not None:
|
||||
if code in POINT_CODES:
|
||||
return Vec3(value)
|
||||
return TYPE_TABLE.get(code, str)(value)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
TAG_TYPES = {
|
||||
int: "<int>",
|
||||
float: "<float>",
|
||||
str: "<str>",
|
||||
}
|
||||
|
||||
|
||||
def tag_type_str(code: int) -> str:
|
||||
if code in GROUP_MARKERS:
|
||||
return "<ctrl>"
|
||||
elif code in HANDLE_CODES:
|
||||
return "<handle>"
|
||||
elif code in POINTER_CODES:
|
||||
return "<ref>"
|
||||
elif is_point_code(code):
|
||||
return "<point>"
|
||||
elif is_binary_data(code):
|
||||
return "<bin>"
|
||||
else:
|
||||
return TAG_TYPES[tag_type(code)]
|
||||
|
||||
|
||||
def render_tag(tag: DXFTag, col: int) -> Any:
|
||||
code, value = tag
|
||||
if col == 0:
|
||||
return str(code)
|
||||
elif col == 1:
|
||||
return tag_type_str(code)
|
||||
elif col == 2:
|
||||
return str(value)
|
||||
else:
|
||||
raise IndexError(col)
|
||||
@@ -0,0 +1,560 @@
|
||||
# Copyright (C) 2018-2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TextIO, Iterable, Optional, cast, Sequence, Iterator
|
||||
import logging
|
||||
import io
|
||||
import bisect
|
||||
import math
|
||||
|
||||
from .const import (
|
||||
DXFStructureError,
|
||||
DXFError,
|
||||
DXFValueError,
|
||||
DXFTypeError,
|
||||
DXFAppDataError,
|
||||
DXFXDataError,
|
||||
APP_DATA_MARKER,
|
||||
HEADER_VAR_MARKER,
|
||||
XDATA_MARKER,
|
||||
INVALID_LAYER_NAME_CHARACTERS,
|
||||
acad_release,
|
||||
VALID_DXF_LINEWEIGHT_VALUES,
|
||||
VALID_DXF_LINEWEIGHTS,
|
||||
LINEWEIGHT_BYLAYER,
|
||||
TRANSPARENCY_BYBLOCK,
|
||||
)
|
||||
|
||||
from .tagger import ascii_tags_loader, binary_tags_loader
|
||||
from .types import is_embedded_object_marker, DXFTag, NONE_TAG
|
||||
from ezdxf.tools.codepage import toencoding
|
||||
from ezdxf.math import NULLVEC, Vec3
|
||||
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
class DXFInfo:
|
||||
"""DXF Info Record
|
||||
|
||||
.. attribute:: release
|
||||
|
||||
.. attribute:: version
|
||||
|
||||
.. attribute:: encoding
|
||||
|
||||
.. attribute:: handseed
|
||||
|
||||
.. attribute:: insert_units
|
||||
|
||||
.. attribute:: insert_base
|
||||
|
||||
"""
|
||||
|
||||
EXPECTED_COUNT = 5
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.release: str = "R12"
|
||||
self.version: str = "AC1009"
|
||||
self.encoding: str = "cp1252"
|
||||
self.handseed: str = "0"
|
||||
self.insert_units: int = 0 # unitless
|
||||
self.insert_base: Vec3 = NULLVEC
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "\n".join(self.data_strings())
|
||||
|
||||
def data_strings(self) -> list[str]:
|
||||
from ezdxf import units
|
||||
|
||||
return [
|
||||
f"release: {self.release}",
|
||||
f"version: {self.version}",
|
||||
f"encoding: {self.encoding}",
|
||||
f"next handle: 0x{self.handseed}",
|
||||
f"insert units: {self.insert_units} <{units.decode(self.insert_units)}>",
|
||||
f"insert base point: {self.insert_base}",
|
||||
]
|
||||
|
||||
def set_header_var(self, name: str, value) -> int:
|
||||
if name == "$ACADVER":
|
||||
self.version = str(value)
|
||||
self.release = acad_release.get(value, "R12")
|
||||
elif name == "$DWGCODEPAGE":
|
||||
self.encoding = toencoding(value)
|
||||
elif name == "$HANDSEED":
|
||||
self.handseed = str(value)
|
||||
elif name == "$INSUNITS":
|
||||
try:
|
||||
self.insert_units = int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
elif name == "$INSBASE":
|
||||
try:
|
||||
self.insert_base = Vec3(value)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
else:
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
def dxf_info(stream: TextIO) -> DXFInfo:
|
||||
"""Scans the HEADER section of an ASCII DXF document and returns a :class:`DXFInfo`
|
||||
object, which contains information about the DXF version, text encoding, drawing
|
||||
units and insertion base point.
|
||||
"""
|
||||
return _detect_dxf_info(ascii_tags_loader(stream))
|
||||
|
||||
|
||||
def binary_dxf_info(data: bytes) -> DXFInfo:
|
||||
"""Scans the HEADER section of a binary DXF document and returns a :class:`DXFInfo`
|
||||
object, which contains information about the DXF version, text encoding, drawing
|
||||
units and insertion base point.
|
||||
"""
|
||||
return _detect_dxf_info(binary_tags_loader(data))
|
||||
|
||||
|
||||
def _detect_dxf_info(tagger: Iterator[DXFTag]) -> DXFInfo:
|
||||
info = DXFInfo()
|
||||
# comments will be skipped
|
||||
if next(tagger) != (0, "SECTION"):
|
||||
# unexpected or invalid DXF structure
|
||||
return info
|
||||
if next(tagger) != (2, "HEADER"):
|
||||
# Without a leading HEADER section the document is processed as DXF R12 file
|
||||
# with only an ENTITIES section.
|
||||
return info
|
||||
tag = NONE_TAG
|
||||
undo_tag = NONE_TAG
|
||||
found: int = 0
|
||||
while tag != (0, "ENDSEC"):
|
||||
if undo_tag is NONE_TAG:
|
||||
tag = next(tagger)
|
||||
else:
|
||||
tag = undo_tag
|
||||
undo_tag = NONE_TAG
|
||||
if tag.code != HEADER_VAR_MARKER:
|
||||
continue
|
||||
var_name = str(tag.value)
|
||||
code, value = next(tagger)
|
||||
if code == 10:
|
||||
x = float(value)
|
||||
y = float(next(tagger).value)
|
||||
z = 0.0
|
||||
tag = next(tagger)
|
||||
if tag.code == 30:
|
||||
z = float(tag.value)
|
||||
else:
|
||||
undo_tag = tag
|
||||
value = Vec3(x, y, z)
|
||||
found += info.set_header_var(var_name, value)
|
||||
if found >= DXFInfo.EXPECTED_COUNT:
|
||||
break
|
||||
return info
|
||||
|
||||
|
||||
def header_validator(tagger: Iterable[DXFTag]) -> Iterable[DXFTag]:
|
||||
"""Checks the tag structure of the content of the header section.
|
||||
|
||||
Do not feed (0, 'SECTION') (2, 'HEADER') and (0, 'ENDSEC') tags!
|
||||
|
||||
Args:
|
||||
tagger: generator/iterator of low level tags or compiled tags
|
||||
|
||||
Raises:
|
||||
DXFStructureError() -> invalid group codes
|
||||
DXFValueError() -> invalid header variable name
|
||||
|
||||
"""
|
||||
variable_name_tag = True
|
||||
for tag in tagger:
|
||||
code, value = tag
|
||||
if variable_name_tag:
|
||||
if code != HEADER_VAR_MARKER:
|
||||
raise DXFStructureError(
|
||||
f"Invalid header variable tag {code}, {value})."
|
||||
)
|
||||
if not value.startswith("$"):
|
||||
raise DXFValueError(
|
||||
f'Invalid header variable name "{value}", missing leading "$".'
|
||||
)
|
||||
variable_name_tag = False
|
||||
else:
|
||||
variable_name_tag = True
|
||||
yield tag
|
||||
|
||||
|
||||
def entity_structure_validator(tags: list[DXFTag]) -> Iterable[DXFTag]:
|
||||
"""Checks for valid DXF entity tag structure.
|
||||
|
||||
- APP DATA can not be nested and every opening tag (102, '{...') needs a
|
||||
closing tag (102, '}')
|
||||
- extended group codes (>=1000) allowed before XDATA section
|
||||
- XDATA section starts with (1001, APPID) and is always at the end of an
|
||||
entity
|
||||
- XDATA section: only group code >= 1000 is allowed
|
||||
- XDATA control strings (1002, '{') and (1002, '}') have to be balanced
|
||||
- embedded objects may follow XDATA
|
||||
|
||||
XRECORD entities will not be checked.
|
||||
|
||||
Args:
|
||||
tags: list of DXFTag()
|
||||
|
||||
Raises:
|
||||
DXFAppDataError: for invalid APP DATA
|
||||
DXFXDataError: for invalid XDATA
|
||||
|
||||
"""
|
||||
assert isinstance(tags, list)
|
||||
dxftype = cast(str, tags[0].value)
|
||||
is_xrecord = dxftype == "XRECORD"
|
||||
handle: str = "???"
|
||||
app_data: bool = False
|
||||
xdata: bool = False
|
||||
xdata_list_level: int = 0
|
||||
app_data_closing_tag: str = "}"
|
||||
embedded_object: bool = False
|
||||
for tag in tags:
|
||||
if tag.code == 5 and handle == "???":
|
||||
handle = cast(str, tag.value)
|
||||
|
||||
if is_embedded_object_marker(tag):
|
||||
embedded_object = True
|
||||
|
||||
if embedded_object: # no further validation
|
||||
yield tag
|
||||
continue # with next tag
|
||||
|
||||
if xdata and not embedded_object:
|
||||
if tag.code < 1000:
|
||||
dxftype = cast(str, tags[0].value)
|
||||
raise DXFXDataError(
|
||||
f"Invalid XDATA structure in entity {dxftype}(#{handle}), "
|
||||
f"only group code >=1000 allowed in XDATA section"
|
||||
)
|
||||
if tag.code == 1002:
|
||||
value = cast(str, tag.value)
|
||||
if value == "{":
|
||||
xdata_list_level += 1
|
||||
elif value == "}":
|
||||
xdata_list_level -= 1
|
||||
else:
|
||||
raise DXFXDataError(
|
||||
f'Invalid XDATA control string (1002, "{value}") entity'
|
||||
f" {dxftype}(#{handle})."
|
||||
)
|
||||
if xdata_list_level < 0: # more closing than opening tags
|
||||
raise DXFXDataError(
|
||||
f"Invalid XDATA structure in entity {dxftype}(#{handle}), "
|
||||
f'unbalanced list markers, missing (1002, "{{").'
|
||||
)
|
||||
|
||||
if tag.code == APP_DATA_MARKER and not is_xrecord:
|
||||
# Ignore control tags (102, ...) tags in XRECORD
|
||||
value = cast(str, tag.value)
|
||||
if value.startswith("{"):
|
||||
if app_data: # already in app data mode
|
||||
raise DXFAppDataError(
|
||||
f"Invalid APP DATA structure in entity {dxftype}"
|
||||
f"(#{handle}), APP DATA can not be nested."
|
||||
)
|
||||
app_data = True
|
||||
# 'APPID}' is also a valid closing tag
|
||||
app_data_closing_tag = value[1:] + "}"
|
||||
elif value == "}" or value == app_data_closing_tag:
|
||||
if not app_data:
|
||||
raise DXFAppDataError(
|
||||
f"Invalid APP DATA structure in entity {dxftype}"
|
||||
f'(#{handle}), found (102, "}}") tag without opening tag.'
|
||||
)
|
||||
app_data = False
|
||||
app_data_closing_tag = "}"
|
||||
else:
|
||||
raise DXFAppDataError(
|
||||
f'Invalid APP DATA structure tag (102, "{value}") in '
|
||||
f"entity {dxftype}(#{handle})."
|
||||
)
|
||||
|
||||
# XDATA section starts with (1001, APPID) and is always at the end of
|
||||
# an entity.
|
||||
if tag.code == XDATA_MARKER and xdata is False:
|
||||
xdata = True
|
||||
if app_data:
|
||||
raise DXFAppDataError(
|
||||
f"Invalid APP DATA structure in entity {dxftype}"
|
||||
f'(#{handle}), missing closing tag (102, "}}").'
|
||||
)
|
||||
yield tag
|
||||
|
||||
if app_data:
|
||||
raise DXFAppDataError(
|
||||
f"Invalid APP DATA structure in entity {dxftype}(#{handle}), "
|
||||
f'missing closing tag (102, "}}").'
|
||||
)
|
||||
if xdata:
|
||||
if xdata_list_level < 0:
|
||||
raise DXFXDataError(
|
||||
f"Invalid XDATA structure in entity {dxftype}(#{handle}), "
|
||||
f'unbalanced list markers, missing (1002, "{{").'
|
||||
)
|
||||
elif xdata_list_level > 0:
|
||||
raise DXFXDataError(
|
||||
f"Invalid XDATA structure in entity {dxftype}(#{handle}), "
|
||||
f'unbalanced list markers, missing (1002, "}}").'
|
||||
)
|
||||
|
||||
|
||||
def is_dxf_file(filename: str) -> bool:
|
||||
"""Returns ``True`` if `filename` is an ASCII DXF file."""
|
||||
with io.open(filename, errors="ignore") as fp:
|
||||
return is_dxf_stream(fp)
|
||||
|
||||
|
||||
def is_binary_dxf_file(filename: str) -> bool:
|
||||
"""Returns ``True`` if `filename` is a binary DXF file."""
|
||||
with open(filename, "rb") as fp:
|
||||
sentinel = fp.read(22)
|
||||
return sentinel == b"AutoCAD Binary DXF\r\n\x1a\x00"
|
||||
|
||||
|
||||
def is_dwg_file(filename: str) -> bool:
|
||||
"""Returns ``True`` if `filename` is a DWG file."""
|
||||
return dwg_version(filename) is not None
|
||||
|
||||
|
||||
def dwg_version(filename: str) -> Optional[str]:
|
||||
"""Returns DWG version of `filename` as string or ``None``."""
|
||||
with open(str(filename), "rb") as fp:
|
||||
try:
|
||||
version = fp.read(6).decode(errors="ignore")
|
||||
except IOError:
|
||||
return None
|
||||
if version not in acad_release:
|
||||
return None
|
||||
return version
|
||||
|
||||
|
||||
def is_dxf_stream(stream: TextIO) -> bool:
|
||||
try:
|
||||
reader = ascii_tags_loader(stream)
|
||||
except DXFError:
|
||||
return False
|
||||
try:
|
||||
for tag in reader:
|
||||
# The common case for well formed DXF files
|
||||
if tag == (0, "SECTION"):
|
||||
return True
|
||||
# Accept/Ignore tags in front of first SECTION - like AutoCAD and
|
||||
# BricsCAD, but group code should be < 1000, until reality proofs
|
||||
# otherwise.
|
||||
if tag.code > 999:
|
||||
return False
|
||||
except DXFStructureError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
# Names used in symbol table records and in dictionaries must follow these rules:
|
||||
#
|
||||
# - Names can be any length in ObjectARX, but symbol names entered by users in
|
||||
# AutoCAD are limited to 255 characters.
|
||||
# - AutoCAD preserves the case of names but does not use the case in
|
||||
# comparisons. For example, AutoCAD considers "Floor" to be the same symbol
|
||||
# as "FLOOR."
|
||||
# - Names can be composed of all characters allowed by Windows or Mac OS for
|
||||
# filenames, except comma (,), backquote (‘), semi-colon (;), and equal
|
||||
# sign (=).
|
||||
# http://help.autodesk.com/view/OARX/2018/ENU/?guid=GUID-83ABF20A-57D4-4AB3-8A49-D91E0F70DBFF
|
||||
|
||||
|
||||
def is_valid_table_name(name: str) -> bool:
|
||||
# remove backslash of special DXF string encodings
|
||||
if "\\" in name:
|
||||
# remove prefix of special DXF unicode encoding
|
||||
name = name.replace(r"\U+", "")
|
||||
# remove prefix of special DXF encoding M+xxxxx
|
||||
# I don't know the real name of this encoding, so I call it "mplus" encoding
|
||||
name = name.replace(r"\M+", "")
|
||||
chars = set(name)
|
||||
return not bool(INVALID_LAYER_NAME_CHARACTERS.intersection(chars))
|
||||
|
||||
|
||||
def make_table_key(name: str) -> str:
|
||||
"""Make unified table entry key."""
|
||||
if not isinstance(name, str):
|
||||
raise DXFTypeError(f"name has to be a string, got {type(name)}")
|
||||
return name.lower()
|
||||
|
||||
|
||||
def is_valid_layer_name(name: str) -> bool:
|
||||
if is_adsk_special_layer(name):
|
||||
return True
|
||||
return is_valid_table_name(name)
|
||||
|
||||
|
||||
def is_adsk_special_layer(name: str) -> bool:
|
||||
if name.startswith("*") and len(name) > 1:
|
||||
# Special Autodesk layers starts with the otherwise invalid character *
|
||||
# These layers do not show up in the layer panel.
|
||||
# Only the first letter can be an asterisk.
|
||||
return is_valid_table_name(name[1:])
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_block_name(name: str) -> bool:
|
||||
if name.startswith("*"):
|
||||
return is_valid_table_name(name[1:])
|
||||
else:
|
||||
return is_valid_table_name(name)
|
||||
|
||||
|
||||
def is_valid_vport_name(name: str) -> bool:
|
||||
if name.startswith("*"):
|
||||
return name.upper() == "*ACTIVE"
|
||||
else:
|
||||
return is_valid_table_name(name)
|
||||
|
||||
|
||||
def is_valid_lineweight(lineweight: int) -> bool:
|
||||
return lineweight in VALID_DXF_LINEWEIGHT_VALUES
|
||||
|
||||
|
||||
def fix_lineweight(lineweight: int) -> int:
|
||||
if lineweight in VALID_DXF_LINEWEIGHT_VALUES:
|
||||
return lineweight
|
||||
if lineweight < -3:
|
||||
return LINEWEIGHT_BYLAYER
|
||||
if lineweight > 211:
|
||||
return 211
|
||||
index = bisect.bisect(VALID_DXF_LINEWEIGHTS, lineweight)
|
||||
return VALID_DXF_LINEWEIGHTS[index]
|
||||
|
||||
|
||||
def is_valid_aci_color(aci: int) -> bool:
|
||||
return 0 <= aci <= 257
|
||||
|
||||
|
||||
def is_valid_rgb(rgb) -> bool:
|
||||
if not isinstance(rgb, Sequence):
|
||||
return False
|
||||
if len(rgb) != 3:
|
||||
return False
|
||||
for value in rgb:
|
||||
if not isinstance(value, int) or value < 0 or value > 255:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_in_integer_range(start: int, end: int):
|
||||
"""Range of integer values, excluding the `end` value."""
|
||||
|
||||
def _validator(value: int) -> bool:
|
||||
return start <= value < end
|
||||
|
||||
return _validator
|
||||
|
||||
|
||||
def fit_into_integer_range(start: int, end: int):
|
||||
def _fixer(value: int) -> int:
|
||||
return min(max(value, start), end - 1)
|
||||
|
||||
return _fixer
|
||||
|
||||
|
||||
def fit_into_float_range(start: float, end: float):
|
||||
def _fixer(value: float) -> float:
|
||||
return min(max(value, start), end)
|
||||
|
||||
return _fixer
|
||||
|
||||
|
||||
def is_in_float_range(start: float, end: float):
|
||||
"""Range of float values, including the `end` value."""
|
||||
|
||||
def _validator(value: float) -> bool:
|
||||
return start <= value <= end
|
||||
|
||||
return _validator
|
||||
|
||||
|
||||
def is_not_null_vector(v) -> bool:
|
||||
return not NULLVEC.isclose(v)
|
||||
|
||||
|
||||
def is_not_zero(v: float) -> bool:
|
||||
return not math.isclose(v, 0.0, abs_tol=1e-12)
|
||||
|
||||
|
||||
def is_not_negative(v) -> bool:
|
||||
return v >= 0
|
||||
|
||||
|
||||
is_greater_or_equal_zero = is_not_negative
|
||||
|
||||
|
||||
def is_positive(v) -> bool:
|
||||
return v > 0
|
||||
|
||||
|
||||
is_greater_zero = is_positive
|
||||
|
||||
|
||||
def is_valid_bitmask(mask: int):
|
||||
def _validator(value: int) -> bool:
|
||||
return not bool(~mask & value)
|
||||
|
||||
return _validator
|
||||
|
||||
|
||||
def fix_bitmask(mask: int):
|
||||
def _fixer(value: int) -> int:
|
||||
return mask & value
|
||||
|
||||
return _fixer
|
||||
|
||||
|
||||
def is_integer_bool(v) -> bool:
|
||||
return v in (0, 1)
|
||||
|
||||
|
||||
def fix_integer_bool(v) -> int:
|
||||
return 1 if v else 0
|
||||
|
||||
|
||||
def is_one_of(values: set):
|
||||
def _validator(v) -> bool:
|
||||
return v in values
|
||||
|
||||
return _validator
|
||||
|
||||
|
||||
def is_valid_one_line_text(text: str) -> bool:
|
||||
has_line_breaks = bool(set(text).intersection({"\n", "\r"}))
|
||||
return not has_line_breaks and not text.endswith("^")
|
||||
|
||||
|
||||
def fix_one_line_text(text: str) -> str:
|
||||
return text.replace("\n", "").replace("\r", "").rstrip("^")
|
||||
|
||||
|
||||
def is_valid_attrib_tag(tag: str) -> bool:
|
||||
return is_valid_one_line_text(tag)
|
||||
|
||||
|
||||
def fix_attrib_tag(tag: str) -> str:
|
||||
return fix_one_line_text(tag)
|
||||
|
||||
|
||||
def is_handle(handle) -> bool:
|
||||
try:
|
||||
int(handle, 16)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_transparency(value) -> bool:
|
||||
if isinstance(value, int):
|
||||
return value == TRANSPARENCY_BYBLOCK or bool(value & 0x02000000)
|
||||
return False
|
||||
Reference in New Issue
Block a user