diff --git a/src/assemblicon/highlevel/address.py b/src/assemblicon/highlevel/address.py index b199713..079e07b 100644 --- a/src/assemblicon/highlevel/address.py +++ b/src/assemblicon/highlevel/address.py @@ -6,9 +6,13 @@ class AddressDomain(enum.IntEnum): Temporary = enum.auto() Output = enum.auto() - FdOutput = enum.auto() + PosixOutput = enum.auto() + PosixFdOutput = enum.auto() WinOutput = enum.auto() + WinIcoOutput = enum.auto() + WinMsixOutput = enum.auto() MacOutput = enum.auto() + MacIcnsOutput = enum.auto() class Address: diff --git a/src/assemblicon/highlevel/environment.py b/src/assemblicon/highlevel/environment.py index 83c4391..57a9d3b 100644 --- a/src/assemblicon/highlevel/environment.py +++ b/src/assemblicon/highlevel/environment.py @@ -7,7 +7,17 @@ from pathlib import Path from typing import TypeVar, Generic, Callable from ..lowlevel.environment import Prologue as LowPrologue, Environment as LowEnv from ..lowlevel.artrdr import SvgRenderKind -from .utils import FdContext, WinCategory, MacCategory, VART_HW +from .utils import ( + FdContext, + IcoCategory, + MsixCategory, + MsixAppLogoKind, + MsixAppPlatedKind, + IcnsCategory, + check_fd_hw, + check_msix_scale, + check_msix_targetsize, +) @dataclass(frozen=True) @@ -30,11 +40,7 @@ class Prologue(LowPrologue): def __post_init__(self) -> None: super().__post_init__() for hw in self.fd_thumbnail_hws: - if hw <= 0 or hw > VART_HW: - raise ValueError( - f"Given FreeDesktop icon thumbnail size mismatch the size requirements. " - f"Expect >= 0 and <= {VART_HW}, got {hw}." - ) + check_fd_hw(hw) _TEMP_USER_DIR: str = "user" @@ -42,8 +48,13 @@ _TEMP_LIB_DIR: str = "assemblicon" _TEMP_LIB_ALLOC_DIR: str = "alloc" _TEMP_LIB_STABLE_DIR: str = "stable" +_ART_POSIX: str = "posix" +_ART_POSIX_FD: str = "freedesktop" _ART_WIN: str = "windows" +_ART_WIN_ICO: str = "ico" +_ART_WIN_MSIX: str = "msix" _ART_MAC: str = "macos" +_ART_MAC_ICNS: str = "icns" P = TypeVar("P", bound=Prologue) @@ -55,36 +66,11 @@ class Environment(Generic[P]): """The lowlevel environment managing temporary directory.""" def __init__(self, prologue: P) -> None: + # assign members self.__prologue = prologue 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( - parents=True, exist_ok=True - ) - self.__temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_STABLE_DIR).mkdir( - parents=True, exist_ok=True - ) - # and output directory - for hw in self.__prologue.fd_thumbnail_hws: - for kind in FdContext: - 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 - ) - for kind in MacCategory: - 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.user_temporary_artifact() - self.__prologue.temp_dir_initializer(temp_dir) + # build directory hierarchy + self.__build_dir_hierarchy() # region: With Visitor Requirements @@ -102,6 +88,64 @@ class Environment(Generic[P]): # endregion + # region: Directory Hierarchy Builder + + def __build_dir_hierarchy(self) -> None: + self.__build_temp_dir_hierarchy() + self.__build_output_dir_hierarchy() + + def __build_temp_dir_hierarchy(self) -> None: + # 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( + parents=True, exist_ok=True + ) + self.__temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_STABLE_DIR).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.user_temporary_artifact() + self.__prologue.temp_dir_initializer(temp_dir) + + def __build_output_dir_hierarchy(self) -> None: + # build top layout + self.__posix_output_artifact().mkdir(parents=True, exist_ok=True) + self.__win_output_artifact().mkdir(parents=True, exist_ok=True) + self.__mac_output_artifact().mkdir(parents=True, exist_ok=True) + + # build sub-layout + self.__posix_output_artifact(_ART_POSIX_FD).mkdir(parents=True, exist_ok=True) + self.__win_output_artifact(_ART_WIN_ICO).mkdir(parents=True, exist_ok=True) + self.__win_output_artifact(_ART_WIN_MSIX).mkdir(parents=True, exist_ok=True) + self.__mac_output_artifact(_ART_MAC_ICNS).mkdir(parents=True, exist_ok=True) + + # build the lowest layout + # for POSIX FreeDesktop + for hw in self.__prologue.fd_thumbnail_hws: + for kind in FdContext: + self.__posix_output_artifact( + _ART_POSIX_FD, f"{hw}x{hw}", str(kind) + ).mkdir(parents=True, exist_ok=True) + # for Windows ICO + for kind in IcoCategory: + self.__win_output_artifact(_ART_WIN_ICO, str(kind)).mkdir( + parents=True, exist_ok=True + ) + # for Windows MSIX + for kind in MsixCategory: + self.__win_output_artifact(_ART_WIN_MSIX, str(kind)).mkdir( + parents=True, exist_ok=True + ) + # for macOS ICNS + for kind in IcnsCategory: + self.__mac_output_artifact(_ART_MAC_ICNS, str(kind)).mkdir( + parents=True, exist_ok=True + ) + + # endregion + # region: Prologue Visitor @property @@ -176,6 +220,24 @@ class Environment(Generic[P]): """ return self.__lowenv.output_artifact(*args) + def __posix_output_artifact(self, *args: str) -> Path: + """ + Get the path to output POSIX artifacts. + """ + return self.__output_artifact(_ART_POSIX, *args) + + def __win_output_artifact(self, *args: str) -> Path: + """ + Get the path to output Windows artifacts. + """ + return self.__output_artifact(_ART_WIN, *args) + + def __mac_output_artifact(self, *args: str) -> Path: + """ + Get the path to output macOS artifacts. + """ + return self.__output_artifact(_ART_MAC, *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. @@ -184,25 +246,121 @@ class Environment(Generic[P]): 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. + :returns: The path to saved artifact. """ - return self.__output_artifact(f"{hw}x{hw}", str(kind), f"{name}.png") + check_fd_hw(hw) + return self.__posix_output_artifact( + _ART_POSIX_FD, f"{hw}x{hw}", str(kind), f"{name}.png" + ) - def win_output_artifact(self, kind: WinCategory, name: str) -> Path: + def ico_output_artifact(self, kind: IcoCategory, name: str) -> Path: """ - Get the path for saving Windows-only icon. + Get the path for saving Windows ICO icon. :param kind: The kind of this icon. :param name: The name of saved artifact. The file extension ".ico" is not required. + :returns: The path to saved artifact. """ - return self.__output_artifact(_ART_WIN, str(kind), f"{name}.ico") + return self.__win_output_artifact(_ART_WIN_ICO, str(kind), f"{name}.ico") - def mac_output_artifact(self, kind: MacCategory, name: str) -> Path: + def __msix_output_artifact(self, kind: MsixCategory, *args: str) -> Path: """ - Get the path for saving macOS-only icon. + Get the path for saving Windows MSIX icon. + + :param kind: The category of this icon. + :returns: The path to saved artifact. + """ + return self.__win_output_artifact(_ART_WIN_MSIX, str(kind), *args) + + def msix_app_output_artifact( + self, + kind: MsixAppLogoKind, + scale: Optional[int], + targetsize: Optional[int], + plated: Optional[MsixAppPlatedKind], + name: str, + ) -> Path: + """ + Get the path for saving Windows MSIX application logo icon. + + Parameter ``scale`` and ``targetsize`` cannot both be non-``None``. + + Parameter ``targetsize`` and ``plated`` must be either both ``None`` or both non-``None``. + + :param kind: The detailed kind of MSIX application icon. + :param scale: The scale variant of this artifact. + ``None`` for no variant suffix. + :param targetsize: The targetsize variant of this artifact. + ``None`` for no targetsize suffix or non-``MsixAppLogoKind.Square44x44`` icon. + :param plated: The plated variant of this artifact. + ``None`` for non-``MsixAppLogoKind.Square44x44`` icon. + :param name: The name of saved artifact. The file extension ".png" is not required. + :returns: The path to saved artifact. + """ + # check parameter restrictions + if scale is not None and targetsize is not None: + raise ValueError( + f"MSIX doesn't allow the combination of scale and targetsize variant" + ) + if kind != MsixAppLogoKind.Square44x44 and targetsize is not None: + raise ValueError( + f"MSIX targetsize variant is not supported on non-Square44x44 logo" + ) + if (targetsize is None) ^ (plated is None): + raise ValueError( + f"MSIX plated feature must be provided with targetsize variant" + ) + + # build file name part + if scale is not None: + check_msix_scale(scale) + filename = f"{name}.{kind}.scale-{scale}.png" + elif targetsize is not None: + check_msix_targetsize(targetsize) + # TODO: This altform code are ugly (do not utilize enum.StrEnum) + # May be improved in future. + altform: Optional[str] + match plated: + case MsixAppPlatedKind.Plated: + altform = None + case MsixAppPlatedKind.Unplated: + altform = "altform-unplated" + case _: + raise ValueError(f"Unknown plated feature: {plated}") + if altform is None: + filename = f"{name}.{kind}.targetsize-{targetsize}.png" + else: + filename = f"{name}.{kind}.targetsize-{targetsize}_{altform}.png" + else: + filename = f"{name}.{kind}.png" + # build result + return self.__msix_output_artifact(MsixCategory.Application, filename) + + def msix_ext_output_artifact(self, targetsize: Optional[int], name: str) -> Path: + """ + Get the path for saving Windows MSIX file association icon. + + :param targetsize: The targetsize variant of this artifact. + ``None`` for no variant suffix. + :param name: The name of saved artifact. The file extension ".png" is not required. + :returns: The path to saved artifact. + """ + if targetsize is None: + filename = f"{name}.png" + else: + check_msix_targetsize(targetsize) + filename = f"{name}.targetsize-{targetsize}.png" + + return self.__msix_output_artifact(MsixCategory.Extension, filename) + + def icns_output_artifact(self, kind: IcnsCategory, name: str) -> Path: + """ + Get the path for saving macOS ICNS icon. :param kind: The kind of this icon. :param name: The name of saved artifact. The file extension ".icns" is not required. + :returns: The path to saved artifact. """ - return self.__output_artifact(_ART_MAC, str(kind), f"{name}.icns") + return self.__mac_output_artifact(_ART_MAC_ICNS, str(kind), f"{name}.icns") # endregion diff --git a/src/assemblicon/highlevel/sink/fdart_sink.py b/src/assemblicon/highlevel/sink/fdart_sink.py index 660d4c5..43e33d5 100644 --- a/src/assemblicon/highlevel/sink/fdart_sink.py +++ b/src/assemblicon/highlevel/sink/fdart_sink.py @@ -23,7 +23,7 @@ class FdArtSink(Sink): @property def address(self) -> Address: - return Address(AddressDomain.FdOutput, str(self.__kind), self.__name) + return Address(AddressDomain.PosixFdOutput, str(self.__kind), self.__name) def push(self, env: Environment, art: Artifact) -> None: LOGGER.info( diff --git a/src/assemblicon/highlevel/sink/icnsart_sink.py b/src/assemblicon/highlevel/sink/icnsart_sink.py index 205037a..fa7aec3 100644 --- a/src/assemblicon/highlevel/sink/icnsart_sink.py +++ b/src/assemblicon/highlevel/sink/icnsart_sink.py @@ -2,28 +2,28 @@ from dataclasses import dataclass from .common import Sink from ..address import Address, AddressDomain from ..environment import Environment -from ..utils import Artifact, MacCategory, check_vart +from ..utils import Artifact, IcnsCategory, check_vart from ...lowlevel import artrdr from ...logger import HIGHLEVEL_LOGGER as LOGGER @dataclass(frozen=True) class IcnsArtSinkHint: - kind: MacCategory + kind: IcnsCategory name: str class IcnsArtSink(Sink): - __kind: MacCategory + __kind: IcnsCategory __name: str - def __init__(self, kind: MacCategory, name: str) -> None: + def __init__(self, kind: IcnsCategory, name: str) -> None: self.__kind = kind self.__name = name @property def address(self) -> Address: - return Address(AddressDomain.MacOutput, str(self.__kind), self.__name) + return Address(AddressDomain.MacIcnsOutput, str(self.__kind), self.__name) def push(self, env: Environment, art: Artifact) -> None: LOGGER.info( @@ -32,7 +32,7 @@ class IcnsArtSink(Sink): self.__name, ) check_vart(art) - artrdr.render_icns(art, env.mac_output_artifact(self.__kind, self.__name)) + artrdr.render_icns(art, env.icns_output_artifact(self.__kind, self.__name)) def push_hint(self) -> IcnsArtSinkHint: return IcnsArtSinkHint(self.__kind, self.__name) diff --git a/src/assemblicon/highlevel/sink/icoart_sink.py b/src/assemblicon/highlevel/sink/icoart_sink.py index f227dca..32e8c98 100644 --- a/src/assemblicon/highlevel/sink/icoart_sink.py +++ b/src/assemblicon/highlevel/sink/icoart_sink.py @@ -2,28 +2,28 @@ from dataclasses import dataclass from .common import Sink from ..address import Address, AddressDomain from ..environment import Environment -from ..utils import Artifact, WinCategory, check_vart +from ..utils import Artifact, IcoCategory, check_vart from ...lowlevel import artrdr from ...logger import HIGHLEVEL_LOGGER as LOGGER @dataclass(frozen=True) class IcoArtSinkHint: - kind: WinCategory + kind: IcoCategory name: str class IcoArtSink(Sink): - __kind: WinCategory + __kind: IcoCategory __name: str - def __init__(self, kind: WinCategory, name: str) -> None: + def __init__(self, kind: IcoCategory, name: str) -> None: self.__kind = kind self.__name = name @property def address(self) -> Address: - return Address(AddressDomain.WinOutput, str(self.__kind), self.__name) + return Address(AddressDomain.WinIcoOutput, str(self.__kind), self.__name) def push(self, env: Environment, art: Artifact) -> None: LOGGER.info( @@ -32,7 +32,7 @@ class IcoArtSink(Sink): self.__name, ) check_vart(art) - artrdr.render_ico(art, env.win_output_artifact(self.__kind, self.__name)) + artrdr.render_ico(art, env.ico_output_artifact(self.__kind, self.__name)) def push_hint(self) -> IcoArtSinkHint: return IcoArtSinkHint(self.__kind, self.__name) diff --git a/src/assemblicon/highlevel/source/fdart_source.py b/src/assemblicon/highlevel/source/fdart_source.py index cd79f2d..c69e0dc 100644 --- a/src/assemblicon/highlevel/source/fdart_source.py +++ b/src/assemblicon/highlevel/source/fdart_source.py @@ -17,7 +17,7 @@ class FdArtSource(Source): @property def address(self) -> Address: - return Address(AddressDomain.FdOutput, str(self.__kind), self.__name) + return Address(AddressDomain.PosixFdOutput, str(self.__kind), self.__name) def link_to(self, env: Environment, fd_sink: FdArtSink) -> None: sink_hint = fd_sink.push_hint() diff --git a/src/assemblicon/highlevel/source/icnsart_source.py b/src/assemblicon/highlevel/source/icnsart_source.py index c3e6d50..10fd372 100644 --- a/src/assemblicon/highlevel/source/icnsart_source.py +++ b/src/assemblicon/highlevel/source/icnsart_source.py @@ -2,22 +2,22 @@ from .common import Source from ..address import Address, AddressDomain from ..environment import Environment from ..sink.icnsart_sink import IcnsArtSink -from ..utils import MacCategory +from ..utils import IcnsCategory from ...lowlevel import artio from ...logger import HIGHLEVEL_LOGGER as LOGGER class IcnsArtSource(Source): - __kind: MacCategory + __kind: IcnsCategory __name: str - def __init__(self, kind: MacCategory, name: str) -> None: + def __init__(self, kind: IcnsCategory, name: str) -> None: self.__kind = kind self.__name = name @property def address(self) -> Address: - return Address(AddressDomain.MacOutput, str(self.__kind), self.__name) + return Address(AddressDomain.MacIcnsOutput, str(self.__kind), self.__name) def link_to(self, env: Environment, mac_sink: IcnsArtSink) -> None: sink_hint = mac_sink.push_hint() @@ -29,6 +29,6 @@ class IcnsArtSource(Source): sink_hint.name, ) artio.link_artifact( - env.mac_output_artifact(self.__kind, self.__name), - env.mac_output_artifact(sink_hint.kind, sink_hint.name), + env.icns_output_artifact(self.__kind, self.__name), + env.icns_output_artifact(sink_hint.kind, sink_hint.name), ) diff --git a/src/assemblicon/highlevel/source/icoart_source.py b/src/assemblicon/highlevel/source/icoart_source.py index 0773bdb..27c8b6b 100644 --- a/src/assemblicon/highlevel/source/icoart_source.py +++ b/src/assemblicon/highlevel/source/icoart_source.py @@ -2,22 +2,22 @@ from .common import Source from ..address import Address, AddressDomain from ..environment import Environment from ..sink.icoart_sink import IcoArtSink -from ..utils import WinCategory +from ..utils import IcoCategory from ...lowlevel import artio from ...logger import HIGHLEVEL_LOGGER as LOGGER class IcoArtSource(Source): - __kind: WinCategory + __kind: IcoCategory __name: str - def __init__(self, kind: WinCategory, name: str) -> None: + def __init__(self, kind: IcoCategory, name: str) -> None: self.__kind = kind self.__name = name @property def address(self) -> Address: - return Address(AddressDomain.WinOutput, str(self.__kind), self.__name) + return Address(AddressDomain.WinIcoOutput, str(self.__kind), self.__name) def link_to(self, env: Environment, win_sink: IcoArtSink) -> None: sink_hint = win_sink.push_hint() @@ -29,6 +29,6 @@ class IcoArtSource(Source): sink_hint.name, ) artio.link_artifact( - env.win_output_artifact(self.__kind, self.__name), - env.win_output_artifact(sink_hint.kind, sink_hint.name), + env.ico_output_artifact(self.__kind, self.__name), + env.ico_output_artifact(sink_hint.kind, sink_hint.name), ) diff --git a/src/assemblicon/highlevel/utils.py b/src/assemblicon/highlevel/utils.py index 22ef7a0..8c8b820 100644 --- a/src/assemblicon/highlevel/utils.py +++ b/src/assemblicon/highlevel/utils.py @@ -12,6 +12,26 @@ def check_vart(art: Artifact) -> None: ) +def check_fd_hw(hw: int) -> None: + if hw <= 0 or hw > VART_HW: + raise ValueError( + f"Invalid FreeDesktop icon thumbnail size. " + f"Expect > 0 and <= {VART_HW}, got {hw}." + ) + + +def check_msix_scale(scale: int) -> None: + if scale <= 100: + raise ValueError(f"Invalid scale value for MSIX icon variant: {scale}") + + +def check_msix_targetsize(targetsize: int) -> None: + if targetsize <= 0: + raise ValueError( + f"Invalid targetsize value for MSIX icon variant: {targetsize}" + ) + + class FdContext(enum.StrEnum): Actions = "actions" Animations = "animations" @@ -26,14 +46,39 @@ class FdContext(enum.StrEnum): Status = "status" -class WinCategory(enum.StrEnum): +class IcoCategory(enum.StrEnum): Application = "apps" """Icons for applications""" Extension = "exts" """Icons for file extensions""" -class MacCategory(enum.StrEnum): +class MsixCategory(enum.StrEnum): + Application = "Apps" + """Icons for application logos""" + Extension = "FileAssocs" + """Icons for file extensions""" + + +class MsixAppLogoKind(enum.StrEnum): + # TODO: We may add more kinds in there. + # But current kinds are enough to upload application to store. + StoreLogo = "StoreLogo" + """The 50x50 icon used for store showcase.""" + Square44x44 = "Square44x44Logo" + """The 44x44 icon used for application list (including taskbar and etc).""" + Square150x150 = "Square150x150Logo" + """The 150x150 icon used for medium tile.""" + + +class MsixAppPlatedKind(enum.StrEnum): + Plated = "plated" + """Plated icon (has solid color box in background)""" + Unplated = "unplated" + """Unplated icon (transparent background)""" + + +class IcnsCategory(enum.StrEnum): Application = "apps" """Icons for applications""" Extension = "exts"