initial
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Low Level DXF modules
|
||||
# Copyright (c) 2011-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
255
.venv/lib/python3.12/site-packages/ezdxf/lldxf/attributes.py
Normal file
255
.venv/lib/python3.12/site-packages/ezdxf/lldxf/attributes.py
Normal file
@@ -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)
|
||||
)
|
||||
702
.venv/lib/python3.12/site-packages/ezdxf/lldxf/const.py
Normal file
702
.venv/lib/python3.12/site-packages/ezdxf/lldxf/const.py
Normal file
@@ -0,0 +1,702 @@
|
||||
# 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
|
||||
|
||||
# '|' 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"
|
||||
85
.venv/lib/python3.12/site-packages/ezdxf/lldxf/encoding.py
Normal file
85
.venv/lib/python3.12/site-packages/ezdxf/lldxf/encoding.py
Normal file
@@ -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
|
||||
463
.venv/lib/python3.12/site-packages/ezdxf/lldxf/extendedtags.py
Normal file
463
.venv/lib/python3.12/site-packages/ezdxf/lldxf/extendedtags.py
Normal file
@@ -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)
|
||||
158
.venv/lib/python3.12/site-packages/ezdxf/lldxf/fileindex.py
Normal file
158
.venv/lib/python3.12/site-packages/ezdxf/lldxf/fileindex.py
Normal file
@@ -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
|
||||
26
.venv/lib/python3.12/site-packages/ezdxf/lldxf/hdrvars.py
Normal file
26
.venv/lib/python3.12/site-packages/ezdxf/lldxf/hdrvars.py
Normal file
@@ -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
|
||||
156
.venv/lib/python3.12/site-packages/ezdxf/lldxf/loader.py
Normal file
156
.venv/lib/python3.12/site-packages/ezdxf/lldxf/loader.py
Normal file
@@ -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)
|
||||
208
.venv/lib/python3.12/site-packages/ezdxf/lldxf/packedtags.py
Normal file
208
.venv/lib/python3.12/site-packages/ezdxf/lldxf/packedtags.py
Normal file
@@ -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)
|
||||
212
.venv/lib/python3.12/site-packages/ezdxf/lldxf/repair.py
Normal file
212
.venv/lib/python3.12/site-packages/ezdxf/lldxf/repair.py
Normal file
@@ -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
|
||||
384
.venv/lib/python3.12/site-packages/ezdxf/lldxf/tagger.py
Normal file
384
.venv/lib/python3.12/site-packages/ezdxf/lldxf/tagger.py
Normal file
@@ -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
|
||||
458
.venv/lib/python3.12/site-packages/ezdxf/lldxf/tags.py
Normal file
458
.venv/lib/python3.12/site-packages/ezdxf/lldxf/tags.py
Normal file
@@ -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
|
||||
316
.venv/lib/python3.12/site-packages/ezdxf/lldxf/tagwriter.py
Normal file
316
.venv/lib/python3.12/site-packages/ezdxf/lldxf/tagwriter.py
Normal file
@@ -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))
|
||||
472
.venv/lib/python3.12/site-packages/ezdxf/lldxf/types.py
Normal file
472
.venv/lib/python3.12/site-packages/ezdxf/lldxf/types.py
Normal file
@@ -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)
|
||||
560
.venv/lib/python3.12/site-packages/ezdxf/lldxf/validator.py
Normal file
560
.venv/lib/python3.12/site-packages/ezdxf/lldxf/validator.py
Normal file
@@ -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