refactor: refactor assemblicon context

This commit is contained in:
2026-08-26 16:39:10 +08:00
parent c9740206d4
commit 55a922aafe
8 changed files with 512 additions and 237 deletions
@@ -1,19 +1,25 @@
import logging import logging
from pathlib import Path from pathlib import Path
import PIL.Image 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) logging.info("Loading asset: %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) -> PIL.ImageFile.ImageFile: def load_artifact(p: Path) -> ArtifactFile:
"""
Load artifact saved previously as the intermediary.
"""
logging.info("Loading artifact: %s", p) logging.info("Loading artifact: %s", p)
return PIL.Image.open(p, "r", ["PNG"]) 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) logging.info("Saving artifact: %s", p)
art.save(p, format="PNG") art.save(p, format="PNG")
@@ -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)
@@ -1,36 +1,53 @@
import logging
import tempfile import tempfile
import os
import enum
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Optional
from types import TracebackType 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: class Context:
__prerequisite: Prerequisite
__input_dir: Path
__output_dir: Path
__temp_dir: tempfile.TemporaryDirectory[str] __temp_dir: tempfile.TemporaryDirectory[str]
def __init__(self, input_dir: Path, output_dir: Path) -> None: def __init__(self, prerequisite: Prerequisite) -> None:
self.__input_dir = input_dir self.__prerequisite = prerequisite
self.__output_dir = output_dir
self.__temp_dir = tempfile.TemporaryDirectory() self.__temp_dir = tempfile.TemporaryDirectory()
@property
def prerequisite(self) -> Prerequisite:
return self.__prerequisite
def input_artifact(self, *args: str) -> Path: 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: def output_artifact(self, *args: str) -> Path:
p = self.__output_dir / Path(*args) return self.__prerequisite.output_directory / Path(*args)
p.parent.mkdir(parents=True, exist_ok=True)
return p
def temporary_artifact(self, *args: str) -> Path: def temporary_artifact(self, *args: str) -> Path:
p = Path(self.__temp_dir.name) / "user" / Path(*args) return Path(self.__temp_dir.name) / 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
def __enter__(self) -> "Context": def __enter__(self) -> "Context":
return self return self
@@ -42,3 +59,214 @@ class Context:
traceback: TracebackType | None, traceback: TracebackType | None,
) -> None: ) -> None:
self.__temp_dir.cleanup() 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
@@ -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)
@@ -1,7 +1,20 @@
import logging import logging
from PIL.Image import Image from PIL.Image import Image
from PIL.ImageFile import ImageFile
type Artifact = Image type Artifact = Image
type ArtifactFile = ImageFile
def setup_logging() -> None: def setup_logging() -> None:
logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.INFO) 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,10 +1,10 @@
import PIL.Image import PIL.Image
import PIL.ImageEnhance 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.context import Context
from ..assemblicon.utils import Artifact from ..assemblicon.utils import Artifact
from ..assemblicon.geometry import MinMaxRect, PosSizeRect 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 ( from ..assemblicon.artproc import (
aspect_ratio_scale_and_crop, aspect_ratio_scale_and_crop,
VerticalAnchor, VerticalAnchor,
@@ -17,10 +17,10 @@ IMAGE_INNER_RECT_POSSIZE = IMAGE_INNER_RECT_MINMAX.to_pos_size_rect()
def build_image_icons(ctx: Context) -> None: def build_image_icons(ctx: Context) -> None:
# build image base # build image base
image_base = inkscape_render( image_base = render_svg(
ctx.input_artifact("component", "image-base.svg"), ctx.input_artifact("component", "image-base.svg"),
ctx.temporary_artifact("component", "image-base.png"), ctx.temporary_artifact("component", "image-base.png"),
InkscapeConfig(1024, 1024), SvgConfig(1024, 1024),
) )
# load image base # load image base
with 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: def _build_jpg_icon(ctx: Context, base: Artifact) -> None:
jpg_image = base.copy() jpg_image = base.copy()
jpg_image_inner = load_asset( jpg_image_inner = load_input_artifact(
ctx.input_artifact("ext", "bitmap", "Niinsaare_järv.jpg") ctx.input_artifact("ext", "bitmap", "Niinsaare_järv.jpg")
) )
with jpg_image_inner: with jpg_image_inner:
@@ -60,7 +60,7 @@ def _build_jpg_icon(ctx: Context, base: Artifact) -> None:
) )
# build artifacts # build artifacts
save_artifact(jpg_image, ctx.temporary_artifact("ext", "jpg-image.png")) save_artifact(jpg_image, ctx.temporary_artifact("ext", "jpg-image.png"))
ico_render( render_ico(
ctx, ctx,
jpg_image, jpg_image,
ctx.output_artifact("ext", "jpg-image.ico"), ctx.output_artifact("ext", "jpg-image.ico"),
@@ -0,0 +1,5 @@
from ..assemblicon.context import Context
def build_non_standard_icons(ctx: Context) -> None:
pass
@@ -1,6 +1,6 @@
from pathlib import Path from pathlib import Path
import PIL.Image import PIL.Image
from ..assemblicon.render import inkscape_render, InkscapeConfig from ..assemblicon.artrender import render_svg, SvgConfig
from ..assemblicon.context import Context from ..assemblicon.context import Context
from ..assemblicon.utils import Artifact from ..assemblicon.utils import Artifact
from ..assemblicon.artio import save_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: def _build_go_icons(ctx: Context) -> None:
# build go-* base # build go-* base
go_base = inkscape_render( go_base = render_svg(
ctx.input_artifact("component", "go-base.svg"), ctx.input_artifact("component", "go-base.svg"),
ctx.temporary_artifact("component", "go-base.png"), ctx.temporary_artifact("component", "go-base.png"),
InkscapeConfig(1024, 1024), SvgConfig(1024, 1024),
) )
# build go-* shining # build go-* shining
go_shining = inkscape_render( go_inner_shining = render_svg(
ctx.input_artifact("component", "go-inner-shining.svg"), ctx.input_artifact("component", "go-inner-shining.svg"),
ctx.temporary_artifact("component", "go-inner-shining.png"), 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: with go_base, go_inner_shining, go_inner:
_build_go_icon( go_mask = go_inner
ctx, for part in ("previous", "down", "next", "up"):
go_base, go = go_base.copy()
go_shining, # render shining with inner mask
ctx.input_artifact("go-previous.svg"), go.paste(go_inner_shining, (0, 0), go_mask)
ctx.temporary_artifact("go-previous.png"), # save as image
ctx.output_artifact("go-previous.png") save_artifact(go, ctx.output_artifact(f"go-{part}.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)
# rotate it for next calling
go_mask = go_mask.transpose(PIL.Image.Transpose.ROTATE_90)