This commit is contained in:
Christian Anetzberger
2026-01-22 20:23:51 +01:00
commit a197de9456
4327 changed files with 1235205 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
# Low Level DXF modules
# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License

View 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)
)

View 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"

View 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

View 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)

View 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

View 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

View 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)

View 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)

View 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

View 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

View 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

View 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))

View 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)

View 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