feat: refactor assemblicon (1/3) sink and source establishment
This commit is contained in:
@@ -20,7 +20,7 @@ def main() -> None:
|
|||||||
# TODO: use explicit temporary directory initializer in prologue to do this.
|
# TODO: use explicit temporary directory initializer in prologue to do this.
|
||||||
|
|
||||||
# create our special temporary subdirectory
|
# 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:
|
try:
|
||||||
register(ctx)
|
register(ctx)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -1,63 +1,25 @@
|
|||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
import enum
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TypeVar, Generic
|
from typing import TypeVar, Generic, Callable
|
||||||
from .utils import Artifact, ArtifactFile, VART_HW, check_vart
|
from ..lowlevel.environment import Prologue as LowPrologue, Environment as LowEnv
|
||||||
from ..logger import HIGHLEVEL_LOGGER as LOGGER
|
from ..lowlevel.artrdr import SvgRenderKind
|
||||||
from ..lowlevel.artrdr import SvgRenderKind, SvgConfig, BlenderConfig
|
from .platform import FdContext, WinCategory, MacCategory, FD_THUMBNAIL_HWS
|
||||||
from ..lowlevel import artio, artrdr
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Prologue:
|
class Prologue(LowPrologue):
|
||||||
input_directory: Path
|
|
||||||
output_directory: Path
|
|
||||||
svg_render: SvgRenderKind
|
svg_render: SvgRenderKind
|
||||||
|
"""The render kind for SVG"""
|
||||||
def __post_init__(self) -> None:
|
temp_dir_initializer: Callable[[Path], None] | None
|
||||||
if not self.input_directory.is_dir():
|
"""
|
||||||
raise ValueError(
|
The extra initializer for temporary directory.
|
||||||
f"Given input path {self.input_directory} is not an existing directory."
|
The only argument in function is the path to temporary directory.
|
||||||
)
|
``None`` for no extra initializer.
|
||||||
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"""
|
|
||||||
|
|
||||||
|
|
||||||
_TEMP_USER_DIR: str = "user"
|
_TEMP_USER_DIR: str = "user"
|
||||||
@@ -68,46 +30,51 @@ _TEMP_LIB_STABLE_DIR: str = "stable"
|
|||||||
_ART_WIN: str = "windows"
|
_ART_WIN: str = "windows"
|
||||||
_ART_MAC: str = "macos"
|
_ART_MAC: str = "macos"
|
||||||
|
|
||||||
_DEFAULT_SVG_CONFIG = artrdr.SvgConfig(VART_HW, VART_HW)
|
P = TypeVar("P", bound=Prologue)
|
||||||
_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)
|
|
||||||
|
|
||||||
|
|
||||||
class Environment(Generic[P]):
|
class Environment(Generic[P]):
|
||||||
__prologue: P
|
__prologue: P
|
||||||
"""The prologue used for environment."""
|
"""The prologue used for environment."""
|
||||||
__temp_dir: tempfile.TemporaryDirectory[str]
|
__lowenv: LowEnv
|
||||||
|
"""The lowlevel environment managing temporary directory."""
|
||||||
|
|
||||||
def __init__(self, prologue: P) -> None:
|
def __init__(self, prologue: P) -> None:
|
||||||
self.__prologue = prologue
|
self.__prologue = prologue
|
||||||
self.__temp_dir = tempfile.TemporaryDirectory()
|
self.__lowenv = LowEnv(self.__prologue)
|
||||||
|
|
||||||
# construct basic layout of temporary directory
|
# construct basic layout of temporary directory
|
||||||
self.temporary_artifact(_TEMP_USER_DIR).mkdir(parents=True, exist_ok=True)
|
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_LIB_DIR, _TEMP_LIB_ALLOC_DIR).mkdir(
|
||||||
parents=True, exist_ok=True
|
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
|
parents=True, exist_ok=True
|
||||||
)
|
)
|
||||||
# and output directory
|
# and output directory
|
||||||
for hw in _FD_THUMBNAIL_HWS:
|
for hw in FD_THUMBNAIL_HWS:
|
||||||
for kind in FdContext:
|
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
|
parents=True, exist_ok=True
|
||||||
)
|
)
|
||||||
for kind in WinCategory:
|
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:
|
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
|
# region: With Visitor Requirements
|
||||||
|
|
||||||
def __enter__(self) -> "Environment":
|
def __enter__(self) -> "Environment":
|
||||||
|
self.__lowenv.__enter__()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(
|
def __exit__(
|
||||||
@@ -116,25 +83,19 @@ class Environment(Generic[P]):
|
|||||||
exc_value: BaseException | None,
|
exc_value: BaseException | None,
|
||||||
traceback: TracebackType | None,
|
traceback: TracebackType | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.__temp_dir.cleanup()
|
self.__lowenv.__exit__(exc_type, exc_value, traceback)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region: Prologue Visitor
|
# region: Prologue Visitor
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prologue(self) -> P:
|
def prologue(self) -> Prologue:
|
||||||
return self.__prologue
|
return self.__prologue
|
||||||
|
|
||||||
# endregion
|
@property
|
||||||
|
def user_prologue(self) -> P:
|
||||||
# region: New Artifact
|
return self.__prologue
|
||||||
|
|
||||||
def new_artifact(self) -> Artifact:
|
|
||||||
"""
|
|
||||||
Create new RGBA vanilla size (1024x1024) empty (#ffffff00) artifact.
|
|
||||||
"""
|
|
||||||
return artio.new_artifact((VART_HW, VART_HW))
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
@@ -144,48 +105,20 @@ class Environment(Generic[P]):
|
|||||||
"""
|
"""
|
||||||
Get the path to input artifact.
|
Get the path to input artifact.
|
||||||
"""
|
"""
|
||||||
return self.__prologue.input_directory / Path(*args)
|
return self.__lowenv.input_artifact(*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)
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region: Temporary Artifact
|
# region: Temporary Artifact
|
||||||
|
|
||||||
def __user_temporary_artifact(self, *args: str) -> Path:
|
def __temporary_artifact(self, *args: str) -> Path:
|
||||||
return self.temporary_artifact(_TEMP_USER_DIR, *args)
|
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:
|
def __lib_temporary_artifact(self, *args: str) -> Path:
|
||||||
return self.temporary_artifact(_TEMP_LIB_DIR, *args)
|
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)
|
|
||||||
|
|
||||||
def __allocate_lib_temporary_artifact(self, suffix: Optional[str] = None) -> Path:
|
def __allocate_lib_temporary_artifact(self, suffix: Optional[str] = None) -> Path:
|
||||||
p = self.__lib_temporary_artifact(_TEMP_LIB_ALLOC_DIR)
|
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:
|
def __fetch_const_lib_temporary_artifact(self, name: str) -> Path:
|
||||||
return self.__lib_temporary_artifact(_TEMP_LIB_STABLE_DIR, name)
|
return self.__lib_temporary_artifact(_TEMP_LIB_STABLE_DIR, name)
|
||||||
|
|
||||||
# endregion
|
def allocate_lib_render_temporary_artifact(self) -> Path:
|
||||||
|
|
||||||
# region: Render Wrapper
|
|
||||||
|
|
||||||
def __intern_render_svg(
|
|
||||||
self, src: Path, dst: Optional[Path], cfg: SvgConfig
|
|
||||||
) -> 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:
|
return self.__allocate_lib_temporary_artifact(".png")
|
||||||
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
|
|
||||||
|
|
||||||
def render_svg(
|
def fetch_lib_blender_script_temporary_artifact(self) -> Path:
|
||||||
self, src: Path, dst: Path, cfg: SvgConfig = _DEFAULT_SVG_CONFIG
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
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.
|
:return: The path to temporary Python file.
|
||||||
:param dst: The path to rendered PNG file.
|
Please note that this file may be presneted in file system,
|
||||||
:param cfg: The configuration when rendering SVG file. Default for vanilla artifact.
|
so caller may need to overwrite it.
|
||||||
|
All Blender render process share the same script file (this).
|
||||||
"""
|
"""
|
||||||
self.__intern_render_svg(src, dst, cfg)
|
# TODO: Strip this "share" behavior to enable multi-threads build ability.
|
||||||
|
return self.__fetch_const_lib_temporary_artifact("blender.py")
|
||||||
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)
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region: Output Artifact
|
# region: Output Artifact
|
||||||
|
|
||||||
def output_artifact(self, *args: str) -> Path:
|
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:
|
|
||||||
"""
|
"""
|
||||||
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.
|
:param name: The name of saved artifact. The file extension ".png" is not required.
|
||||||
"""
|
"""
|
||||||
LOGGER.info(
|
return self.__output_artifact(f"{hw}x{hw}", str(kind), f"{name}.png")
|
||||||
'Saving FreeDesktop artifact with kind "%s" and name "%s".', kind, name
|
|
||||||
)
|
|
||||||
|
|
||||||
check_vart(art)
|
def win_output_artifact(self, kind: WinCategory, name: str) -> Path:
|
||||||
|
|
||||||
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:
|
|
||||||
"""
|
"""
|
||||||
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.
|
:param name: The name of saved artifact. The file extension ".ico" is not required.
|
||||||
"""
|
"""
|
||||||
LOGGER.info(
|
return self.__output_artifact(_ART_WIN, str(kind), f"{name}.ico")
|
||||||
'Saving Windows-only artifact with kind "%s" and name "%s".', kind, name
|
|
||||||
)
|
|
||||||
check_vart(art)
|
|
||||||
artrdr.render_ico(art, self.__win_artifact(kind, name))
|
|
||||||
|
|
||||||
def dup_win_artifact(
|
def mac_output_artifact(self, kind: MacCategory, name: str) -> Path:
|
||||||
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:
|
|
||||||
"""
|
"""
|
||||||
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.
|
:param name: The name of saved artifact. The file extension ".icns" is not required.
|
||||||
"""
|
"""
|
||||||
LOGGER.info(
|
return self.__output_artifact(_ART_MAC, str(kind), f"{name}.icns")
|
||||||
'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),
|
|
||||||
)
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from .common import Sink
|
from .common import Sink
|
||||||
from .tempdir_sink import TempDirSink
|
from .tempart_sink import TempArtSink
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Sink",
|
"Sink",
|
||||||
"TempDirSink"
|
"TempArtSink"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from ..utils import Artifact
|
from ..address import Address
|
||||||
|
|
||||||
|
|
||||||
class Sink(ABC):
|
class Sink(ABC):
|
||||||
@@ -7,15 +7,6 @@ class Sink(ABC):
|
|||||||
Abstract base class for all sinks.
|
Abstract base class for all sinks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def push_artifact(self, art: Artifact) -> None:
|
def address(self) -> Address: ...
|
||||||
"""Push given artifact into this sink."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def __eq__(self, other) -> bool:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from .common import Source
|
from .common import Source
|
||||||
from .tempdir_source import TempDirSource
|
from .tempart_source import TempArtSource
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Source",
|
"Source",
|
||||||
"TempDirSource"
|
"TempArtSource"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from ..utils import Artifact
|
from ..address import Address
|
||||||
|
|
||||||
|
|
||||||
class Source(ABC):
|
class Source(ABC):
|
||||||
@@ -7,15 +7,14 @@ class Source(ABC):
|
|||||||
Abstract base class for all sources.
|
Abstract base class for all sources.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def pull_artifact(self) -> Artifact:
|
def address(self) -> Address | None:
|
||||||
"""Pull artifact from this source."""
|
"""
|
||||||
pass
|
Get the address representation of this source.
|
||||||
|
|
||||||
@abstractmethod
|
:return: The address representing this source.
|
||||||
def __hash__(self) -> int:
|
``None`` if there is no producer for this source.
|
||||||
pass
|
It means that this source do not depend anything.
|
||||||
|
"""
|
||||||
@abstractmethod
|
...
|
||||||
def __eq__(self, other) -> bool:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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))
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from ..lowlevel.utils import Artifact, ArtifactFile
|
from ..lowlevel.utils import Artifact
|
||||||
|
|
||||||
VART_HW: int = 1024
|
VART_HW: int = 1024
|
||||||
"""The height or width of Vanilla Artifact."""
|
"""The height or width of Vanilla Artifact."""
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
import shutil
|
import shutil
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import PIL.Image
|
import PIL.Image
|
||||||
from .utils import Artifact, ArtifactFile
|
from .utils import Artifact
|
||||||
from ..logger import LOWLEVEL_LOGGER as LOGGER
|
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:
|
def new_artifact(size: tuple[int, int]) -> Artifact:
|
||||||
"""
|
"""
|
||||||
Create new RGBA empty (#ffffff00) artifact with given size.
|
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")
|
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.
|
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"])
|
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.
|
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:
|
def dup_artifact(src: Path, dst: Path) -> None:
|
||||||
LOGGER.debug("Duplicating artifact: %s -> %s", src, dst)
|
LOGGER.debug("Duplicating artifact: %s -> %s", src, dst)
|
||||||
shutil.copyfile(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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
from PIL.Image import Image
|
from PIL.Image import Image
|
||||||
from PIL.ImageFile import ImageFile
|
|
||||||
|
|
||||||
type Artifact = Image
|
type Artifact = Image
|
||||||
type ArtifactFile = ImageFile
|
|
||||||
|
|||||||
@@ -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:
|
def build_image_base_icon(env: AuroraEnvironment) -> None:
|
||||||
env.render_svg(
|
env.render_svg(
|
||||||
env.input_artifact("component", "image-base.svg"),
|
env.input_artifact("component", "image-base.svg"),
|
||||||
env.temporary_artifact("component", "image-base.png"),
|
env.__temporary_artifact("component", "image-base.png"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ def register(ctx: AuroraContext) -> None:
|
|||||||
def build_generic_background(env: AuroraEnvironment) -> None:
|
def build_generic_background(env: AuroraEnvironment) -> None:
|
||||||
env.render_svg(
|
env.render_svg(
|
||||||
env.input_artifact("component", "generic-background.svg"),
|
env.input_artifact("component", "generic-background.svg"),
|
||||||
env.temporary_artifact("component", "generic-background.png"),
|
env.__temporary_artifact("component", "generic-background.png"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ def build_dialog_infomation_icon(env: AuroraEnvironment) -> None:
|
|||||||
# save dialog icon with FreeDesktop icon and temp icon for reuse.
|
# save dialog icon with FreeDesktop icon and temp icon for reuse.
|
||||||
art = env.render_svg_then_load(
|
art = env.render_svg_then_load(
|
||||||
env.input_artifact("dialog-infomation.svg"),
|
env.input_artifact("dialog-infomation.svg"),
|
||||||
env.temporary_artifact("dialog-infomation.png"),
|
env.__temporary_artifact("dialog-infomation.png"),
|
||||||
)
|
)
|
||||||
with art:
|
with art:
|
||||||
env.save_fd_artifact(art, FdContext.Status, "dialog-infomation")
|
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.
|
# save dialog icon with FreeDesktop icon and temp icon for reuse.
|
||||||
art = env.render_svg_then_load(
|
art = env.render_svg_then_load(
|
||||||
env.input_artifact("dialog-warning.svg"),
|
env.input_artifact("dialog-warning.svg"),
|
||||||
env.temporary_artifact("dialog-warning.png"),
|
env.__temporary_artifact("dialog-warning.png"),
|
||||||
)
|
)
|
||||||
with art:
|
with art:
|
||||||
env.save_fd_artifact(art, FdContext.Status, "dialog-warning")
|
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.
|
# save dialog icon with FreeDesktop icon and temp icon for reuse.
|
||||||
art = env.render_svg_then_load(
|
art = env.render_svg_then_load(
|
||||||
env.input_artifact("dialog-error.svg"),
|
env.input_artifact("dialog-error.svg"),
|
||||||
env.temporary_artifact("dialog-error.png"),
|
env.__temporary_artifact("dialog-error.png"),
|
||||||
)
|
)
|
||||||
with art:
|
with art:
|
||||||
env.save_fd_artifact(art, FdContext.Status, "dialog-error")
|
env.save_fd_artifact(art, FdContext.Status, "dialog-error")
|
||||||
|
|||||||
Reference in New Issue
Block a user