refactor: refactor assemblicon
This commit is contained in:
@@ -1,32 +1,42 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from .assemblicon.context import Prerequisite, Context, AdvancedContext
|
from dataclasses import dataclass
|
||||||
from .assemblicon.utils import setup_logging
|
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 .std_icons import build_standard_icons
|
||||||
from .nostd_icons import build_non_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:
|
def main() -> None:
|
||||||
opts = parse()
|
parser_cfg = ArgParserSetup(require_input=False, require_output=False)
|
||||||
|
parser = create_argparser(parser_cfg)
|
||||||
setup_logging()
|
ns = parser.parse_args()
|
||||||
|
opts = parse_standard_opts(parser_cfg, ns)
|
||||||
|
|
||||||
repository_root = Path(__file__).resolve().parent.parent.parent.parent
|
repository_root = Path(__file__).resolve().parent.parent.parent.parent
|
||||||
iconset_input = repository_root / "src"
|
iconset_input = repository_root / "src"
|
||||||
iconset_output = repository_root / "artifact"
|
iconset_output = repository_root / "artifact"
|
||||||
prereq = Prerequisite(
|
prologue = AuroraPrologue(
|
||||||
iconset_input, iconset_output, opts.svg_render.to_assemblicon_type()
|
iconset_input, iconset_output, opts.svg_render.to_assemblicon_type()
|
||||||
)
|
)
|
||||||
ctx = Context(prereq)
|
env = Environment(prologue)
|
||||||
advctx = AdvancedContext(ctx)
|
ctx = Context(env)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_build_icons(advctx)
|
register(ctx)
|
||||||
|
ctx.run(opts.selection)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logging.fatal("Runtime error: %s", e)
|
logging.fatal("Runtime error: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def _build_icons(ctx: AdvancedContext) -> None:
|
def register(ctx: Context) -> None:
|
||||||
build_standard_icons(ctx)
|
build_standard_icons(ctx)
|
||||||
build_non_standard_icons(ctx)
|
build_non_standard_icons(ctx)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
+65
-86
@@ -1,18 +1,20 @@
|
|||||||
import logging
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
import enum
|
import enum
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from .utils import Artifact, ArtifactFile, VANILLA_ARTIFACT_HW
|
from dataclasses import dataclass
|
||||||
from .artrender import SvgRenderKind, SvgConfig, BlenderConfig
|
from pathlib import Path
|
||||||
from . import artio, artrender
|
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)
|
@dataclass(frozen=True)
|
||||||
class Prerequisite:
|
class Prologue:
|
||||||
input_directory: Path
|
input_directory: Path
|
||||||
output_directory: Path
|
output_directory: Path
|
||||||
svg_render: SvgRenderKind
|
svg_render: SvgRenderKind
|
||||||
@@ -20,45 +22,15 @@ class Prerequisite:
|
|||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not self.input_directory.is_dir():
|
if not self.input_directory.is_dir():
|
||||||
raise ValueError(
|
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():
|
if not self.output_directory.is_dir():
|
||||||
raise ValueError(
|
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:
|
P = TypeVar("P", bound=Prologue)
|
||||||
__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()
|
|
||||||
|
|
||||||
|
|
||||||
class FdContext(enum.StrEnum):
|
class FdContext(enum.StrEnum):
|
||||||
@@ -97,8 +69,8 @@ _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 = artrender.SvgConfig(VANILLA_ARTIFACT_HW, VANILLA_ARTIFACT_HW)
|
_DEFAULT_SVG_CONFIG = artrdr.SvgConfig(VART_HW, VART_HW)
|
||||||
_DEFAULT_BLD_CONFIG = artrender.BlenderConfig(VANILLA_ARTIFACT_HW, VANILLA_ARTIFACT_HW)
|
_DEFAULT_BLD_CONFIG = artrdr.BlenderConfig(VART_HW, VART_HW)
|
||||||
|
|
||||||
# TODO: For easy testing, we only build 32x32 and 48x48.
|
# TODO: For easy testing, we only build 32x32 and 48x48.
|
||||||
# Enable full generation after testing.
|
# 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)
|
# _FD_THUMBNAIL_HWS: tuple[int, ...] = (16, 32, 48, 64, 128, 256)
|
||||||
|
|
||||||
|
|
||||||
class AdvancedContext:
|
class Environment(Generic[P]):
|
||||||
__context: Context
|
__prologue: P
|
||||||
|
"""The prologue used for environment."""
|
||||||
|
__temp_dir: tempfile.TemporaryDirectory[str]
|
||||||
|
|
||||||
def __init__(self, ctx: Context) -> None:
|
def __init__(self, prologue: P) -> None:
|
||||||
self.__context = ctx
|
self.__prologue = prologue
|
||||||
|
|
||||||
# construct basic layout of temporary directory
|
# 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
|
parents=True, exist_ok=True
|
||||||
)
|
)
|
||||||
self.__context.temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_ALLOC_DIR).mkdir(
|
self.temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_STABLE_DIR).mkdir(
|
||||||
parents=True, exist_ok=True
|
|
||||||
)
|
|
||||||
self.__context.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.__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
|
parents=True, exist_ok=True
|
||||||
)
|
)
|
||||||
for kind in WinCategory:
|
for kind in WinCategory:
|
||||||
self.__context.output_artifact(_ART_WIN, str(kind)).mkdir(
|
self.output_artifact(_ART_WIN, str(kind)).mkdir(parents=True, exist_ok=True)
|
||||||
parents=True, exist_ok=True
|
|
||||||
)
|
|
||||||
for kind in MacCategory:
|
for kind in MacCategory:
|
||||||
self.__context.output_artifact(_ART_MAC, str(kind)).mkdir(
|
self.output_artifact(_ART_MAC, str(kind)).mkdir(parents=True, exist_ok=True)
|
||||||
parents=True, exist_ok=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def __enter__(self) -> "AdvancedContext":
|
# region: With Visitor Requirements
|
||||||
|
|
||||||
|
def __enter__(self) -> "Environment":
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(
|
def __exit__(
|
||||||
@@ -146,7 +116,9 @@ class AdvancedContext:
|
|||||||
exc_value: BaseException | None,
|
exc_value: BaseException | None,
|
||||||
traceback: TracebackType | None,
|
traceback: TracebackType | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.__context.__exit__(exc_type, exc_value, traceback)
|
self.__temp_dir.cleanup()
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
# region: New Artifact
|
# region: New Artifact
|
||||||
|
|
||||||
@@ -154,7 +126,7 @@ class AdvancedContext:
|
|||||||
"""
|
"""
|
||||||
Create new RGBA vanilla size (1024x1024) empty (#ffffff00) artifact.
|
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
|
# endregion
|
||||||
|
|
||||||
@@ -164,14 +136,14 @@ class AdvancedContext:
|
|||||||
"""
|
"""
|
||||||
Get the path to input artifact.
|
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:
|
def load_input_bitmap_artifact(self, *args: str) -> ArtifactFile:
|
||||||
"""
|
"""
|
||||||
Load given input bitmap asset.
|
Load given input bitmap asset.
|
||||||
"""
|
"""
|
||||||
p = self.input_artifact(*args)
|
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)
|
return artio.load_input_artifact(p)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
@@ -179,10 +151,10 @@ class AdvancedContext:
|
|||||||
# region: Temporary Artifact
|
# region: Temporary Artifact
|
||||||
|
|
||||||
def __user_temporary_artifact(self, *args: str) -> Path:
|
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:
|
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:
|
def temporary_artifact(self, *args: str) -> Path:
|
||||||
"""
|
"""
|
||||||
@@ -190,11 +162,11 @@ class AdvancedContext:
|
|||||||
|
|
||||||
All existence of sub-directory should be ensured on your own.
|
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:
|
def load_temporary_artifact(self, *args: str) -> ArtifactFile:
|
||||||
p = self.temporary_artifact(*args)
|
p = self.temporary_artifact(*args)
|
||||||
logging.info("Loading temporary artifact: %s", p)
|
LOGGER.info("Loading temporary artifact: %s", p)
|
||||||
return artio.load_artifact(p)
|
return artio.load_artifact(p)
|
||||||
|
|
||||||
def save_temporary_artifact(self, art: Artifact, *args: str) -> None:
|
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.
|
All existence of sub-directory should be ensured on your own.
|
||||||
"""
|
"""
|
||||||
p = self.temporary_artifact(*args)
|
p = self.temporary_artifact(*args)
|
||||||
logging.info("Saving temporary artifact: %s", p)
|
LOGGER.info("Saving temporary artifact: %s", p)
|
||||||
artio.save_artifact(art, 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:
|
||||||
@@ -229,11 +201,11 @@ class AdvancedContext:
|
|||||||
:return: The path to destination storing render result.
|
:return: The path to destination storing render result.
|
||||||
"""
|
"""
|
||||||
if dst is None:
|
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")
|
dst = self.__allocate_lib_temporary_artifact(".png")
|
||||||
else:
|
else:
|
||||||
logging.info("Rendering SVG: %s -> %s", src, dst)
|
LOGGER.info("Rendering SVG: %s -> %s", src, dst)
|
||||||
artrender.render_svg(src, dst, self.__context.prerequisite.svg_render, cfg)
|
artrdr.render_svg(src, dst, self.__prologue.svg_render, cfg)
|
||||||
return dst
|
return dst
|
||||||
|
|
||||||
def render_svg(
|
def render_svg(
|
||||||
@@ -276,11 +248,11 @@ class AdvancedContext:
|
|||||||
"""
|
"""
|
||||||
temp_script = self.__fetch_const_lib_temporary_artifact("blender.py")
|
temp_script = self.__fetch_const_lib_temporary_artifact("blender.py")
|
||||||
if dst is None:
|
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")
|
dst = self.__allocate_lib_temporary_artifact(".png")
|
||||||
else:
|
else:
|
||||||
logging.info("Rendering Blender composition: %s -> %s", src, dst)
|
LOGGER.info("Rendering Blender composition: %s -> %s", src, dst)
|
||||||
artrender.render_blender(src, dst, temp_script, cfg)
|
artrdr.render_blender(src, dst, temp_script, cfg)
|
||||||
return dst
|
return dst
|
||||||
|
|
||||||
def render_blender(
|
def render_blender(
|
||||||
@@ -315,10 +287,17 @@ class AdvancedContext:
|
|||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
# region: Output Artifact
|
||||||
|
|
||||||
|
def output_artifact(self, *args: str) -> Path:
|
||||||
|
return self.__prologue.output_directory / Path(*args)
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
# region: Advanced Saver
|
# region: Advanced Saver
|
||||||
|
|
||||||
def __fd_artifact(self, hw: int, kind: FdContext, name: str) -> Path:
|
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:
|
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.
|
: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
|
'Saving FreeDesktop artifact with kind "%s" and name "%s".', kind, name
|
||||||
)
|
)
|
||||||
|
|
||||||
for hw, thumbnail in zip(
|
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)
|
dst = self.__fd_artifact(hw, kind, name)
|
||||||
artio.save_artifact(thumbnail, dst)
|
artio.save_artifact(thumbnail, dst)
|
||||||
@@ -339,7 +318,7 @@ class AdvancedContext:
|
|||||||
def dup_fd_artifact(
|
def dup_fd_artifact(
|
||||||
self, src_kind: FdContext, src_name: str, dst_kind: FdContext, dst_name: str
|
self, src_kind: FdContext, src_name: str, dst_kind: FdContext, dst_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
logging.info(
|
LOGGER.info(
|
||||||
'Duplicating FreeDesktop artifact from kind "%s" name "%s" to kind "%s" name "%s".',
|
'Duplicating FreeDesktop artifact from kind "%s" name "%s" to kind "%s" name "%s".',
|
||||||
src_kind,
|
src_kind,
|
||||||
src_name,
|
src_name,
|
||||||
@@ -353,7 +332,7 @@ class AdvancedContext:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __win_artifact(self, kind: WinCategory, name: str) -> Path:
|
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:
|
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.
|
: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
|
'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(
|
def dup_win_artifact(
|
||||||
self, src_kind: WinCategory, src_name: str, dst_kind: WinCategory, dst_name: str
|
self, src_kind: WinCategory, src_name: str, dst_kind: WinCategory, dst_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
logging.info(
|
LOGGER.info(
|
||||||
'Duplicating Windows-only artifact from kind "%s" name "%s" to kind "%s" name "%s".',
|
'Duplicating Windows-only artifact from kind "%s" name "%s" to kind "%s" name "%s".',
|
||||||
src_kind,
|
src_kind,
|
||||||
src_name,
|
src_name,
|
||||||
@@ -382,7 +361,7 @@ class AdvancedContext:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __mac_artifact(self, kind: MacCategory, name: str) -> Path:
|
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:
|
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.
|
: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
|
'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(
|
def dup_mac_artifact(
|
||||||
self, src_kind: MacCategory, src_name: str, dst_kind: MacCategory, dst_name: str
|
self, src_kind: MacCategory, src_name: str, dst_kind: MacCategory, dst_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
logging.info(
|
LOGGER.info(
|
||||||
'Duplicating macOS-only artifact from kind "%s" name "%s" to kind "%s" name "%s".',
|
'Duplicating macOS-only artifact from kind "%s" name "%s" to kind "%s" name "%s".',
|
||||||
src_kind,
|
src_kind,
|
||||||
src_name,
|
src_name,
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
+8
-10
@@ -1,40 +1,38 @@
|
|||||||
import logging
|
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import PIL.Image
|
import PIL.Image
|
||||||
from .utils import Artifact, ArtifactFile
|
from .utils import Artifact, ArtifactFile
|
||||||
|
from ..logger import LOWLEVEL_LOGGER as LOGGER
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
return PIL.Image.new(
|
return PIL.Image.new("RGBA", size, "#ffffff00")
|
||||||
"RGBA", size, "#ffffff00"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_input_artifact(p: Path) -> ArtifactFile:
|
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"])
|
return PIL.Image.open(p, "r", ["PNG", "JPEG", "BMP"])
|
||||||
|
|
||||||
|
|
||||||
def load_artifact(p: Path) -> ArtifactFile:
|
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"])
|
return PIL.Image.open(p, "r", ["PNG"])
|
||||||
|
|
||||||
|
|
||||||
def save_artifact(art: Artifact, p: Path) -> None:
|
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")
|
art.save(p, format="PNG")
|
||||||
|
|
||||||
|
|
||||||
def dup_artifact(src: Path, dst: Path) -> None:
|
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)
|
shutil.copyfile(src, dst)
|
||||||
+19
-6
@@ -39,7 +39,16 @@ def render_svg(src: Path, dst: Path, kind: SvgRenderKind, cfg: SvgConfig) -> Non
|
|||||||
:param kind: The selected SVG render kind.
|
:param kind: The selected SVG render kind.
|
||||||
:param cfg: The configuration when rendering.
|
: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:
|
match kind:
|
||||||
case SvgRenderKind.Inkscape:
|
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)
|
proc = subprocess.run(cmd, capture_output=False)
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Fail to execute SVG render {kind.name}. "
|
f"Fail to execute SVG render {kind.name}. Return code is {proc.returncode}"
|
||||||
f"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)
|
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(
|
script = _BLENDER_RENDER_SCRIPT.format(
|
||||||
width=cfg.width, height=cfg.height, dst=repr(str(dst))
|
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]:
|
def render_thumbnail(art: Artifact, hws: Iterable[int]) -> Iterator[Artifact]:
|
||||||
"""
|
"""
|
||||||
Render given artifact as multiple resolution thumbnails.
|
Render given artifact as multiple resolution thumbnails.
|
||||||
|
|
||||||
:param art: The artifact to be rendered.
|
:param art: The artifact to be rendered.
|
||||||
:param hws: The int iterable holding all resolution of thumbnails (square thumbnail only)
|
: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.
|
: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:
|
def render_ico(art: Artifact, dst: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Render given artifact as Windows ICO in given path.
|
Render given artifact as Windows ICO in given path.
|
||||||
|
|
||||||
:param art: The artifact to be rendered.
|
:param art: The artifact to be rendered.
|
||||||
:param dst: The path to rendered result.
|
: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:
|
def render_icns(art: Artifact, dst: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Render given artifact as macOS ICNS in given path.
|
Render given artifact as macOS ICNS in given path.
|
||||||
|
|
||||||
:param art: The artifact to be rendered.
|
:param art: The artifact to be rendered.
|
||||||
:param dst: The path to rendered result.
|
:param dst: The path to rendered result.
|
||||||
"""
|
"""
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from PIL.Image import Image
|
||||||
|
from PIL.ImageFile import ImageFile
|
||||||
|
|
||||||
|
type Artifact = Image
|
||||||
|
type ArtifactFile = ImageFile
|
||||||
@@ -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"
|
|
||||||
)
|
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user