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.
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -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 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
|
||||
|
||||
@@ -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 .tempdir_sink import TempDirSink
|
||||
from .tempart_sink import TempArtSink
|
||||
|
||||
__all__ = [
|
||||
"Sink",
|
||||
"TempDirSink"
|
||||
"TempArtSink"
|
||||
]
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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 .tempdir_source import TempDirSource
|
||||
from .tempart_source import TempArtSource
|
||||
|
||||
__all__ = [
|
||||
"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 ..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.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -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
|
||||
"""The height or width of Vanilla Artifact."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.ImageFile import ImageFile
|
||||
|
||||
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:
|
||||
env.render_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:
|
||||
env.render_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.
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user