# Copyright (c) 2021-2023, Manfred Moitzi # License: MIT License from __future__ import annotations from typing import Any, Optional, Iterator, TYPE_CHECKING from ezdxf import colors from ezdxf.lldxf import validator, const if TYPE_CHECKING: from ezdxf.document import Drawing from ezdxf.entities import DXFEntity __all__ = ["GfxAttribs", "TRANSPARENCY_BYBLOCK"] DEFAULT_LAYER = "0" DEFAULT_ACI_COLOR = colors.BYLAYER DEFAULT_LINETYPE = "ByLayer" DEFAULT_LINEWEIGHT = const.LINEWEIGHT_BYLAYER DEFAULT_LTSCALE = 1.0 TRANSPARENCY_BYBLOCK = -1.0 # special value class GfxAttribs: """ Represents often used DXF attributes of graphical entities. Args: layer (str): layer name as string color (int): :ref:`ACI` color value as integer rgb: RGB true color (red, green, blue) tuple, each channel value in the range from 0 to 255, ``None`` for not set linetype (str): linetype name, does not check if the linetype exist! lineweight (int): see :ref:`lineweights` documentation for valid values transparency (float): transparency value in the range from 0.0 to 1.0, where 0.0 is opaque and 1.0 if fully transparent, -1.0 for transparency by block, ``None`` for transparency by layer ltscale (float): linetype scaling factor > 0.0, default factor is 1.0 Raises: DXFValueError: invalid attribute value """ _layer: str = DEFAULT_LAYER _aci_color: int = DEFAULT_ACI_COLOR _true_color: Optional[colors.RGB] = None _linetype: str = DEFAULT_LINETYPE _lineweight: int = DEFAULT_LINEWEIGHT _transparency: Optional[float] = None _ltscale: float = DEFAULT_LTSCALE def __init__( self, *, layer: str = DEFAULT_LAYER, color: int = DEFAULT_ACI_COLOR, rgb: Optional[colors.RGB] = None, linetype: str = DEFAULT_LINETYPE, lineweight: int = DEFAULT_LINEWEIGHT, transparency: Optional[float] = None, ltscale: float = DEFAULT_LTSCALE, ): self.layer = layer self.color = color self.rgb = rgb self.linetype = linetype self.lineweight = lineweight self.transparency = transparency self.ltscale = ltscale def __str__(self) -> str: s = [] if self._layer != DEFAULT_LAYER: s.append(f"layer='{self._layer}'") if self._aci_color != DEFAULT_ACI_COLOR: s.append(f"color={self._aci_color}") if self._true_color is not None: s.append(f"rgb={self._true_color}") if self._linetype != DEFAULT_LINETYPE: s.append(f"linetype='{self._linetype}'") if self._lineweight != DEFAULT_LINEWEIGHT: s.append(f"lineweight={self._lineweight}") if self._transparency is not None: s.append(f"transparency={round(self._transparency, 3)}") if self._ltscale != DEFAULT_LTSCALE: s.append(f"ltscale={round(self._ltscale, 3)}") return ", ".join(s) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.__str__()})" def __iter__(self) -> Iterator[tuple[str, Any]]: """Returns iter(self).""" return iter(self.items()) def items(self, default_values=False) -> list[tuple[str, Any]]: """Returns the DXF attributes as list of name, value pairs, returns also the default values if argument `default_values` is ``True``. The true_color and transparency attributes do not have default values, the absence of these attributes is the default value. """ data: list[tuple[str, Any]] = [] if default_values or self._layer != DEFAULT_LAYER: data.append(("layer", self._layer)) if default_values or self._aci_color != DEFAULT_ACI_COLOR: data.append(("color", self._aci_color)) if self._true_color is not None: # absence is the default value data.append(("true_color", colors.rgb2int(self._true_color))) if default_values or self._linetype != DEFAULT_LINETYPE: data.append(("linetype", self._linetype)) if default_values or self._lineweight != DEFAULT_LINEWEIGHT: data.append(("lineweight", self.lineweight)) if self._transparency is not None: # absence is the default value if self._transparency == TRANSPARENCY_BYBLOCK: data.append(("transparency", colors.TRANSPARENCY_BYBLOCK)) else: data.append( ( "transparency", colors.float2transparency(self._transparency), ) ) if default_values or self._ltscale != DEFAULT_LTSCALE: data.append(("ltscale", self._ltscale)) return data def asdict(self, default_values=False) -> dict[str, Any]: """Returns the DXF attributes as :class:`dict`, returns also the default values if argument `default_values` is ``True``. The true_color and transparency attributes do not have default values, the absence of these attributes is the default value. """ return dict(self.items(default_values)) @property def layer(self) -> str: """layer name""" return self._layer @layer.setter def layer(self, name: str): if validator.is_valid_layer_name(name): self._layer = name else: raise const.DXFValueError(f"invalid layer name '{name}'") @property def color(self) -> int: """:ref:`ACI` color value""" return self._aci_color @color.setter def color(self, value: int): if validator.is_valid_aci_color(value): self._aci_color = value else: raise const.DXFValueError(f"invalid ACI color value '{value}'") @property def rgb(self) -> Optional[colors.RGB]: """true color value as (red, green, blue) tuple, ``None`` for not set""" return self._true_color @rgb.setter def rgb(self, value: Optional[colors.RGB]): if value is None: self._true_color = None elif validator.is_valid_rgb(value): self._true_color = value else: raise const.DXFValueError(f"invalid true color value '{value}'") @property def linetype(self) -> str: """linetype name""" return self._linetype @linetype.setter def linetype(self, name: str): if validator.is_valid_table_name(name): self._linetype = name else: raise const.DXFValueError(f"invalid linetype name '{name}'") @property def lineweight(self) -> int: """lineweight""" return self._lineweight @lineweight.setter def lineweight(self, value: int): if validator.is_valid_lineweight(value): self._lineweight = value else: raise const.DXFValueError(f"invalid lineweight value '{value}'") @property def transparency(self) -> Optional[float]: """transparency value from 0.0 for opaque to 1.0 is fully transparent, -1.0 is for transparency by block and ``None`` if for transparency by layer """ return self._transparency @transparency.setter def transparency(self, value: Optional[float]): if value is None: self._transparency = None elif value == TRANSPARENCY_BYBLOCK: self._transparency = TRANSPARENCY_BYBLOCK elif isinstance(value, float) and (0.0 <= value <= 1.0): self._transparency = value else: raise const.DXFValueError(f"invalid transparency value '{value}'") @property def ltscale(self) -> float: """linetype scaling factor""" return self._ltscale @ltscale.setter def ltscale(self, value: float): if isinstance(value, (float, int)) and (value > 1e-6): self._ltscale = float(value) else: raise const.DXFValueError(f"invalid linetype scale value '{value}'") @classmethod def load_from_header(cls, doc: Drawing) -> GfxAttribs: """Load default DXF attributes from the HEADER section. There is no default true color value and the default transparency is not stored in the HEADER section. Loads following header variables: - ``$CLAYER`` - current layer name - ``$CECOLOR`` - current ACI color - ``$CELTYPE`` - current linetype name - ``$CELWEIGHT`` - current lineweight - ``$CELTSCALE`` - current linetype scaling factor """ header = doc.header return cls( layer=header.get("$CLAYER", DEFAULT_LAYER), color=header.get("$CECOLOR", DEFAULT_ACI_COLOR), linetype=header.get("$CELTYPE", DEFAULT_LINETYPE), lineweight=header.get("$CELWEIGHT", DEFAULT_LINEWEIGHT), ltscale=header.get("$CELTSCALE", DEFAULT_LTSCALE), ) def write_to_header(self, doc: "Drawing") -> None: """Write DXF attributes as default values to the HEADER section. Writes following header variables: - ``$CLAYER`` - current layer name, if a layer table entry exist in `doc` - ``$CECOLOR`` - current ACI color - ``$CELTYPE`` - current linetype name, if a linetype table entry exist in `doc` - ``$CELWEIGHT`` - current lineweight - ``$CELTSCALE`` - current linetype scaling factor """ header = doc.header if doc.layers.has_entry(self.layer): header["$CLAYER"] = self.layer header["$CECOLOR"] = self.color if doc.linetypes.has_entry(self.linetype): header["$CELTYPE"] = self.linetype header["$CELWEIGHT"] = self.lineweight header["$CELTSCALE"] = self.ltscale @classmethod def from_entity(cls, entity: DXFEntity) -> GfxAttribs: """Get the graphical attributes of an `entity` as :class:`GfxAttribs` object. """ try: d = entity.graphic_properties() # type: ignore except AttributeError: return cls() return cls.from_dict(d) @classmethod def from_dict(cls, d: dict[str, Any]) -> GfxAttribs: """Construct :class:`GfxAttribs` from a dictionary of raw DXF values. Supported attributes are: - layer: layer name as string - color: :ref:`ACI` value as int - true_color: raw DXF integer value for RGB colors - rgb: RGB tuple of int or ``None`` - linetype: linetype name as string - lineweight: lineweight as int, see basic concept of :ref:`lineweights` - transparency: raw DXF integer value of transparency or a float in the range from 0.0 to 1.0 - ltscale: linetype scaling factor as float """ attribs = cls() for attrib_name in [ "layer", "color", "linetype", "lineweight", "ltscale", "rgb", ]: if attrib_name in d: setattr(attribs, attrib_name, d[attrib_name]) if "true_color" in d: attribs.rgb = colors.int2rgb(d["true_color"]) if "transparency" in d: transparency = d["transparency"] if isinstance(transparency, float): attribs.transparency = transparency elif transparency == colors.TRANSPARENCY_BYBLOCK: attribs.transparency = TRANSPARENCY_BYBLOCK else: attribs.transparency = colors.transparency2float(transparency) return attribs