refactor: basically finish core refactor
This commit is contained in:
@@ -1,11 +1,39 @@
|
|||||||
import logging
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dataclasses import dataclass
|
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.environment import Prologue, Environment
|
||||||
from .assemblicon.highlevel.context import Context
|
from .assemblicon.highlevel.context import Context
|
||||||
from .std_icons import build_standard_icons
|
from .assemblicon.logger import APP_LOGGER as LOGGER
|
||||||
from .nostd_icons import build_non_standard_icons
|
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)
|
@dataclass(frozen=True)
|
||||||
@@ -16,17 +44,12 @@ class AuroraPrologue(Prologue):
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser_cfg = ArgParserSetup(require_input=False, require_output=False)
|
opts = parse_opts()
|
||||||
parser = create_argparser(parser_cfg)
|
|
||||||
ns = parser.parse_args()
|
|
||||||
opts = parse_standard_opts(parser_cfg, ns)
|
|
||||||
|
|
||||||
repository_root = Path(__file__).resolve().parent.parent.parent.parent
|
repository_root = Path(__file__).resolve().parent.parent.parent.parent
|
||||||
iconset_input = repository_root / "src"
|
iconset_input = repository_root / "src"
|
||||||
iconset_output = repository_root / "artifact"
|
iconset_output = repository_root / "artifact"
|
||||||
prologue = AuroraPrologue(
|
prologue = AuroraPrologue(iconset_input, iconset_output, opts.svg_render)
|
||||||
iconset_input, iconset_output, opts.svg_render.to_assemblicon_type()
|
|
||||||
)
|
|
||||||
env = Environment(prologue)
|
env = Environment(prologue)
|
||||||
ctx = Context(env)
|
ctx = Context(env)
|
||||||
|
|
||||||
@@ -34,9 +57,9 @@ def main() -> None:
|
|||||||
register(ctx)
|
register(ctx)
|
||||||
ctx.run(opts.selection)
|
ctx.run(opts.selection)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logging.fatal("Runtime error: %s", e)
|
LOGGER.fatal("Runtime error: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def register(ctx: Context) -> None:
|
def register(ctx: Context) -> None:
|
||||||
build_standard_icons(ctx)
|
std_icons.register(ctx)
|
||||||
build_non_standard_icons(ctx)
|
nostd_icons.register(ctx)
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ class StdOpts:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ArgParserSetup:
|
class StdOptsSetup:
|
||||||
require_input: bool
|
require_input: bool
|
||||||
"""Whether add input path option in argument parser."""
|
"""Whether add input path option in argument parser."""
|
||||||
require_output: bool
|
require_output: bool
|
||||||
"""Whether add outputput path option in argument parser."""
|
"""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.
|
Create Assemblicon standard argument parser.
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ def create_argparser(cfg: ArgParserSetup) -> ArgumentParser:
|
|||||||
prog="Assemblicon",
|
prog="Assemblicon",
|
||||||
description="The Programmatic Icon Assembly Workflow.",
|
description="The Programmatic Icon Assembly Workflow.",
|
||||||
)
|
)
|
||||||
if cfg.require_input:
|
if setup.require_input:
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-i",
|
"-i",
|
||||||
"--input",
|
"--input",
|
||||||
@@ -65,7 +65,7 @@ def create_argparser(cfg: ArgParserSetup) -> ArgumentParser:
|
|||||||
help="The path to input directory.",
|
help="The path to input directory.",
|
||||||
metavar="DIR",
|
metavar="DIR",
|
||||||
)
|
)
|
||||||
if cfg.require_output:
|
if setup.require_output:
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-o",
|
"-o",
|
||||||
"--output",
|
"--output",
|
||||||
@@ -99,7 +99,7 @@ def create_argparser(cfg: ArgParserSetup) -> ArgumentParser:
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def parse_standard_opts(cfg: ArgParserSetup, ns: Namespace) -> StdOpts:
|
def parse_standard_opts(cfg: StdOptsSetup, ns: Namespace) -> StdOpts:
|
||||||
return StdOpts(
|
return StdOpts(
|
||||||
input_directory=ns.input if cfg.require_input else None,
|
input_directory=ns.input if cfg.require_input else None,
|
||||||
output_directory=ns.output if cfg.require_output else None,
|
output_directory=ns.output if cfg.require_output else None,
|
||||||
|
|||||||
@@ -1,60 +1,121 @@
|
|||||||
|
import math
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MinMaxRect:
|
class Point:
|
||||||
x1: int
|
"""A point (or vector) on 2D plane with integer coordinates."""
|
||||||
y1: int
|
|
||||||
x2: int
|
|
||||||
y2: int
|
|
||||||
|
|
||||||
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
|
x: int
|
||||||
y: int
|
y: int
|
||||||
width: int
|
|
||||||
height: int
|
|
||||||
|
|
||||||
def to_min_max_rect(self) -> MinMaxRect:
|
@staticmethod
|
||||||
return MinMaxRect(
|
def from_tuple(t: tuple[int, int]) -> "Point":
|
||||||
x1=self.x, y1=self.y, x2=self.x + self.width, y2=self.y + self.height
|
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)
|
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
|
@dataclass
|
||||||
class PaddingRect:
|
class Rectangle:
|
||||||
top: int
|
"""
|
||||||
right: int
|
A rectangle framed by two corner points.
|
||||||
bottom: int
|
|
||||||
left: int
|
|
||||||
|
|
||||||
def to_min_max_rect(self, x: int, y: int) -> MinMaxRect:
|
pos1 is always assumed to be the top-left corner and pos2 the
|
||||||
return MinMaxRect(
|
bottom-right one. This relation is a pure convention and is never
|
||||||
x1=x - self.left, y1=y - self.top, x2=x + self.right, y2=y + self.bottom
|
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 collections import deque
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Callable, Iterable
|
from typing import Callable, Iterable, TypeVar, Generic
|
||||||
from .environment import Environment
|
from .environment import Environment, Prologue
|
||||||
from ..logger import HIGHLEVEL_LOGGER as LOGGER
|
from ..logger import HIGHLEVEL_LOGGER as LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
P = TypeVar("P", bound=Prologue)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Job:
|
class Job(Generic[P]):
|
||||||
"""The class represents a job."""
|
"""The class represents a job."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
"""The name of this job."""
|
"""The name of this job."""
|
||||||
dependencies: set[str]
|
dependencies: set[str]
|
||||||
"""The dependencies of this job. Each item is a job name. Empty for no dependencies."""
|
"""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."""
|
"""The executor of this job."""
|
||||||
|
|
||||||
|
|
||||||
class Context:
|
class Context(Generic[P]):
|
||||||
__jobs: dict[str, Job]
|
__jobs: dict[str, Job[P]]
|
||||||
"""The dict storing all jobs. Key is job name and value is job payload."""
|
"""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."""
|
"""The shared environment passed to every job executor."""
|
||||||
|
|
||||||
def __init__(self, env: Environment) -> None:
|
def __init__(self, env: Environment[P]) -> None:
|
||||||
self.__jobs = {}
|
self.__jobs = {}
|
||||||
self.__env = env
|
self.__env = env
|
||||||
|
|
||||||
@@ -92,7 +95,7 @@ class Context:
|
|||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
deps: Iterable[str],
|
deps: Iterable[str],
|
||||||
exec: Callable[[Environment], None],
|
exec: Callable[[Environment[P]], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Register a new job with given name, dependencies and executor.
|
Register a new job with given name, dependencies and executor.
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ from types import TracebackType
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TypeVar, Generic
|
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 ..logger import HIGHLEVEL_LOGGER as LOGGER
|
||||||
from ..lowlevel.artrdr import SvgRenderKind
|
|
||||||
from ..lowlevel.artrdr import SvgRenderKind, SvgConfig, BlenderConfig
|
from ..lowlevel.artrdr import SvgRenderKind, SvgConfig, BlenderConfig
|
||||||
from ..lowlevel import artio, artrdr
|
from ..lowlevel import artio, artrdr
|
||||||
|
|
||||||
@@ -120,6 +119,14 @@ class Environment(Generic[P]):
|
|||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
# region: Prologue Visitor
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prologue(self) -> P:
|
||||||
|
return self.__prologue
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
# region: New Artifact
|
# region: New Artifact
|
||||||
|
|
||||||
def new_artifact(self) -> 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
|
'Saving FreeDesktop artifact with kind "%s" and name "%s".', kind, name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
check_vart(art)
|
||||||
|
|
||||||
for hw, thumbnail in zip(
|
for hw, thumbnail in zip(
|
||||||
_FD_THUMBNAIL_HWS, artrdr.render_thumbnail(art, _FD_THUMBNAIL_HWS)
|
_FD_THUMBNAIL_HWS, artrdr.render_thumbnail(art, _FD_THUMBNAIL_HWS)
|
||||||
):
|
):
|
||||||
@@ -343,6 +352,7 @@ class Environment(Generic[P]):
|
|||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
'Saving Windows-only artifact with kind "%s" and name "%s".', kind, name
|
'Saving Windows-only artifact with kind "%s" and name "%s".', kind, name
|
||||||
)
|
)
|
||||||
|
check_vart(art)
|
||||||
artrdr.render_ico(art, self.__win_artifact(kind, name))
|
artrdr.render_ico(art, self.__win_artifact(kind, name))
|
||||||
|
|
||||||
def dup_win_artifact(
|
def dup_win_artifact(
|
||||||
@@ -372,6 +382,7 @@ class Environment(Generic[P]):
|
|||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
'Saving macOS-only artifact with kind "%s" and name "%s".', kind, name
|
'Saving macOS-only artifact with kind "%s" and name "%s".', kind, name
|
||||||
)
|
)
|
||||||
|
check_vart(art)
|
||||||
artrdr.render_icns(art, self.__mac_artifact(kind, name))
|
artrdr.render_icns(art, self.__mac_artifact(kind, name))
|
||||||
|
|
||||||
def dup_mac_artifact(
|
def dup_mac_artifact(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ VART_HW: int = 1024
|
|||||||
"""The height or width of Vanilla Artifact."""
|
"""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):
|
if art.size != (VART_HW, VART_HW):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"The size of general highlevel used artifact must be {VART_HW}x{VART_HW}, got {art.width}x{art.height} instead"
|
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
|
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:
|
def get_lowlevel_logger(self) -> logging.Logger:
|
||||||
return logging.getLogger("Lowlevel")
|
return logging.getLogger("Lowlevel")
|
||||||
|
|
||||||
@@ -18,3 +21,4 @@ class LoggerFactory:
|
|||||||
LOGGER_FACTORY = LoggerFactory()
|
LOGGER_FACTORY = LoggerFactory()
|
||||||
LOWLEVEL_LOGGER = LOGGER_FACTORY.get_lowlevel_logger()
|
LOWLEVEL_LOGGER = LOGGER_FACTORY.get_lowlevel_logger()
|
||||||
HIGHLEVEL_LOGGER = LOGGER_FACTORY.get_highlevel_logger()
|
HIGHLEVEL_LOGGER = LOGGER_FACTORY.get_highlevel_logger()
|
||||||
|
APP_LOGGER = LOGGER_FACTORY.get_app_logger()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import enum
|
import enum
|
||||||
import math
|
import math
|
||||||
from .utils import Artifact
|
from .utils import Artifact
|
||||||
|
from ..geometry import Point
|
||||||
|
|
||||||
|
|
||||||
class HorizontalAnchor(enum.IntEnum):
|
class HorizontalAnchor(enum.IntEnum):
|
||||||
@@ -15,14 +16,32 @@ class VerticalAnchor(enum.IntEnum):
|
|||||||
Bottom = enum.auto()
|
Bottom = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
def _anchor_offset(oversize: int, anchor: HorizontalAnchor | VerticalAnchor) -> int:
|
def anchor_to_offset(
|
||||||
match anchor:
|
oversize: Point,
|
||||||
case HorizontalAnchor.Left | VerticalAnchor.Top:
|
horizontal_anchor: HorizontalAnchor,
|
||||||
return 0
|
vertical_anchor: VerticalAnchor,
|
||||||
case HorizontalAnchor.Center | VerticalAnchor.Center:
|
) -> Point:
|
||||||
return oversize // 2
|
half_oversize = oversize // 2
|
||||||
case HorizontalAnchor.Right | VerticalAnchor.Bottom:
|
|
||||||
return oversize
|
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(
|
def aspect_ratio_scale_and_crop(
|
||||||
@@ -45,16 +64,20 @@ def aspect_ratio_scale_and_crop(
|
|||||||
scaled_height = math.ceil(art.height * scale)
|
scaled_height = math.ceil(art.height * scale)
|
||||||
scaled = art.resize((scaled_width, scaled_height), scale_resample)
|
scaled = art.resize((scaled_width, scaled_height), scale_resample)
|
||||||
|
|
||||||
crop_x = _anchor_offset(scaled_width - target_width, crop_horizontal_anchor)
|
crop = anchor_to_offset(
|
||||||
crop_y = _anchor_offset(scaled_height - target_height, crop_vertical_anchor)
|
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))
|
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,
|
art: Artifact,
|
||||||
horizontal_anchor: HorizontalAnchor,
|
horizontal_anchor: HorizontalAnchor,
|
||||||
vertical_anchor: VerticalAnchor,
|
vertical_anchor: VerticalAnchor,
|
||||||
) -> tuple[int, int]:
|
) -> Point:
|
||||||
"""
|
"""
|
||||||
Resolve the anchor point position inside given artifact.
|
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
|
anchors this is the corresponding edge; for Right/Bottom anchors
|
||||||
this is just outside the artifact (the exact corner coordinate).
|
this is just outside the artifact (the exact corner coordinate).
|
||||||
"""
|
"""
|
||||||
return (
|
return anchor_to_offset(
|
||||||
_anchor_offset(art.width, horizontal_anchor),
|
Point.from_tuple(art.size), horizontal_anchor, vertical_anchor
|
||||||
_anchor_offset(art.height, vertical_anchor),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def align_paste(
|
def align_paste(
|
||||||
art: Artifact,
|
art: Artifact,
|
||||||
clipboard: Artifact,
|
clipboard: Artifact,
|
||||||
art_pos: tuple[int, int],
|
art_pos: Point,
|
||||||
clipboard_pos: tuple[int, int],
|
clipboard_pos: Point,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Paste clipboard onto given artifact with two anchor points aligned.
|
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
|
:param clipboard_pos: The (x, y) point on the clipboard to be
|
||||||
aligned, usually resolved by anchor_to_pos.
|
aligned, usually resolved by anchor_to_pos.
|
||||||
"""
|
"""
|
||||||
art.paste(
|
art.paste(clipboard, (art_pos - clipboard_pos).to_tuple())
|
||||||
clipboard,
|
|
||||||
(art_pos[0] - clipboard_pos[0], art_pos[1] - clipboard_pos[1]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def align_alpha_composite(
|
def align_alpha_composite(
|
||||||
art: Artifact,
|
art: Artifact,
|
||||||
clipboard: Artifact,
|
clipboard: Artifact,
|
||||||
art_pos: tuple[int, int],
|
art_pos: Point,
|
||||||
clipboard_pos: tuple[int, int],
|
clipboard_pos: Point,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Alpha-supported ``align_paste``.
|
Alpha-supported ``align_paste``.
|
||||||
"""
|
"""
|
||||||
art.alpha_composite(
|
art.alpha_composite(clipboard, (art_pos - clipboard_pos).to_tuple())
|
||||||
clipboard, (art_pos[0] - clipboard_pos[0], art_pos[1] - clipboard_pos[1])
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import enum
|
import enum
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -6,7 +5,8 @@ from typing import Iterable, Iterator
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import PIL.Image
|
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):
|
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 kind: The selected SVG render kind.
|
||||||
:param cfg: The configuration when rendering.
|
:param cfg: The configuration when rendering.
|
||||||
"""
|
"""
|
||||||
logging.debug(
|
LOGGER.debug(
|
||||||
"Rendering SVG artifact with render %s: %s -> %s", kind.name, src, dst
|
"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.
|
The caller must make sure that this file is immutable during calling this function.
|
||||||
:param cfg: The configuration when rendering.
|
: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():
|
if not src.is_file():
|
||||||
raise ValueError(
|
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)
|
: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.
|
: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:
|
for hw in hws:
|
||||||
logging.debug("Rendering thumbnail %dx%d.", hw, hw)
|
LOGGER.debug("Rendering thumbnail %dx%d.", hw, hw)
|
||||||
thumbnail = art.copy()
|
thumbnail = art.copy()
|
||||||
thumbnail.thumbnail((hw, hw), PIL.Image.Resampling.LANCZOS)
|
thumbnail.thumbnail((hw, hw), PIL.Image.Resampling.LANCZOS)
|
||||||
yield thumbnail
|
yield thumbnail
|
||||||
@@ -193,9 +203,8 @@ def render_ico(art: Artifact, dst: Path) -> None:
|
|||||||
:param art: The artifact to be rendered.
|
:param art: The artifact to be rendered.
|
||||||
:param dst: The path to rendered result.
|
: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]
|
sizes = [16, 32, 48, 64, 128, 256]
|
||||||
# provide every frame explicitly so that we never depend on
|
# provide every frame explicitly so that we never depend on
|
||||||
# undocumented plugin-side resizing behavior
|
# 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 art: The artifact to be rendered.
|
||||||
:param dst: The path to rendered result.
|
: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
|
# provide every smaller frame explicitly; the 1024x1024 entry is
|
||||||
# covered by the art itself
|
# covered by the art itself
|
||||||
frames = list(render_thumbnail(art, (512, 256, 128, 64, 32)))
|
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 .action_icons import build_action_icons
|
||||||
from .mimetype_icons import build_mimetype_icons
|
from .mimetype_icons import build_mimetype_icons
|
||||||
from .application_icons import build_application_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_mimetype_icons(ctx)
|
||||||
build_application_icons(ctx)
|
build_application_icons(ctx)
|
||||||
build_action_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 .action_icons import build_action_icons
|
||||||
from .status_icons import build_status_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_action_icons(ctx)
|
||||||
build_status_icons(ctx)
|
build_status_icons(ctx)
|
||||||
|
|||||||
Reference in New Issue
Block a user