refactor: basically finish core refactor
This commit is contained in:
@@ -1,11 +1,39 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from .assemblicon.cli import ArgParserSetup, create_argparser, parse_standard_opts
|
||||
from typing import Optional
|
||||
from .assemblicon.cli import (
|
||||
SvgRenderKind,
|
||||
StdOptsSetup,
|
||||
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 .assemblicon.logger import APP_LOGGER as LOGGER
|
||||
from . import std_icons, nostd_icons
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AuroraOpts:
|
||||
selection: Optional[list[str]]
|
||||
"""The list holding all name of jobs for building."""
|
||||
svg_render: SvgRenderKind
|
||||
"""The render kind for SVG file."""
|
||||
|
||||
|
||||
def parse_opts() -> AuroraOpts:
|
||||
setup = StdOptsSetup(require_input=False, require_output=False)
|
||||
|
||||
parser = create_argparser(setup)
|
||||
parser.prog = "Aurora Icon Set Builder"
|
||||
parser.description = "The Programmatic Icon Assembly Workflow for Aurora Icon Set."
|
||||
|
||||
ns = parser.parse_args()
|
||||
opts = parse_standard_opts(setup, ns)
|
||||
|
||||
return AuroraOpts(
|
||||
selection=opts.selection, svg_render=opts.svg_render.to_assemblicon_type()
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -16,17 +44,12 @@ class AuroraPrologue(Prologue):
|
||||
|
||||
|
||||
def main() -> None:
|
||||
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)
|
||||
opts = parse_opts()
|
||||
|
||||
repository_root = Path(__file__).resolve().parent.parent.parent.parent
|
||||
iconset_input = repository_root / "src"
|
||||
iconset_output = repository_root / "artifact"
|
||||
prologue = AuroraPrologue(
|
||||
iconset_input, iconset_output, opts.svg_render.to_assemblicon_type()
|
||||
)
|
||||
prologue = AuroraPrologue(iconset_input, iconset_output, opts.svg_render)
|
||||
env = Environment(prologue)
|
||||
ctx = Context(env)
|
||||
|
||||
@@ -34,9 +57,9 @@ def main() -> None:
|
||||
register(ctx)
|
||||
ctx.run(opts.selection)
|
||||
except BaseException as e:
|
||||
logging.fatal("Runtime error: %s", e)
|
||||
LOGGER.fatal("Runtime error: %s", e)
|
||||
|
||||
|
||||
def register(ctx: Context) -> None:
|
||||
build_standard_icons(ctx)
|
||||
build_non_standard_icons(ctx)
|
||||
std_icons.register(ctx)
|
||||
nostd_icons.register(ctx)
|
||||
|
||||
@@ -33,14 +33,14 @@ class StdOpts:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ArgParserSetup:
|
||||
class StdOptsSetup:
|
||||
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:
|
||||
def create_argparser(setup: StdOptsSetup) -> ArgumentParser:
|
||||
"""
|
||||
Create Assemblicon standard argument parser.
|
||||
|
||||
@@ -54,7 +54,7 @@ def create_argparser(cfg: ArgParserSetup) -> ArgumentParser:
|
||||
prog="Assemblicon",
|
||||
description="The Programmatic Icon Assembly Workflow.",
|
||||
)
|
||||
if cfg.require_input:
|
||||
if setup.require_input:
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input",
|
||||
@@ -65,7 +65,7 @@ def create_argparser(cfg: ArgParserSetup) -> ArgumentParser:
|
||||
help="The path to input directory.",
|
||||
metavar="DIR",
|
||||
)
|
||||
if cfg.require_output:
|
||||
if setup.require_output:
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
@@ -99,7 +99,7 @@ def create_argparser(cfg: ArgParserSetup) -> ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def parse_standard_opts(cfg: ArgParserSetup, ns: Namespace) -> StdOpts:
|
||||
def parse_standard_opts(cfg: StdOptsSetup, 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,
|
||||
|
||||
@@ -1,60 +1,121 @@
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MinMaxRect:
|
||||
x1: int
|
||||
y1: int
|
||||
x2: int
|
||||
y2: int
|
||||
class Point:
|
||||
"""A point (or vector) on 2D plane with integer coordinates."""
|
||||
|
||||
def to_pos_size_rect(self) -> "PosSizeRect":
|
||||
return PosSizeRect(
|
||||
x=self.x1, y=self.y1, width=self.x2 - self.x1, height=self.y2 - self.y1
|
||||
)
|
||||
|
||||
def to_pillow_tuple(self) -> tuple[int, int, int, int]:
|
||||
return (self.x1, self.y1, self.x2, self.y2)
|
||||
|
||||
@dataclass
|
||||
class PosSizeRect:
|
||||
x: int
|
||||
y: int
|
||||
width: int
|
||||
height: int
|
||||
|
||||
def to_min_max_rect(self) -> MinMaxRect:
|
||||
return MinMaxRect(
|
||||
x1=self.x, y1=self.y, x2=self.x + self.width, y2=self.y + self.height
|
||||
)
|
||||
@staticmethod
|
||||
def from_tuple(t: tuple[int, int]) -> "Point":
|
||||
return Point(t[0], t[1])
|
||||
|
||||
def to_pillow_tuple(self) -> tuple[int, int]:
|
||||
def __add__(self, other: "Point") -> "Point":
|
||||
if not isinstance(other, Point):
|
||||
return NotImplemented
|
||||
return Point(self.x + other.x, self.y + other.y)
|
||||
|
||||
def __sub__(self, other: "Point") -> "Point":
|
||||
if not isinstance(other, Point):
|
||||
return NotImplemented
|
||||
return Point(self.x - other.x, self.y - other.y)
|
||||
|
||||
def __neg__(self) -> "Point":
|
||||
return Point(-self.x, -self.y)
|
||||
|
||||
def __mul__(self, other: int | float) -> "Point":
|
||||
if not isinstance(other, (int, float)):
|
||||
return NotImplemented
|
||||
return Point(math.floor(self.x * other), math.floor(self.y * other))
|
||||
|
||||
def __rmul__(self, other: int | float) -> "Point":
|
||||
return self.__mul__(other)
|
||||
|
||||
def __floordiv__(self, other: int | float) -> "Point":
|
||||
if not isinstance(other, (int, float)):
|
||||
return NotImplemented
|
||||
return Point(math.floor(self.x / other), math.floor(self.y / other))
|
||||
|
||||
def to_tuple(self) -> tuple[int, int]:
|
||||
"""
|
||||
Create the tuple representing this point by ``(x, y)``
|
||||
which can be used by Pillow.
|
||||
"""
|
||||
return (self.x, self.y)
|
||||
|
||||
@dataclass
|
||||
class MarginRect:
|
||||
top: int
|
||||
right: int
|
||||
bottom: int
|
||||
left: int
|
||||
|
||||
def to_min_max_rect(self, width: int, height: int) -> MinMaxRect:
|
||||
return MinMaxRect(
|
||||
x1=self.left,
|
||||
y1=self.top,
|
||||
x2=width - self.right,
|
||||
y2=height - self.bottom,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaddingRect:
|
||||
top: int
|
||||
right: int
|
||||
bottom: int
|
||||
left: int
|
||||
class Rectangle:
|
||||
"""
|
||||
A rectangle framed by two corner points.
|
||||
|
||||
def to_min_max_rect(self, x: int, y: int) -> MinMaxRect:
|
||||
return MinMaxRect(
|
||||
x1=x - self.left, y1=y - self.top, x2=x + self.right, y2=y + self.bottom
|
||||
)
|
||||
pos1 is always assumed to be the top-left corner and pos2 the
|
||||
bottom-right one. This relation is a pure convention and is never
|
||||
validated: constructing a Rectangle with swapped or inverted corners
|
||||
produces a meaningless-but-silent result.
|
||||
"""
|
||||
|
||||
pos1: Point
|
||||
pos2: Point
|
||||
|
||||
@staticmethod
|
||||
def from_tuple(t: tuple[int, int, int, int]) -> "Rectangle":
|
||||
return Rectangle(Point(t[0], t[1]), Point(t[2], t[3]))
|
||||
|
||||
@staticmethod
|
||||
def from_min_max_rect(pos1: Point, pos2: Point) -> "Rectangle":
|
||||
return Rectangle(pos1, pos2)
|
||||
|
||||
@staticmethod
|
||||
def from_pos_size_rect(pos: Point, width: int, height: int) -> "Rectangle":
|
||||
return Rectangle(pos, pos + Point(width, height))
|
||||
|
||||
@staticmethod
|
||||
def from_margin_rect(
|
||||
width: int, height: int, top: int, right: int, bottom: int, left: int
|
||||
) -> "Rectangle":
|
||||
"""
|
||||
TODO
|
||||
|
||||
:param width: The width of parent.
|
||||
:param height: The height of parent.
|
||||
:param top: The distance from this rectangle to the top border of parent.
|
||||
:param right: The distance from this rectangle to the right border of parent.
|
||||
:param bottom: The distance from this rectangle to the bottom border of parent.
|
||||
:param left: The distance from this rectangle to the top border of parent.
|
||||
"""
|
||||
return Rectangle(Point(left, top), Point(width - right, height - bottom))
|
||||
|
||||
@staticmethod
|
||||
def from_padding_rect(
|
||||
x: int, y: int, top: int, right: int, bottom: int, left: int
|
||||
) -> "Rectangle":
|
||||
"""
|
||||
TODO
|
||||
|
||||
:param x: The X factor of reference point.
|
||||
:param y: The Y factor of reference point.
|
||||
:param top: The distance from reference point to this rectangle's top border.
|
||||
:param right: The distance from reference point to this rectangle's right border.
|
||||
:param bottom: The distance from reference point to this rectangle's bottom border.
|
||||
:param left: The distance from reference point to this rectangle's left border.
|
||||
"""
|
||||
return Rectangle(Point(x - left, y - top), Point(x + right, y + bottom))
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return self.pos2.x - self.pos1.x
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
return self.pos2.y - self.pos1.y
|
||||
|
||||
def to_tuple(self) -> tuple[int, int, int, int]:
|
||||
"""
|
||||
Create the tuple representing this rectangle by ``(x1, y2 x2, y2)``
|
||||
which can be used by Pillow.
|
||||
"""
|
||||
return (self.pos1.x, self.pos1.y, self.pos2.x, self.pos2.y)
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Iterable
|
||||
from .environment import Environment
|
||||
from typing import Callable, Iterable, TypeVar, Generic
|
||||
from .environment import Environment, Prologue
|
||||
from ..logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
P = TypeVar("P", bound=Prologue)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Job:
|
||||
class Job(Generic[P]):
|
||||
"""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]
|
||||
executor: Callable[[Environment[P]], None]
|
||||
"""The executor of this job."""
|
||||
|
||||
|
||||
class Context:
|
||||
__jobs: dict[str, Job]
|
||||
class Context(Generic[P]):
|
||||
__jobs: dict[str, Job[P]]
|
||||
"""The dict storing all jobs. Key is job name and value is job payload."""
|
||||
__env: Environment
|
||||
__env: Environment[P]
|
||||
"""The shared environment passed to every job executor."""
|
||||
|
||||
def __init__(self, env: Environment) -> None:
|
||||
def __init__(self, env: Environment[P]) -> None:
|
||||
self.__jobs = {}
|
||||
self.__env = env
|
||||
|
||||
@@ -92,7 +95,7 @@ class Context:
|
||||
self,
|
||||
name: str,
|
||||
deps: Iterable[str],
|
||||
exec: Callable[[Environment], None],
|
||||
exec: Callable[[Environment[P]], None],
|
||||
) -> None:
|
||||
"""
|
||||
Register a new job with given name, dependencies and executor.
|
||||
|
||||
@@ -6,9 +6,8 @@ from types import TracebackType
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TypeVar, Generic
|
||||
from .utils import Artifact, ArtifactFile, VART_HW
|
||||
from .utils import Artifact, ArtifactFile, VART_HW, check_vart
|
||||
from ..logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
from ..lowlevel.artrdr import SvgRenderKind
|
||||
from ..lowlevel.artrdr import SvgRenderKind, SvgConfig, BlenderConfig
|
||||
from ..lowlevel import artio, artrdr
|
||||
|
||||
@@ -120,6 +119,14 @@ class Environment(Generic[P]):
|
||||
|
||||
# endregion
|
||||
|
||||
# region: Prologue Visitor
|
||||
|
||||
@property
|
||||
def prologue(self) -> P:
|
||||
return self.__prologue
|
||||
|
||||
# endregion
|
||||
|
||||
# region: New Artifact
|
||||
|
||||
def new_artifact(self) -> Artifact:
|
||||
@@ -309,6 +316,8 @@ class Environment(Generic[P]):
|
||||
'Saving FreeDesktop artifact with kind "%s" and name "%s".', kind, name
|
||||
)
|
||||
|
||||
check_vart(art)
|
||||
|
||||
for hw, thumbnail in zip(
|
||||
_FD_THUMBNAIL_HWS, artrdr.render_thumbnail(art, _FD_THUMBNAIL_HWS)
|
||||
):
|
||||
@@ -343,6 +352,7 @@ class Environment(Generic[P]):
|
||||
LOGGER.info(
|
||||
'Saving Windows-only artifact with kind "%s" and name "%s".', kind, name
|
||||
)
|
||||
check_vart(art)
|
||||
artrdr.render_ico(art, self.__win_artifact(kind, name))
|
||||
|
||||
def dup_win_artifact(
|
||||
@@ -372,6 +382,7 @@ class Environment(Generic[P]):
|
||||
LOGGER.info(
|
||||
'Saving macOS-only artifact with kind "%s" and name "%s".', kind, name
|
||||
)
|
||||
check_vart(art)
|
||||
artrdr.render_icns(art, self.__mac_artifact(kind, name))
|
||||
|
||||
def dup_mac_artifact(
|
||||
|
||||
@@ -4,7 +4,7 @@ VART_HW: int = 1024
|
||||
"""The height or width of Vanilla Artifact."""
|
||||
|
||||
|
||||
def check_vanilla_artifact(art: Artifact) -> None:
|
||||
def check_vart(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"
|
||||
|
||||
@@ -8,6 +8,9 @@ class LoggerFactory:
|
||||
format="[%(levelname)s][%(name)s] %(message)s", level=logging.INFO
|
||||
)
|
||||
|
||||
def get_app_logger(self) -> logging.Logger:
|
||||
return logging.getLogger("App")
|
||||
|
||||
def get_lowlevel_logger(self) -> logging.Logger:
|
||||
return logging.getLogger("Lowlevel")
|
||||
|
||||
@@ -18,3 +21,4 @@ class LoggerFactory:
|
||||
LOGGER_FACTORY = LoggerFactory()
|
||||
LOWLEVEL_LOGGER = LOGGER_FACTORY.get_lowlevel_logger()
|
||||
HIGHLEVEL_LOGGER = LOGGER_FACTORY.get_highlevel_logger()
|
||||
APP_LOGGER = LOGGER_FACTORY.get_app_logger()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import enum
|
||||
import math
|
||||
from .utils import Artifact
|
||||
from ..geometry import Point
|
||||
|
||||
|
||||
class HorizontalAnchor(enum.IntEnum):
|
||||
@@ -15,14 +16,32 @@ class VerticalAnchor(enum.IntEnum):
|
||||
Bottom = enum.auto()
|
||||
|
||||
|
||||
def _anchor_offset(oversize: int, anchor: HorizontalAnchor | VerticalAnchor) -> int:
|
||||
match anchor:
|
||||
case HorizontalAnchor.Left | VerticalAnchor.Top:
|
||||
return 0
|
||||
case HorizontalAnchor.Center | VerticalAnchor.Center:
|
||||
return oversize // 2
|
||||
case HorizontalAnchor.Right | VerticalAnchor.Bottom:
|
||||
return oversize
|
||||
def anchor_to_offset(
|
||||
oversize: Point,
|
||||
horizontal_anchor: HorizontalAnchor,
|
||||
vertical_anchor: VerticalAnchor,
|
||||
) -> Point:
|
||||
half_oversize = oversize // 2
|
||||
|
||||
x: int
|
||||
match horizontal_anchor:
|
||||
case HorizontalAnchor.Left:
|
||||
x = 0
|
||||
case HorizontalAnchor.Center:
|
||||
x = half_oversize.x
|
||||
case HorizontalAnchor.Right:
|
||||
x = oversize.x
|
||||
|
||||
y: int
|
||||
match vertical_anchor:
|
||||
case VerticalAnchor.Top:
|
||||
y = 0
|
||||
case VerticalAnchor.Center:
|
||||
y = half_oversize.y
|
||||
case VerticalAnchor.Bottom:
|
||||
y = oversize.y
|
||||
|
||||
return Point(x, y)
|
||||
|
||||
|
||||
def aspect_ratio_scale_and_crop(
|
||||
@@ -45,16 +64,20 @@ def aspect_ratio_scale_and_crop(
|
||||
scaled_height = math.ceil(art.height * scale)
|
||||
scaled = art.resize((scaled_width, scaled_height), scale_resample)
|
||||
|
||||
crop_x = _anchor_offset(scaled_width - target_width, crop_horizontal_anchor)
|
||||
crop_y = _anchor_offset(scaled_height - target_height, crop_vertical_anchor)
|
||||
crop = anchor_to_offset(
|
||||
Point(scaled_width - target_width, scaled_height - target_height),
|
||||
crop_horizontal_anchor,
|
||||
crop_vertical_anchor,
|
||||
)
|
||||
(crop_x, crop_y) = crop.to_tuple()
|
||||
return scaled.crop((crop_x, crop_y, crop_x + target_width, crop_y + target_height))
|
||||
|
||||
|
||||
def anchor_to_pos(
|
||||
def art_anchor_to_offset(
|
||||
art: Artifact,
|
||||
horizontal_anchor: HorizontalAnchor,
|
||||
vertical_anchor: VerticalAnchor,
|
||||
) -> tuple[int, int]:
|
||||
) -> Point:
|
||||
"""
|
||||
Resolve the anchor point position inside given artifact.
|
||||
|
||||
@@ -65,17 +88,16 @@ def anchor_to_pos(
|
||||
anchors this is the corresponding edge; for Right/Bottom anchors
|
||||
this is just outside the artifact (the exact corner coordinate).
|
||||
"""
|
||||
return (
|
||||
_anchor_offset(art.width, horizontal_anchor),
|
||||
_anchor_offset(art.height, vertical_anchor),
|
||||
return anchor_to_offset(
|
||||
Point.from_tuple(art.size), horizontal_anchor, vertical_anchor
|
||||
)
|
||||
|
||||
|
||||
def align_paste(
|
||||
art: Artifact,
|
||||
clipboard: Artifact,
|
||||
art_pos: tuple[int, int],
|
||||
clipboard_pos: tuple[int, int],
|
||||
art_pos: Point,
|
||||
clipboard_pos: Point,
|
||||
) -> None:
|
||||
"""
|
||||
Paste clipboard onto given artifact with two anchor points aligned.
|
||||
@@ -93,21 +115,16 @@ def align_paste(
|
||||
:param clipboard_pos: The (x, y) point on the clipboard to be
|
||||
aligned, usually resolved by anchor_to_pos.
|
||||
"""
|
||||
art.paste(
|
||||
clipboard,
|
||||
(art_pos[0] - clipboard_pos[0], art_pos[1] - clipboard_pos[1]),
|
||||
)
|
||||
art.paste(clipboard, (art_pos - clipboard_pos).to_tuple())
|
||||
|
||||
|
||||
def align_alpha_composite(
|
||||
art: Artifact,
|
||||
clipboard: Artifact,
|
||||
art_pos: tuple[int, int],
|
||||
clipboard_pos: tuple[int, int],
|
||||
art_pos: Point,
|
||||
clipboard_pos: Point,
|
||||
) -> None:
|
||||
"""
|
||||
Alpha-supported ``align_paste``.
|
||||
"""
|
||||
art.alpha_composite(
|
||||
clipboard, (art_pos[0] - clipboard_pos[0], art_pos[1] - clipboard_pos[1])
|
||||
)
|
||||
art.alpha_composite(clipboard, (art_pos - clipboard_pos).to_tuple())
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import os
|
||||
import enum
|
||||
import subprocess
|
||||
@@ -6,7 +5,8 @@ from typing import Iterable, Iterator
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
import PIL.Image
|
||||
from .utils import Artifact, check_vanilla_artifact
|
||||
from .utils import Artifact
|
||||
from ..logger import LOWLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
class SvgRenderKind(enum.IntEnum):
|
||||
@@ -39,7 +39,7 @@ 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(
|
||||
LOGGER.debug(
|
||||
"Rendering SVG artifact with render %s: %s -> %s", kind.name, src, dst
|
||||
)
|
||||
|
||||
@@ -141,7 +141,7 @@ def render_blender(src: Path, dst: Path, temp_script: Path, cfg: BlenderConfig)
|
||||
The caller must make sure that this file is immutable during calling this function.
|
||||
:param cfg: The configuration when rendering.
|
||||
"""
|
||||
logging.debug("Rendering Blender artifact: %s -> %s", src, dst)
|
||||
LOGGER.debug("Rendering Blender artifact: %s -> %s", src, dst)
|
||||
|
||||
if not src.is_file():
|
||||
raise ValueError(
|
||||
@@ -178,9 +178,19 @@ def render_thumbnail(art: Artifact, hws: Iterable[int]) -> Iterator[Artifact]:
|
||||
: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)
|
||||
LOGGER.debug("Rendering thumbnail...")
|
||||
|
||||
# check whether given artifact is square rectangle.
|
||||
if art.width != art.height:
|
||||
raise ValueError(f'Thumbnail render only accept square rectangle.')
|
||||
# scale artifact up to the maximum resolution hws if it is too small.
|
||||
max_hws = max(hws)
|
||||
if art.width < max_hws:
|
||||
art = art.resize((max_hws, max_hws), PIL.Image.Resampling.LANCZOS)
|
||||
|
||||
# render thumbnail
|
||||
for hw in hws:
|
||||
logging.debug("Rendering thumbnail %dx%d.", hw, hw)
|
||||
LOGGER.debug("Rendering thumbnail %dx%d.", hw, hw)
|
||||
thumbnail = art.copy()
|
||||
thumbnail.thumbnail((hw, hw), PIL.Image.Resampling.LANCZOS)
|
||||
yield thumbnail
|
||||
@@ -193,9 +203,8 @@ def render_ico(art: Artifact, dst: Path) -> None:
|
||||
:param art: The artifact to be rendered.
|
||||
:param dst: The path to rendered result.
|
||||
"""
|
||||
logging.debug("Rendering .ICO artifact: %s", dst)
|
||||
LOGGER.debug("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
|
||||
@@ -215,9 +224,8 @@ def render_icns(art: Artifact, dst: Path) -> None:
|
||||
:param art: The artifact to be rendered.
|
||||
:param dst: The path to rendered result.
|
||||
"""
|
||||
logging.debug("Rendering .ICNS artifact: %s", dst)
|
||||
LOGGER.debug("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)))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from ..assemblicon.context import AdvancedContext
|
||||
from ..assemblicon.highlevel.context import Context
|
||||
from .action_icons import build_action_icons
|
||||
from .mimetype_icons import build_mimetype_icons
|
||||
from .application_icons import build_application_icons
|
||||
|
||||
|
||||
def build_non_standard_icons(ctx: AdvancedContext) -> None:
|
||||
def register(ctx: Context) -> None:
|
||||
build_mimetype_icons(ctx)
|
||||
build_application_icons(ctx)
|
||||
build_action_icons(ctx)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from ..assemblicon.context import AdvancedContext
|
||||
from ..assemblicon.highlevel.context import Context
|
||||
from .action_icons import build_action_icons
|
||||
from .status_icons import build_status_icons
|
||||
|
||||
|
||||
def build_standard_icons(ctx: AdvancedContext) -> None:
|
||||
def register(ctx: Context) -> None:
|
||||
build_action_icons(ctx)
|
||||
build_status_icons(ctx)
|
||||
|
||||
Reference in New Issue
Block a user