| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | import pathlib |
| | import re |
| | import xml.etree.ElementTree as ET |
| | from typing import Mapping, Optional |
| | from functools import cached_property |
| | from ...assets import Asset, AssetUri, AssetSerializer, DummyAssetSerializer |
| | import Path.Tool.shape.util as util |
| | from PySide import QtCore, QtGui, QtSvg |
| |
|
| | _svg_ns = {"s": "http://www.w3.org/2000/svg"} |
| |
|
| |
|
| | class ToolBitShapeIcon(Asset): |
| | """Abstract base class for tool bit shape icons.""" |
| |
|
| | def __init__(self, id: str, data: bytes): |
| | """ |
| | Initialize the icon. |
| | |
| | Args: |
| | id (str): The unique identifier for the icon, including extension. |
| | data (bytes): The raw icon data (e.g., SVG or PNG bytes). |
| | """ |
| | self.id: str = id |
| | self.data: bytes = data |
| |
|
| | def get_id(self) -> str: |
| | """ |
| | Get the ID of the icon. |
| | |
| | Returns: |
| | str: The ID of the icon. |
| | """ |
| | return self.id |
| |
|
| | @classmethod |
| | def from_bytes( |
| | cls, |
| | data: bytes, |
| | id: str, |
| | dependencies: Optional[Mapping[AssetUri, Asset]], |
| | serializer: AssetSerializer, |
| | ) -> "ToolBitShapeIcon": |
| | """ |
| | Create a ToolBitShapeIcon instance from raw bytes. |
| | |
| | Args: |
| | data (bytes): The raw bytes of the icon file. |
| | id (str): The ID of the asset, including extension. |
| | dependencies (Optional[Mapping[AssetUri, Asset]]): A mapping of resolved dependencies (not used for icons). |
| | |
| | Returns: |
| | ToolBitShapeIcon: An instance of ToolBitShapeIcon. |
| | """ |
| | assert serializer == DummyAssetSerializer, "ToolBitShapeIcon supports only native import" |
| | return cls(id=id, data=data) |
| |
|
| | def to_bytes(self, serializer: AssetSerializer) -> bytes: |
| | """ |
| | Serializes a ToolBitShapeIcon object to bytes. |
| | """ |
| | assert serializer == DummyAssetSerializer, "ToolBitShapeIcon supports only native export" |
| | return self.data |
| |
|
| | @classmethod |
| | def from_file(cls, filepath: pathlib.Path, id: str) -> "ToolBitShapeIcon": |
| | """ |
| | Create a ToolBitShapeIcon instance from a file. |
| | |
| | Args: |
| | filepath (pathlib.Path): Path to the icon file (.svg or .png). |
| | shape_id_base (str): The base ID of the associated shape. |
| | |
| | Returns: |
| | ToolBitShapeIcon: An instance of ToolBitShapeIcon. |
| | |
| | Raises: |
| | FileNotFoundError: If the file does not exist. |
| | """ |
| | if not filepath.exists(): |
| | raise FileNotFoundError(f"Icon file not found: {filepath}") |
| |
|
| | data = filepath.read_bytes() |
| | if filepath.suffix.lower() == ".png": |
| | return ToolBitShapePngIcon(id, data) |
| | elif filepath.suffix.lower() == ".svg": |
| | return ToolBitShapeSvgIcon(id, data) |
| | else: |
| | raise NotImplementedError(f"unsupported icon file: {filepath}") |
| |
|
| | @classmethod |
| | def from_shape_data(cls, shape_data: bytes, id: str) -> Optional["ToolBitShapeIcon"]: |
| | """ |
| | Create a thumbnail icon from shape data bytes. |
| | |
| | Args: |
| | shape_data (bytes): The raw bytes of the shape file (.FCStd). |
| | shape_id_base (str): The base ID of the associated shape. |
| | |
| | Returns: |
| | Optional[ToolBitShapeIcon]: An instance of ToolBitShapeIcon (PNG), or None. |
| | """ |
| | image_bytes = util.create_thumbnail_from_data(shape_data) |
| | if not image_bytes: |
| | return None |
| |
|
| | |
| | return ToolBitShapePngIcon(id=id, data=image_bytes) |
| |
|
| | def get_size_in_bytes(self) -> int: |
| | """ |
| | Get the size of the icon data in bytes. |
| | """ |
| | return len(self.data) |
| |
|
| | @cached_property |
| | def abbreviations(self) -> Mapping[str, str]: |
| | """ |
| | Returns a cached mapping of parameter abbreviations from the icon data. |
| | """ |
| | return {} |
| |
|
| | def get_abbr(self, param_name: str) -> Optional[str]: |
| | """ |
| | Retrieves the abbreviation for a given parameter name. |
| | |
| | Args: |
| | param_name: The name of the parameter. |
| | |
| | Returns: |
| | The abbreviation string, or None if not found. |
| | """ |
| | normalized_param_name = param_name.lower().replace(" ", "_") |
| | return self.abbreviations.get(normalized_param_name) |
| |
|
| | def get_png(self, icon_size: Optional[QtCore.QSize] = None) -> bytes: |
| | """ |
| | Returns the icon data as PNG bytes. |
| | """ |
| | raise NotImplementedError |
| |
|
| | def get_qpixmap(self, icon_size: Optional[QtCore.QSize] = None) -> QtGui.QPixmap: |
| | """ |
| | Returns the icon data as a QPixmap. |
| | """ |
| | raise NotImplementedError |
| |
|
| |
|
| | class ToolBitShapeSvgIcon(ToolBitShapeIcon): |
| | asset_type: str = "toolbitshapesvg" |
| |
|
| | def get_png(self, icon_size: Optional[QtCore.QSize] = None) -> bytes: |
| | """ |
| | Converts SVG icon data to PNG and returns it using QtSvg. |
| | """ |
| | if icon_size is None: |
| | icon_size = QtCore.QSize(48, 48) |
| | image = QtGui.QImage(icon_size, QtGui.QImage.Format_ARGB32) |
| | image.fill(QtGui.Qt.transparent) |
| | painter = QtGui.QPainter(image) |
| |
|
| | buffer = QtCore.QBuffer(QtCore.QByteArray(self.data)) |
| | buffer.open(QtCore.QIODevice.ReadOnly) |
| | svg_renderer = QtSvg.QSvgRenderer(buffer) |
| | svg_renderer.setAspectRatioMode(QtCore.Qt.KeepAspectRatio) |
| | svg_renderer.render(painter) |
| | painter.end() |
| |
|
| | byte_array = QtCore.QByteArray() |
| | buffer = QtCore.QBuffer(byte_array) |
| | buffer.open(QtCore.QIODevice.WriteOnly) |
| | image.save(buffer, "PNG") |
| |
|
| | return bytes(byte_array) |
| |
|
| | def get_qpixmap(self, icon_size: Optional[QtCore.QSize] = None) -> QtGui.QPixmap: |
| | """ |
| | Returns the SVG icon data as a QPixmap using QtSvg. |
| | """ |
| | if icon_size is None: |
| | icon_size = QtCore.QSize(48, 48) |
| | icon_ba = QtCore.QByteArray(self.data) |
| | image = QtGui.QImage(icon_size, QtGui.QImage.Format_ARGB32) |
| | image.fill(QtGui.Qt.transparent) |
| | painter = QtGui.QPainter(image) |
| |
|
| | buffer = QtCore.QBuffer(icon_ba) |
| | buffer.open(QtCore.QIODevice.ReadOnly) |
| | data = QtCore.QXmlStreamReader(buffer) |
| | renderer = QtSvg.QSvgRenderer(data) |
| | renderer.setAspectRatioMode(QtCore.Qt.KeepAspectRatio) |
| | renderer.render(painter) |
| | painter.end() |
| |
|
| | return QtGui.QPixmap.fromImage(image) |
| |
|
| | @cached_property |
| | def abbreviations(self) -> Mapping[str, str]: |
| | """ |
| | Returns a cached mapping of parameter abbreviations from the icon data. |
| | |
| | Only applicable for SVG icons. |
| | """ |
| | if self.data: |
| | return self.get_abbreviations_from_svg(self.data) |
| | return {} |
| |
|
| | def get_abbr(self, param_name: str) -> Optional[str]: |
| | """ |
| | Retrieves the abbreviation for a given parameter name. |
| | |
| | Args: |
| | param_name: The name of the parameter. |
| | |
| | Returns: |
| | The abbreviation string, or None if not found. |
| | """ |
| | return self.abbreviations.get(param_name) |
| |
|
| | @staticmethod |
| | def get_abbreviations_from_svg(svg: bytes) -> Mapping[str, str]: |
| | """ |
| | Extract abbreviations from SVG text elements. |
| | """ |
| | try: |
| | tree = ET.fromstring(svg) |
| | except ET.ParseError: |
| | return {} |
| |
|
| | result = {} |
| | for text_elem in tree.findall(".//s:text", _svg_ns): |
| | id = text_elem.attrib.get("id", _svg_ns) |
| | if id is None or not isinstance(id, str): |
| | continue |
| |
|
| | |
| | |
| | |
| | def _upper(match): |
| | return match.group(1).upper() |
| |
|
| | id = re.sub(r"_(\w)", _upper, id.capitalize()) |
| |
|
| | abbr = text_elem.text |
| | if abbr is not None: |
| | result[id] = abbr |
| |
|
| | span_elem = text_elem.find(".//s:tspan", _svg_ns) |
| | if span_elem is None: |
| | continue |
| | abbr = span_elem.text |
| | result[id] = abbr |
| |
|
| | return result |
| |
|
| |
|
| | class ToolBitShapePngIcon(ToolBitShapeIcon): |
| | asset_type: str = "toolbitshapepng" |
| |
|
| | def get_png(self, icon_size: Optional[QtCore.QSize] = None) -> bytes: |
| | """ |
| | Returns the PNG icon data. |
| | """ |
| | |
| | |
| | return self.data |
| |
|
| | def get_qpixmap(self, icon_size: Optional[QtCore.QSize] = None) -> QtGui.QPixmap: |
| | """ |
| | Returns the PNG icon data as a QPixmap. |
| | """ |
| | if icon_size is None: |
| | icon_size = QtCore.QSize(48, 48) |
| | pixmap = QtGui.QPixmap() |
| | pixmap.loadFromData(self.data, "PNG") |
| | |
| | if pixmap.size() != icon_size: |
| | pixmap = pixmap.scaled( |
| | icon_size, |
| | QtCore.Qt.KeepAspectRatio, |
| | QtCore.Qt.SmoothTransformation, |
| | ) |
| | return pixmap |
| |
|