From bcac59ceb1883fb5503db24fe2de6b04e29cbe7c Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Tue, 1 Sep 2026 22:32:00 +0800 Subject: [PATCH] refactor: refactor assemblicon --- tool/src/aurora_iconset_builder/__init__.py | 32 ++-- .../aurora_iconset_builder/assemblicon/cli.py | 108 +++++++++++++ .../assemblicon/highlevel/__init__.py | 0 .../assemblicon/highlevel/context.py | 111 +++++++++++++ .../{context.py => highlevel/environment.py} | 151 ++++++++---------- .../assemblicon/highlevel/utils.py | 11 ++ .../assemblicon/logger.py | 20 +++ .../assemblicon/lowlevel/__init__.py | 0 .../assemblicon/{ => lowlevel}/artio.py | 18 +-- .../assemblicon/{ => lowlevel}/artproc.py | 0 .../{artrender.py => lowlevel/artrdr.py} | 25 ++- .../assemblicon/lowlevel/utils.py | 5 + .../assemblicon/utils.py | 20 --- tool/src/aurora_iconset_builder/cli.py | 42 ----- 14 files changed, 368 insertions(+), 175 deletions(-) create mode 100644 tool/src/aurora_iconset_builder/assemblicon/cli.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/__init__.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/context.py rename tool/src/aurora_iconset_builder/assemblicon/{context.py => highlevel/environment.py} (74%) create mode 100644 tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/logger.py create mode 100644 tool/src/aurora_iconset_builder/assemblicon/lowlevel/__init__.py rename tool/src/aurora_iconset_builder/assemblicon/{ => lowlevel}/artio.py (56%) rename tool/src/aurora_iconset_builder/assemblicon/{ => lowlevel}/artproc.py (100%) rename tool/src/aurora_iconset_builder/assemblicon/{artrender.py => lowlevel/artrdr.py} (90%) create mode 100644 tool/src/aurora_iconset_builder/assemblicon/lowlevel/utils.py delete mode 100644 tool/src/aurora_iconset_builder/assemblicon/utils.py delete mode 100644 tool/src/aurora_iconset_builder/cli.py diff --git a/tool/src/aurora_iconset_builder/__init__.py b/tool/src/aurora_iconset_builder/__init__.py index ff283f9..a9965d4 100644 --- a/tool/src/aurora_iconset_builder/__init__.py +++ b/tool/src/aurora_iconset_builder/__init__.py @@ -1,32 +1,42 @@ import logging from pathlib import Path -from .assemblicon.context import Prerequisite, Context, AdvancedContext -from .assemblicon.utils import setup_logging +from dataclasses import dataclass +from .assemblicon.cli import ArgParserSetup, create_argparser, parse_standard_opts +from .assemblicon.highlevel.environment import Prologue, Environment +from .assemblicon.highlevel.context import Context from .std_icons import build_standard_icons from .nostd_icons import build_non_standard_icons -from .cli import parse + + +@dataclass(frozen=True) +class AuroraPrologue(Prologue): + """Aurora icon set builder specific prologue""" + + pass def main() -> None: - opts = parse() - - setup_logging() + parser_cfg = ArgParserSetup(require_input=False, require_output=False) + parser = create_argparser(parser_cfg) + ns = parser.parse_args() + opts = parse_standard_opts(parser_cfg, ns) repository_root = Path(__file__).resolve().parent.parent.parent.parent iconset_input = repository_root / "src" iconset_output = repository_root / "artifact" - prereq = Prerequisite( + prologue = AuroraPrologue( iconset_input, iconset_output, opts.svg_render.to_assemblicon_type() ) - ctx = Context(prereq) - advctx = AdvancedContext(ctx) + env = Environment(prologue) + ctx = Context(env) try: - _build_icons(advctx) + register(ctx) + ctx.run(opts.selection) except BaseException as e: logging.fatal("Runtime error: %s", e) -def _build_icons(ctx: AdvancedContext) -> None: +def register(ctx: Context) -> None: build_standard_icons(ctx) build_non_standard_icons(ctx) diff --git a/tool/src/aurora_iconset_builder/assemblicon/cli.py b/tool/src/aurora_iconset_builder/assemblicon/cli.py new file mode 100644 index 0000000..2e6e58a --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/cli.py @@ -0,0 +1,108 @@ +import enum +from typing import Optional +from pathlib import Path +from argparse import ArgumentParser, Namespace +from dataclasses import dataclass +from .lowlevel.artrdr import SvgRenderKind + + +class CliSvgRenderKind(enum.StrEnum): + Inkscape = "inkscape" + ReSvg = "resvg" + + def to_assemblicon_type(self) -> SvgRenderKind: + match self: + case CliSvgRenderKind.Inkscape: + return SvgRenderKind.Inkscape + case CliSvgRenderKind.ReSvg: + return SvgRenderKind.ReSvg + + +@dataclass(frozen=True) +class StdOpts: + """Assemblicon standard command line options""" + + input_directory: Optional[Path] + """The path to input directory. None if you doesn't request it.""" + output_directory: Optional[Path] + """The path to output directory. None if you doesn't request it.""" + selection: Optional[list[str]] + """The list holding all name of jobs for building.""" + svg_render: CliSvgRenderKind + """The render kind for SVG file.""" + + +@dataclass(frozen=True) +class ArgParserSetup: + require_input: bool + """Whether add input path option in argument parser.""" + require_output: bool + """Whether add outputput path option in argument parser.""" + + +def create_argparser(cfg: ArgParserSetup) -> ArgumentParser: + """ + Create Assemblicon standard argument parser. + + This argument parser are filled properly with the requirement of Assemblicon itself. + It can be directly used or modified with extra options. + + :param cfg: The setup of argument parser. + :return: The built argument parser. + """ + parser = ArgumentParser( + prog="Assemblicon", + description="The Programmatic Icon Assembly Workflow.", + ) + if cfg.require_input: + parser.add_argument( + "-i", + "--input", + dest="input", + action="store", + type=Path, + required=True, + help="The path to input directory.", + metavar="DIR", + ) + if cfg.require_output: + parser.add_argument( + "-o", + "--output", + dest="output", + action="store", + type=Path, + required=True, + help="The path to output directory.", + metavar="DIR", + ) + parser.add_argument( + "-t", + "--select", + dest="selection", + action="extend", + nargs="+", + type=str, + help="Only build given name represented jobs.", + metavar="RENDER", + ) + parser.add_argument( + "-s", + "--svg-render", + dest="svg_render", + action="store", + type=CliSvgRenderKind, + default=CliSvgRenderKind.Inkscape, + help="The render used for rendering SVG into PNG.", + metavar="RENDER", + ) + return parser + + +def parse_standard_opts(cfg: ArgParserSetup, ns: Namespace) -> StdOpts: + return StdOpts( + input_directory=ns.input if cfg.require_input else None, + output_directory=ns.output if cfg.require_output else None, + selection=ns.selection, + svg_render=ns.svg_render, + ) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/__init__.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/context.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/context.py new file mode 100644 index 0000000..a36cc8d --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/context.py @@ -0,0 +1,111 @@ +from collections import deque +from dataclasses import dataclass +from typing import Callable, Iterable +from .environment import Environment +from ..logger import HIGHLEVEL_LOGGER as LOGGER + + +@dataclass(frozen=True) +class Job: + """The class represents a job.""" + + name: str + """The name of this job.""" + dependencies: set[str] + """The dependencies of this job. Each item is a job name. Empty for no dependencies.""" + executor: Callable[[Environment], None] + """The executor of this job.""" + + +class Context: + __jobs: dict[str, Job] + """The dict storing all jobs. Key is job name and value is job payload.""" + __env: Environment + """The shared environment passed to every job executor.""" + + def __init__(self, env: Environment) -> None: + self.__jobs = {} + self.__env = env + + def run(self, selected: Iterable[str] | None) -> None: + """ + Execute builder with given selections. + + The selected jobs, together with all their transitive dependencies, + are resolved and topologically sorted (Kahn's algorithm), then + executed sequentially with the shared environment. The chain is + aborted on the first failing executor. + + :param selected: Only build jobs matching these given names. + Their dependencies are always built first. None for building + all jobs. + """ + # Step 1: resolve the selection closure. Collect the selected jobs + # themselves plus all their transitive dependencies into "needed". + needed: set[str] = set() + if selected is None: + needed = set(self.__jobs.keys()) + else: + pending = list(selected) + while pending: + name = pending.pop() + if name in needed: + continue + job = self.__jobs.get(name) + if job is None: + raise ValueError(f'Unknown job name: "{name}"') + needed.add(name) + pending.extend(job.dependencies) + + # Step 2: Kahn topological sort over the sub-graph induced by "needed". + # "in_degree": key is a job name, value is how many of its (needed) + # dependencies are not built yet. + # "dependents": key is a job name, value is the list of jobs in + # "needed" that directly depend on the key job. + in_degree: dict[str, int] = {name: 0 for name in needed} + dependents: dict[str, list[str]] = {name: [] for name in needed} + for name in needed: + for dep in self.__jobs[name].dependencies: + dependents[dep].append(name) + in_degree[name] += 1 + queue = deque( + name for name in self.__jobs if name in needed and in_degree[name] == 0 + ) + order: list[str] = [] + while queue: + name = queue.popleft() + order.append(name) + for dependent in dependents[name]: + in_degree[dependent] -= 1 + if in_degree[dependent] == 0: + queue.append(dependent) + if len(order) != len(needed): + cyclic = sorted(needed - set(order)) + raise ValueError(f"Dependency cycle detected among jobs: {cyclic}") + + # Step 3: execute the build chain in resolved order. + for name in order: + LOGGER.info('Running job "%s"...', name) + self.__jobs[name].executor(self.__env) + + def register( + self, + name: str, + deps: Iterable[str], + exec: Callable[[Environment], None], + ) -> None: + """ + Register a new job with given name, dependencies and executor. + + :param name: The name of job. + :param deps: An iterable whose each item is the name of dependencies of this job. + :param exec: The executor of this job. + """ + if name in self.__jobs: + raise ValueError(f'Can not register 2 jobs with same name: "{name}"') + + job_deps = set(deps) + if name in job_deps: + raise ValueError(f'Self-dependency is not allowed in job: "{name}"') + + self.__jobs[name] = Job(name, job_deps, exec) diff --git a/tool/src/aurora_iconset_builder/assemblicon/context.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py similarity index 74% rename from tool/src/aurora_iconset_builder/assemblicon/context.py rename to tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py index 5c3d046..4d3eb81 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/context.py +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py @@ -1,18 +1,20 @@ -import logging import tempfile import os import enum -from dataclasses import dataclass -from pathlib import Path from typing import Optional from types import TracebackType -from .utils import Artifact, ArtifactFile, VANILLA_ARTIFACT_HW -from .artrender import SvgRenderKind, SvgConfig, BlenderConfig -from . import artio, artrender +from dataclasses import dataclass +from pathlib import Path +from typing import TypeVar, Generic +from .utils import Artifact, ArtifactFile, VART_HW +from ..logger import HIGHLEVEL_LOGGER as LOGGER +from ..lowlevel.artrdr import SvgRenderKind +from ..lowlevel.artrdr import SvgRenderKind, SvgConfig, BlenderConfig +from ..lowlevel import artio, artrdr @dataclass(frozen=True) -class Prerequisite: +class Prologue: input_directory: Path output_directory: Path svg_render: SvgRenderKind @@ -20,45 +22,15 @@ class Prerequisite: def __post_init__(self) -> None: if not self.input_directory.is_dir(): raise ValueError( - f"Given input path {self.input_directory} is not existing directory." + 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 existing directory." + f"Given output path {self.output_directory} is not an existing directory." ) -class Context: - __prerequisite: Prerequisite - __temp_dir: tempfile.TemporaryDirectory[str] - - def __init__(self, prerequisite: Prerequisite) -> None: - self.__prerequisite = prerequisite - self.__temp_dir = tempfile.TemporaryDirectory() - - @property - def prerequisite(self) -> Prerequisite: - return self.__prerequisite - - def input_artifact(self, *args: str) -> Path: - return self.__prerequisite.input_directory / Path(*args) - - def output_artifact(self, *args: str) -> Path: - return self.__prerequisite.output_directory / Path(*args) - - def temporary_artifact(self, *args: str) -> Path: - return Path(self.__temp_dir.name) / Path(*args) - - def __enter__(self) -> "Context": - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - self.__temp_dir.cleanup() +P = TypeVar("P", bound=Prologue) class FdContext(enum.StrEnum): @@ -97,8 +69,8 @@ _TEMP_LIB_STABLE_DIR: str = "stable" _ART_WIN: str = "windows" _ART_MAC: str = "macos" -_DEFAULT_SVG_CONFIG = artrender.SvgConfig(VANILLA_ARTIFACT_HW, VANILLA_ARTIFACT_HW) -_DEFAULT_BLD_CONFIG = artrender.BlenderConfig(VANILLA_ARTIFACT_HW, VANILLA_ARTIFACT_HW) +_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. @@ -106,38 +78,36 @@ _FD_THUMBNAIL_HWS: tuple[int, ...] = (32, 48) # _FD_THUMBNAIL_HWS: tuple[int, ...] = (16, 32, 48, 64, 128, 256) -class AdvancedContext: - __context: Context +class Environment(Generic[P]): + __prologue: P + """The prologue used for environment.""" + __temp_dir: tempfile.TemporaryDirectory[str] - def __init__(self, ctx: Context) -> None: - self.__context = ctx + def __init__(self, prologue: P) -> None: + self.__prologue = prologue # construct basic layout of temporary directory - self.__context.temporary_artifact(_TEMP_USER_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.__context.temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_ALLOC_DIR).mkdir( - parents=True, exist_ok=True - ) - self.__context.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 kind in FdContext: - self.__context.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.__context.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.__context.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) - def __enter__(self) -> "AdvancedContext": + # region: With Visitor Requirements + + def __enter__(self) -> "Environment": return self def __exit__( @@ -146,7 +116,9 @@ class AdvancedContext: exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: - self.__context.__exit__(exc_type, exc_value, traceback) + self.__temp_dir.cleanup() + + # endregion # region: New Artifact @@ -154,7 +126,7 @@ class AdvancedContext: """ Create new RGBA vanilla size (1024x1024) empty (#ffffff00) artifact. """ - return artio.new_artifact((VANILLA_ARTIFACT_HW, VANILLA_ARTIFACT_HW)) + return artio.new_artifact((VART_HW, VART_HW)) # endregion @@ -164,14 +136,14 @@ class AdvancedContext: """ Get the path to input artifact. """ - return self.__context.input_artifact(*args) + 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) - logging.info("Loading input artifact: %s", p) + LOGGER.info("Loading input artifact: %s", p) return artio.load_input_artifact(p) # endregion @@ -179,10 +151,10 @@ class AdvancedContext: # region: Temporary Artifact def __user_temporary_artifact(self, *args: str) -> Path: - return self.__context.temporary_artifact(_TEMP_USER_DIR, *args) + return self.temporary_artifact(_TEMP_USER_DIR, *args) def __lib_temporary_artifact(self, *args: str) -> Path: - return self.__context.temporary_artifact(_TEMP_LIB_DIR, *args) + return self.temporary_artifact(_TEMP_LIB_DIR, *args) def temporary_artifact(self, *args: str) -> Path: """ @@ -190,11 +162,11 @@ class AdvancedContext: All existence of sub-directory should be ensured on your own. """ - return self.__user_temporary_artifact(*args) + return Path(self.__temp_dir.name) / Path(*args) def load_temporary_artifact(self, *args: str) -> ArtifactFile: p = self.temporary_artifact(*args) - logging.info("Loading temporary artifact: %s", p) + LOGGER.info("Loading temporary artifact: %s", p) return artio.load_artifact(p) def save_temporary_artifact(self, art: Artifact, *args: str) -> None: @@ -204,7 +176,7 @@ class AdvancedContext: All existence of sub-directory should be ensured on your own. """ p = self.temporary_artifact(*args) - logging.info("Saving temporary artifact: %s", p) + LOGGER.info("Saving temporary artifact: %s", p) artio.save_artifact(art, p) def __allocate_lib_temporary_artifact(self, suffix: Optional[str] = None) -> Path: @@ -229,11 +201,11 @@ class AdvancedContext: :return: The path to destination storing render result. """ if dst is None: - logging.info("Rendering temporary SVG: %s", src) + LOGGER.info("Rendering temporary SVG: %s", src) dst = self.__allocate_lib_temporary_artifact(".png") else: - logging.info("Rendering SVG: %s -> %s", src, dst) - artrender.render_svg(src, dst, self.__context.prerequisite.svg_render, cfg) + LOGGER.info("Rendering SVG: %s -> %s", src, dst) + artrdr.render_svg(src, dst, self.__prologue.svg_render, cfg) return dst def render_svg( @@ -276,11 +248,11 @@ class AdvancedContext: """ temp_script = self.__fetch_const_lib_temporary_artifact("blender.py") if dst is None: - logging.info("Rendering temporary Blender composition: %s", src) + LOGGER.info("Rendering temporary Blender composition: %s", src) dst = self.__allocate_lib_temporary_artifact(".png") else: - logging.info("Rendering Blender composition: %s -> %s", src, dst) - artrender.render_blender(src, dst, temp_script, cfg) + LOGGER.info("Rendering Blender composition: %s -> %s", src, dst) + artrdr.render_blender(src, dst, temp_script, cfg) return dst def render_blender( @@ -315,10 +287,17 @@ class AdvancedContext: # 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.__context.output_artifact(f"{hw}x{hw}", str(kind), f"{name}.png") + 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: """ @@ -326,12 +305,12 @@ class AdvancedContext: :param name: The name of saved artifact. The file extension ".png" is not required. """ - logging.info( + LOGGER.info( 'Saving FreeDesktop artifact with kind "%s" and name "%s".', kind, name ) for hw, thumbnail in zip( - _FD_THUMBNAIL_HWS, artrender.render_thumbnail(art, _FD_THUMBNAIL_HWS) + _FD_THUMBNAIL_HWS, artrdr.render_thumbnail(art, _FD_THUMBNAIL_HWS) ): dst = self.__fd_artifact(hw, kind, name) artio.save_artifact(thumbnail, dst) @@ -339,7 +318,7 @@ class AdvancedContext: def dup_fd_artifact( self, src_kind: FdContext, src_name: str, dst_kind: FdContext, dst_name: str ) -> None: - logging.info( + LOGGER.info( 'Duplicating FreeDesktop artifact from kind "%s" name "%s" to kind "%s" name "%s".', src_kind, src_name, @@ -353,7 +332,7 @@ class AdvancedContext: ) def __win_artifact(self, kind: WinCategory, name: str) -> Path: - return self.__context.output_artifact(_ART_WIN, str(kind), f"{name}.ico") + return self.output_artifact(_ART_WIN, str(kind), f"{name}.ico") def save_win_artifact(self, art: Artifact, kind: WinCategory, name: str) -> None: """ @@ -361,15 +340,15 @@ class AdvancedContext: :param name: The name of saved artifact. The file extension ".ico" is not required. """ - logging.info( + LOGGER.info( 'Saving Windows-only artifact with kind "%s" and name "%s".', kind, name ) - artrender.render_ico(art, self.__win_artifact(kind, name)) + artrdr.render_ico(art, self.__win_artifact(kind, name)) def dup_win_artifact( self, src_kind: WinCategory, src_name: str, dst_kind: WinCategory, dst_name: str ) -> None: - logging.info( + LOGGER.info( 'Duplicating Windows-only artifact from kind "%s" name "%s" to kind "%s" name "%s".', src_kind, src_name, @@ -382,7 +361,7 @@ class AdvancedContext: ) def __mac_artifact(self, kind: MacCategory, name: str) -> Path: - return self.__context.output_artifact(_ART_MAC, str(kind), f"{name}.icns") + return self.output_artifact(_ART_MAC, str(kind), f"{name}.icns") def save_mac_artifact(self, art: Artifact, kind: MacCategory, name: str) -> None: """ @@ -390,15 +369,15 @@ class AdvancedContext: :param name: The name of saved artifact. The file extension ".icns" is not required. """ - logging.info( + LOGGER.info( 'Saving macOS-only artifact with kind "%s" and name "%s".', kind, name ) - artrender.render_icns(art, self.__mac_artifact(kind, name)) + 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: - logging.info( + LOGGER.info( 'Duplicating macOS-only artifact from kind "%s" name "%s" to kind "%s" name "%s".', src_kind, src_name, diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py new file mode 100644 index 0000000..57abbf3 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py @@ -0,0 +1,11 @@ +from ..lowlevel.utils import Artifact, ArtifactFile + +VART_HW: int = 1024 +"""The height or width of Vanilla Artifact.""" + + +def check_vanilla_artifact(art: Artifact) -> None: + if art.size != (VART_HW, VART_HW): + raise ValueError( + f"The size of general highlevel used artifact must be {VART_HW}x{VART_HW}, got {art.width}x{art.height} instead" + ) diff --git a/tool/src/aurora_iconset_builder/assemblicon/logger.py b/tool/src/aurora_iconset_builder/assemblicon/logger.py new file mode 100644 index 0000000..bead702 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/logger.py @@ -0,0 +1,20 @@ +import logging + + +class LoggerFactory: + def __init__(self) -> None: + # setup log format and level + logging.basicConfig( + format="[%(levelname)s][%(name)s] %(message)s", level=logging.INFO + ) + + def get_lowlevel_logger(self) -> logging.Logger: + return logging.getLogger("Lowlevel") + + def get_highlevel_logger(self) -> logging.Logger: + return logging.getLogger("Highlevel") + + +LOGGER_FACTORY = LoggerFactory() +LOWLEVEL_LOGGER = LOGGER_FACTORY.get_lowlevel_logger() +HIGHLEVEL_LOGGER = LOGGER_FACTORY.get_highlevel_logger() diff --git a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/__init__.py b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tool/src/aurora_iconset_builder/assemblicon/artio.py b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artio.py similarity index 56% rename from tool/src/aurora_iconset_builder/assemblicon/artio.py rename to tool/src/aurora_iconset_builder/assemblicon/lowlevel/artio.py index efd0ff3..dec1e03 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/artio.py +++ b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artio.py @@ -1,40 +1,38 @@ -import logging import shutil from pathlib import Path import PIL.Image from .utils import Artifact, ArtifactFile +from ..logger import LOWLEVEL_LOGGER as LOGGER def new_artifact(size: tuple[int, int]) -> Artifact: """ Create new RGBA empty (#ffffff00) artifact with given size. """ - return PIL.Image.new( - "RGBA", size, "#ffffff00" - ) + return PIL.Image.new("RGBA", size, "#ffffff00") def load_input_artifact(p: Path) -> ArtifactFile: """ - Load input artifact used for building temporary and output artifact. + Load bitmap artifact (PNG, JPEG or BMP) which usually is located in input directory. """ - logging.debug("Loading input artifact: %s", p) + LOGGER.debug("Loading input artifact: %s", p) return PIL.Image.open(p, "r", ["PNG", "JPEG", "BMP"]) def load_artifact(p: Path) -> ArtifactFile: """ - Load artifact saved previously as the intermediary. + Load bitmap artifact (PNG-only) saved previously as the intermediary. """ - logging.debug("Loading temporary or output artifact: %s", p) + LOGGER.debug("Loading temporary or output artifact: %s", p) return PIL.Image.open(p, "r", ["PNG"]) def save_artifact(art: Artifact, p: Path) -> None: - logging.debug("Saving artifact: %s", p) + LOGGER.debug("Saving artifact: %s", p) art.save(p, format="PNG") def dup_artifact(src: Path, dst: Path) -> None: - logging.debug("Duplicating artifact: %s -> %s", src, dst) + LOGGER.debug("Duplicating artifact: %s -> %s", src, dst) shutil.copyfile(src, dst) diff --git a/tool/src/aurora_iconset_builder/assemblicon/artproc.py b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artproc.py similarity index 100% rename from tool/src/aurora_iconset_builder/assemblicon/artproc.py rename to tool/src/aurora_iconset_builder/assemblicon/lowlevel/artproc.py diff --git a/tool/src/aurora_iconset_builder/assemblicon/artrender.py b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artrdr.py similarity index 90% rename from tool/src/aurora_iconset_builder/assemblicon/artrender.py rename to tool/src/aurora_iconset_builder/assemblicon/lowlevel/artrdr.py index 15d658a..d102a9f 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/artrender.py +++ b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artrdr.py @@ -39,7 +39,16 @@ def render_svg(src: Path, dst: Path, kind: SvgRenderKind, cfg: SvgConfig) -> Non :param kind: The selected SVG render kind. :param cfg: The configuration when rendering. """ - logging.debug("Rendering SVG artifact with render %s: %s -> %s", kind.name, src, dst) + logging.debug( + "Rendering SVG artifact with render %s: %s -> %s", kind.name, src, dst + ) + + # YYC MARK: + # Inkscape will silently quit with return code 0 + # if given source file doesn't exist in file system. + # So we need to check whether it is existing and popularize it to all render. + if not src.is_file(): + raise ValueError(f"Source SVG file is not presented in file system: {src}") match kind: case SvgRenderKind.Inkscape: @@ -78,8 +87,7 @@ def render_svg(src: Path, dst: Path, kind: SvgRenderKind, cfg: SvgConfig) -> Non proc = subprocess.run(cmd, capture_output=False) if proc.returncode != 0: raise RuntimeError( - f"Fail to execute SVG render {kind.name}. " - f"Return code is {proc.returncode}" + f"Fail to execute SVG render {kind.name}. Return code is {proc.returncode}" ) @@ -135,6 +143,11 @@ def render_blender(src: Path, dst: Path, temp_script: Path, cfg: BlenderConfig) """ logging.debug("Rendering Blender artifact: %s -> %s", src, dst) + if not src.is_file(): + raise ValueError( + f"Source Blender composition file is not presented in file system: {src}" + ) + script = _BLENDER_RENDER_SCRIPT.format( width=cfg.width, height=cfg.height, dst=repr(str(dst)) ) @@ -160,7 +173,7 @@ def render_blender(src: Path, dst: Path, temp_script: Path, cfg: BlenderConfig) def render_thumbnail(art: Artifact, hws: Iterable[int]) -> Iterator[Artifact]: """ Render given artifact as multiple resolution thumbnails. - + :param art: The artifact to be rendered. :param hws: The int iterable holding all resolution of thumbnails (square thumbnail only) :return: A generator outputing thumbnail one by one with the order of given resolution. @@ -176,7 +189,7 @@ def render_thumbnail(art: Artifact, hws: Iterable[int]) -> Iterator[Artifact]: def render_ico(art: Artifact, dst: Path) -> None: """ Render given artifact as Windows ICO in given path. - + :param art: The artifact to be rendered. :param dst: The path to rendered result. """ @@ -198,7 +211,7 @@ def render_ico(art: Artifact, dst: Path) -> None: def render_icns(art: Artifact, dst: Path) -> None: """ Render given artifact as macOS ICNS in given path. - + :param art: The artifact to be rendered. :param dst: The path to rendered result. """ diff --git a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/utils.py b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/utils.py new file mode 100644 index 0000000..46c0b2e --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/utils.py @@ -0,0 +1,5 @@ +from PIL.Image import Image +from PIL.ImageFile import ImageFile + +type Artifact = Image +type ArtifactFile = ImageFile diff --git a/tool/src/aurora_iconset_builder/assemblicon/utils.py b/tool/src/aurora_iconset_builder/assemblicon/utils.py deleted file mode 100644 index 5e80e08..0000000 --- a/tool/src/aurora_iconset_builder/assemblicon/utils.py +++ /dev/null @@ -1,20 +0,0 @@ -import logging -from PIL.Image import Image -from PIL.ImageFile import ImageFile - -type Artifact = Image -type ArtifactFile = ImageFile - - -def setup_logging() -> None: - logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.INFO) - - -VANILLA_ARTIFACT_HW: int = 1024 - - -def check_vanilla_artifact(art: Artifact) -> None: - if art.size != (VANILLA_ARTIFACT_HW, VANILLA_ARTIFACT_HW): - raise ValueError( - f"Icon art must be {VANILLA_ARTIFACT_HW}x{VANILLA_ARTIFACT_HW}, got {art.width}x{art.height} instead" - ) diff --git a/tool/src/aurora_iconset_builder/cli.py b/tool/src/aurora_iconset_builder/cli.py deleted file mode 100644 index 0b75e85..0000000 --- a/tool/src/aurora_iconset_builder/cli.py +++ /dev/null @@ -1,42 +0,0 @@ -import argparse -import enum -from dataclasses import dataclass -from .assemblicon import artrender - - -class SvgRender(enum.StrEnum): - Inkscape = "inkscape" - ReSvg = "resvg" - - def to_assemblicon_type(self) -> artrender.SvgRenderKind: - match self: - case SvgRender.Inkscape: - return artrender.SvgRenderKind.Inkscape - case SvgRender.ReSvg: - return artrender.SvgRenderKind.ReSvg - - -@dataclass(frozen=True) -class Cli: - svg_render: SvgRender - - -def parse() -> Cli: - parser = argparse.ArgumentParser( - prog="Aurora Icon Set Builder", - description="The Programmatic Icon Assembly Workflow for Aurora Icon Set.", - ) - parser.add_argument( - "-s", - "--svg-render", - dest="svg_render", - action="store", - type=SvgRender, - default=SvgRender.Inkscape, - help="The render used for rendering SVG into PNG.", - metavar="RENDER", - ) - args = parser.parse_args() - return Cli( - svg_render=args.svg_render, - )