Compare commits

...
3 Commits
Author SHA1 Message Date
yyc12345 1d48f54c40 doc: add some docstring for assemblicon context 2026-08-26 16:45:13 +08:00
yyc12345 55a922aafe refactor: refactor assemblicon context 2026-08-26 16:39:10 +08:00
yyc12345 c9740206d4 feat: add go icon support 2026-08-24 13:25:03 +08:00
10 changed files with 578 additions and 226 deletions
+2 -2
View File
@@ -2,8 +2,8 @@ import logging
from pathlib import Path from pathlib import Path
from .assemblicon.context import Context from .assemblicon.context import Context
from .assemblicon.utils import setup_logging from .assemblicon.utils import setup_logging
from .image_icons import build_image_icons
from .std_icons import build_standard_icons from .std_icons import build_standard_icons
from .ext_icons import build_ext_icons
def main() -> None: def main() -> None:
@@ -22,4 +22,4 @@ def main() -> None:
def _build_icons(ctx: Context) -> None: def _build_icons(ctx: Context) -> None:
build_standard_icons(ctx) build_standard_icons(ctx)
#build_image_icons(ctx) #build_ext_icons(ctx)
@@ -1,10 +1,25 @@
import logging
from pathlib import Path from pathlib import Path
import PIL.Image import PIL.Image
from .utils import Artifact, ArtifactFile
def load_artifact(p: Path) -> PIL.Image.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) -> ArtifactFile:
"""
Load artifact saved previously as the intermediary.
"""
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)
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,256 @@ 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:
"""
Internal used function for rendering SVG.
:return: The path to destination storing render result.
"""
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: Path, cfg: SvgConfig = _DEFAULT_SVG_CONFIG
) -> None:
"""
Just render given SVG to PNG file.
:param src: The path to SVG file to be rendered.
:param dst: The path to rendered PNG file.
:param cfg: The configuration when rendering SVG file. Default for vanilla artifact.
"""
self.__intern_render_svg(src, dst, cfg)
def render_svg_then_load(
self, src: Path, dst: Optional[Path], cfg: SvgConfig = _DEFAULT_SVG_CONFIG
) -> Artifact:
"""
Render given SVG to PNG file, then load it directly.
:param src: The path to SVG file to be rendered.
:param dst: The path to rendered PNG file.
``None`` if you don't need to store it in temporary or output directory.
:param cfg: The configuration when rendering SVG file. Default for vanilla artifact.
:return: The loaded built PNG file.
"""
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:
"""
Internal used function for rendering Blender composition.
:return: The path to destination storing render result.
"""
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: Path, cfg: BlenderConfig = _DEFAULT_BLD_CONFIG
) -> None:
"""
Just render given Blender composition to PNG file.
:param src: The path to Blender composition file to be rendered.
:param dst: The path to rendered PNG file.
:param cfg: The configuration when rendering Blender composition file. Default for vanilla artifact.
"""
self.__intern_render_blender(src, dst, cfg)
def render_blender_then_load(
self, src: Path, dst: Optional[Path], cfg: BlenderConfig = _DEFAULT_BLD_CONFIG
) -> Artifact:
"""
Render given Blender composition to PNG file, then load it directly.
:param src: The path to Blender composition file to be rendered.
:param dst: The path to rendered PNG file.
``None`` if you don't need to store it in temporary or output directory.
:param cfg: The configuration when rendering Blender composition file. Default for vanilla artifact.
:return: The loaded built PNG file.
"""
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,168 +0,0 @@
import logging
import os
import subprocess
from pathlib import Path
from dataclasses import dataclass
import PIL.Image
from .context import Context
from .utils import 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"
)
def inkscape_render(src: Path, dst: Path, cfg: InkscapeConfig) -> None:
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}"
)
@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) -> None:
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"
)
_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"
)
@@ -0,0 +1,6 @@
from ..assemblicon.context import Context
from .image_icons import build_image_icons
def build_ext_icons(ctx: Context) -> None:
build_image_icons(ctx)
@@ -1,10 +1,11 @@
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.artproc import ( from ..assemblicon.artio import load_input_artifact, save_artifact
from ..assemblicon.artproc import (
aspect_ratio_scale_and_crop, aspect_ratio_scale_and_crop,
VerticalAnchor, VerticalAnchor,
HorizontalAnchor, HorizontalAnchor,
@@ -16,24 +17,23 @@ 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
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 PIL.Image.open( with image_base:
ctx.temporary_artifact("component", "image-base.png")
) as image_base:
_build_jpg_icon(ctx, image_base) _build_jpg_icon(ctx, image_base)
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()
with PIL.Image.open( jpg_image_inner = load_input_artifact(
ctx.input_artifact("ext", "bitmap", "Niinsaare_järv.jpg") ctx.input_artifact("ext", "bitmap", "Niinsaare_järv.jpg")
) as jpg_image_inner: )
with jpg_image_inner:
# crop the area which is interested by ourselves # crop the area which is interested by ourselves
interest_area = PosSizeRect(2700, 0, 5016, 3888).to_min_max_rect() interest_area = PosSizeRect(2700, 0, 5016, 3888).to_min_max_rect()
crop_jpg_image_inner = jpg_image_inner.crop(interest_area.to_pillow_tuple()) crop_jpg_image_inner = jpg_image_inner.crop(interest_area.to_pillow_tuple())
@@ -59,8 +59,8 @@ def _build_jpg_icon(ctx: Context, base: Artifact) -> None:
bestfit_jpg_image_inner, IMAGE_INNER_RECT_POSSIZE.to_pillow_tuple() bestfit_jpg_image_inner, IMAGE_INNER_RECT_POSSIZE.to_pillow_tuple()
) )
# build artifacts # build artifacts
jpg_image.save(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,14 +1,9 @@
from pathlib import Path from pathlib import Path
import PIL.Image import PIL.Image
from ..assemblicon.render import inkscape_render, ico_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.geometry import MinMaxRect, PosSizeRect from ..assemblicon.artio import save_artifact
from ..assemblicon.artproc import (
aspect_ratio_scale_and_crop,
VerticalAnchor,
HorizontalAnchor,
)
def build_action_icons(ctx: Context) -> None: def build_action_icons(ctx: Context) -> None:
@@ -17,27 +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
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
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 PIL.Image.open(ctx.temporary_artifact("component", "go-base.png")) as go_base: with go_base, go_inner_shining, go_inner:
with PIL.Image.open( go_mask = go_inner
ctx.temporary_artifact("component", "go-inner-shining.png") for part in ("previous", "down", "next", "up"):
) as go_shining: go = go_base.copy()
_build_go_icon(ctx, go_base, go_shining, ctx.input_artifact("go-previous.svg")) # render shining with inner mask
_build_go_icon(ctx, go_base, go_shining, ctx.input_artifact("go-next.svg")) go.paste(go_inner_shining, (0, 0), go_mask)
# save as image
save_artifact(go, ctx.output_artifact(f"go-{part}.png"))
def _build_go_icon(ctx: Context, base: Artifact, shining: Artifact, mask: Path) -> None:
go = base.copy()
# rotate it for next calling
go_mask = go_mask.transpose(PIL.Image.Transpose.ROTATE_90)