diff --git a/tool/src/aurora_iconset_builder/assemblicon/artio.py b/tool/src/aurora_iconset_builder/assemblicon/artio.py index 63b433c..186a7a3 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/artio.py +++ b/tool/src/aurora_iconset_builder/assemblicon/artio.py @@ -1,19 +1,25 @@ import logging from pathlib import Path import PIL.Image -import PIL.ImageFile +from .utils import Artifact, ArtifactFile -def load_asset(p: Path) -> PIL.ImageFile.ImageFile: +def load_input_artifact(p: Path) -> ArtifactFile: + """ + Load input artifact used for building temporary and output artifact. + """ logging.info("Loading asset: %s", p) return PIL.Image.open(p, "r", ["PNG", "JPEG", "BMP"]) -def load_artifact(p: Path) -> PIL.ImageFile.ImageFile: +def load_artifact(p: Path) -> ArtifactFile: + """ + Load artifact saved previously as the intermediary. + """ logging.info("Loading artifact: %s", p) return PIL.Image.open(p, "r", ["PNG"]) -def save_artifact(art: PIL.Image.Image, p: Path) -> None: +def save_artifact(art: Artifact, p: Path) -> None: logging.info("Saving artifact: %s", p) art.save(p, format="PNG") diff --git a/tool/src/aurora_iconset_builder/assemblicon/artrender.py b/tool/src/aurora_iconset_builder/assemblicon/artrender.py new file mode 100644 index 0000000..0d625f4 --- /dev/null +++ b/tool/src/aurora_iconset_builder/assemblicon/artrender.py @@ -0,0 +1,211 @@ +import logging +import os +import enum +import subprocess +from typing import Iterable, Iterator +from pathlib import Path +from dataclasses import dataclass +import PIL.Image +from .utils import Artifact, check_vanilla_artifact + + +class SvgRenderKind(enum.IntEnum): + Inkscape = enum.auto() + ReSvg = enum.auto() + + +@dataclass +class SvgConfig: + width: int + height: int + + def __post_init__(self) -> None: + if self.width <= 0: + raise ValueError( + f"The image width in Inkscape config must be a positive integer" + ) + if self.height <= 0: + raise ValueError( + f"The image height in Inkscape config must be a positive integer" + ) + + +def render_svg(src: Path, dst: Path, kind: SvgRenderKind, cfg: SvgConfig) -> None: + """ + Render given SVG file to PNG file. + + :param src: The path to SVG file to be rendered + :param dst: The path to rendered PNG file. + :param kind: The selected SVG render kind. + :param cfg: The configuration when rendering. + """ + logging.info("Rendering SVG artifact with render {kind.name}: %s -> %s", src, dst) + + match kind: + case SvgRenderKind.Inkscape: + inkscape_bin = os.getenv("ASSEMBLICON_INKSCAPE", "inkscape") + cmd = [ + inkscape_bin, + "--export-area-page", + "--export-width", + str(cfg.width), + "--export-height", + str(cfg.height), + "--export-type", + "png", + "--export-png-color-mode", + "RGBA_8", + "--export-background-opacity", + "0.0", + "--export-png-use-dithering", + "false", + "--export-filename", + str(dst), + str(src), + ] + case SvgRenderKind.ReSvg: + resvg_bin = os.getenv("ASSEMBLICON_RESVG", "resvg") + cmd = [ + resvg_bin, + "--width", + str(cfg.width), + "--height", + str(cfg.height), + str(src), + str(dst), + ] + + 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}" + ) + + +@dataclass +class BlenderConfig: + width: int + height: int + + def __post_init__(self) -> None: + if self.width <= 0: + raise ValueError( + f"The image width in Blender config must be a positive integer" + ) + if self.height <= 0: + raise ValueError( + f"The image height in Blender config must be a positive integer" + ) + + +_BLENDER_RENDER_SCRIPT = """\ +import sys +import traceback + +try: + import bpy + + render = bpy.context.scene.render + render.resolution_x = {width} + render.resolution_y = {height} + render.resolution_percentage = 100 + render.image_settings.file_format = "PNG" + render.image_settings.color_mode = "RGBA" + render.image_settings.color_depth = "8" + render.film_transparent = True + render.use_file_extension = False + render.filepath = {dst} + bpy.ops.render.render(write_still=True) +except BaseException: + traceback.print_exc() + sys.exit(1) +""" + + +def render_blender(src: Path, dst: Path, temp_script: Path, cfg: BlenderConfig) -> None: + """ + Render given Blender composition into PNG file. + + :param src: The path to Blender composition file to be rendered + :param dst: The path to rendered PNG file. + :param temp_script: The path to temporary Python script instructing Blender rendering. + The caller must make sure that this file is immutable during calling this function. + :param cfg: The configuration when rendering. + """ + logging.info("Rendering Blender artifact: %s -> %s", src, dst) + + script = _BLENDER_RENDER_SCRIPT.format( + width=cfg.width, height=cfg.height, dst=repr(str(dst)) + ) + temp_script.write_text(script, encoding="utf-8") + + blender_bin = os.getenv("ASSEMBLICON_BLENDER", "blender") + cmd = [ + blender_bin, + "--background", + str(src), + "--python", + str(temp_script), + ] + proc = subprocess.run(cmd, capture_output=False) + if proc.returncode != 0: + raise RuntimeError(f"Fail to execute Blender. Return code is {proc.returncode}") + if not dst.exists(): + raise RuntimeError( + f"Blender exited successfully but output file {dst} is missing" + ) + + +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. + """ + check_vanilla_artifact(art) + for hw in hws: + logging.info("Rendering thumbnail %dx%d.", hw, hw) + thumbnail = art.copy() + thumbnail.thumbnail((hw, hw), PIL.Image.Resampling.LANCZOS) + yield thumbnail + + +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. + """ + logging.info("Rendering .ICO artifact: -> %s", dst) + + check_vanilla_artifact(art) + sizes = [16, 32, 48, 64, 128, 256] + # provide every frame explicitly so that we never depend on + # undocumented plugin-side resizing behavior + frames = list(render_thumbnail(art, sizes)) + art.save( + dst, + format="ICO", + sizes=[(size, size) for size in sizes], + append_images=frames, + ) + + +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. + """ + logging.info("Rendering .ICNS artifact: -> %s", dst) + + check_vanilla_artifact(art) + # provide every smaller frame explicitly; the 1024x1024 entry is + # covered by the art itself + frames = list(render_thumbnail(art, (512, 256, 128, 64, 32))) + art.save(dst, format="ICNS", append_images=frames) diff --git a/tool/src/aurora_iconset_builder/assemblicon/context.py b/tool/src/aurora_iconset_builder/assemblicon/context.py index adcf5c2..83f7866 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/context.py +++ b/tool/src/aurora_iconset_builder/assemblicon/context.py @@ -1,36 +1,53 @@ +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 + + +@dataclass(frozen=True) +class Prerequisite: + input_directory: Path + output_directory: Path + svg_render: SvgRenderKind + + def __post_init__(self) -> None: + if not self.input_directory.is_dir(): + raise ValueError( + f"Given input path {self.input_directory} is not existing directory." + ) + if not self.output_directory.is_dir(): + raise ValueError( + f"Given output path {self.output_directory} is not existing directory." + ) class Context: - - __input_dir: Path - __output_dir: Path + __prerequisite: Prerequisite __temp_dir: tempfile.TemporaryDirectory[str] - def __init__(self, input_dir: Path, output_dir: Path) -> None: - self.__input_dir = input_dir - self.__output_dir = output_dir + 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.__input_dir / Path(*args) + return self.__prerequisite.input_directory / Path(*args) def output_artifact(self, *args: str) -> Path: - p = self.__output_dir / Path(*args) - p.parent.mkdir(parents=True, exist_ok=True) - return p + return self.__prerequisite.output_directory / Path(*args) def temporary_artifact(self, *args: str) -> Path: - p = Path(self.__temp_dir.name) / "user" / Path(*args) - p.parent.mkdir(parents=True, exist_ok=True) - return p - - def _intern_temporary_artifact(self, *args: str) -> Path: - p = Path(self.__temp_dir.name) / "assemblicon" / Path(*args) - p.parent.mkdir(parents=True, exist_ok=True) - return p + return Path(self.__temp_dir.name) / Path(*args) def __enter__(self) -> "Context": return self @@ -42,3 +59,214 @@ class Context: traceback: TracebackType | None, ) -> None: self.__temp_dir.cleanup() + + +class FdContext(enum.StrEnum): + Actions = "actions" + Animations = "animations" + Applications = "apps" + Categories = "categories" + Devices = "devices" + Emblems = "emblems" + Emotes = "emotes" + International = "intl" + MimeTypes = "mimetypes" + Places = "places" + Status = "status" + + +class WinCategory(enum.StrEnum): + Application = "apps" + """Icons for applications""" + Extension = "exts" + """Icons for file extensions""" + + +class MacCategory(enum.StrEnum): + Application = "apps" + """Icons for applications""" + Extension = "exts" + """Icons for file extensions""" + + +_TEMP_USER_DIR: str = "user" +_TEMP_LIB_DIR: str = "assemblicon" +_TEMP_LIB_ALLOC_DIR: str = "alloc" +_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) + +# TODO: For easy testing, we only build 48x48. +# Enable full generation after testing. +_FD_THUMBNAIL_HWS: tuple[int, ...] = (48,) +# _FD_THUMBNAIL_HWS: tuple[int, ...] = (16, 32, 48, 64, 128, 256) + + +class AdvancedContext: + __context: Context + + def __init__(self, ctx: Context) -> None: + self.__context = ctx + + # construct basic layout of temporary directory and output directory. + + def __enter__(self) -> "AdvancedContext": + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.__context.__exit__(exc_type, exc_value, traceback) + + # region: Input Artifact + + def input_artifact(self, *args: str) -> Path: + """ + Get the path to input artifact. + """ + return self.__context.input_artifact(*args) + + def load_input_bitmap_artifact(self, *args: str) -> ArtifactFile: + """ + Load given input bitmap asset. + """ + return artio.load_input_artifact(self.input_artifact(*args)) + + # endregion + + # region: Temporary Artifact + + def __user_temporary_artifact(self, *args: str) -> Path: + return self.__context.temporary_artifact(_TEMP_USER_DIR, *args) + + def __lib_temporary_artifact(self, *args: str) -> Path: + return self.__context.temporary_artifact(_TEMP_LIB_DIR, *args) + + def temporary_artifact(self, *args: str) -> Path: + """ + Get the path to temporary artifact. + + All existence of sub-directory should be ensured on your own. + """ + return self.__user_temporary_artifact(*args) + + def load_temporary_artifact(self, *args: str) -> ArtifactFile: + return artio.load_artifact(self.temporary_artifact(*args)) + + def save_temporary_artifact(self, art: Artifact, *args: str) -> None: + """ + Save given artifact as temporary artifact. + + All existence of sub-directory should be ensured on your own. + """ + artio.save_artifact(art, self.temporary_artifact(*args)) + + def __allocate_lib_temporary_artifact(self, suffix: Optional[str] = None) -> Path: + p = self.__lib_temporary_artifact(_TEMP_LIB_ALLOC_DIR) + (fd, pf) = tempfile.mkstemp(suffix=suffix, prefix=None, dir=str(p), text=False) + os.close(fd) + return Path(pf) + + def __fetch_const_lib_temporary_artifact(self, name: str) -> Path: + return self.__lib_temporary_artifact(_TEMP_LIB_STABLE_DIR, name) + + # endregion + + # region: Render Wrapper + + def __intern_render_svg( + self, src: Path, dst: Optional[Path], cfg: SvgConfig + ) -> Path: + if dst is None: + dst = self.__allocate_lib_temporary_artifact(".png") + artrender.render_svg(src, dst, self.__context.prerequisite.svg_render, cfg) + return dst + + def render_svg( + self, src: Path, dst: Optional[Path], cfg: SvgConfig = _DEFAULT_SVG_CONFIG + ) -> None: + self.__intern_render_svg(src, dst, cfg) + + def render_svg_then_load( + self, src: Path, dst: Optional[Path], cfg: SvgConfig = _DEFAULT_SVG_CONFIG + ) -> Artifact: + dst = self.__intern_render_svg(src, dst, cfg) + return artio.load_artifact(dst) + + def __intern_render_blender( + self, src: Path, dst: Optional[Path], cfg: BlenderConfig + ) -> Path: + temp_script = self.__fetch_const_lib_temporary_artifact("blender.py") + if dst is None: + dst = self.__allocate_lib_temporary_artifact(".png") + artrender.render_blender(src, dst, temp_script, cfg) + return dst + + def render_blender( + self, src: Path, dst: Optional[Path], cfg: BlenderConfig = _DEFAULT_BLD_CONFIG + ) -> None: + self.__intern_render_blender(src, dst, cfg) + + def render_blender_then_load( + self, src: Path, dst: Optional[Path], cfg: BlenderConfig = _DEFAULT_BLD_CONFIG + ) -> Artifact: + dst = self.__intern_render_blender(src, dst, cfg) + return artio.load_artifact(dst) + + # 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") + + def save_fd_artifact(self, art: Artifact, kind: FdContext, name: str) -> None: + """ + Save icon following FreeDesktop icon theme specification. + + :param name: The name of saved artifact. The file extension ".png" is not required. + """ + logging.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) + ): + dst = self.__fd_artifact(hw, kind, name) + artio.save_artifact(thumbnail, dst) + + def __win_artifact(self, kind: WinCategory, name: str) -> Path: + return self.__context.output_artifact(_ART_WIN, str(kind), f"{name}.ico") + + def save_win_artifact(self, art: Artifact, kind: WinCategory, name: str) -> None: + """ + Save icon for Windows-only + + :param name: The name of saved artifact. The file extension ".ico" is not required. + """ + logging.info( + "Saving Windows-only artifact with kind %s and name %s.", kind, name + ) + artrender.render_ico(art, self.__win_artifact(kind, name)) + + def __mac_artifact(self, kind: MacCategory, name: str) -> Path: + return self.__context.output_artifact(_ART_MAC, str(kind), f"{name}.icns") + + def save_mac_artifact(self, art: Artifact, kind: MacCategory, name: str) -> None: + """ + Save icon for macOS-only + + :param name: The name of saved artifact. The file extension ".icns" is not required. + """ + logging.info("Saving macOS-only artifact with kind %s and name %s.", kind, name) + artrender.render_icns(art, self.__mac_artifact(kind, name)) + + # endregion diff --git a/tool/src/aurora_iconset_builder/assemblicon/render.py b/tool/src/aurora_iconset_builder/assemblicon/render.py deleted file mode 100644 index 2b94927..0000000 --- a/tool/src/aurora_iconset_builder/assemblicon/render.py +++ /dev/null @@ -1,176 +0,0 @@ -import logging -import os -import subprocess -from pathlib import Path -from dataclasses import dataclass -import PIL.Image -import PIL.ImageFile -from .context import Context -from .utils import Artifact -from .artio import load_artifact - - -@dataclass -class InkscapeConfig: - width: int - height: int - - def __post_init__(self) -> None: - if self.width <= 0: - raise ValueError( - f"The image width in Inkscape config must be a positive integer" - ) - if self.height <= 0: - raise ValueError( - f"The image height in Inkscape config must be a positive integer" - ) - -# TODO: Add "resvg" alternative for Inkscape -# because Inkscape is too slow. - -def inkscape_render( - src: Path, dst: Path, cfg: InkscapeConfig -) -> PIL.ImageFile.ImageFile: - logging.info("Rendering Inkscape artifact: %s -> %s", src, dst) - - inkscape_bin = os.getenv("ASSEMBLICON_INKSCAPE", "inkscape") - cmd = [ - inkscape_bin, - "--export-area-page", - "--export-width", - str(cfg.width), - "--export-height", - str(cfg.height), - "--export-type", - "png", - "--export-png-color-mode", - "RGBA_8", - "--export-background-opacity", - "0.0", - "--export-png-use-dithering", - "false", - "--export-filename", - str(dst), - str(src), - ] - proc = subprocess.run(cmd, capture_output=False) - if proc.returncode != 0: - raise RuntimeError( - f"Fail to execute Inkscape. Return code is {proc.returncode}" - ) - - return load_artifact(dst) - - -@dataclass -class BlenderConfig: - width: int - height: int - - def __post_init__(self) -> None: - if self.width <= 0: - raise ValueError( - f"The image width in Blender config must be a positive integer" - ) - if self.height <= 0: - raise ValueError( - f"The image height in Blender config must be a positive integer" - ) - - -_BLENDER_RENDER_SCRIPT = """\ -import sys -import traceback - -try: - import bpy - - render = bpy.context.scene.render - render.resolution_x = {width} - render.resolution_y = {height} - render.resolution_percentage = 100 - render.image_settings.file_format = "PNG" - render.image_settings.color_mode = "RGBA" - render.image_settings.color_depth = "8" - render.film_transparent = True - render.use_file_extension = False - render.filepath = {dst} - bpy.ops.render.render(write_still=True) -except BaseException: - traceback.print_exc() - sys.exit(1) -""" - - -def blender_render( - ctx: Context, src: Path, dst: Path, cfg: BlenderConfig -) -> PIL.ImageFile.ImageFile: - logging.info("Rendering Blender artifact: %s -> %s", src, dst) - - script = _BLENDER_RENDER_SCRIPT.format( - width=cfg.width, height=cfg.height, dst=repr(str(dst)) - ) - script_path = ctx._intern_temporary_artifact("blender_render.py") - script_path.write_text(script, encoding="utf-8") - - blender_bin = os.getenv("ASSEMBLICON_BLENDER", "blender") - cmd = [ - blender_bin, - "--background", - str(src), - "--python", - str(script_path), - ] - proc = subprocess.run(cmd, capture_output=False) - if proc.returncode != 0: - raise RuntimeError(f"Fail to execute Blender. Return code is {proc.returncode}") - if not dst.exists(): - raise RuntimeError( - f"Blender exited successfully but output file {dst} is missing" - ) - - return load_artifact(dst) - - -_ICON_ART_SIZE = 1024 - - -def _ensure_1024_art(art: Artifact) -> None: - if art.size != (_ICON_ART_SIZE, _ICON_ART_SIZE): - raise ValueError( - f"Icon art must be {_ICON_ART_SIZE}x{_ICON_ART_SIZE}, " - f"got {art.width}x{art.height} instead" - ) - - -def _scaled_frame(art: Artifact, size: int) -> Artifact: - _ensure_1024_art(art) - frame = art.copy() - frame.thumbnail((size, size), PIL.Image.Resampling.LANCZOS) - return frame - - -def ico_render(ctx: Context, art: Artifact, dst: Path) -> None: - logging.info("Rendering .ICO artifact via Pillow: -> %s", dst) - - _ensure_1024_art(art) - sizes = [16, 32, 48, 64, 128, 256] - # provide every frame explicitly so that we never depend on - # undocumented plugin-side resizing behavior - frames = [_scaled_frame(art, size) for size in sizes] - art.save( - dst, - format="ICO", - sizes=[(size, size) for size in sizes], - append_images=frames, - ) - - -def icns_render(ctx: Context, art: Artifact, dst: Path) -> None: - logging.info("Rendering .ICNS artifact via Pillow: -> %s", dst) - - _ensure_1024_art(art) - # provide every smaller frame explicitly; the 1024x1024 entry is - # covered by the art itself - frames = [_scaled_frame(art, size) for size in (512, 256, 128, 64, 32)] - art.save(dst, format="ICNS", append_images=frames) diff --git a/tool/src/aurora_iconset_builder/assemblicon/utils.py b/tool/src/aurora_iconset_builder/assemblicon/utils.py index e83f3f2..5e80e08 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/utils.py +++ b/tool/src/aurora_iconset_builder/assemblicon/utils.py @@ -1,7 +1,20 @@ import logging from PIL.Image import Image +from PIL.ImageFile import ImageFile type Artifact = Image +type ArtifactFile = ImageFile + def setup_logging() -> None: logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.INFO) + + +VANILLA_ARTIFACT_HW: int = 1024 + + +def check_vanilla_artifact(art: Artifact) -> None: + if art.size != (VANILLA_ARTIFACT_HW, VANILLA_ARTIFACT_HW): + raise ValueError( + f"Icon art must be {VANILLA_ARTIFACT_HW}x{VANILLA_ARTIFACT_HW}, got {art.width}x{art.height} instead" + ) diff --git a/tool/src/aurora_iconset_builder/ext_icons/image_icons.py b/tool/src/aurora_iconset_builder/ext_icons/image_icons.py index 2146e66..0635f7b 100644 --- a/tool/src/aurora_iconset_builder/ext_icons/image_icons.py +++ b/tool/src/aurora_iconset_builder/ext_icons/image_icons.py @@ -1,10 +1,10 @@ import PIL.Image import PIL.ImageEnhance -from ..assemblicon.render import inkscape_render, ico_render, InkscapeConfig +from ..assemblicon.artrender import render_svg, render_ico, SvgConfig from ..assemblicon.context import Context from ..assemblicon.utils import Artifact from ..assemblicon.geometry import MinMaxRect, PosSizeRect -from ..assemblicon.artio import load_asset, save_artifact +from ..assemblicon.artio import load_input_artifact, save_artifact from ..assemblicon.artproc import ( aspect_ratio_scale_and_crop, VerticalAnchor, @@ -17,10 +17,10 @@ IMAGE_INNER_RECT_POSSIZE = IMAGE_INNER_RECT_MINMAX.to_pos_size_rect() def build_image_icons(ctx: Context) -> None: # build image base - image_base = inkscape_render( + image_base = render_svg( ctx.input_artifact("component", "image-base.svg"), ctx.temporary_artifact("component", "image-base.png"), - InkscapeConfig(1024, 1024), + SvgConfig(1024, 1024), ) # load image base with image_base: @@ -30,7 +30,7 @@ def build_image_icons(ctx: Context) -> None: def _build_jpg_icon(ctx: Context, base: Artifact) -> None: jpg_image = base.copy() - jpg_image_inner = load_asset( + jpg_image_inner = load_input_artifact( ctx.input_artifact("ext", "bitmap", "Niinsaare_järv.jpg") ) with jpg_image_inner: @@ -60,7 +60,7 @@ def _build_jpg_icon(ctx: Context, base: Artifact) -> None: ) # build artifacts save_artifact(jpg_image, ctx.temporary_artifact("ext", "jpg-image.png")) - ico_render( + render_ico( ctx, jpg_image, ctx.output_artifact("ext", "jpg-image.ico"), diff --git a/tool/src/aurora_iconset_builder/nostd_icons/__init__.py b/tool/src/aurora_iconset_builder/nostd_icons/__init__.py new file mode 100644 index 0000000..561a451 --- /dev/null +++ b/tool/src/aurora_iconset_builder/nostd_icons/__init__.py @@ -0,0 +1,5 @@ +from ..assemblicon.context import Context + + +def build_non_standard_icons(ctx: Context) -> None: + pass diff --git a/tool/src/aurora_iconset_builder/std_icons/action_icons.py b/tool/src/aurora_iconset_builder/std_icons/action_icons.py index b80cf7b..fd8768a 100644 --- a/tool/src/aurora_iconset_builder/std_icons/action_icons.py +++ b/tool/src/aurora_iconset_builder/std_icons/action_icons.py @@ -1,6 +1,6 @@ from pathlib import Path import PIL.Image -from ..assemblicon.render import inkscape_render, InkscapeConfig +from ..assemblicon.artrender import render_svg, SvgConfig from ..assemblicon.context import Context from ..assemblicon.utils import Artifact from ..assemblicon.artio import save_artifact @@ -12,44 +12,32 @@ def build_action_icons(ctx: Context) -> None: def _build_go_icons(ctx: Context) -> None: # build go-* base - go_base = inkscape_render( + go_base = render_svg( ctx.input_artifact("component", "go-base.svg"), ctx.temporary_artifact("component", "go-base.png"), - InkscapeConfig(1024, 1024), + SvgConfig(1024, 1024), ) # build go-* shining - go_shining = inkscape_render( + go_inner_shining = render_svg( ctx.input_artifact("component", "go-inner-shining.svg"), ctx.temporary_artifact("component", "go-inner-shining.png"), - InkscapeConfig(1024, 1024), + SvgConfig(1024, 1024), + ) + # build go-* inner + go_inner = render_svg( + ctx.input_artifact("component", "go-inner.svg"), + ctx.temporary_artifact("component", "go-inner.png"), + SvgConfig(1024, 1024), ) - with go_base, go_shining: - _build_go_icon( - ctx, - go_base, - go_shining, - ctx.input_artifact("go-previous.svg"), - ctx.temporary_artifact("go-previous.png"), - ctx.output_artifact("go-previous.png") - ) - _build_go_icon( - ctx, - go_base, - go_shining, - ctx.input_artifact("go-next.svg"), - ctx.temporary_artifact("go-next.png"), - ctx.output_artifact("go-next.png") - ) - - -def _build_go_icon( - ctx: Context, base: Artifact, shining: Artifact, mask: Path, mask_dst: Path, dst: Path -) -> None: - go = base.copy() - # render shining with mask - go_mask = inkscape_render(mask, mask_dst, InkscapeConfig(1024, 1024)) - go.paste(shining, (0, 0), go_mask) - # save as image - save_artifact(go, dst) + with go_base, go_inner_shining, go_inner: + go_mask = go_inner + for part in ("previous", "down", "next", "up"): + go = go_base.copy() + # render shining with inner mask + go.paste(go_inner_shining, (0, 0), go_mask) + # save as image + save_artifact(go, ctx.output_artifact(f"go-{part}.png")) + # rotate it for next calling + go_mask = go_mask.transpose(PIL.Image.Transpose.ROTATE_90)