refactor: refactor assemblicon
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 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,
|
||||
@@ -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
|
||||
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)
|
||||
+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 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.
|
||||
"""
|
||||
@@ -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