refactor: refactor assemblicon

This commit is contained in:
2026-09-01 22:32:00 +08:00
parent 54af04a48f
commit bcac59ceb1
14 changed files with 368 additions and 175 deletions
+21 -11
View File
@@ -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)
@@ -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()
@@ -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)
@@ -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))
)
@@ -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"
)
-42
View File
@@ -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,
)