From 43e3bd39ef69c97947e987b81312a21607fae2c8 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Sat, 5 Sep 2026 22:56:14 +0800 Subject: [PATCH] feat: refactor assemblicon (1/3) sink and source establishment --- tool/src/aurora_iconset_builder/__init__.py | 2 +- .../assemblicon/highlevel/address.py | 37 ++ .../assemblicon/highlevel/environment.py | 369 ++++-------------- .../assemblicon/highlevel/platform.py | 35 ++ .../assemblicon/highlevel/sink/__init__.py | 4 +- .../assemblicon/highlevel/sink/common.py | 15 +- .../assemblicon/highlevel/sink/fdart_sink.py | 42 ++ .../assemblicon/highlevel/sink/macart_sink.py | 38 ++ .../highlevel/sink/tempart_sink.py | 32 ++ .../highlevel/sink/tempdir_sink.py | 24 -- .../assemblicon/highlevel/sink/winart_sink.py | 38 ++ .../assemblicon/highlevel/source/__init__.py | 4 +- .../highlevel/source/blender_source.py | 58 +++ .../assemblicon/highlevel/source/common.py | 21 +- .../highlevel/source/fdoutart_source.py | 35 ++ .../highlevel/source/inart_source.py | 24 ++ .../highlevel/source/macoutart_source.py | 34 ++ .../highlevel/source/newart_source.py | 16 + .../highlevel/source/svg_source.py | 52 +++ .../highlevel/source/tempart_source.py | 24 ++ .../highlevel/source/tempdir_source.py | 24 -- .../highlevel/source/winoutart_source.py | 34 ++ .../assemblicon/highlevel/utils.py | 2 +- .../assemblicon/lowlevel/artio.py | 110 +++++- .../assemblicon/lowlevel/environment.py | 68 ++++ .../assemblicon/lowlevel/utils.py | 2 - .../nostd_icons/mimetype_icons.py | 2 +- .../std_icons/components.py | 2 +- .../std_icons/status_icons.py | 6 +- 29 files changed, 777 insertions(+), 377 deletions(-) create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/address.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/platform.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/fdart_sink.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/macart_sink.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/tempart_sink.py delete mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/tempdir_sink.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/winart_sink.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/source/blender_source.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/source/fdoutart_source.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/source/inart_source.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/source/macoutart_source.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/source/newart_source.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/source/svg_source.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/source/tempart_source.py delete mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/source/tempdir_source.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/source/winoutart_source.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/lowlevel/environment.py diff --git a/tool/src/aurora_iconset_builder/__init__.py b/tool/src/aurora_iconset_builder/__init__.py index b0b8925..b727131 100644 --- a/tool/src/aurora_iconset_builder/__init__.py +++ b/tool/src/aurora_iconset_builder/__init__.py @@ -20,7 +20,7 @@ def main() -> None: # TODO: use explicit temporary directory initializer in prologue to do this. # create our special temporary subdirectory - env.temporary_artifact("component").mkdir(parents=True, exist_ok=True) + env.__temporary_artifact("component").mkdir(parents=True, exist_ok=True) try: register(ctx) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/address.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/address.py new file mode 100644 index 0000000..9b4eb08 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/address.py @@ -0,0 +1,37 @@ +import enum + + +class AddressDomain(enum.IntEnum): + Input = enum.auto() + Temporary = enum.auto() + Output = enum.auto() + + FdOutput = enum.auto() + WinOutput = enum.auto() + MacOutput = enum.auto() + + +class Address: + """The unique identity of an artifact slot (source/sink).""" + + __domain: AddressDomain + __subdirectories: tuple[str, ...] + + def __init__(self, domain: AddressDomain, *args: str) -> None: + self.__domain = domain + self.__subdirectories = tuple(args) + + def __hash__(self) -> int: + return hash((self.__domain.value,) + self.__subdirectories) + + def __eq__(self, other: object) -> bool: + if self is other: + return True + + if not isinstance(other, Address): + return NotImplemented + + return ( + self.__domain == other.__domain + and self.__subdirectories == other.__subdirectories + ) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py index def1394..c3ace1b 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py @@ -1,63 +1,25 @@ import tempfile import os -import enum from typing import Optional from types import TracebackType from dataclasses import dataclass from pathlib import Path -from typing import TypeVar, Generic -from .utils import Artifact, ArtifactFile, VART_HW, check_vart -from ..logger import HIGHLEVEL_LOGGER as LOGGER -from ..lowlevel.artrdr import SvgRenderKind, SvgConfig, BlenderConfig -from ..lowlevel import artio, artrdr +from typing import TypeVar, Generic, Callable +from ..lowlevel.environment import Prologue as LowPrologue, Environment as LowEnv +from ..lowlevel.artrdr import SvgRenderKind +from .platform import FdContext, WinCategory, MacCategory, FD_THUMBNAIL_HWS @dataclass(frozen=True) -class Prologue: - input_directory: Path - output_directory: Path +class Prologue(LowPrologue): svg_render: SvgRenderKind - - def __post_init__(self) -> None: - if not self.input_directory.is_dir(): - raise ValueError( - f"Given input path {self.input_directory} is not an existing directory." - ) - if not self.output_directory.is_dir(): - raise ValueError( - f"Given output path {self.output_directory} is not an existing directory." - ) - - -P = TypeVar("P", bound=Prologue) - - -class FdContext(enum.StrEnum): - Actions = "actions" - Animations = "animations" - Applications = "apps" - Categories = "categories" - Devices = "devices" - Emblems = "emblems" - Emotes = "emotes" - International = "intl" - MimeTypes = "mimetypes" - Places = "places" - Status = "status" - - -class WinCategory(enum.StrEnum): - Application = "apps" - """Icons for applications""" - Extension = "exts" - """Icons for file extensions""" - - -class MacCategory(enum.StrEnum): - Application = "apps" - """Icons for applications""" - Extension = "exts" - """Icons for file extensions""" + """The render kind for SVG""" + temp_dir_initializer: Callable[[Path], None] | None + """ + The extra initializer for temporary directory. + The only argument in function is the path to temporary directory. + ``None`` for no extra initializer. + """ _TEMP_USER_DIR: str = "user" @@ -68,46 +30,51 @@ _TEMP_LIB_STABLE_DIR: str = "stable" _ART_WIN: str = "windows" _ART_MAC: str = "macos" -_DEFAULT_SVG_CONFIG = artrdr.SvgConfig(VART_HW, VART_HW) -_DEFAULT_BLD_CONFIG = artrdr.BlenderConfig(VART_HW, VART_HW) - -# TODO: For easy testing, we only build 32x32 and 48x48. -# Enable full generation after testing. -_FD_THUMBNAIL_HWS: tuple[int, ...] = (32, 48) -# _FD_THUMBNAIL_HWS: tuple[int, ...] = (16, 32, 48, 64, 128, 256) +P = TypeVar("P", bound=Prologue) class Environment(Generic[P]): __prologue: P """The prologue used for environment.""" - __temp_dir: tempfile.TemporaryDirectory[str] + __lowenv: LowEnv + """The lowlevel environment managing temporary directory.""" def __init__(self, prologue: P) -> None: self.__prologue = prologue - self.__temp_dir = tempfile.TemporaryDirectory() + self.__lowenv = LowEnv(self.__prologue) # construct basic layout of temporary directory - self.temporary_artifact(_TEMP_USER_DIR).mkdir(parents=True, exist_ok=True) - self.temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_ALLOC_DIR).mkdir( + self.__temporary_artifact(_TEMP_USER_DIR).mkdir(parents=True, exist_ok=True) + self.__temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_ALLOC_DIR).mkdir( parents=True, exist_ok=True ) - self.temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_STABLE_DIR).mkdir( + self.__temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_STABLE_DIR).mkdir( parents=True, exist_ok=True ) # and output directory - for hw in _FD_THUMBNAIL_HWS: + for hw in FD_THUMBNAIL_HWS: for kind in FdContext: - self.output_artifact(f"{hw}x{hw}", str(kind)).mkdir( + self.__output_artifact(f"{hw}x{hw}", str(kind)).mkdir( parents=True, exist_ok=True ) for kind in WinCategory: - self.output_artifact(_ART_WIN, str(kind)).mkdir(parents=True, exist_ok=True) + self.__output_artifact(_ART_WIN, str(kind)).mkdir( + parents=True, exist_ok=True + ) for kind in MacCategory: - self.output_artifact(_ART_MAC, str(kind)).mkdir(parents=True, exist_ok=True) + self.__output_artifact(_ART_MAC, str(kind)).mkdir( + parents=True, exist_ok=True + ) + + # construct user custom temporary layout of possible + if self.__prologue.temp_dir_initializer is not None: + temp_dir = self.__temporary_artifact() + self.__prologue.temp_dir_initializer(temp_dir) # region: With Visitor Requirements def __enter__(self) -> "Environment": + self.__lowenv.__enter__() return self def __exit__( @@ -116,25 +83,19 @@ class Environment(Generic[P]): exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: - self.__temp_dir.cleanup() + self.__lowenv.__exit__(exc_type, exc_value, traceback) # endregion # region: Prologue Visitor @property - def prologue(self) -> P: + def prologue(self) -> Prologue: return self.__prologue - # endregion - - # region: New Artifact - - def new_artifact(self) -> Artifact: - """ - Create new RGBA vanilla size (1024x1024) empty (#ffffff00) artifact. - """ - return artio.new_artifact((VART_HW, VART_HW)) + @property + def user_prologue(self) -> P: + return self.__prologue # endregion @@ -144,48 +105,20 @@ class Environment(Generic[P]): """ Get the path to input artifact. """ - return self.__prologue.input_directory / Path(*args) - - def load_input_bitmap_artifact(self, *args: str) -> ArtifactFile: - """ - Load given input bitmap asset. - """ - p = self.input_artifact(*args) - LOGGER.info("Loading input artifact: %s", p) - return artio.load_input_artifact(p) + return self.__lowenv.input_artifact(*args) # endregion # region: Temporary Artifact - def __user_temporary_artifact(self, *args: str) -> Path: - return self.temporary_artifact(_TEMP_USER_DIR, *args) + def __temporary_artifact(self, *args: str) -> Path: + return self.__lowenv.temporary_artifact(*args) + + def user_temporary_artifact(self, *args: str) -> Path: + return self.__temporary_artifact(_TEMP_USER_DIR, *args) def __lib_temporary_artifact(self, *args: str) -> Path: - return self.temporary_artifact(_TEMP_LIB_DIR, *args) - - def temporary_artifact(self, *args: str) -> Path: - """ - Get the path to temporary artifact. - - All existence of sub-directory should be ensured on your own. - """ - return Path(self.__temp_dir.name) / Path(*args) - - def load_temporary_artifact(self, *args: str) -> ArtifactFile: - p = self.temporary_artifact(*args) - LOGGER.info("Loading temporary artifact: %s", p) - return artio.load_artifact(p) - - def save_temporary_artifact(self, art: Artifact, *args: str) -> None: - """ - Save given artifact as temporary artifact. - - All existence of sub-directory should be ensured on your own. - """ - p = self.temporary_artifact(*args) - LOGGER.info("Saving temporary artifact: %s", p) - artio.save_artifact(art, p) + return self.__temporary_artifact(_TEMP_LIB_DIR, *args) def __allocate_lib_temporary_artifact(self, suffix: Optional[str] = None) -> Path: p = self.__lib_temporary_artifact(_TEMP_LIB_ALLOC_DIR) @@ -196,209 +129,65 @@ class Environment(Generic[P]): def __fetch_const_lib_temporary_artifact(self, name: str) -> Path: return self.__lib_temporary_artifact(_TEMP_LIB_STABLE_DIR, name) - # endregion - - # region: Render Wrapper - - def __intern_render_svg( - self, src: Path, dst: Optional[Path], cfg: SvgConfig - ) -> Path: + def allocate_lib_render_temporary_artifact(self) -> Path: """ - Internal used function for rendering SVG. + Allocated a path pointing to temporary render result. - :return: The path to destination storing render result. + :return: The path to temporary PNG file. + Please note that this file is already presneted in file system, + so caller need to overwrite it. """ - if dst is None: - LOGGER.info("Rendering temporary SVG: %s", src) - dst = self.__allocate_lib_temporary_artifact(".png") - else: - LOGGER.info("Rendering SVG: %s -> %s", src, dst) - artrdr.render_svg(src, dst, self.__prologue.svg_render, cfg) - return dst + return self.__allocate_lib_temporary_artifact(".png") - def render_svg( - self, src: Path, dst: Path, cfg: SvgConfig = _DEFAULT_SVG_CONFIG - ) -> None: + def fetch_lib_blender_script_temporary_artifact(self) -> Path: """ - Just render given SVG to PNG file. + Allocated a path pointing to temporary script instructing Blender render. - :param src: The path to SVG file to be rendered. - :param dst: The path to rendered PNG file. - :param cfg: The configuration when rendering SVG file. Default for vanilla artifact. + :return: The path to temporary Python file. + Please note that this file may be presneted in file system, + so caller may need to overwrite it. + All Blender render process share the same script file (this). """ - self.__intern_render_svg(src, dst, cfg) - - def render_svg_then_load( - self, - src: Path, - dst: Optional[Path] = None, - cfg: SvgConfig = _DEFAULT_SVG_CONFIG, - ) -> Artifact: - """ - Render given SVG to PNG file, then load it directly. - - :param src: The path to SVG file to be rendered. - :param dst: The path to rendered PNG file. - ``None`` if you don't need to store it in temporary or output directory. - :param cfg: The configuration when rendering SVG file. Default for vanilla artifact. - :return: The loaded built PNG file. - """ - dst = self.__intern_render_svg(src, dst, cfg) - return artio.load_artifact(dst) - - def __intern_render_blender( - self, src: Path, dst: Optional[Path], cfg: BlenderConfig - ) -> Path: - """ - Internal used function for rendering Blender composition. - - :return: The path to destination storing render result. - """ - temp_script = self.__fetch_const_lib_temporary_artifact("blender.py") - if dst is None: - LOGGER.info("Rendering temporary Blender composition: %s", src) - dst = self.__allocate_lib_temporary_artifact(".png") - else: - LOGGER.info("Rendering Blender composition: %s -> %s", src, dst) - artrdr.render_blender(src, dst, temp_script, cfg) - return dst - - def render_blender( - self, src: Path, dst: Path, cfg: BlenderConfig = _DEFAULT_BLD_CONFIG - ) -> None: - """ - Just render given Blender composition to PNG file. - - :param src: The path to Blender composition file to be rendered. - :param dst: The path to rendered PNG file. - :param cfg: The configuration when rendering Blender composition file. Default for vanilla artifact. - """ - self.__intern_render_blender(src, dst, cfg) - - def render_blender_then_load( - self, - src: Path, - dst: Optional[Path] = None, - cfg: BlenderConfig = _DEFAULT_BLD_CONFIG, - ) -> Artifact: - """ - Render given Blender composition to PNG file, then load it directly. - - :param src: The path to Blender composition file to be rendered. - :param dst: The path to rendered PNG file. - ``None`` if you don't need to store it in temporary or output directory. - :param cfg: The configuration when rendering Blender composition file. Default for vanilla artifact. - :return: The loaded built PNG file. - """ - dst = self.__intern_render_blender(src, dst, cfg) - return artio.load_artifact(dst) + # TODO: Strip this "share" behavior to enable multi-threads build ability. + return self.__fetch_const_lib_temporary_artifact("blender.py") # endregion # region: Output Artifact - def output_artifact(self, *args: str) -> Path: - return self.__prologue.output_directory / Path(*args) - - # endregion - - # region: Advanced Saver - - def __fd_artifact(self, hw: int, kind: FdContext, name: str) -> Path: - return self.output_artifact(f"{hw}x{hw}", str(kind), f"{name}.png") - - def save_fd_artifact(self, art: Artifact, kind: FdContext, name: str) -> None: + def __output_artifact(self, *args: str) -> Path: """ - Save icon following FreeDesktop icon theme specification. + Get the path to output artifact. + """ + return self.__lowenv.output_artifact(*args) + def fd_output_artifact(self, hw: int, kind: FdContext, name: str) -> Path: + """ + Get the path for saving FreeDesktop icon with given image size, FreeDesktop icon and name. + + :param hw: The height or width of this FreeDesktop icon. + Required because FreeDesktop need to save icon with different size variant. + :param kind: The FreeDesktop kind of this icon. :param name: The name of saved artifact. The file extension ".png" is not required. """ - LOGGER.info( - 'Saving FreeDesktop artifact with kind "%s" and name "%s".', kind, name - ) + return self.__output_artifact(f"{hw}x{hw}", str(kind), f"{name}.png") - check_vart(art) - - for hw, thumbnail in zip( - _FD_THUMBNAIL_HWS, artrdr.render_thumbnail(art, _FD_THUMBNAIL_HWS) - ): - dst = self.__fd_artifact(hw, kind, name) - artio.save_artifact(thumbnail, dst) - - def dup_fd_artifact( - self, src_kind: FdContext, src_name: str, dst_kind: FdContext, dst_name: str - ) -> None: - LOGGER.info( - 'Duplicating FreeDesktop artifact from kind "%s" name "%s" to kind "%s" name "%s".', - src_kind, - src_name, - dst_kind, - dst_name, - ) - for hw in _FD_THUMBNAIL_HWS: - artio.dup_artifact( - self.__fd_artifact(hw, src_kind, src_name), - self.__fd_artifact(hw, dst_kind, dst_name), - ) - - def __win_artifact(self, kind: WinCategory, name: str) -> Path: - return self.output_artifact(_ART_WIN, str(kind), f"{name}.ico") - - def save_win_artifact(self, art: Artifact, kind: WinCategory, name: str) -> None: + def win_output_artifact(self, kind: WinCategory, name: str) -> Path: """ - Save icon for Windows-only + Get the path for saving Windows-only icon. + :param kind: The kind of this icon. :param name: The name of saved artifact. The file extension ".ico" is not required. """ - LOGGER.info( - 'Saving Windows-only artifact with kind "%s" and name "%s".', kind, name - ) - check_vart(art) - artrdr.render_ico(art, self.__win_artifact(kind, name)) + return self.__output_artifact(_ART_WIN, str(kind), f"{name}.ico") - def dup_win_artifact( - self, src_kind: WinCategory, src_name: str, dst_kind: WinCategory, dst_name: str - ) -> None: - LOGGER.info( - 'Duplicating Windows-only artifact from kind "%s" name "%s" to kind "%s" name "%s".', - src_kind, - src_name, - dst_kind, - dst_name, - ) - artio.dup_artifact( - self.__win_artifact(src_kind, src_name), - self.__win_artifact(dst_kind, dst_name), - ) - - def __mac_artifact(self, kind: MacCategory, name: str) -> Path: - return self.output_artifact(_ART_MAC, str(kind), f"{name}.icns") - - def save_mac_artifact(self, art: Artifact, kind: MacCategory, name: str) -> None: + def mac_output_artifact(self, kind: MacCategory, name: str) -> Path: """ - Save icon for macOS-only + Get the path for saving macOS-only icon. + :param kind: The kind of this icon. :param name: The name of saved artifact. The file extension ".icns" is not required. """ - LOGGER.info( - 'Saving macOS-only artifact with kind "%s" and name "%s".', kind, name - ) - check_vart(art) - artrdr.render_icns(art, self.__mac_artifact(kind, name)) - - def dup_mac_artifact( - self, src_kind: MacCategory, src_name: str, dst_kind: MacCategory, dst_name: str - ) -> None: - LOGGER.info( - 'Duplicating macOS-only artifact from kind "%s" name "%s" to kind "%s" name "%s".', - src_kind, - src_name, - dst_kind, - dst_name, - ) - artio.dup_artifact( - self.__mac_artifact(src_kind, src_name), - self.__mac_artifact(dst_kind, dst_name), - ) + return self.__output_artifact(_ART_MAC, str(kind), f"{name}.icns") # endregion diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/platform.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/platform.py new file mode 100644 index 0000000..8f7d0b6 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/platform.py @@ -0,0 +1,35 @@ +import enum + + +class FdContext(enum.StrEnum): + Actions = "actions" + Animations = "animations" + Applications = "apps" + Categories = "categories" + Devices = "devices" + Emblems = "emblems" + Emotes = "emotes" + International = "intl" + MimeTypes = "mimetypes" + Places = "places" + Status = "status" + + +class WinCategory(enum.StrEnum): + Application = "apps" + """Icons for applications""" + Extension = "exts" + """Icons for file extensions""" + + +class MacCategory(enum.StrEnum): + Application = "apps" + """Icons for applications""" + Extension = "exts" + """Icons for file extensions""" + + +# TODO: For easy testing, we only build 32x32 and 48x48. +# Enable full generation after testing. +FD_THUMBNAIL_HWS: tuple[int, ...] = (32, 48) +# FD_THUMBNAIL_HWS: tuple[int, ...] = (16, 32, 48, 64, 128, 256) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/__init__.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/__init__.py index a5507eb..0042f45 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/__init__.py +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/__init__.py @@ -1,7 +1,7 @@ from .common import Sink -from .tempdir_sink import TempDirSink +from .tempart_sink import TempArtSink __all__ = [ "Sink", - "TempDirSink" + "TempArtSink" ] diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/common.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/common.py index c812b1a..e17a40e 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/common.py +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/common.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from ..utils import Artifact +from ..address import Address class Sink(ABC): @@ -7,15 +7,6 @@ class Sink(ABC): Abstract base class for all sinks. """ + @property @abstractmethod - def push_artifact(self, art: Artifact) -> None: - """Push given artifact into this sink.""" - pass - - @abstractmethod - def __hash__(self) -> int: - pass - - @abstractmethod - def __eq__(self, other) -> bool: - pass + def address(self) -> Address: ... diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/fdart_sink.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/fdart_sink.py new file mode 100644 index 0000000..5572c8f --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/fdart_sink.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass +from .common import Sink +from ..address import Address, AddressDomain +from ..environment import Environment +from ..platform import FdContext, FD_THUMBNAIL_HWS +from ..utils import Artifact +from ...lowlevel import artio, artrdr +from ...logger import HIGHLEVEL_LOGGER as LOGGER + + +@dataclass(frozen=True) +class FdArtSinkHint: + kind: FdContext + name: str + + +class FdArtSink(Sink): + __kind: FdContext + __name: str + + def __init__(self, kind: FdContext, name: str) -> None: + self.__kind = kind + self.__name = name + + @property + def address(self) -> Address: + return Address(AddressDomain.FdOutput, str(self.__kind), self.__name) + + def push(self, env: Environment, art: Artifact) -> None: + LOGGER.info( + 'Saving FreeDesktop artifact with kind "%s" and name "%s".', + self.__kind, + self.__name, + ) + for hw, thumbnail in zip( + FD_THUMBNAIL_HWS, artrdr.render_thumbnail(art, FD_THUMBNAIL_HWS) + ): + dst = env.fd_output_artifact(hw, self.__kind, self.__name) + artio.save_artifact(thumbnail, dst) + + def push_hint(self) -> FdArtSinkHint: + return FdArtSinkHint(self.__kind, self.__name) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/macart_sink.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/macart_sink.py new file mode 100644 index 0000000..b9ee814 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/macart_sink.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass +from .common import Sink +from ..address import Address, AddressDomain +from ..environment import Environment +from ..platform import MacCategory +from ..utils import Artifact +from ...lowlevel import artrdr +from ...logger import HIGHLEVEL_LOGGER as LOGGER + + +@dataclass(frozen=True) +class MacArtSinkHint: + kind: MacCategory + name: str + + +class MacArtSink(Sink): + __kind: MacCategory + __name: str + + def __init__(self, kind: MacCategory, name: str) -> None: + self.__kind = kind + self.__name = name + + @property + def address(self) -> Address: + return Address(AddressDomain.MacOutput, str(self.__kind), self.__name) + + def push(self, env: Environment, art: Artifact) -> None: + LOGGER.info( + 'Saving macOS-only artifact with kind "%s" and name "%s".', + self.__kind, + self.__name, + ) + artrdr.render_ico(art, env.mac_output_artifact(self.__kind, self.__name)) + + def push_hint(self) -> MacArtSinkHint: + return MacArtSinkHint(self.__kind, self.__name) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/tempart_sink.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/tempart_sink.py new file mode 100644 index 0000000..24953e3 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/tempart_sink.py @@ -0,0 +1,32 @@ +from pathlib import Path +from .common import Sink +from ..address import Address, AddressDomain +from ..environment import Environment +from ..utils import Artifact +from ...lowlevel import artio +from ...logger import HIGHLEVEL_LOGGER as LOGGER + + +class TempArtSink(Sink): + """ + The sink to artifact (PNG-only) located in temporary directory. + + All existence of sub-directory should be ensured on your own. + """ + + __comps: tuple[str, ...] + + def __init__(self, *args: str) -> None: + self.__comps = tuple(args) + + @property + def address(self) -> Address: + return Address(AddressDomain.Temporary, *self.__comps) + + def push(self, env: Environment, art: Artifact) -> None: + p = self.push_hint(env) + LOGGER.info("Saving temporary artifact: %s", p) + artio.save_artifact(art, p) + + def push_hint(self, env: Environment) -> Path: + return env.user_temporary_artifact(*self.__comps) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/tempdir_sink.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/tempdir_sink.py deleted file mode 100644 index ebccdcd..0000000 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/tempdir_sink.py +++ /dev/null @@ -1,24 +0,0 @@ -from ..utils import Artifact -from .common import Sink - - -class TempDirSink(Sink): - __path_comps: tuple[str, ...] - - def __init__(self, *args: str) -> None: - self.__path_comps = tuple(args) - - def push_artifact(self, art: Artifact) -> None: - pass - - def __hash__(self) -> int: - return hash(self.__path_comps) - - def __eq__(self, other) -> bool: - if self is other: - return True - - if not isinstance(other, TempDirSink): - return NotImplemented - - return self.__path_comps == other.__path_comps diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/winart_sink.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/winart_sink.py new file mode 100644 index 0000000..2c93292 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/sink/winart_sink.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass +from .common import Sink +from ..address import Address, AddressDomain +from ..environment import Environment +from ..platform import WinCategory +from ..utils import Artifact +from ...lowlevel import artrdr +from ...logger import HIGHLEVEL_LOGGER as LOGGER + + +@dataclass(frozen=True) +class WinArtSinkHint: + kind: WinCategory + name: str + + +class WinArtSink(Sink): + __kind: WinCategory + __name: str + + def __init__(self, kind: WinCategory, name: str) -> None: + self.__kind = kind + self.__name = name + + @property + def address(self) -> Address: + return Address(AddressDomain.WinOutput, str(self.__kind), self.__name) + + def push(self, env: Environment, art: Artifact) -> None: + LOGGER.info( + 'Saving Windows-only artifact with kind "%s" and name "%s".', + self.__kind, + self.__name, + ) + artrdr.render_ico(art, env.win_output_artifact(self.__kind, self.__name)) + + def push_hint(self) -> WinArtSinkHint: + return WinArtSinkHint(self.__kind, self.__name) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/__init__.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/__init__.py index 9108bdf..20d6193 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/__init__.py +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/__init__.py @@ -1,7 +1,7 @@ from .common import Source -from .tempdir_source import TempDirSource +from .tempart_source import TempArtSource __all__ = [ "Source", - "TempDirSource" + "TempArtSource" ] diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/blender_source.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/blender_source.py new file mode 100644 index 0000000..8efa972 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/blender_source.py @@ -0,0 +1,58 @@ +from pathlib import Path +from typing import Optional +from .common import Source +from ..address import Address +from ..environment import Environment +from ..sink.tempart_sink import TempArtSink +from ..utils import Artifact, VART_HW +from ...lowlevel import artio, artrdr +from ...lowlevel.artrdr import BlenderConfig +from ...logger import HIGHLEVEL_LOGGER as LOGGER + + +DEFAULT_BLD_CONFIG = artrdr.BlenderConfig(VART_HW, VART_HW) + + +class BlenderSource(Source): + """The source to Blender composition located in input directory.""" + + __comps: tuple[str, ...] + + def __init__(self, *args: str) -> None: + self.__comps = tuple(args) + + @property + def address(self) -> Address | None: + return None + + def pull( + self, env: Environment, cfg: BlenderConfig = DEFAULT_BLD_CONFIG + ) -> Artifact: + dst = self.__render(env, None, cfg) + return artio.load_artifact(dst) + + def pull_to_temp( + self, + env: Environment, + temp_sink: TempArtSink, + cfg: BlenderConfig = DEFAULT_BLD_CONFIG, + ) -> None: + self.__render(env, temp_sink.push_hint(env), cfg) + + def __render( + self, env: Environment, dst: Optional[Path], cfg: BlenderConfig + ) -> Path: + """ + Internal used function for rendering Blender composition. + + :return: The path to destination storing render result. + """ + src = env.input_artifact(*self.__comps) + temp_script = env.fetch_lib_blender_script_temporary_artifact() + if dst is None: + LOGGER.info("Rendering temporary Blender composition: %s", src) + dst = env.allocate_lib_render_temporary_artifact() + else: + LOGGER.info("Rendering Blender composition: %s -> %s", src, dst) + artrdr.render_blender(src, dst, temp_script, cfg) + return dst diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/common.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/common.py index 8b78b07..01eeb39 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/common.py +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/common.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from ..utils import Artifact +from ..address import Address class Source(ABC): @@ -7,15 +7,14 @@ class Source(ABC): Abstract base class for all sources. """ + @property @abstractmethod - def pull_artifact(self) -> Artifact: - """Pull artifact from this source.""" - pass + def address(self) -> Address | None: + """ + Get the address representation of this source. - @abstractmethod - def __hash__(self) -> int: - pass - - @abstractmethod - def __eq__(self, other) -> bool: - pass + :return: The address representing this source. + ``None`` if there is no producer for this source. + It means that this source do not depend anything. + """ + ... diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/fdoutart_source.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/fdoutart_source.py new file mode 100644 index 0000000..3465de6 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/fdoutart_source.py @@ -0,0 +1,35 @@ +from .common import Source +from ..address import Address, AddressDomain +from ..environment import Environment +from ..sink.fdart_sink import FdArtSink +from ..platform import FdContext, FD_THUMBNAIL_HWS +from ...lowlevel import artio +from ...logger import HIGHLEVEL_LOGGER as LOGGER + + +class FdOutArtSource(Source): + __kind: FdContext + __name: str + + def __init__(self, kind: FdContext, name: str) -> None: + self.__kind = kind + self.__name = name + + @property + def address(self) -> Address: + return Address(AddressDomain.FdOutput, str(self.__kind), self.__name) + + def pull_to_fd(self, env: Environment, fd_sink: FdArtSink) -> None: + sink_hint = fd_sink.push_hint() + LOGGER.info( + 'Duplicating FreeDesktop artifact from kind "%s" name "%s" to kind "%s" name "%s".', + sink_hint.kind, + sink_hint.name, + self.__kind, + self.__name, + ) + for hw in FD_THUMBNAIL_HWS: + artio.link_artifact( + env.fd_output_artifact(hw, sink_hint.kind, sink_hint.name), + env.fd_output_artifact(hw, self.__kind, self.__name), + ) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/inart_source.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/inart_source.py new file mode 100644 index 0000000..95663a1 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/inart_source.py @@ -0,0 +1,24 @@ +from .common import Source +from ..address import Address +from ..environment import Environment +from ..utils import Artifact +from ...lowlevel import artio +from ...logger import HIGHLEVEL_LOGGER as LOGGER + + +class InArtSource(Source): + """The source to bimap artifact (PNG, JPEG or BMP) located in input directory.""" + + __comps: tuple[str, ...] + + def __init__(self, *args: str) -> None: + self.__comps = tuple(args) + + @property + def address(self) -> Address | None: + return None + + def pull(self, env: Environment) -> Artifact: + p = env.input_artifact(*self.__comps) + LOGGER.info("Loading input artifact: %s", p) + return artio.load_input_artifact(p) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/macoutart_source.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/macoutart_source.py new file mode 100644 index 0000000..90d6791 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/macoutart_source.py @@ -0,0 +1,34 @@ +from .common import Source +from ..address import Address, AddressDomain +from ..environment import Environment +from ..sink.macart_sink import MacArtSink +from ..platform import MacCategory +from ...lowlevel import artio +from ...logger import HIGHLEVEL_LOGGER as LOGGER + + +class MacOutArtSource(Source): + __kind: MacCategory + __name: str + + def __init__(self, kind: MacCategory, name: str) -> None: + self.__kind = kind + self.__name = name + + @property + def address(self) -> Address: + return Address(AddressDomain.MacOutput, str(self.__kind), self.__name) + + def pull_to_fd(self, env: Environment, mac_sink: MacArtSink) -> None: + sink_hint = mac_sink.push_hint() + LOGGER.info( + 'Duplicating Macdows-only artifact from kind "%s" name "%s" to kind "%s" name "%s".', + sink_hint.kind, + sink_hint.name, + self.__kind, + self.__name, + ) + artio.link_artifact( + env.mac_output_artifact(sink_hint.kind, sink_hint.name), + env.mac_output_artifact(self.__kind, self.__name), + ) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/newart_source.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/newart_source.py new file mode 100644 index 0000000..1a4eeea --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/newart_source.py @@ -0,0 +1,16 @@ +from .common import Source +from ..address import Address +from ..utils import Artifact, VART_HW +from ...lowlevel import artio + + +class NewArtSource(Source): + def __init__(self) -> None: + pass + + @property + def address(self) -> Address | None: + return None + + def pull(self) -> Artifact: + return artio.new_artifact((VART_HW, VART_HW)) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/svg_source.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/svg_source.py new file mode 100644 index 0000000..dfc4af5 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/svg_source.py @@ -0,0 +1,52 @@ +from pathlib import Path +from typing import Optional +from .common import Source +from ..address import Address +from ..environment import Environment +from ..sink.tempart_sink import TempArtSink +from ..utils import Artifact, VART_HW +from ...lowlevel import artio, artrdr +from ...lowlevel.artrdr import SvgConfig +from ...logger import HIGHLEVEL_LOGGER as LOGGER + +DEFAULT_SVG_CONFIG = SvgConfig(VART_HW, VART_HW) + + +class SvgSource(Source): + """The source to SVG located in input directory.""" + + __comps: tuple[str, ...] + + def __init__(self, *args: str) -> None: + self.__comps = tuple(args) + + @property + def address(self) -> Address | None: + return None + + def pull(self, env: Environment, cfg: SvgConfig = DEFAULT_SVG_CONFIG) -> Artifact: + dst = self.__render(env, None, cfg) + return artio.load_artifact(dst) + + def pull_to_temp( + self, + env: Environment, + temp_sink: TempArtSink, + cfg: SvgConfig = DEFAULT_SVG_CONFIG, + ) -> None: + self.__render(env, temp_sink.push_hint(env), cfg) + + def __render(self, env: Environment, dst: Optional[Path], cfg: SvgConfig) -> Path: + """ + Internal used function for rendering SVG. + + :return: The path to destination storing render result. + """ + src = env.input_artifact(*self.__comps) + if dst is None: + LOGGER.info("Rendering temporary SVG: %s", src) + dst = env.allocate_lib_render_temporary_artifact() + else: + LOGGER.info("Rendering SVG: %s -> %s", src, dst) + artrdr.render_svg(src, dst, env.prologue.svg_render, cfg) + return dst diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/tempart_source.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/tempart_source.py new file mode 100644 index 0000000..7547ae2 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/tempart_source.py @@ -0,0 +1,24 @@ +from .common import Source +from ..address import Address, AddressDomain +from ..environment import Environment +from ..utils import Artifact +from ...lowlevel import artio +from ...logger import HIGHLEVEL_LOGGER as LOGGER + + +class TempArtSource(Source): + """The source to artifact (PNG-only) located in temporary directory.""" + + __comps: tuple[str, ...] + + def __init__(self, *args: str) -> None: + self.__comps = tuple(args) + + @property + def address(self) -> Address | None: + return Address(AddressDomain.Temporary, *self.__comps) + + def pull(self, env: Environment) -> Artifact: + p = env.user_temporary_artifact(*self.__comps) + LOGGER.info("Loading temporary artifact: %s", p) + return artio.load_artifact(p) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/tempdir_source.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/tempdir_source.py deleted file mode 100644 index 0b30073..0000000 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/tempdir_source.py +++ /dev/null @@ -1,24 +0,0 @@ -from ..utils import Artifact -from .common import Source - -class TempDirSource(Source): - - __path_comps: tuple[str, ...] - - def __init__(self, *args: str) -> None: - self.__path_comps = tuple(args) - - def pull_artifact(self) -> Artifact: - pass - - def __hash__(self) -> int: - return hash(self.__path_comps) - - def __eq__(self, other) -> bool: - if self is other: - return True - - if not isinstance(other, TempDirSource): - return NotImplemented - - return self.__path_comps == other.__path_comps diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/winoutart_source.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/winoutart_source.py new file mode 100644 index 0000000..151e87e --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/source/winoutart_source.py @@ -0,0 +1,34 @@ +from .common import Source +from ..address import Address, AddressDomain +from ..environment import Environment +from ..sink.winart_sink import WinArtSink +from ..platform import WinCategory +from ...lowlevel import artio +from ...logger import HIGHLEVEL_LOGGER as LOGGER + + +class WinOutArtSource(Source): + __kind: WinCategory + __name: str + + def __init__(self, kind: WinCategory, name: str) -> None: + self.__kind = kind + self.__name = name + + @property + def address(self) -> Address: + return Address(AddressDomain.WinOutput, str(self.__kind), self.__name) + + def pull_to_fd(self, env: Environment, win_sink: WinArtSink) -> None: + sink_hint = win_sink.push_hint() + LOGGER.info( + 'Duplicating Windows-only artifact from kind "%s" name "%s" to kind "%s" name "%s".', + sink_hint.kind, + sink_hint.name, + self.__kind, + self.__name, + ) + artio.link_artifact( + env.win_output_artifact(sink_hint.kind, sink_hint.name), + env.win_output_artifact(self.__kind, self.__name), + ) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py index cd207fa..610c063 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py @@ -1,4 +1,4 @@ -from ..lowlevel.utils import Artifact, ArtifactFile +from ..lowlevel.utils import Artifact VART_HW: int = 1024 """The height or width of Vanilla Artifact.""" diff --git a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artio.py b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artio.py index dec1e03..d2c485a 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artio.py +++ b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artio.py @@ -1,10 +1,24 @@ import shutil +import os +import sys +import enum from pathlib import Path import PIL.Image -from .utils import Artifact, ArtifactFile +from .utils import Artifact from ..logger import LOWLEVEL_LOGGER as LOGGER +_LINK_MODE_ENV: str = "ASSEMBLICON_LINK_MODE" +"""The environment variable selecting how artifacts are linked.""" + + +class LinkMode(enum.StrEnum): + """All valid values of the ``ASSEMBLICON_LINK_MODE`` environment variable.""" + + Copy = "copy" + Symlink = "symlink" + + def new_artifact(size: tuple[int, int]) -> Artifact: """ Create new RGBA empty (#ffffff00) artifact with given size. @@ -12,7 +26,7 @@ def new_artifact(size: tuple[int, int]) -> Artifact: return PIL.Image.new("RGBA", size, "#ffffff00") -def load_input_artifact(p: Path) -> ArtifactFile: +def load_input_artifact(p: Path) -> Artifact: """ Load bitmap artifact (PNG, JPEG or BMP) which usually is located in input directory. """ @@ -20,7 +34,7 @@ def load_input_artifact(p: Path) -> ArtifactFile: return PIL.Image.open(p, "r", ["PNG", "JPEG", "BMP"]) -def load_artifact(p: Path) -> ArtifactFile: +def load_artifact(p: Path) -> Artifact: """ Load bitmap artifact (PNG-only) saved previously as the intermediary. """ @@ -36,3 +50,93 @@ def save_artifact(art: Artifact, p: Path) -> None: def dup_artifact(src: Path, dst: Path) -> None: LOGGER.debug("Duplicating artifact: %s -> %s", src, dst) shutil.copyfile(src, dst) + + +def link_artifact(src: Path, dst: Path) -> None: + """ + Make given artifact reachable from another path, either via a + relative symlink or a content copy. + + The linking mode is controlled by the ``ASSEMBLICON_LINK_MODE`` + environment variable: ``symlink`` (default) creates a symlink with a + path relative to the destination; ``copy`` duplicates the artifact + content. An existing destination is only removed after it is proven + to be a regular file, or a symlink whose target is a file path + (the target is allowed to be missing), and only when it shares the + same filename suffix as the source. Symlink creation failure falls + back to copying. + + :param src: The path to source artifact file. + :param dst: The path to destination which should reach the artifact. + :raises ValueError: If the link mode is unknown, the destination + exists but is not safely deletable (see above), its suffix + differs from the source one, or no common directory exists + between source and destination. + """ + try: + mode = LinkMode(os.getenv(_LINK_MODE_ENV, LinkMode.Symlink)) + except ValueError: + raise ValueError( + f"Invalid link mode for env {_LINK_MODE_ENV}. " + f"Valid modes are: {', '.join(m.value for m in LinkMode)}." + ) + + LOGGER.debug("Linking artifact: %s -> %s (mode: %s)", src, dst, mode) + + # SAFETY: the code below deletes an existing destination file. These + # checks are the only guards against accidental data deletion, so + # never relax them. A destination is deletable only when it is a + # regular file, or a symlink whose target is a file path (the target + # itself is allowed to be missing, i.e. a broken symlink is + # deletable). Additionally, the source and the destination must + # share the same filename suffix. + if os.path.lexists(dst): + if dst.is_symlink(): + # a symlink is deletable unless it provably points to a + # non-file, e.g. an existing directory + if dst.exists() and not dst.is_file(): + raise ValueError( + f'Refusing to remove existing destination "{dst}": ' + f"it is a symlink not pointing to a file." + ) + elif not dst.is_file(): + raise ValueError( + f'Refusing to remove existing destination "{dst}": ' + f"it is not a regular file." + ) + if src.suffix != dst.suffix: + raise ValueError( + f'Refusing to remove existing destination "{dst}": its ' + f'suffix "{dst.suffix}" differs from source suffix "{src.suffix}".' + ) + dst.unlink() + + if mode == LinkMode.Copy: + shutil.copyfile(src, dst) + return + + # symlinks are always created with a path relative to the + # destination, so that the whole artifact directory stays relocatable + try: + link_target = src.relative_to(dst.parent, walk_up=True) + except ValueError: + raise ValueError( + f'No common directory between "{src}" and "{dst}", ' + f"can not create relative symlink." + ) + try: + dst.symlink_to(link_target) + except OSError as e: + LOGGER.warning( + "Fail to create symlink for artifact %s: %s. Falling back to copy.", + dst, + e, + ) + if sys.platform == "win32": + LOGGER.warning( + "Symlink creation on Windows usually requires administrator " + "privileges. Consider rerunning as administrator, enabling " + "Developer Mode, or setting %s=copy.", + _LINK_MODE_ENV, + ) + shutil.copyfile(src, dst) diff --git a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/environment.py b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/environment.py new file mode 100644 index 0000000..7de252c --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/environment.py @@ -0,0 +1,68 @@ +import tempfile +from dataclasses import dataclass +from types import TracebackType +from pathlib import Path + + +@dataclass(frozen=True) +class Prologue: + input_directory: Path + """The path to input directory.""" + output_directory: Path + """The path to output directory.""" + + def __post_init__(self) -> None: + if not self.input_directory.is_dir(): + raise ValueError( + f"Given input path {self.input_directory} is not an existing directory." + ) + if not self.output_directory.is_dir(): + raise ValueError( + f"Given output path {self.output_directory} is not an existing directory." + ) + + +class Environment: + __prologue: Prologue + """The prologue used for environment.""" + __temp_dir: tempfile.TemporaryDirectory[str] + """Allocated temporary directory.""" + + def __init__(self, prologue: Prologue) -> None: + self.__prologue = prologue + self.__temp_dir = tempfile.TemporaryDirectory() + + def __enter__(self) -> "Environment": + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.__temp_dir.cleanup() + + def input_artifact(self, *args: str) -> Path: + """ + Get the path to input artifact. + + :return: The built absolute path to input artifact. + """ + return self.__prologue.input_directory / Path(*args) + + def temporary_artifact(self, *args: str) -> Path: + """ + Get the path to temporary artifact. + + :return: The built absolute path to temporary artifact. + """ + return Path(self.__temp_dir.name) / Path(*args) + + def output_artifact(self, *args: str) -> Path: + """ + Get the path to output artifact. + + :return: The built absolute path to output artifact. + """ + return self.__prologue.output_directory / Path(*args) diff --git a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/utils.py b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/utils.py index 46c0b2e..cd2348a 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/utils.py +++ b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/utils.py @@ -1,5 +1,3 @@ from PIL.Image import Image -from PIL.ImageFile import ImageFile type Artifact = Image -type ArtifactFile = ImageFile diff --git a/tool/src/aurora_iconset_builder/nostd_icons/mimetype_icons.py b/tool/src/aurora_iconset_builder/nostd_icons/mimetype_icons.py index 4af499b..06b2b1b 100644 --- a/tool/src/aurora_iconset_builder/nostd_icons/mimetype_icons.py +++ b/tool/src/aurora_iconset_builder/nostd_icons/mimetype_icons.py @@ -24,7 +24,7 @@ IMAGE_INNER_RECT = Rectangle(Point(20 * 4, 52 * 4), Point(236 * 4, 204 * 4)) def build_image_base_icon(env: AuroraEnvironment) -> None: env.render_svg( env.input_artifact("component", "image-base.svg"), - env.temporary_artifact("component", "image-base.png"), + env.__temporary_artifact("component", "image-base.png"), ) diff --git a/tool/src/aurora_iconset_builder/std_icons/components.py b/tool/src/aurora_iconset_builder/std_icons/components.py index d58e1c1..fb0a515 100644 --- a/tool/src/aurora_iconset_builder/std_icons/components.py +++ b/tool/src/aurora_iconset_builder/std_icons/components.py @@ -14,7 +14,7 @@ def register(ctx: AuroraContext) -> None: def build_generic_background(env: AuroraEnvironment) -> None: env.render_svg( env.input_artifact("component", "generic-background.svg"), - env.temporary_artifact("component", "generic-background.png"), + env.__temporary_artifact("component", "generic-background.png"), ) diff --git a/tool/src/aurora_iconset_builder/std_icons/status_icons.py b/tool/src/aurora_iconset_builder/std_icons/status_icons.py index c4f40a4..ab146d8 100644 --- a/tool/src/aurora_iconset_builder/std_icons/status_icons.py +++ b/tool/src/aurora_iconset_builder/std_icons/status_icons.py @@ -13,7 +13,7 @@ def build_dialog_infomation_icon(env: AuroraEnvironment) -> None: # save dialog icon with FreeDesktop icon and temp icon for reuse. art = env.render_svg_then_load( env.input_artifact("dialog-infomation.svg"), - env.temporary_artifact("dialog-infomation.png"), + env.__temporary_artifact("dialog-infomation.png"), ) with art: env.save_fd_artifact(art, FdContext.Status, "dialog-infomation") @@ -23,7 +23,7 @@ def build_dialog_warning_icon(env: AuroraEnvironment) -> None: # save dialog icon with FreeDesktop icon and temp icon for reuse. art = env.render_svg_then_load( env.input_artifact("dialog-warning.svg"), - env.temporary_artifact("dialog-warning.png"), + env.__temporary_artifact("dialog-warning.png"), ) with art: env.save_fd_artifact(art, FdContext.Status, "dialog-warning") @@ -33,7 +33,7 @@ def build_dialog_error_icon(env: AuroraEnvironment) -> None: # save dialog icon with FreeDesktop icon and temp icon for reuse. art = env.render_svg_then_load( env.input_artifact("dialog-error.svg"), - env.temporary_artifact("dialog-error.png"), + env.__temporary_artifact("dialog-error.png"), ) with art: env.save_fd_artifact(art, FdContext.Status, "dialog-error")