Compare commits

...
20 Commits
Author SHA1 Message Date
yyc12345 6a951c6d11 fix: fix sarasacw-picture name error 2026-09-22 16:38:01 +08:00
yyc12345 890019bc54 feat: adapt to new prologue with custom FreeDesktop thumbnail sizes 2026-09-10 09:28:53 +08:00
yyc12345 0d43fe3962 chore: use REUSE instead of non-standard license declaration 2026-09-09 22:02:14 +08:00
yyc12345 a33c3538f5 refactor: peel assemblicon off as indenpendent project
peel assemblicon off as indenpendent project
in SarasasChipWorkshop/assemblicon
2026-09-09 20:40:21 +08:00
yyc12345 72edad6edc doc: update README and LICENSE 2026-09-09 19:56:09 +08:00
yyc12345 2015ebf925 feat: cleanup bitmaps in references 2026-09-09 14:08:57 +08:00
yyc12345 1e15fb4783 feat: support monitor zoom fit best 2026-09-09 14:02:58 +08:00
yyc12345 42a2b08abf feat: support image-png/bmp/gif 2026-09-09 12:09:33 +08:00
yyc12345 61e003c741 fix: fix fatal source link issue 2026-09-08 22:29:50 +08:00
yyc12345 7b216b8865 doc: update TODO 2026-09-08 22:15:13 +08:00
yyc12345 36adf8029a feat: change src layout and write license infos 2026-09-08 22:13:59 +08:00
yyc12345 e0d6aa63e6 feat: add magnifying glass, zoom in, out, notepad icons 2026-09-08 14:31:15 +08:00
yyc12345 765375f04c feat: add plus/minus icons 2026-09-07 21:02:14 +08:00
yyc12345 a563917aa8 feat: split rotation and flip icon build functions 2026-09-07 20:52:24 +08:00
yyc12345 bb5032d502 feat: refactor (3/3) finish user side adaption 2026-09-06 14:38:04 +08:00
yyc12345 db04b9c809 fix: fix refactor issues again 2026-09-06 14:30:00 +08:00
yyc12345 44ba5fbbee fix: fix refactor issues 2026-09-06 13:59:32 +08:00
yyc12345 f650d3f98a feat: refactor assemblicon (2/3) context job topology 2026-09-06 13:35:47 +08:00
yyc12345 43e3bd39ef feat: refactor assemblicon (1/3) sink and source establishment 2026-09-05 22:56:14 +08:00
yyc12345 49edad7d33 feat: add todo 2026-09-03 19:33:07 +08:00
45 changed files with 1013 additions and 1572 deletions
+14
View File
@@ -0,0 +1,14 @@
# License
This project has mixed licencing.
You are free to copy, redistribute and/or modify aspects of this work under the terms of each licence accordingly.
## Icon Set Builder
The builder of this icon set is under **MIT License**.
The content of these part is under `tool` directory.
## Icon Set Assets
The assets for generating this icon set is under different licenses.
This project follows the [REUSE Specification](https://reuse.software/). You can collect the licensing information using the REUSE tool, or browse the `REUSE.toml` files located in their directory manually.
+9 -1
View File
@@ -5,4 +5,12 @@ Yet Another Aero Icon Set.
## Repository Notes
The content submitted in Git repository doesn't include any binary assets.
It only includes code part of this icon set for tracing the change of build code.
It only includes the code part of this repository for tracing the changes of code building this icon set.
## Status
Work in progress...
## License
See [LICENSES](LICENSE.md) for more details.
-15
View File
@@ -1,15 +0,0 @@
Ivar Leidus, CC BY-SA 3.0 <https://creativecommons.org/licenses/by-sa/3.0>, via Wikimedia Commons
https://commons.wikimedia.org/wiki/File:Niinsaare_j%C3%A4rv.jpg
Pascalou petit, CC BY-SA 3.0 <https://creativecommons.org/licenses/by-sa/3.0>, via Wikimedia Commons
https://commons.wikimedia.org/wiki/File:Aster_Tataricus.JPG
https://commons.wikimedia.org/wiki/File:Great_pershing_balloon_derby_2005_09_04.jpg
Balloon launch at the Great Pershing Balloon Derby near Brookfield, Missouri on September 4, 2005. Photo taken by Joe DeShon.
This file is licensed under the Creative Commons Attribution 2.5 Generic license.
https://commons.wikimedia.org/wiki/File:Wild_Petunia_Blue_Flower_1.JPG
I, Jonathan Zander, CC BY-SA 3.0 <http://creativecommons.org/licenses/by-sa/3.0/>, via Wikimedia Commons
https://commons.wikimedia.org/wiki/File:Daisy_(Argyranthemum_frutescens).jpg
Fir0002, CC BY-SA 3.0 <http://creativecommons.org/licenses/by-sa/3.0/>, via Wikimedia Commons
+8
View File
@@ -0,0 +1,8 @@
version = 1
[[annotations]]
path = "Nycticorax_nycticorax12.jpg"
precedence = "override"
SPDX-FileComment = "Source: https://commons.wikimedia.org/wiki/File:Nycticorax_nycticorax12.jpg"
SPDX-FileCopyrightText = "Calibas"
SPDX-License-Identifier = "CC-BY-SA-3.0"
-23
View File
@@ -1,23 +0,0 @@
[[licenses]]
file = 'object-flip-horizontal.svg'
license = "LGPL-2.1-only"
authors = ["KDE Oxygen Icon Theme Team", "yyc12345"]
source = 'https://invent.kde.org/frameworks/oxygen-icons'
[[licenses]]
file = 'object-flip-vertical.svg'
license = "LGPL-2.1-only"
authors = ["KDE Oxygen Icon Theme Team", "yyc12345"]
source = 'https://invent.kde.org/frameworks/oxygen-icons'
[[licenses]]
file = 'object-rotate-left.svg'
license = "LGPL-2.1-only"
authors = ["KDE Oxygen Icon Theme Team", "yyc12345"]
source = 'https://invent.kde.org/frameworks/oxygen-icons'
[[licenses]]
file = 'object-rotate-right.svg'
license = "LGPL-2.1-only"
authors = ["KDE Oxygen Icon Theme Team", "yyc12345"]
source = 'https://invent.kde.org/frameworks/oxygen-icons'
+75
View File
@@ -0,0 +1,75 @@
version = 1
[[annotations]]
path = "go-base.svg"
precedence = "override"
SPDX-FileCopyrightText = "2026 yyc12345"
SPDX-License-Identifier = "CC-BY-SA-4.0"
[[annotations]]
path = "go-inner-shining.svg"
precedence = "override"
SPDX-FileCopyrightText = "2026 yyc12345"
SPDX-License-Identifier = "CC-BY-SA-4.0"
[[annotations]]
path = "go-inner.svg"
precedence = "override"
SPDX-FileComment = "Source: https://github.com/google/material-design-icon"
SPDX-FileCopyrightText = ["2014-2026 Google", "2026 yyc12345"]
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = "object-flip-horizontal.svg"
precedence = "override"
SPDX-FileComment = "Source: https://invent.kde.org/frameworks/oxygen-icons"
SPDX-FileCopyrightText = ["2015-2026 KDE Oxygen Icon Theme Team", "2026 yyc12345"]
SPDX-License-Identifier = "LGPL-2.1-only"
[[annotations]]
path = "object-flip-vertical.svg"
precedence = "override"
SPDX-FileComment = "Source: https://invent.kde.org/frameworks/oxygen-icons"
SPDX-FileCopyrightText = ["2015-2026 KDE Oxygen Icon Theme Team", "2026 yyc12345"]
SPDX-License-Identifier = "LGPL-2.1-only"
[[annotations]]
path = "object-rotate-left.svg"
precedence = "override"
SPDX-FileComment = "Source: https://invent.kde.org/frameworks/oxygen-icons"
SPDX-FileCopyrightText = ["2015-2026 KDE Oxygen Icon Theme Team", "2026 yyc12345"]
SPDX-License-Identifier = "LGPL-2.1-only"
[[annotations]]
path = "object-rotate-right.svg"
precedence = "override"
SPDX-FileComment = "Source: https://invent.kde.org/frameworks/oxygen-icons"
SPDX-FileCopyrightText = ["2015-2026 KDE Oxygen Icon Theme Team", "2026 yyc12345"]
SPDX-License-Identifier = "LGPL-2.1-only"
[[annotations]]
path = "image-png-checkerboard.svg"
precedence = "override"
SPDX-FileCopyrightText = "2026 yyc12345"
SPDX-License-Identifier = "CC-BY-SA-4.0"
[[annotations]]
path = "dialog-infomation.svg"
precedence = "override"
SPDX-FileComment = "Source: https://invent.kde.org/frameworks/oxygen-icons"
SPDX-FileCopyrightText = ["2015-2026 KDE Oxygen Icon Theme Team", "2026 yyc12345"]
SPDX-License-Identifier = "LGPL-2.1-only"
[[annotations]]
path = "dialog-warning.svg"
precedence = "override"
SPDX-FileComment = "Source: https://invent.kde.org/frameworks/oxygen-icons"
SPDX-FileCopyrightText = ["2015-2026 KDE Oxygen Icon Theme Team", "2026 yyc12345"]
SPDX-License-Identifier = "LGPL-2.1-only"
[[annotations]]
path = "dialog-error.svg"
precedence = "override"
SPDX-FileComment = "Source: https://invent.kde.org/frameworks/oxygen-icons"
SPDX-FileCopyrightText = "2015-2026 KDE Oxygen Icon Theme Team"
SPDX-License-Identifier = "LGPL-2.1-only"
+13
View File
@@ -0,0 +1,13 @@
version = 1
[[annotations]]
path = "sarasacw-picture-frame.svg"
precedence = "override"
SPDX-FileCopyrightText = "2026 yyc12345"
SPDX-License-Identifier = "CC-BY-SA-4.0"
[[annotations]]
path = "notepad.blend"
precedence = "override"
SPDX-FileCopyrightText = "2026 yyc12345"
SPDX-License-Identifier = "CC-BY-SA-4.0"
+29
View File
@@ -0,0 +1,29 @@
version = 1
[[annotations]]
path = "Niinsaare_järv.jpg"
precedence = "override"
SPDX-FileComment = "Source: https://commons.wikimedia.org/wiki/File:Niinsaare_j%C3%A4rv.jpg"
SPDX-FileCopyrightText = "2013 Ivar Leidus"
SPDX-License-Identifier = "CC-BY-SA-3.0"
[[annotations]]
path = "Tulipa_suaveolens_floriade_to_Canberra.jpg"
precedence = "override"
SPDX-FileComment = "Source: https://commons.wikimedia.org/wiki/File:Tulipa_suaveolens_floriade_to_Canberra.jpg"
SPDX-FileCopyrightText = "2005 John O'Neill"
SPDX-License-Identifier = "CC-BY-SA-3.0"
[[annotations]]
path = "Hot-air_balloon_in_Tambov_-_02.jpg"
precedence = "override"
SPDX-FileComment = "Source: https://commons.wikimedia.org/wiki/File:Hot-air_balloon_in_Tambov_-_02.jpg"
SPDX-FileCopyrightText = "2022 Alexander Novikov"
SPDX-License-Identifier = "CC-BY-SA-4.0"
[[annotations]]
path = "Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg"
precedence = "override"
SPDX-FileComment = "Source: https://commons.wikimedia.org/wiki/File:Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg"
SPDX-FileCopyrightText = ["1889 Vincent van Gogh", "Google Arts & Culture"]
SPDX-License-Identifier = "CC0-1.0" # Public Domain actually.
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

-20
View File
@@ -1,20 +0,0 @@
[[licenses]]
file = 'go-base.svg'
license = "CC-BY-SA-4.0"
authors = ["yyc12345"]
[[licenses]]
file = 'go-inner-shining.svg'
license = "CC-BY-SA-4.0"
authors = ["yyc12345"]
[[licenses]]
file = 'go-inner.svg'
license = "Apache-2.0"
authors = ["Google", "yyc12345"]
source = 'https://github.com/google/material-design-icon'
[[licenses]]
file = 'notepad.blend'
license = "CC-BY-SA-4.0"
authors = ["yyc12345"]
+40
View File
@@ -0,0 +1,40 @@
version = 1
[[annotations]]
path = "image-base.svg"
precedence = "override"
SPDX-FileCopyrightText = "2026 yyc12345"
SPDX-License-Identifier = "CC-BY-SA-4.0"
[[annotations]]
path = "generic-background.svg"
precedence = "override"
SPDX-FileCopyrightText = "2026 yyc12345"
SPDX-License-Identifier = "CC-BY-SA-4.0"
[[annotations]]
path = "triangle.svg"
precedence = "override"
SPDX-FileComment = "Source: https://github.com/Templarian/MaterialDesign"
SPDX-FileCopyrightText = ["2014-2025 Austin Andrews", "2026 yyc12345"]
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = "magnifying-glass.blend"
precedence = "override"
SPDX-FileCopyrightText = "2026 yyc12345"
SPDX-License-Identifier = "CC-BY-SA-4.0"
[[annotations]]
path = "minus.svg"
precedence = "override"
SPDX-FileComment = "Source: https://github.com/google/material-design-icon"
SPDX-FileCopyrightText = ["2014-2026 Google", "2026 yyc12345"]
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = "plus.svg"
precedence = "override"
SPDX-FileComment = "Source: https://github.com/google/material-design-icon"
SPDX-FileCopyrightText = ["2014-2026 Google", "2026 yyc12345"]
SPDX-License-Identifier = "Apache-2.0"
-5
View File
@@ -1,5 +0,0 @@
[[licenses]]
file = 'Niinsaare_järv.jpg'
license = "CC-BY-SA-3.0"
authors = ["Ivar Leidus", "Wikimedia Commons"]
source = 'https://commons.wikimedia.org/wiki/File:Niinsaare_j%C3%A4rv.jpg'
+7
View File
@@ -0,0 +1,7 @@
version = 1
[[annotations]]
path = "**"
precedence = "override"
SPDX-FileCopyrightText = "2026 Sarasa's Chip Workshop"
SPDX-License-Identifier = "MIT"
+4 -1
View File
@@ -8,7 +8,7 @@ authors = [
]
requires-python = ">=3.13"
dependencies = [
"pillow>=12.3.0",
"assemblicon",
]
[project.scripts]
@@ -17,3 +17,6 @@ aurora-iconset-builder = "aurora_iconset_builder:main"
[build-system]
requires = ["uv_build>=0.8.0,<0.9"]
build-backend = "uv_build"
[tool.uv.sources]
assemblicon = { git = "https://github.com/SarasasChipWorkshop/assemblicon" }
+19 -9
View File
@@ -1,7 +1,8 @@
from pathlib import Path
from .assemblicon.highlevel.environment import Environment
from .assemblicon.highlevel.context import Context
from .assemblicon.logger import APP_LOGGER as LOGGER
from assemblicon.highlevel.utils import DEFAULT_FD_THUMBNAIL_HWS
from assemblicon.highlevel.environment import Environment
from assemblicon.highlevel.context import Context
from assemblicon.logger import APP_LOGGER as LOGGER
from .cli import parse
from .types import AuroraPrologue, AuroraContext
from . import std_icons, nostd_icons
@@ -10,18 +11,27 @@ from . import std_icons, nostd_icons
def main() -> None:
opts = parse()
def tempdir_initializer(p: Path) -> None:
# create our special temporary subdirectory
comp = p / "component"
comp.mkdir(parents=True, exist_ok=True)
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)
prologue = AuroraPrologue(
iconset_input,
iconset_output,
opts.svg_render,
# TODO: For easy testing, we only build 32x32 and 48x48.
# Enable full generation after testing.
# DEFAULT_FD_THUMBNAIL_HWS,
(32, 48, 256),
tempdir_initializer,
)
env = Environment(prologue)
ctx = Context(env)
# TODO: use explicit temporary directory initializer in prologue to do this.
# create our special temporary subdirectory
env.temporary_artifact("component").mkdir(parents=True, exist_ok=True)
try:
register(ctx)
ctx.run(opts.selection)
@@ -1,108 +0,0 @@
import enum
from typing import Optional
from pathlib import Path
from argparse import ArgumentParser, Namespace
from dataclasses import dataclass
from .lowlevel.artrdr import SvgRenderKind
class CliSvgRenderKind(enum.StrEnum):
Inkscape = "inkscape"
ReSvg = "resvg"
def to_assemblicon_type(self) -> SvgRenderKind:
match self:
case CliSvgRenderKind.Inkscape:
return SvgRenderKind.Inkscape
case CliSvgRenderKind.ReSvg:
return SvgRenderKind.ReSvg
@dataclass(frozen=True)
class StdOpts:
"""Assemblicon standard command line options"""
input_directory: Optional[Path]
"""The path to input directory. None if you doesn't request it."""
output_directory: Optional[Path]
"""The path to output directory. None if you doesn't request it."""
selection: Optional[list[str]]
"""The list holding all name of jobs for building."""
svg_render: CliSvgRenderKind
"""The render kind for SVG file."""
@dataclass(frozen=True)
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(setup: StdOptsSetup) -> ArgumentParser:
"""
Create Assemblicon standard argument parser.
This argument parser are filled properly with the requirement of Assemblicon itself.
It can be directly used or modified with extra options.
:param cfg: The setup of argument parser.
:return: The built argument parser.
"""
parser = ArgumentParser(
prog="Assemblicon",
description="The Programmatic Icon Assembly Workflow.",
)
if setup.require_input:
parser.add_argument(
"-i",
"--input",
dest="input",
action="store",
type=Path,
required=True,
help="The path to input directory.",
metavar="DIR",
)
if setup.require_output:
parser.add_argument(
"-o",
"--output",
dest="output",
action="store",
type=Path,
required=True,
help="The path to output directory.",
metavar="DIR",
)
parser.add_argument(
"-t",
"--select",
dest="selection",
action="extend",
nargs="+",
type=str,
help="Only build given name represented jobs.",
metavar="RENDER",
)
parser.add_argument(
"-s",
"--svg-render",
dest="svg_render",
action="store",
type=CliSvgRenderKind,
default=CliSvgRenderKind.Inkscape,
help="The render used for rendering SVG into PNG.",
metavar="RENDER",
)
return parser
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,
selection=ns.selection,
svg_render=ns.svg_render,
)
@@ -1,121 +0,0 @@
import math
from dataclasses import dataclass
@dataclass
class Point:
"""A point (or vector) on 2D plane with integer coordinates."""
x: int
y: int
@staticmethod
def from_tuple(t: tuple[int, int]) -> "Point":
return Point(t[0], t[1])
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 Rectangle:
"""
A rectangle framed by two corner points.
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(
pos: Point, 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(pos.x - left, pos.y - top), Point(pos.x + right, pos.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,114 +0,0 @@
from collections import deque
from dataclasses import dataclass
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(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[P]], None]
"""The executor of this 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[P]
"""The shared environment passed to every job executor."""
def __init__(self, env: Environment[P]) -> None:
self.__jobs = {}
self.__env = env
def run(self, selected: Iterable[str] | None) -> None:
"""
Execute builder with given selections.
The selected jobs, together with all their transitive dependencies,
are resolved and topologically sorted (Kahn's algorithm), then
executed sequentially with the shared environment. The chain is
aborted on the first failing executor.
:param selected: Only build jobs matching these given names.
Their dependencies are always built first. None for building
all jobs.
"""
# Step 1: resolve the selection closure. Collect the selected jobs
# themselves plus all their transitive dependencies into "needed".
needed: set[str] = set()
if selected is None:
needed = set(self.__jobs.keys())
else:
pending = list(selected)
while pending:
name = pending.pop()
if name in needed:
continue
job = self.__jobs.get(name)
if job is None:
raise ValueError(f'Unknown job name: "{name}"')
needed.add(name)
pending.extend(job.dependencies)
# Step 2: Kahn topological sort over the sub-graph induced by "needed".
# "in_degree": key is a job name, value is how many of its (needed)
# dependencies are not built yet.
# "dependents": key is a job name, value is the list of jobs in
# "needed" that directly depend on the key job.
in_degree: dict[str, int] = {name: 0 for name in needed}
dependents: dict[str, list[str]] = {name: [] for name in needed}
for name in needed:
for dep in self.__jobs[name].dependencies:
dependents[dep].append(name)
in_degree[name] += 1
queue = deque(
name for name in self.__jobs if name in needed and in_degree[name] == 0
)
order: list[str] = []
while queue:
name = queue.popleft()
order.append(name)
for dependent in dependents[name]:
in_degree[dependent] -= 1
if in_degree[dependent] == 0:
queue.append(dependent)
if len(order) != len(needed):
cyclic = sorted(needed - set(order))
raise ValueError(f"Dependency cycle detected among jobs: {cyclic}")
# Step 3: execute the build chain in resolved order.
for name in order:
LOGGER.info('Running job "%s"...', name)
self.__jobs[name].executor(self.__env)
def register(
self,
name: str,
deps: Iterable[str],
exec: Callable[[Environment[P]], None],
) -> None:
"""
Register a new job with given name, dependencies and executor.
:param name: The name of job.
:param deps: An iterable whose each item is the name of dependencies of this job.
:param exec: The executor of this job.
"""
if name in self.__jobs:
raise ValueError(f'Can not register 2 jobs with same name: "{name}"')
job_deps = set(deps)
if name in job_deps:
raise ValueError(f'Self-dependency is not allowed in job: "{name}"')
self.__jobs[name] = Job(name, job_deps, exec)
@@ -1,404 +0,0 @@
import tempfile
import os
import enum
from typing import Optional
from types import TracebackType
from dataclasses import dataclass
from pathlib import Path
from typing import TypeVar, Generic
from .utils import Artifact, ArtifactFile, VART_HW, check_vart
from ..logger import HIGHLEVEL_LOGGER as LOGGER
from ..lowlevel.artrdr import SvgRenderKind, SvgConfig, BlenderConfig
from ..lowlevel import artio, artrdr
@dataclass(frozen=True)
class Prologue:
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 an existing directory."
)
if not self.output_directory.is_dir():
raise ValueError(
f"Given output path {self.output_directory} is not an existing directory."
)
P = TypeVar("P", bound=Prologue)
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 = artrdr.SvgConfig(VART_HW, VART_HW)
_DEFAULT_BLD_CONFIG = artrdr.BlenderConfig(VART_HW, VART_HW)
# TODO: For easy testing, we only build 32x32 and 48x48.
# Enable full generation after testing.
_FD_THUMBNAIL_HWS: tuple[int, ...] = (32, 48)
# _FD_THUMBNAIL_HWS: tuple[int, ...] = (16, 32, 48, 64, 128, 256)
class Environment(Generic[P]):
__prologue: P
"""The prologue used for environment."""
__temp_dir: tempfile.TemporaryDirectory[str]
def __init__(self, prologue: P) -> None:
self.__prologue = prologue
self.__temp_dir = tempfile.TemporaryDirectory()
# construct basic layout of temporary directory
self.temporary_artifact(_TEMP_USER_DIR).mkdir(parents=True, exist_ok=True)
self.temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_ALLOC_DIR).mkdir(
parents=True, exist_ok=True
)
self.temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_STABLE_DIR).mkdir(
parents=True, exist_ok=True
)
# and output directory
for hw in _FD_THUMBNAIL_HWS:
for kind in FdContext:
self.output_artifact(f"{hw}x{hw}", str(kind)).mkdir(
parents=True, exist_ok=True
)
for kind in WinCategory:
self.output_artifact(_ART_WIN, str(kind)).mkdir(parents=True, exist_ok=True)
for kind in MacCategory:
self.output_artifact(_ART_MAC, str(kind)).mkdir(parents=True, exist_ok=True)
# region: With Visitor Requirements
def __enter__(self) -> "Environment":
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
self.__temp_dir.cleanup()
# endregion
# region: Prologue Visitor
@property
def prologue(self) -> P:
return self.__prologue
# endregion
# region: New Artifact
def new_artifact(self) -> Artifact:
"""
Create new RGBA vanilla size (1024x1024) empty (#ffffff00) artifact.
"""
return artio.new_artifact((VART_HW, VART_HW))
# endregion
# region: Input Artifact
def input_artifact(self, *args: str) -> Path:
"""
Get the path to input artifact.
"""
return self.__prologue.input_directory / Path(*args)
def load_input_bitmap_artifact(self, *args: str) -> ArtifactFile:
"""
Load given input bitmap asset.
"""
p = self.input_artifact(*args)
LOGGER.info("Loading input artifact: %s", p)
return artio.load_input_artifact(p)
# endregion
# region: Temporary Artifact
def __user_temporary_artifact(self, *args: str) -> Path:
return self.temporary_artifact(_TEMP_USER_DIR, *args)
def __lib_temporary_artifact(self, *args: str) -> Path:
return self.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 Path(self.__temp_dir.name) / Path(*args)
def load_temporary_artifact(self, *args: str) -> ArtifactFile:
p = self.temporary_artifact(*args)
LOGGER.info("Loading temporary artifact: %s", p)
return artio.load_artifact(p)
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.
"""
p = self.temporary_artifact(*args)
LOGGER.info("Saving temporary artifact: %s", p)
artio.save_artifact(art, p)
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:
LOGGER.info("Rendering temporary SVG: %s", src)
dst = self.__allocate_lib_temporary_artifact(".png")
else:
LOGGER.info("Rendering SVG: %s -> %s", src, dst)
artrdr.render_svg(src, dst, self.__prologue.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] = None,
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:
LOGGER.info("Rendering temporary Blender composition: %s", src)
dst = self.__allocate_lib_temporary_artifact(".png")
else:
LOGGER.info("Rendering Blender composition: %s -> %s", src, dst)
artrdr.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] = None,
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: Output Artifact
def output_artifact(self, *args: str) -> Path:
return self.__prologue.output_directory / Path(*args)
# endregion
# region: Advanced Saver
def __fd_artifact(self, hw: int, kind: FdContext, name: str) -> Path:
return self.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.
"""
LOGGER.info(
'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)
):
dst = self.__fd_artifact(hw, kind, name)
artio.save_artifact(thumbnail, dst)
def dup_fd_artifact(
self, src_kind: FdContext, src_name: str, dst_kind: FdContext, dst_name: str
) -> None:
LOGGER.info(
'Duplicating FreeDesktop artifact from kind "%s" name "%s" to kind "%s" name "%s".',
src_kind,
src_name,
dst_kind,
dst_name,
)
for hw in _FD_THUMBNAIL_HWS:
artio.dup_artifact(
self.__fd_artifact(hw, src_kind, src_name),
self.__fd_artifact(hw, dst_kind, dst_name),
)
def __win_artifact(self, kind: WinCategory, name: str) -> Path:
return self.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.
"""
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(
self, src_kind: WinCategory, src_name: str, dst_kind: WinCategory, dst_name: str
) -> None:
LOGGER.info(
'Duplicating Windows-only artifact from kind "%s" name "%s" to kind "%s" name "%s".',
src_kind,
src_name,
dst_kind,
dst_name,
)
artio.dup_artifact(
self.__win_artifact(src_kind, src_name),
self.__win_artifact(dst_kind, dst_name),
)
def __mac_artifact(self, kind: MacCategory, name: str) -> Path:
return self.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.
"""
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(
self, src_kind: MacCategory, src_name: str, dst_kind: MacCategory, dst_name: str
) -> None:
LOGGER.info(
'Duplicating macOS-only artifact from kind "%s" name "%s" to kind "%s" name "%s".',
src_kind,
src_name,
dst_kind,
dst_name,
)
artio.dup_artifact(
self.__mac_artifact(src_kind, src_name),
self.__mac_artifact(dst_kind, dst_name),
)
# endregion
@@ -1,7 +0,0 @@
from .common import Sink
from .tempdir_sink import TempDirSink
__all__ = [
"Sink",
"TempDirSink"
]
@@ -1,11 +0,0 @@
from abc import ABC, abstractmethod
from ..utils import Artifact
class Sink(ABC):
"""
Abstract base class for all sinks.
"""
@abstractmethod
def push_artifact(self, art: Artifact) -> None:
pass
@@ -1,12 +0,0 @@
from ..utils import Artifact
from .common import Sink
class TempDirSink(Sink):
__args: tuple[str, ...]
def __init__(self, *args: str) -> None:
self.__args = tuple(args)
def push_artifact(self, art: Artifact) -> None:
pass
@@ -1,7 +0,0 @@
from .common import Source
from .tempdir_source import TempDirSource
__all__ = [
"Source",
"TempDirSource"
]
@@ -1,11 +0,0 @@
from abc import ABC, abstractmethod
from ..utils import Artifact
class Source(ABC):
"""
Abstract base class for all sources.
"""
@abstractmethod
def pull_artifact(self) -> Artifact:
pass
@@ -1,12 +0,0 @@
from ..utils import Artifact
from .common import Source
class TempDirSource(Source):
__path_comps: tuple[str, ...]
def __init__(self, *args: str) -> None:
self.__path_comps = tuple(args)
def pull_artifact(self) -> Artifact:
pass
@@ -1,11 +0,0 @@
from ..lowlevel.utils import Artifact, ArtifactFile
VART_HW: int = 1024
"""The height or width of Vanilla Artifact."""
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"
)
@@ -1,24 +0,0 @@
import logging
class LoggerFactory:
def __init__(self) -> None:
# setup log format and level
logging.basicConfig(
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")
def get_highlevel_logger(self) -> logging.Logger:
return logging.getLogger("Highlevel")
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,38 +0,0 @@
import shutil
from pathlib import Path
import PIL.Image
from .utils import Artifact, ArtifactFile
from ..logger import LOWLEVEL_LOGGER as LOGGER
def new_artifact(size: tuple[int, int]) -> Artifact:
"""
Create new RGBA empty (#ffffff00) artifact with given size.
"""
return PIL.Image.new("RGBA", size, "#ffffff00")
def load_input_artifact(p: Path) -> ArtifactFile:
"""
Load bitmap artifact (PNG, JPEG or BMP) which usually is located in input directory.
"""
LOGGER.debug("Loading input artifact: %s", p)
return PIL.Image.open(p, "r", ["PNG", "JPEG", "BMP"])
def load_artifact(p: Path) -> ArtifactFile:
"""
Load bitmap artifact (PNG-only) saved previously as the intermediary.
"""
LOGGER.debug("Loading temporary or output artifact: %s", p)
return PIL.Image.open(p, "r", ["PNG"])
def save_artifact(art: Artifact, p: Path) -> None:
LOGGER.debug("Saving artifact: %s", p)
art.save(p, format="PNG")
def dup_artifact(src: Path, dst: Path) -> None:
LOGGER.debug("Duplicating artifact: %s -> %s", src, dst)
shutil.copyfile(src, dst)
@@ -1,136 +0,0 @@
import enum
import math
import PIL.Image
from .utils import Artifact
from ..geometry import Point
class HorizontalAnchor(enum.IntEnum):
Left = enum.auto()
Center = enum.auto()
Right = enum.auto()
class VerticalAnchor(enum.IntEnum):
Top = enum.auto()
Center = enum.auto()
Bottom = enum.auto()
def anchor_to_offset(
oversize_width: int,
oversize_height: int,
horizontal_anchor: HorizontalAnchor,
vertical_anchor: VerticalAnchor,
) -> Point:
x: int
match horizontal_anchor:
case HorizontalAnchor.Left:
x = 0
case HorizontalAnchor.Center:
x = oversize_width // 2
case HorizontalAnchor.Right:
x = oversize_width
y: int
match vertical_anchor:
case VerticalAnchor.Top:
y = 0
case VerticalAnchor.Center:
y = oversize_height // 2
case VerticalAnchor.Bottom:
y = oversize_height
return Point(x, y)
def aspect_ratio_scale_and_crop(
art: Artifact,
target_width: int,
target_height: int,
scale_resample: int,
crop_horizontal_anchor: HorizontalAnchor,
crop_vertical_anchor: VerticalAnchor,
) -> Artifact:
if target_width <= 0:
raise ValueError("Target width must be a positive integer")
if target_height <= 0:
raise ValueError("Target height must be positive integer")
scale = max(target_width / art.width, target_height / art.height)
# use math.ceil to ensure that resized image at least has required size
# (floating point value error may cause this)
scaled_width = math.ceil(art.width * scale)
scaled_height = math.ceil(art.height * scale)
scaled = art.resize((scaled_width, scaled_height), scale_resample)
crop = anchor_to_offset(
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 default_art_resize(art: Artifact, width: int, height: int) -> Artifact:
"""
Resize given artifact with default resample (LANCZOS).
"""
return art.resize((width, height), PIL.Image.Resampling.LANCZOS)
def art_anchor_to_offset(
art: Artifact,
horizontal_anchor: HorizontalAnchor,
vertical_anchor: VerticalAnchor,
) -> Point:
"""
Resolve the anchor point position inside given artifact.
:param art: The artifact whose anchor point should be resolved.
:param horizontal_anchor: The anchor selection at horizontal direction.
:param vertical_anchor: The anchor selection at vertical direction.
:return: The (x, y) coordinate of the anchor point. For Left/Top
anchors this is the corresponding edge; for Right/Bottom anchors
this is just outside the artifact (the exact corner coordinate).
"""
return anchor_to_offset(*art.size, horizontal_anchor, vertical_anchor)
def align_paste(
art: Artifact,
clipboard: Artifact,
art_pos: Point,
clipboard_pos: Point,
) -> None:
"""
Paste clipboard onto given artifact with two anchor points aligned.
The clipboard is pasted in the way that the point indicated by
clipboard_pos on the clipboard exactly overlaps the point indicated
by art_pos on the artifact. The art is modified in place. Positions
resolving to negative paste origins are allowed and clipped by PIL
just like a normal paste call.
:param art: The artifact pasted onto. Modified in place.
:param clipboard: The artifact pasted as the clipboard.
:param art_pos: The (x, y) point on the art to be aligned, usually
resolved by anchor_to_pos.
:param clipboard_pos: The (x, y) point on the clipboard to be
aligned, usually resolved by anchor_to_pos.
"""
art.paste(clipboard, (art_pos - clipboard_pos).to_tuple())
def align_alpha_composite(
art: Artifact,
clipboard: Artifact,
art_pos: Point,
clipboard_pos: Point,
) -> None:
"""
Alpha-supported ``align_paste``.
"""
art.alpha_composite(clipboard, (art_pos - clipboard_pos).to_tuple())
@@ -1,232 +0,0 @@
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
from ..logger import LOWLEVEL_LOGGER as LOGGER
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.
"""
LOGGER.debug(
"Rendering SVG artifact with render %s: %s -> %s", kind.name, src, dst
)
# YYC MARK:
# Inkscape will silently quit with return code 0
# if given source file doesn't exist in file system.
# So we need to check whether it is existing and popularize it to all render.
if not src.is_file():
raise ValueError(f"Source SVG file is not presented in file system: {src}")
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}. 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.
"""
LOGGER.debug("Rendering Blender artifact: %s -> %s", src, dst)
if not src.is_file():
raise ValueError(
f"Source Blender composition file is not presented in file system: {src}"
)
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.
"""
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:
LOGGER.debug("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.
"""
LOGGER.debug("Rendering .ICO artifact: %s", dst)
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.
"""
LOGGER.debug("Rendering .ICNS artifact: %s", dst)
# 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,5 +0,0 @@
from PIL.Image import Image
from PIL.ImageFile import ImageFile
type Artifact = Image
type ArtifactFile = ImageFile
+1 -1
View File
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from typing import Optional
from .assemblicon.cli import (
from assemblicon.cli import (
SvgRenderKind,
StdOptsSetup,
create_argparser,
@@ -1,8 +1,9 @@
from ..types import AuroraContext, AuroraEnvironment
from ..assemblicon.highlevel.environment import FdContext
from ..assemblicon.highlevel.utils import VART_HW
from ..assemblicon.geometry import Rectangle
from ..assemblicon.lowlevel.artproc import (
from assemblicon.highlevel.utils import FdContext, VART_HW
from assemblicon.highlevel.source import TempArtSource, NewArtSource
from assemblicon.highlevel.sink import FdArtSink
from assemblicon.geometry import Rectangle
from assemblicon.lowlevel.artproc import (
art_anchor_to_offset,
anchor_to_offset,
default_art_resize,
@@ -16,20 +17,50 @@ from ..assemblicon.lowlevel.artproc import (
def register(ctx: AuroraContext) -> None:
ctx.register(
"nostd/action/image-properties",
("nostd/mimetype/image-jpeg", "std/status/dialog-infomation"),
(
TempArtSource("image-jpeg.png"),
TempArtSource("dialog-infomation.png"),
FdArtSink(FdContext.Actions, "image-properties"),
),
build_image_properties_icon,
)
ctx.register(
"nostd/action/image-zoom-original",
("nostd/mimetype/image-jpeg", "component/triangles"),
(
TempArtSource("component", "triangle-left.png"),
TempArtSource("component", "triangle-down.png"),
TempArtSource("component", "triangle-right.png"),
TempArtSource("component", "triangle-up.png"),
TempArtSource("image-jpeg.png"),
NewArtSource(),
FdArtSink(FdContext.Actions, "image-zoom-original"),
),
build_image_zoom_original_icon,
)
ctx.register(
"nostd/action/monitor-zoom-fit-best",
(
TempArtSource("component", "triangle-left.png"),
TempArtSource("component", "triangle-down.png"),
TempArtSource("component", "triangle-right.png"),
TempArtSource("component", "triangle-up.png"),
TempArtSource("component", "monitor.png"),
NewArtSource(),
FdArtSink(FdContext.Actions, "monitor-zoom-fit-best"),
),
build_monitor_zoom_fit_best_icon,
)
def build_image_properties_icon(env: AuroraEnvironment) -> None:
art = env.load_temporary_artifact("image-jpeg.png")
def build_image_properties_icon(
env: AuroraEnvironment,
jpeg_src: TempArtSource,
infomation_src: TempArtSource,
fd_sink: FdArtSink,
) -> None:
art = jpeg_src.pull(env)
info_mark = env.load_temporary_artifact("dialog-infomation.png")
info_mark = infomation_src.pull(env)
resized_info_mark = default_art_resize(info_mark, 500, 500)
align_alpha_composite(
@@ -42,31 +73,32 @@ def build_image_properties_icon(env: AuroraEnvironment) -> None:
)
# save artifact
env.save_fd_artifact(art, FdContext.Actions, "image-properties")
fd_sink.push(env, art)
def build_image_zoom_original_icon(env: AuroraEnvironment) -> None:
def build_image_zoom_original_icon(
env: AuroraEnvironment,
tri_left_src: TempArtSource,
tri_down_src: TempArtSource,
tri_right_src: TempArtSource,
tri_up_src: TempArtSource,
inner_src: TempArtSource,
new_art: NewArtSource,
fd_sink: FdArtSink,
) -> None:
# load triangle assets
TRI_SIZE = (200, 200)
tri_left = default_art_resize(
env.load_temporary_artifact("component", "triangle-left.png"), *TRI_SIZE
)
tri_down = default_art_resize(
env.load_temporary_artifact("component", "triangle-down.png"), *TRI_SIZE
)
tri_right = default_art_resize(
env.load_temporary_artifact("component", "triangle-right.png"), *TRI_SIZE
)
tri_up = default_art_resize(
env.load_temporary_artifact("component", "triangle-up.png"), *TRI_SIZE
)
tri_left = default_art_resize(tri_left_src.pull(env), *TRI_SIZE)
tri_down = default_art_resize(tri_down_src.pull(env), *TRI_SIZE)
tri_right = default_art_resize(tri_right_src.pull(env), *TRI_SIZE)
tri_up = default_art_resize(tri_up_src.pull(env), *TRI_SIZE)
# load inner image
inner = env.load_temporary_artifact("image-jpeg.png")
inner = inner_src.pull(env)
resized_inner = default_art_resize(inner, 680, 680)
# composite this icon
icon = env.new_artifact()
icon = new_art.pull()
align_paste(
icon,
@@ -125,4 +157,87 @@ def build_image_zoom_original_icon(env: AuroraEnvironment) -> None:
)
# save it
env.save_fd_artifact(icon, FdContext.Actions, "image-zoom-original")
fd_sink.push(env, icon)
def build_monitor_zoom_fit_best_icon(
env: AuroraEnvironment,
tri_left_src: TempArtSource,
tri_down_src: TempArtSource,
tri_right_src: TempArtSource,
tri_up_src: TempArtSource,
inner_src: TempArtSource,
new_art: NewArtSource,
fd_sink: FdArtSink,
) -> None:
# load triangle assets
TRI_SIZE = (200, 200)
tri_left = default_art_resize(tri_left_src.pull(env), *TRI_SIZE)
tri_down = default_art_resize(tri_down_src.pull(env), *TRI_SIZE)
tri_right = default_art_resize(tri_right_src.pull(env), *TRI_SIZE)
tri_up = default_art_resize(tri_up_src.pull(env), *TRI_SIZE)
# load inner image
inner = inner_src.pull(env)
resized_inner = default_art_resize(inner, 620, 620)
# composite this icon
icon = new_art.pull()
align_paste(
icon,
resized_inner,
art_anchor_to_offset(icon, HorizontalAnchor.Center, VerticalAnchor.Center),
art_anchor_to_offset(
resized_inner, HorizontalAnchor.Center, VerticalAnchor.Center
),
)
tri_rect = Rectangle.from_margin_rect(VART_HW, VART_HW, 100, 0, 100, 0)
align_alpha_composite(
icon,
tri_right,
anchor_to_offset(
tri_rect.width,
tri_rect.height,
HorizontalAnchor.Left,
VerticalAnchor.Center,
)
+ tri_rect.pos1,
art_anchor_to_offset(tri_right, HorizontalAnchor.Left, VerticalAnchor.Center),
)
align_alpha_composite(
icon,
tri_left,
anchor_to_offset(
tri_rect.width,
tri_rect.height,
HorizontalAnchor.Right,
VerticalAnchor.Center,
)
+ tri_rect.pos1,
art_anchor_to_offset(tri_left, HorizontalAnchor.Right, VerticalAnchor.Center),
)
align_alpha_composite(
icon,
tri_down,
anchor_to_offset(
tri_rect.width, tri_rect.height, HorizontalAnchor.Center, VerticalAnchor.Top
)
+ tri_rect.pos1,
art_anchor_to_offset(tri_down, HorizontalAnchor.Center, VerticalAnchor.Top),
)
align_alpha_composite(
icon,
tri_up,
anchor_to_offset(
tri_rect.width,
tri_rect.height,
HorizontalAnchor.Center,
VerticalAnchor.Bottom,
)
+ tri_rect.pos1,
art_anchor_to_offset(tri_up, HorizontalAnchor.Center, VerticalAnchor.Bottom),
)
# save it
fd_sink.push(env, icon)
@@ -1,7 +1,13 @@
import PIL.Image
from ..types import AuroraContext, AuroraEnvironment
from ..assemblicon.highlevel.environment import FdContext, WinCategory, MacCategory
from ..assemblicon.lowlevel.artproc import (
from assemblicon.highlevel.utils import FdContext, WinCategory, MacCategory
from assemblicon.highlevel.source import (
SvgSource,
TempArtSource,
NewArtSource,
BlenderSource,
)
from assemblicon.highlevel.sink import FdArtSink, WinArtSink, MacArtSink, TempArtSink
from assemblicon.lowlevel.artproc import (
art_anchor_to_offset,
default_art_resize,
align_paste,
@@ -12,19 +18,41 @@ from ..assemblicon.lowlevel.artproc import (
def register(ctx: AuroraContext) -> None:
ctx.register(
"nostd/application/org.sarasachipworkshop.picture",
("nostd/mimetype/image-jpeg", ),
build_sarasacw_picture_icons,
"nostd/application/org.sarasaschipworkshop.picture",
(
SvgSource("apps", "sarasacw-picture-frame.svg"),
TempArtSource("image-jpeg.png"),
NewArtSource(),
FdArtSink(FdContext.Applications, "org.sarasaschipworkshop.picture"),
WinArtSink(WinCategory.Application, "sarasacw-picture"),
MacArtSink(MacCategory.Application, "sarasacw-picture"),
),
build_sarasacw_picture_icon,
)
ctx.register(
"nostd/application/notepad",
(
BlenderSource("apps", "notepad.blend"),
TempArtSink("notepad.png"),
WinArtSink(WinCategory.Application, "notepad"),
),
build_notepad_icon,
)
def build_sarasacw_picture_icons(env: AuroraEnvironment) -> None:
app = env.new_artifact()
frame = env.render_svg_then_load(
env.input_artifact("app", "sarasacw-picture", "app-frame.svg")
)
def build_sarasacw_picture_icon(
env: AuroraEnvironment,
frame_src: SvgSource,
inner_src: TempArtSource,
new_art: NewArtSource,
fd_sink: FdArtSink,
win_sink: WinArtSink,
mac_sink: MacArtSink,
) -> None:
app = new_art.pull()
frame = frame_src.pull(env)
inner = env.load_temporary_artifact("image-jpeg.png")
inner = inner_src.pull(env)
resized_inner = default_art_resize(inner, 990, 990)
align_paste(
@@ -38,6 +66,17 @@ def build_sarasacw_picture_icons(env: AuroraEnvironment) -> None:
app.alpha_composite(frame, (0, 0))
# save artifact
env.save_fd_artifact(app, FdContext.Applications, "org.sarasachipworkshop.picture")
env.save_win_artifact(app, WinCategory.Application, "sarasacw-picture")
env.save_mac_artifact(app, MacCategory.Application, "sarasacw-picture")
fd_sink.push(env, app)
win_sink.push(env, app)
mac_sink.push(env, app)
def build_notepad_icon(
env: AuroraEnvironment,
src: BlenderSource,
temp_sink: TempArtSink,
win_sink: WinArtSink,
) -> None:
src.pull_to_temp(env, temp_sink)
with temp_sink.repull(env) as art:
win_sink.push(env, art)
@@ -1,68 +1,8 @@
import PIL.Image
import PIL.ImageEnhance
from ..types import AuroraContext, AuroraEnvironment
from ..assemblicon.highlevel.environment import FdContext, WinCategory, MacCategory
from ..assemblicon.highlevel.utils import Artifact
from ..assemblicon.geometry import Rectangle, Point
from ..assemblicon.lowlevel.artproc import (
aspect_ratio_scale_and_crop,
VerticalAnchor,
HorizontalAnchor,
)
from assemblicon.highlevel.utils import FdContext, WinCategory, MacCategory
from assemblicon.highlevel.source import SvgSource, TempArtSource
from assemblicon.highlevel.sink import TempArtSink, FdArtSink, WinArtSink, MacArtSink
def register(ctx: AuroraContext) -> None:
ctx.register("component/image-base", (), build_image_base_icon)
ctx.register(
"nostd/mimetype/image-jpeg", ("component/image-base",), build_image_jpeg_icon
)
IMAGE_INNER_RECT = Rectangle(Point(20 * 4, 52 * 4), Point(236 * 4, 204 * 4))
def build_image_base_icon(env: AuroraEnvironment) -> None:
env.render_svg(
env.input_artifact("component", "image-base.svg"),
env.temporary_artifact("component", "image-base.png"),
)
def build_image_jpeg_icon(env: AuroraEnvironment) -> None:
jpg_image = env.load_temporary_artifact("component", "image-base.png")
jpg_image_inner = env.load_input_bitmap_artifact(
"ext", "bitmap", "Niinsaare_järv.jpg"
)
with jpg_image_inner:
# crop the area which is interested by ourselves
interest_area = Rectangle.from_pos_size_rect(Point(2700, 0), 5016, 3888)
crop_jpg_image_inner = jpg_image_inner.crop(interest_area.to_tuple())
# do a flip
flip_jpg_image_inner = crop_jpg_image_inner.transpose(
PIL.Image.Transpose.FLIP_LEFT_RIGHT
)
# enhance its saturation
enhancer = PIL.ImageEnhance.Color(flip_jpg_image_inner)
saturated_jpg_image_inner = enhancer.enhance(1.4)
# fit to inner rect
bestfit_jpg_image_inner = aspect_ratio_scale_and_crop(
saturated_jpg_image_inner,
IMAGE_INNER_RECT.width,
IMAGE_INNER_RECT.height,
PIL.Image.Resampling.NEAREST,
HorizontalAnchor.Center,
VerticalAnchor.Center,
)
# paste into inner rect
jpg_image.paste(bestfit_jpg_image_inner, IMAGE_INNER_RECT.to_tuple())
# build artifacts
# save as temporary for future use
env.save_temporary_artifact(jpg_image, "image-jpeg.png")
# save for FD
env.save_fd_artifact(jpg_image, FdContext.MimeTypes, "image-jpeg")
# save for windows and mac
env.save_win_artifact(jpg_image, WinCategory.Extension, "jpg")
env.save_mac_artifact(jpg_image, MacCategory.Extension, "jpg")
pass
@@ -1,101 +1,176 @@
import PIL.Image
from ..types import AuroraContext, AuroraEnvironment
from ..assemblicon.highlevel.environment import FdContext
from ..assemblicon.highlevel.utils import Artifact
from assemblicon.geometry import Rectangle, Point
from assemblicon.lowlevel.artproc import default_art_resize, align_alpha_composite
from assemblicon.highlevel.utils import FdContext
from assemblicon.highlevel.source import SvgSource, TempArtSource
from assemblicon.highlevel.sink import FdArtSink
def register(ctx: AuroraContext) -> None:
ctx.register("std/action/go-[previous|down|next|up]", (), build_go_icons)
ctx.register(
"std/action/go-[previous|down|next|up]",
(
SvgSource("go-base.svg"),
SvgSource("go-inner-shining.svg"),
SvgSource("go-inner.svg"),
FdArtSink(FdContext.Actions, "go-previous"),
FdArtSink(FdContext.Actions, "go-down"),
FdArtSink(FdContext.Actions, "go-next"),
FdArtSink(FdContext.Actions, "go-up"),
),
build_go_icons,
)
ctx.register("std/action/object-flip-*", (), build_object_flip_icons)
ctx.register("std/action/object-rotate-*", (), build_object_rotate_icons)
ctx.register(
"std/action/object-flip-horizontal",
(
SvgSource("object-flip-horizontal.svg"),
FdArtSink(FdContext.Actions, "object-flip-horizontal"),
),
build_object_flip_horizontal_icon,
)
ctx.register(
"std/action/object-flip-vertical",
(
SvgSource("object-flip-vertical.svg"),
FdArtSink(FdContext.Actions, "object-flip-vertical"),
),
build_object_flip_vertical_icon,
)
ctx.register(
"std/action/object-rotate-left",
(
SvgSource("object-rotate-left.svg"),
FdArtSink(FdContext.Actions, "object-rotate-left"),
),
build_object_rotate_left_icon,
)
ctx.register(
"std/action/object-rotate-right",
(
SvgSource("object-rotate-right.svg"),
FdArtSink(FdContext.Actions, "object-rotate-right"),
),
build_object_rotate_right_icon,
)
ctx.register(
"std/action/zoom-[original|fit-best]",
("component/triangles",),
(
TempArtSource("component", "triangle-left.png"),
TempArtSource("component", "triangle-down.png"),
TempArtSource("component", "triangle-right.png"),
TempArtSource("component", "triangle-up.png"),
FdArtSink(FdContext.Actions, "zoom-original"),
FdArtSink(FdContext.Actions, "zoom-fit-best"),
),
build_zoom_origin_and_fit_best_icons,
)
ctx.register("std/action/zoom-[in|out]", (), build_zoom_in_out_icons)
ctx.register(
"std/action/zoom-[in|out]",
(
TempArtSource("component", "magnifying-glass.png"),
TempArtSource("component", "plus.png"),
TempArtSource("component", "minus.png"),
FdArtSink(FdContext.Actions, "zoom-in"),
FdArtSink(FdContext.Actions, "zoom-out"),
),
build_zoom_in_out_icons,
)
def build_go_icons(env: AuroraEnvironment) -> None:
# build go-* base
go_base = env.render_svg_then_load(env.input_artifact("component", "go-base.svg"))
# build go-* shining
go_inner_shining = env.render_svg_then_load(
env.input_artifact("component", "go-inner-shining.svg"),
)
# build go-* inner
go_inner = env.render_svg_then_load(
env.input_artifact("component", "go-inner.svg"),
)
def build_go_icons(
env: AuroraEnvironment,
base_src: SvgSource,
shining_src: SvgSource,
inner_src: SvgSource,
sink_previous: FdArtSink,
sink_down: FdArtSink,
sink_next: FdArtSink,
sink_up: FdArtSink,
) -> None:
# build go-* base, shining and inner
go_base = base_src.pull(env)
go_inner_shining = shining_src.pull(env)
go_inner = inner_src.pull(env)
with go_base, go_inner_shining, go_inner:
go_mask = go_inner
for part in ("previous", "down", "next", "up"):
for sink in (sink_previous, sink_down, sink_next, sink_up):
go = go_base.copy()
# render shining with inner mask
go.paste(go_inner_shining, (0, 0), go_mask)
# save as FD action
env.save_fd_artifact(go, FdContext.Actions, f"go-{part}")
sink.push(env, go)
# rotate it for next calling
go_mask = go_mask.transpose(PIL.Image.Transpose.ROTATE_90)
def build_object_flip_icons(env: AuroraEnvironment) -> None:
art = env.render_svg_then_load(env.input_artifact("object-flip-horizontal.svg"))
with art:
env.save_fd_artifact(art, FdContext.Actions, "object-flip-horizontal")
art = env.render_svg_then_load(env.input_artifact("object-flip-vertical.svg"))
with art:
env.save_fd_artifact(art, FdContext.Actions, "object-flip-vertical")
def build_object_rotate_icons(env: AuroraEnvironment) -> None:
art = env.render_svg_then_load(env.input_artifact("object-rotate-left.svg"))
with art:
env.save_fd_artifact(art, FdContext.Actions, "object-rotate-left")
art = env.render_svg_then_load(env.input_artifact("object-rotate-right.svg"))
with art:
env.save_fd_artifact(art, FdContext.Actions, "object-rotate-right")
def build_zoom_origin_and_fit_best_icons(env: AuroraEnvironment) -> None:
# load triangle assets
tri_left = env.load_temporary_artifact("component", "triangle-left.png")
tri_down = env.load_temporary_artifact("component", "triangle-down.png")
tri_right = env.load_temporary_artifact("component", "triangle-right.png")
tri_up = env.load_temporary_artifact("component", "triangle-up.png")
# build icons
with tri_left, tri_down, tri_right, tri_up:
build_zoom_origin_icon(env, tri_left, tri_down, tri_right, tri_up)
build_zoom_fit_best_icon(env, tri_left, tri_down, tri_right, tri_up)
def build_zoom_origin_icon(
env: AuroraEnvironment,
tri_left: Artifact,
tri_down: Artifact,
tri_right: Artifact,
tri_up: Artifact,
def build_object_flip_horizontal_icon(
env: AuroraEnvironment, src: SvgSource, sink: FdArtSink
) -> None:
# TODO: finihs this in future
pass
with src.pull(env) as art:
sink.push(env, art)
def build_zoom_fit_best_icon(
env: AuroraEnvironment,
tri_left: Artifact,
tri_down: Artifact,
tri_right: Artifact,
tri_up: Artifact,
def build_object_flip_vertical_icon(
env: AuroraEnvironment, src: SvgSource, sink: FdArtSink
) -> None:
with src.pull(env) as art:
sink.push(env, art)
def build_object_rotate_left_icon(
env: AuroraEnvironment, src: SvgSource, sink: FdArtSink
) -> None:
with src.pull(env) as art:
sink.push(env, art)
def build_object_rotate_right_icon(
env: AuroraEnvironment, src: SvgSource, sink: FdArtSink
) -> None:
with src.pull(env) as art:
sink.push(env, art)
def build_zoom_origin_and_fit_best_icons(
env: AuroraEnvironment,
tri_left_src: TempArtSource,
tri_down_src: TempArtSource,
tri_right_src: TempArtSource,
tri_up_src: TempArtSource,
zoom_original_sink: FdArtSink,
zoom_fit_best_sink: FdArtSink,
) -> None:
# TODO: finish this in future
pass
def build_zoom_in_out_icons(env: AuroraEnvironment) -> None:
# TODO: finihs this in future
pass
def build_zoom_in_out_icons(
env: AuroraEnvironment,
magnifying_glass_source: TempArtSource,
plus_source: TempArtSource,
minus_source: TempArtSource,
zoom_in_sink: FdArtSink,
zoom_out_sink: FdArtSink,
) -> None:
magnifying_glass = magnifying_glass_source.pull(env)
magnifying_glass_rect = Rectangle.from_padding_rect(
Point(665, 355), 200, 200, 200, 200
)
inner_size = (magnifying_glass_rect.width, magnifying_glass_rect.height)
plus = default_art_resize(plus_source.pull(env), *inner_size)
minus = default_art_resize(minus_source.pull(env), *inner_size)
zoom_in = magnifying_glass.copy()
align_alpha_composite(zoom_in, plus, magnifying_glass_rect.pos1, Point(0, 0))
zoom_in_sink.push(env, zoom_in)
zoom_out = magnifying_glass.copy()
align_alpha_composite(zoom_out, minus, magnifying_glass_rect.pos1, Point(0, 0))
zoom_out_sink.push(env, zoom_out)
@@ -1,46 +1,138 @@
import PIL.Image
from ..types import AuroraContext, AuroraEnvironment
from assemblicon.highlevel.source import (
SvgSource,
TempArtSource,
NewArtSource,
BlenderSource,
)
from assemblicon.highlevel.sink import TempArtSink
def register(ctx: AuroraContext) -> None:
ctx.register("component/generic-background", (), build_generic_background)
ctx.register(
"component/generic-background",
(
SvgSource("component", "generic-background.svg"),
TempArtSink("component", "generic-background.png"),
),
build_generic_background,
)
ctx.register(
"component/triangles",
("component/generic-background",),
(
SvgSource("component", "triangle.svg"),
TempArtSource("component", "generic-background.png"),
NewArtSource(),
TempArtSink("component", "triangle-left.png"),
TempArtSink("component", "triangle-down.png"),
TempArtSink("component", "triangle-right.png"),
TempArtSink("component", "triangle-up.png"),
),
build_triangle_icons,
)
def build_generic_background(env: AuroraEnvironment) -> None:
env.render_svg(
env.input_artifact("component", "generic-background.svg"),
env.temporary_artifact("component", "generic-background.png"),
ctx.register(
"component/plus_minus",
(
TempArtSource("component", "generic-background.png"),
SvgSource("component", "plus.svg"),
SvgSource("component", "minus.svg"),
NewArtSource(),
TempArtSink("component", "plus.png"),
TempArtSink("component", "minus.png"),
),
build_plus_minus_icons,
)
ctx.register(
"component/magnifying-glass",
(
BlenderSource("component", "magnifying-glass.blend"),
TempArtSink("component", "magnifying-glass.png"),
),
build_magnifying_glass_icon,
)
ctx.register(
"component/monitor",
(
SvgSource("component", "monitor.svg"),
TempArtSink("component", "monitor.png"),
),
build_generic_background,
)
def build_triangle_icons(env: AuroraEnvironment) -> None:
def build_generic_background(
env: AuroraEnvironment, src: SvgSource, sink: TempArtSink
) -> None:
src.pull_to_temp(env, sink)
def build_triangle_icons(
env: AuroraEnvironment,
tri_src: SvgSource,
bg_src: TempArtSource,
new_art: NewArtSource,
sink_left: TempArtSink,
sink_down: TempArtSink,
sink_right: TempArtSink,
sink_up: TempArtSink,
) -> None:
# load triangle and background assets
tri = env.render_svg_then_load(env.input_artifact("component", "triangle.svg"))
bg = env.load_temporary_artifact("component", "generic-background.png")
tri = tri_src.pull(env)
bg = bg_src.pull(env)
# create 4 triangle icons for following building
tri_left = env.new_artifact()
tri_left = new_art.pull()
tri_left.paste(bg, (0, 0), tri)
tri = tri.transpose(PIL.Image.Transpose.ROTATE_90)
tri_down = env.new_artifact()
tri_down = new_art.pull()
tri_down.paste(bg, (0, 0), tri)
tri = tri.transpose(PIL.Image.Transpose.ROTATE_90)
tri_right = env.new_artifact()
tri_right = new_art.pull()
tri_right.paste(bg, (0, 0), tri)
tri = tri.transpose(PIL.Image.Transpose.ROTATE_90)
tri_up = env.new_artifact()
tri_up = new_art.pull()
tri_up.paste(bg, (0, 0), tri)
# save them for reuse
env.save_temporary_artifact(tri_left, "component", "triangle-left.png")
env.save_temporary_artifact(tri_down, "component", "triangle-down.png")
env.save_temporary_artifact(tri_right, "component", "triangle-right.png")
env.save_temporary_artifact(tri_up, "component", "triangle-up.png")
sink_left.push(env, tri_left)
sink_down.push(env, tri_down)
sink_right.push(env, tri_right)
sink_up.push(env, tri_up)
def build_plus_minus_icons(
env: AuroraEnvironment,
bg_src: TempArtSource,
plus_src: SvgSource,
minus_src: SvgSource,
new_art: NewArtSource,
plus_sink: TempArtSink,
minus_sink: TempArtSink,
) -> None:
bg = bg_src.pull(env)
plus = plus_src.pull(env)
art = new_art.pull()
art.paste(bg, (0, 0), plus)
plus_sink.push(env, art)
minus = minus_src.pull(env)
art = new_art.pull()
art.paste(bg, (0, 0), minus)
minus_sink.push(env, art)
def build_magnifying_glass_icon(
env: AuroraEnvironment, src: BlenderSource, sink: TempArtSink
) -> None:
src.pull_to_temp(env, sink)
def build_monitor_icon(
env: AuroraEnvironment, src: SvgSource, sink: TempArtSink
) -> None:
src.pull_to_temp(env, sink)
@@ -1,17 +1,270 @@
import PIL.Image
import PIL.ImageEnhance
from ..types import AuroraContext, AuroraEnvironment
from ..assemblicon.highlevel.environment import FdContext
from assemblicon.highlevel.utils import FdContext, WinCategory, MacCategory
from assemblicon.highlevel.source import (
SvgSource,
BitmapSource,
TempArtSource,
FdArtSource,
)
from assemblicon.highlevel.sink import TempArtSink, FdArtSink, WinArtSink, MacArtSink
from assemblicon.geometry import Rectangle, Point
from assemblicon.lowlevel.artio import new_artifact
from assemblicon.lowlevel.artproc import (
aspect_ratio_scale_and_crop,
VerticalAnchor,
HorizontalAnchor,
)
def register(ctx: AuroraContext) -> None:
ctx.register(
"component/image-base",
(
SvgSource("component", "image-base.svg"),
TempArtSink("component", "image-base.png"),
),
build_image_base_icon,
)
ctx.register(
"std/mimetype/image-x-generic",
("nostd/mimetype/image-jpeg",),
(
FdArtSource(FdContext.MimeTypes, "image-jpeg"),
FdArtSink(FdContext.MimeTypes, "image-x-generic"),
),
build_image_x_generic,
)
def build_image_x_generic(env: AuroraEnvironment) -> None:
# use image/jpeg as the default image icon
env.dup_fd_artifact(
FdContext.MimeTypes, "image-jpeg", FdContext.MimeTypes, "image-x-generic"
ctx.register(
"std/mimetype/image-jpeg",
(
TempArtSource("component", "image-base.png"),
BitmapSource("bitmap", "Niinsaare_järv.jpg"),
TempArtSink("image-jpeg.png"),
FdArtSink(FdContext.MimeTypes, "image-jpeg"),
WinArtSink(WinCategory.Extension, "jpg"),
MacArtSink(MacCategory.Extension, "jpg"),
),
build_image_jpeg_icon,
)
ctx.register(
"std/mimetype/image-png",
(
TempArtSource("component", "image-base.png"),
BitmapSource("bitmap", "Tulipa_suaveolens_floriade_to_Canberra.jpg"),
BitmapSource("bitmap", "Tulipa_suaveolens_floriade_to_Canberra-mask.png"),
SvgSource("image-png-checkerboard.svg"),
FdArtSink(FdContext.MimeTypes, "image-png"),
WinArtSink(WinCategory.Extension, "png"),
MacArtSink(MacCategory.Extension, "png"),
),
build_image_png_icon,
)
ctx.register(
"std/mimetype/image-bmp",
(
TempArtSource("component", "image-base.png"),
BitmapSource("bitmap", "Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg"),
FdArtSink(FdContext.MimeTypes, "image-bmp"),
WinArtSink(WinCategory.Extension, "bmp"),
MacArtSink(MacCategory.Extension, "bmp"),
),
build_image_bmp_icon,
)
ctx.register(
"std/mimetype/image-gif",
(
TempArtSource("component", "image-base.png"),
BitmapSource("bitmap", "Hot-air_balloon_in_Tambov_-_02.jpg"),
FdArtSink(FdContext.MimeTypes, "image-gif"),
WinArtSink(WinCategory.Extension, "gif"),
MacArtSink(MacCategory.Extension, "gif"),
),
build_image_gif_icon,
)
def build_image_base_icon(
env: AuroraEnvironment, src: SvgSource, sink: TempArtSink
) -> None:
src.pull_to_temp(env, sink)
def build_image_x_generic(
env: AuroraEnvironment, src: FdArtSource, sink: FdArtSink
) -> None:
# use image/jpeg as the default image icon
src.link_to(env, sink)
IMAGE_INNER_RECT = Rectangle(Point(20 * 4, 52 * 4), Point(236 * 4, 204 * 4))
"""The valid region of image-base for putting inner image."""
def build_image_jpeg_icon(
env: AuroraEnvironment,
base_src: TempArtSource,
bitmap_src: BitmapSource,
temp_sink: TempArtSink,
fd_sink: FdArtSink,
win_sink: WinArtSink,
mac_sink: MacArtSink,
) -> None:
jpg_image = base_src.pull(env)
jpg_image_inner = bitmap_src.pull(env)
with jpg_image_inner:
# crop the area which is interested by ourselves
interest_area = Rectangle.from_pos_size_rect(Point(2700, 0), 5016, 3888)
crop_jpg_image_inner = jpg_image_inner.crop(interest_area.to_tuple())
# do a flip
flip_jpg_image_inner = crop_jpg_image_inner.transpose(
PIL.Image.Transpose.FLIP_LEFT_RIGHT
)
# enhance its saturation
enhancer = PIL.ImageEnhance.Color(flip_jpg_image_inner)
saturated_jpg_image_inner = enhancer.enhance(1.4)
# fit to inner rect
bestfit_jpg_image_inner = aspect_ratio_scale_and_crop(
saturated_jpg_image_inner,
IMAGE_INNER_RECT.width,
IMAGE_INNER_RECT.height,
PIL.Image.Resampling.NEAREST,
HorizontalAnchor.Center,
VerticalAnchor.Center,
)
# paste into inner rect
jpg_image.paste(bestfit_jpg_image_inner, IMAGE_INNER_RECT.to_tuple())
# build artifacts
# save as temporary for future use
temp_sink.push(env, jpg_image)
# save for FD
fd_sink.push(env, jpg_image)
# save for windows and mac
win_sink.push(env, jpg_image)
mac_sink.push(env, jpg_image)
def build_image_png_icon(
env: AuroraEnvironment,
base_src: TempArtSource,
bitmap_src: BitmapSource,
bitmap_mask_src: BitmapSource,
checkerboard_src: SvgSource,
fd_sink: FdArtSink,
win_sink: WinArtSink,
mac_sink: MacArtSink,
) -> None:
png_image = base_src.pull(env)
bitmap = bitmap_src.pull(env)
bitmap_mask = bitmap_mask_src.pull(env)
checkerboard = checkerboard_src.pull(env)
with bitmap, bitmap_mask, checkerboard:
# mask bitmap first
# force mask as grayscale format to apply black-white mask instead of alpha channel mask
bitmap_masked = new_artifact(bitmap.size)
bitmap_masked.paste(bitmap, (0, 0), bitmap_mask.convert("L"))
# we use the whole image as the inner,
# so we directly fit it to inner.
bestfit_bitmap = aspect_ratio_scale_and_crop(
bitmap_masked,
IMAGE_INNER_RECT.width,
IMAGE_INNER_RECT.height,
PIL.Image.Resampling.NEAREST,
HorizontalAnchor.Center,
VerticalAnchor.Top,
)
# TODO: When bestfit this checkerboard, the border of this checkerboard aer lost.
# We may need resolve this in future but currently it is so subtle to notice.
bestfit_checkerboard = aspect_ratio_scale_and_crop(
checkerboard,
IMAGE_INNER_RECT.width,
IMAGE_INNER_RECT.height,
PIL.Image.Resampling.NEAREST,
HorizontalAnchor.Center,
VerticalAnchor.Center,
)
# composite them together
png_image.alpha_composite(
bestfit_checkerboard, IMAGE_INNER_RECT.pos1.to_tuple()
)
png_image.alpha_composite(bestfit_bitmap, IMAGE_INNER_RECT.pos1.to_tuple())
# build artifacts
# save for FD
fd_sink.push(env, png_image)
# save for windows and mac
win_sink.push(env, png_image)
mac_sink.push(env, png_image)
def build_image_bmp_icon(
env: AuroraEnvironment,
base_src: TempArtSource,
bitmap_src: BitmapSource,
fd_sink: FdArtSink,
win_sink: WinArtSink,
mac_sink: MacArtSink,
) -> None:
bmp_image = base_src.pull(env)
bmp_image_inner = bitmap_src.pull(env)
with bmp_image_inner:
# we use the whole image as the inner,
# so we directly fit it to inner.
bestfit_inner = aspect_ratio_scale_and_crop(
bmp_image_inner,
IMAGE_INNER_RECT.width,
IMAGE_INNER_RECT.height,
PIL.Image.Resampling.NEAREST,
HorizontalAnchor.Center,
VerticalAnchor.Center,
)
# paste into inner rect
bmp_image.paste(bestfit_inner, IMAGE_INNER_RECT.to_tuple())
# build artifacts
# save for FD
fd_sink.push(env, bmp_image)
# save for windows and mac
win_sink.push(env, bmp_image)
mac_sink.push(env, bmp_image)
def build_image_gif_icon(
env: AuroraEnvironment,
base_src: TempArtSource,
bitmap_src: BitmapSource,
fd_sink: FdArtSink,
win_sink: WinArtSink,
mac_sink: MacArtSink,
) -> None:
gif_image = base_src.pull(env)
gif_image_inner = bitmap_src.pull(env)
with gif_image_inner:
# we use the whole image as the inner,
# so we directly fit it to inner.
bestfit_inner = aspect_ratio_scale_and_crop(
gif_image_inner,
IMAGE_INNER_RECT.width,
IMAGE_INNER_RECT.height,
PIL.Image.Resampling.NEAREST,
HorizontalAnchor.Center,
VerticalAnchor.Center,
)
# paste into inner rect
gif_image.paste(bestfit_inner, IMAGE_INNER_RECT.to_tuple())
# build artifacts
# save for FD
fd_sink.push(env, gif_image)
# save for windows and mac
win_sink.push(env, gif_image)
mac_sink.push(env, gif_image)
@@ -1,51 +1,79 @@
from ..types import AuroraContext, AuroraEnvironment
from ..assemblicon.highlevel.environment import FdContext
from assemblicon.highlevel.utils import FdContext
from assemblicon.highlevel.source import SvgSource
from assemblicon.highlevel.sink import TempArtSink, FdArtSink
def register(ctx: AuroraContext) -> None:
ctx.register("std/status/dialog-infomation", (), build_dialog_infomation_icon)
ctx.register("std/status/dialog-warning", (), build_dialog_warning_icon)
ctx.register("std/status/dialog-error", (), build_dialog_error_icon)
ctx.register("std/status/dialog-question", (), build_dialog_question_icon)
def build_dialog_infomation_icon(env: AuroraEnvironment) -> None:
# save dialog icon with FreeDesktop icon and temp icon for reuse.
art = env.render_svg_then_load(
env.input_artifact("dialog-infomation.svg"),
env.temporary_artifact("dialog-infomation.png"),
ctx.register(
"std/status/dialog-infomation",
(
SvgSource("dialog-infomation.svg"),
TempArtSink("dialog-infomation.png"),
FdArtSink(FdContext.Status, "dialog-infomation"),
),
build_dialog_infomation_icon,
)
with art:
env.save_fd_artifact(art, FdContext.Status, "dialog-infomation")
def build_dialog_warning_icon(env: AuroraEnvironment) -> None:
# save dialog icon with FreeDesktop icon and temp icon for reuse.
art = env.render_svg_then_load(
env.input_artifact("dialog-warning.svg"),
env.temporary_artifact("dialog-warning.png"),
ctx.register(
"std/status/dialog-warning",
(
SvgSource("dialog-warning.svg"),
TempArtSink("dialog-warning.png"),
FdArtSink(FdContext.Status, "dialog-warning"),
),
build_dialog_warning_icon,
)
with art:
env.save_fd_artifact(art, FdContext.Status, "dialog-warning")
def build_dialog_error_icon(env: AuroraEnvironment) -> None:
# save dialog icon with FreeDesktop icon and temp icon for reuse.
art = env.render_svg_then_load(
env.input_artifact("dialog-error.svg"),
env.temporary_artifact("dialog-error.png"),
ctx.register(
"std/status/dialog-error",
(
SvgSource("dialog-error.svg"),
TempArtSink("dialog-error.png"),
FdArtSink(FdContext.Status, "dialog-error"),
),
build_dialog_error_icon,
)
ctx.register(
"std/status/dialog-question",
(
SvgSource("dialog-question.svg"),
TempArtSink("dialog-question.png"),
FdArtSink(FdContext.Status, "dialog-question"),
),
build_dialog_question_icon,
)
with art:
env.save_fd_artifact(art, FdContext.Status, "dialog-error")
def build_dialog_question_icon(env: AuroraEnvironment) -> None:
def build_dialog_infomation_icon(
env: AuroraEnvironment, src: SvgSource, temp_sink: TempArtSink, fd_sink: FdArtSink
) -> None:
# save dialog icon with FreeDesktop icon and temp icon for reuse.
src.pull_to_temp(env, temp_sink)
with temp_sink.repull(env) as art:
fd_sink.push(env, art)
def build_dialog_warning_icon(
env: AuroraEnvironment, src: SvgSource, temp_sink: TempArtSink, fd_sink: FdArtSink
) -> None:
# save dialog icon with FreeDesktop icon and temp icon for reuse.
src.pull_to_temp(env, temp_sink)
with temp_sink.repull(env) as art:
fd_sink.push(env, art)
def build_dialog_error_icon(
env: AuroraEnvironment, src: SvgSource, temp_sink: TempArtSink, fd_sink: FdArtSink
) -> None:
# TODO: update this icon from rectangle to circle
# save dialog icon with FreeDesktop icon and temp icon for reuse.
src.pull_to_temp(env, temp_sink)
with temp_sink.repull(env) as art:
fd_sink.push(env, art)
def build_dialog_question_icon(
env: AuroraEnvironment, src: SvgSource, temp_sink: TempArtSink, fd_sink: FdArtSink
) -> None:
# TODO: add dialog-question generation
# # save dialog icon with FreeDesktop icon and temp icon for reuse.
# art = env.render_svg_then_load(
# env.input_artifact("dialog-question.svg"),
# env.temporary_artifact("dialog-question.png"),
# )
# with art:
# env.save_fd_artifact(art, FdContext.Status, "dialog-question")
pass
+2 -2
View File
@@ -1,6 +1,6 @@
from dataclasses import dataclass
from .assemblicon.highlevel.environment import Environment, Prologue
from .assemblicon.highlevel.context import Context
from assemblicon.highlevel.environment import Environment, Prologue
from assemblicon.highlevel.context import Context
@dataclass(frozen=True)
Generated
+10 -2
View File
@@ -2,16 +2,24 @@ version = 1
revision = 2
requires-python = ">=3.13"
[[package]]
name = "assemblicon"
version = "0.1.0"
source = { git = "https://github.com/SarasasChipWorkshop/assemblicon#0073298d45d6c44fc164456ebdec5f0d3ada5dff" }
dependencies = [
{ name = "pillow" },
]
[[package]]
name = "aurora-iconset-builder"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "pillow" },
{ name = "assemblicon" },
]
[package.metadata]
requires-dist = [{ name = "pillow", specifier = ">=12.3.0" }]
requires-dist = [{ name = "assemblicon", git = "https://github.com/SarasasChipWorkshop/assemblicon" }]
[[package]]
name = "pillow"