Compare commits
11
Commits
e0d6aa63e6
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a951c6d11 | ||
|
|
890019bc54 | ||
|
|
0d43fe3962 | ||
|
|
a33c3538f5 | ||
|
|
72edad6edc | ||
|
|
2015ebf925 | ||
|
|
1e15fb4783 | ||
|
|
42a2b08abf | ||
|
|
61e003c741 | ||
|
|
7b216b8865 | ||
|
|
36adf8029a |
+14
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Todo
|
||||
|
||||
- Use "source" and "sink" to explicitly represent the input and output of job to replace current circumstance that use explicit job name to declare dependencies. After this change, the dependency network will be automaticlaly calculated from "source" and "sink" relation.
|
||||
- Peel `assemblicon` off as an independent repository and let the builder of this repository refer it.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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 |
@@ -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"]
|
||||
@@ -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"
|
||||
@@ -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'
|
||||
@@ -0,0 +1,7 @@
|
||||
version = 1
|
||||
|
||||
[[annotations]]
|
||||
path = "**"
|
||||
precedence = "override"
|
||||
SPDX-FileCopyrightText = "2026 Sarasa's Chip Workshop"
|
||||
SPDX-License-Identifier = "MIT"
|
||||
+4
-1
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
@@ -19,7 +20,14 @@ def main() -> None:
|
||||
iconset_input = repository_root / "src"
|
||||
iconset_output = repository_root / "artifact"
|
||||
prologue = AuroraPrologue(
|
||||
iconset_input, iconset_output, opts.svg_render, tempdir_initializer
|
||||
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)
|
||||
|
||||
@@ -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,40 +0,0 @@
|
||||
import enum
|
||||
|
||||
|
||||
class AddressDomain(enum.IntEnum):
|
||||
Input = enum.auto()
|
||||
Temporary = enum.auto()
|
||||
Output = enum.auto()
|
||||
|
||||
FdOutput = enum.auto()
|
||||
WinOutput = enum.auto()
|
||||
MacOutput = enum.auto()
|
||||
|
||||
|
||||
class Address:
|
||||
"""The unique identity of an artifact slot (source/sink)."""
|
||||
|
||||
__domain: AddressDomain
|
||||
__subdirectories: tuple[str, ...]
|
||||
|
||||
def __init__(self, domain: AddressDomain, *args: str) -> None:
|
||||
self.__domain = domain
|
||||
self.__subdirectories = tuple(args)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.__domain.value,) + self.__subdirectories)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if self is other:
|
||||
return True
|
||||
|
||||
if not isinstance(other, Address):
|
||||
return NotImplemented
|
||||
|
||||
return (
|
||||
self.__domain == other.__domain
|
||||
and self.__subdirectories == other.__subdirectories
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Address({self.__domain.name}, {'/'.join(self.__subdirectories)})"
|
||||
@@ -1,191 +0,0 @@
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, TypeVar, Generic, Protocol, Callable, cast
|
||||
from .environment import Environment, Prologue
|
||||
from .address import Address
|
||||
from .source import Source
|
||||
from .sink import Sink
|
||||
from ..logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
P = TypeVar("P", bound=Prologue)
|
||||
|
||||
|
||||
class Executor[*Ts](Protocol):
|
||||
def __call__(self, *args: *Ts) -> None: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Job:
|
||||
"""
|
||||
The class represents a type-erased job.
|
||||
Precise typing lives only in register's signature.
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""The name of this job."""
|
||||
ios: tuple[Source | Sink, ...]
|
||||
"""The sources and sinks of this job."""
|
||||
executor: Callable[..., None]
|
||||
"""The executor of this job."""
|
||||
|
||||
|
||||
class Context(Generic[P]):
|
||||
__jobs: dict[str, Job]
|
||||
"""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.
|
||||
|
||||
Job dependencies are not declared explicitly. They are inferred
|
||||
from the job IO declarations instead: a job depends on the
|
||||
producer of every address required by one of its sources.
|
||||
|
||||
:param selected: Only build jobs matching these given names.
|
||||
Their dependencies are always built first. None for building
|
||||
all jobs.
|
||||
:raises ValueError: If a selected job name is unknown, some
|
||||
address is produced more than once (by one or several
|
||||
jobs), some required address has no producer at all, or the
|
||||
inferred dependency graph contains a cycle (including
|
||||
self-dependencies).
|
||||
"""
|
||||
# Step 0: build the producer table over all registered jobs,
|
||||
# mapping every produced address to its producing job name.
|
||||
# An address produced more than once is rejected right here,
|
||||
# even if it is never actually required: the producers may be
|
||||
# several jobs, or a single job declaring several identical
|
||||
# sinks.
|
||||
producers: dict[Address, str] = {}
|
||||
for name, job in self.__jobs.items():
|
||||
for item in job.ios:
|
||||
if not isinstance(item, Sink):
|
||||
continue
|
||||
addr = item.address
|
||||
prev = producers.get(addr)
|
||||
if prev is not None and prev == name:
|
||||
raise ValueError(
|
||||
f'Job "{name}" declares multiple sinks '
|
||||
f"producing the same address {addr!r}."
|
||||
)
|
||||
if prev is not None:
|
||||
raise ValueError(
|
||||
f"Address {addr!r} is produced by both job "
|
||||
f'"{prev}" and "{name}".'
|
||||
)
|
||||
producers[addr] = name
|
||||
|
||||
# Step 1: resolve the selection closure. Collect the selected
|
||||
# jobs themselves plus, transitively, every job producing an
|
||||
# address one of their sources requires. A source whose address
|
||||
# is ``None`` depends on nothing.
|
||||
needed: set[str] = set()
|
||||
pending = list(self.__jobs.keys()) if selected is None else 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)
|
||||
for item in job.ios:
|
||||
if not isinstance(item, Source):
|
||||
continue
|
||||
addr = item.address
|
||||
if addr is None:
|
||||
continue
|
||||
producer = producers.get(addr)
|
||||
if producer is None:
|
||||
raise ValueError(
|
||||
f'Job "{name}" requires resource {addr!r} '
|
||||
f"but no registered job produces it."
|
||||
)
|
||||
if producer == name:
|
||||
raise ValueError(
|
||||
f'Job "{name}" requires resource {addr!r} '
|
||||
f"which is produced by itself."
|
||||
)
|
||||
pending.append(producer)
|
||||
|
||||
# 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:
|
||||
# "deps": the names of jobs producing resources this job
|
||||
# requires, deduplicated
|
||||
deps: set[str] = set()
|
||||
for item in self.__jobs[name].ios:
|
||||
if not isinstance(item, Source):
|
||||
continue
|
||||
addr = item.address
|
||||
if addr is None:
|
||||
continue
|
||||
deps.add(producers[addr])
|
||||
for dep in deps:
|
||||
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, feeding
|
||||
# the shared environment and the declared IO items to each
|
||||
# executor.
|
||||
for name in order:
|
||||
LOGGER.info('Running job "%s"...', name)
|
||||
self.__jobs[name].executor(self.__env, *self.__jobs[name].ios)
|
||||
|
||||
def register[*Ts](
|
||||
self,
|
||||
name: str,
|
||||
ios: tuple[*Ts],
|
||||
executor: Executor[Environment[P], *Ts],
|
||||
) -> None:
|
||||
"""
|
||||
Register a new job with given name, dependencies and executor.
|
||||
|
||||
:param name: The name of job.
|
||||
:param ios: A tuple declares ths sources and sinks of this job.
|
||||
These sources and sinks will be directly passed to executor.
|
||||
:param exec: The executor of this job.
|
||||
"""
|
||||
# check job name collision
|
||||
if name in self.__jobs:
|
||||
raise ValueError(f'Can not register 2 jobs with same name: "{name}"')
|
||||
# check the type of sources and sinks
|
||||
for item in ios:
|
||||
if not isinstance(item, (Source, Sink)):
|
||||
raise TypeError(
|
||||
f"All items in the ios of job {name!r} must be instances "
|
||||
f"of Source or Sink, but got {type(item)}."
|
||||
)
|
||||
|
||||
self.__jobs[name] = Job(name, cast(tuple[Source | Sink, ...], ios), executor)
|
||||
@@ -1,193 +0,0 @@
|
||||
import tempfile
|
||||
import os
|
||||
from typing import Optional
|
||||
from types import TracebackType
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TypeVar, Generic, Callable
|
||||
from ..lowlevel.environment import Prologue as LowPrologue, Environment as LowEnv
|
||||
from ..lowlevel.artrdr import SvgRenderKind
|
||||
from .utils import FdContext, WinCategory, MacCategory, FD_THUMBNAIL_HWS
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Prologue(LowPrologue):
|
||||
svg_render: SvgRenderKind
|
||||
"""The render kind for SVG"""
|
||||
temp_dir_initializer: Callable[[Path], None] | None
|
||||
"""
|
||||
The extra initializer for user temporary directory.
|
||||
The only argument in function is the path to user temporary directory.
|
||||
``None`` for no extra initializer.
|
||||
"""
|
||||
|
||||
|
||||
_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"
|
||||
|
||||
P = TypeVar("P", bound=Prologue)
|
||||
|
||||
|
||||
class Environment(Generic[P]):
|
||||
__prologue: P
|
||||
"""The prologue used for environment."""
|
||||
__lowenv: LowEnv
|
||||
"""The lowlevel environment managing temporary directory."""
|
||||
|
||||
def __init__(self, prologue: P) -> None:
|
||||
self.__prologue = prologue
|
||||
self.__lowenv = LowEnv(self.__prologue)
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# construct user custom temporary layout of possible
|
||||
if self.__prologue.temp_dir_initializer is not None:
|
||||
temp_dir = self.user_temporary_artifact()
|
||||
self.__prologue.temp_dir_initializer(temp_dir)
|
||||
|
||||
# region: With Visitor Requirements
|
||||
|
||||
def __enter__(self) -> "Environment":
|
||||
self.__lowenv.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self.__lowenv.__exit__(exc_type, exc_value, traceback)
|
||||
|
||||
# endregion
|
||||
|
||||
# region: Prologue Visitor
|
||||
|
||||
@property
|
||||
def prologue(self) -> Prologue:
|
||||
return self.__prologue
|
||||
|
||||
@property
|
||||
def user_prologue(self) -> P:
|
||||
return self.__prologue
|
||||
|
||||
# endregion
|
||||
|
||||
# region: Input Artifact
|
||||
|
||||
def input_artifact(self, *args: str) -> Path:
|
||||
"""
|
||||
Get the path to input artifact.
|
||||
"""
|
||||
return self.__lowenv.input_artifact(*args)
|
||||
|
||||
# endregion
|
||||
|
||||
# region: Temporary Artifact
|
||||
|
||||
def __temporary_artifact(self, *args: str) -> Path:
|
||||
return self.__lowenv.temporary_artifact(*args)
|
||||
|
||||
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 __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)
|
||||
|
||||
def allocate_lib_render_temporary_artifact(self) -> Path:
|
||||
"""
|
||||
Allocated a path pointing to temporary render result.
|
||||
|
||||
:return: The path to temporary PNG file.
|
||||
Please note that this file is already presneted in file system,
|
||||
so caller need to overwrite it.
|
||||
"""
|
||||
return self.__allocate_lib_temporary_artifact(".png")
|
||||
|
||||
def fetch_lib_blender_script_temporary_artifact(self) -> Path:
|
||||
"""
|
||||
Allocated a path pointing to temporary script instructing Blender render.
|
||||
|
||||
:return: The path to temporary Python file.
|
||||
Please note that this file may be presneted in file system,
|
||||
so caller may need to overwrite it.
|
||||
All Blender render process share the same script file (this).
|
||||
"""
|
||||
# TODO: Strip this "share" behavior to enable multi-threads build ability.
|
||||
return self.__fetch_const_lib_temporary_artifact("blender.py")
|
||||
|
||||
# endregion
|
||||
|
||||
# region: Output Artifact
|
||||
|
||||
def __output_artifact(self, *args: str) -> Path:
|
||||
"""
|
||||
Get the path to output artifact.
|
||||
"""
|
||||
return self.__lowenv.output_artifact(*args)
|
||||
|
||||
def fd_output_artifact(self, hw: int, kind: FdContext, name: str) -> Path:
|
||||
"""
|
||||
Get the path for saving FreeDesktop icon with given image size, FreeDesktop icon and name.
|
||||
|
||||
:param hw: The height or width of this FreeDesktop icon.
|
||||
Required because FreeDesktop need to save icon with different size variant.
|
||||
:param kind: The FreeDesktop kind of this icon.
|
||||
:param name: The name of saved artifact. The file extension ".png" is not required.
|
||||
"""
|
||||
return self.__output_artifact(f"{hw}x{hw}", str(kind), f"{name}.png")
|
||||
|
||||
def win_output_artifact(self, kind: WinCategory, name: str) -> Path:
|
||||
"""
|
||||
Get the path for saving Windows-only icon.
|
||||
|
||||
:param kind: The kind of this icon.
|
||||
:param name: The name of saved artifact. The file extension ".ico" is not required.
|
||||
"""
|
||||
return self.__output_artifact(_ART_WIN, str(kind), f"{name}.ico")
|
||||
|
||||
def mac_output_artifact(self, kind: MacCategory, name: str) -> Path:
|
||||
"""
|
||||
Get the path for saving macOS-only icon.
|
||||
|
||||
:param kind: The kind of this icon.
|
||||
:param name: The name of saved artifact. The file extension ".icns" is not required.
|
||||
"""
|
||||
return self.__output_artifact(_ART_MAC, str(kind), f"{name}.icns")
|
||||
|
||||
# endregion
|
||||
@@ -1,13 +0,0 @@
|
||||
from .common import Sink
|
||||
from .tempart_sink import TempArtSink
|
||||
from .fdart_sink import FdArtSink
|
||||
from .winart_sink import WinArtSink
|
||||
from .macart_sink import MacArtSink
|
||||
|
||||
__all__ = [
|
||||
"Sink",
|
||||
"TempArtSink",
|
||||
"FdArtSink",
|
||||
"WinArtSink",
|
||||
"MacArtSink"
|
||||
]
|
||||
@@ -1,12 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from ..address import Address
|
||||
|
||||
|
||||
class Sink(ABC):
|
||||
"""
|
||||
Abstract base class for all sinks.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def address(self) -> Address: ...
|
||||
@@ -1,42 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from .common import Sink
|
||||
from ..address import Address, AddressDomain
|
||||
from ..environment import Environment
|
||||
from ..utils import Artifact, FdContext, FD_THUMBNAIL_HWS, check_vart
|
||||
from ...lowlevel import artio, artrdr
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FdArtSinkHint:
|
||||
kind: FdContext
|
||||
name: str
|
||||
|
||||
|
||||
class FdArtSink(Sink):
|
||||
__kind: FdContext
|
||||
__name: str
|
||||
|
||||
def __init__(self, kind: FdContext, name: str) -> None:
|
||||
self.__kind = kind
|
||||
self.__name = name
|
||||
|
||||
@property
|
||||
def address(self) -> Address:
|
||||
return Address(AddressDomain.FdOutput, str(self.__kind), self.__name)
|
||||
|
||||
def push(self, env: Environment, art: Artifact) -> None:
|
||||
LOGGER.info(
|
||||
'Saving FreeDesktop artifact with kind "%s" and name "%s".',
|
||||
self.__kind,
|
||||
self.__name,
|
||||
)
|
||||
check_vart(art)
|
||||
for hw, thumbnail in zip(
|
||||
FD_THUMBNAIL_HWS, artrdr.render_thumbnail(art, FD_THUMBNAIL_HWS)
|
||||
):
|
||||
dst = env.fd_output_artifact(hw, self.__kind, self.__name)
|
||||
artio.save_artifact(thumbnail, dst)
|
||||
|
||||
def push_hint(self) -> FdArtSinkHint:
|
||||
return FdArtSinkHint(self.__kind, self.__name)
|
||||
@@ -1,38 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from .common import Sink
|
||||
from ..address import Address, AddressDomain
|
||||
from ..environment import Environment
|
||||
from ..utils import Artifact, MacCategory, check_vart
|
||||
from ...lowlevel import artrdr
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MacArtSinkHint:
|
||||
kind: MacCategory
|
||||
name: str
|
||||
|
||||
|
||||
class MacArtSink(Sink):
|
||||
__kind: MacCategory
|
||||
__name: str
|
||||
|
||||
def __init__(self, kind: MacCategory, name: str) -> None:
|
||||
self.__kind = kind
|
||||
self.__name = name
|
||||
|
||||
@property
|
||||
def address(self) -> Address:
|
||||
return Address(AddressDomain.MacOutput, str(self.__kind), self.__name)
|
||||
|
||||
def push(self, env: Environment, art: Artifact) -> None:
|
||||
LOGGER.info(
|
||||
'Saving macOS-only artifact with kind "%s" and name "%s".',
|
||||
self.__kind,
|
||||
self.__name,
|
||||
)
|
||||
check_vart(art)
|
||||
artrdr.render_icns(art, env.mac_output_artifact(self.__kind, self.__name))
|
||||
|
||||
def push_hint(self) -> MacArtSinkHint:
|
||||
return MacArtSinkHint(self.__kind, self.__name)
|
||||
@@ -1,43 +0,0 @@
|
||||
from pathlib import Path
|
||||
from .common import Sink
|
||||
from ..address import Address, AddressDomain
|
||||
from ..environment import Environment
|
||||
from ..utils import Artifact
|
||||
from ...lowlevel import artio
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
class TempArtSink(Sink):
|
||||
"""
|
||||
The sink to artifact (PNG-only) located in temporary directory.
|
||||
|
||||
All existence of sub-directory should be ensured on your own.
|
||||
"""
|
||||
|
||||
__comps: tuple[str, ...]
|
||||
|
||||
def __init__(self, *args: str) -> None:
|
||||
self.__comps = tuple(args)
|
||||
|
||||
@property
|
||||
def address(self) -> Address:
|
||||
return Address(AddressDomain.Temporary, *self.__comps)
|
||||
|
||||
def push(self, env: Environment, art: Artifact) -> None:
|
||||
p = self.push_hint(env)
|
||||
LOGGER.info("Saving temporary artifact: %s", p)
|
||||
artio.save_artifact(art, p)
|
||||
|
||||
def repull(self, env: Environment) -> Artifact:
|
||||
"""
|
||||
Re-pull the artifact from this sink, after the artifact has
|
||||
been pushed to it (usually via a direct rendering).
|
||||
Only meaningful for artifacts produced by the job declaring
|
||||
this sink.
|
||||
"""
|
||||
p = self.push_hint(env)
|
||||
LOGGER.info("Re-pulling temporary artifact: %s", p)
|
||||
return artio.load_artifact(p)
|
||||
|
||||
def push_hint(self, env: Environment) -> Path:
|
||||
return env.user_temporary_artifact(*self.__comps)
|
||||
@@ -1,38 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from .common import Sink
|
||||
from ..address import Address, AddressDomain
|
||||
from ..environment import Environment
|
||||
from ..utils import Artifact, WinCategory, check_vart
|
||||
from ...lowlevel import artrdr
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WinArtSinkHint:
|
||||
kind: WinCategory
|
||||
name: str
|
||||
|
||||
|
||||
class WinArtSink(Sink):
|
||||
__kind: WinCategory
|
||||
__name: str
|
||||
|
||||
def __init__(self, kind: WinCategory, name: str) -> None:
|
||||
self.__kind = kind
|
||||
self.__name = name
|
||||
|
||||
@property
|
||||
def address(self) -> Address:
|
||||
return Address(AddressDomain.WinOutput, str(self.__kind), self.__name)
|
||||
|
||||
def push(self, env: Environment, art: Artifact) -> None:
|
||||
LOGGER.info(
|
||||
'Saving Windows-only artifact with kind "%s" and name "%s".',
|
||||
self.__kind,
|
||||
self.__name,
|
||||
)
|
||||
check_vart(art)
|
||||
artrdr.render_ico(art, env.win_output_artifact(self.__kind, self.__name))
|
||||
|
||||
def push_hint(self) -> WinArtSinkHint:
|
||||
return WinArtSinkHint(self.__kind, self.__name)
|
||||
@@ -1,22 +0,0 @@
|
||||
from .common import Source
|
||||
from .newart_source import NewArtSource
|
||||
from .bitmap_source import BitmapSource
|
||||
from .svg_source import SvgSource
|
||||
from .blender_source import BlenderSource
|
||||
from .tempart_source import TempArtSource
|
||||
from .fdart_source import FdArtSource
|
||||
from .winart_source import WinArtSource
|
||||
from .macart_source import MacArtSource
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Source",
|
||||
"NewArtSource",
|
||||
"BitmapSource",
|
||||
"SvgSource",
|
||||
"BlenderSource",
|
||||
"TempArtSource",
|
||||
"FdArtSource",
|
||||
"WinArtSource",
|
||||
"MacArtSource",
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
from .common import Source
|
||||
from ..address import Address
|
||||
from ..environment import Environment
|
||||
from ..utils import Artifact
|
||||
from ...lowlevel import artio
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
class BitmapSource(Source):
|
||||
"""The source to bimap artifact (PNG, JPEG or BMP) located in input directory."""
|
||||
|
||||
__comps: tuple[str, ...]
|
||||
|
||||
def __init__(self, *args: str) -> None:
|
||||
self.__comps = tuple(args)
|
||||
|
||||
@property
|
||||
def address(self) -> Address | None:
|
||||
return None
|
||||
|
||||
def pull(self, env: Environment) -> Artifact:
|
||||
p = env.input_artifact(*self.__comps)
|
||||
LOGGER.info("Loading input artifact: %s", p)
|
||||
return artio.load_input_artifact(p)
|
||||
@@ -1,58 +0,0 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from .common import Source
|
||||
from ..address import Address
|
||||
from ..environment import Environment
|
||||
from ..sink.tempart_sink import TempArtSink
|
||||
from ..utils import Artifact, VART_HW
|
||||
from ...lowlevel import artio, artrdr
|
||||
from ...lowlevel.artrdr import BlenderConfig
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
DEFAULT_BLD_CONFIG = artrdr.BlenderConfig(VART_HW, VART_HW)
|
||||
|
||||
|
||||
class BlenderSource(Source):
|
||||
"""The source to Blender composition located in input directory."""
|
||||
|
||||
__comps: tuple[str, ...]
|
||||
|
||||
def __init__(self, *args: str) -> None:
|
||||
self.__comps = tuple(args)
|
||||
|
||||
@property
|
||||
def address(self) -> Address | None:
|
||||
return None
|
||||
|
||||
def pull(
|
||||
self, env: Environment, cfg: BlenderConfig = DEFAULT_BLD_CONFIG
|
||||
) -> Artifact:
|
||||
dst = self.__render(env, None, cfg)
|
||||
return artio.load_artifact(dst)
|
||||
|
||||
def pull_to_temp(
|
||||
self,
|
||||
env: Environment,
|
||||
temp_sink: TempArtSink,
|
||||
cfg: BlenderConfig = DEFAULT_BLD_CONFIG,
|
||||
) -> None:
|
||||
self.__render(env, temp_sink.push_hint(env), cfg)
|
||||
|
||||
def __render(
|
||||
self, env: Environment, dst: Optional[Path], cfg: BlenderConfig
|
||||
) -> Path:
|
||||
"""
|
||||
Internal used function for rendering Blender composition.
|
||||
|
||||
:return: The path to destination storing render result.
|
||||
"""
|
||||
src = env.input_artifact(*self.__comps)
|
||||
temp_script = env.fetch_lib_blender_script_temporary_artifact()
|
||||
if dst is None:
|
||||
LOGGER.info("Rendering temporary Blender composition: %s", src)
|
||||
dst = env.allocate_lib_render_temporary_artifact()
|
||||
else:
|
||||
LOGGER.info("Rendering Blender composition: %s -> %s", src, dst)
|
||||
artrdr.render_blender(src, dst, temp_script, cfg)
|
||||
return dst
|
||||
@@ -1,20 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from ..address import Address
|
||||
|
||||
|
||||
class Source(ABC):
|
||||
"""
|
||||
Abstract base class for all sources.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def address(self) -> Address | None:
|
||||
"""
|
||||
Get the address representation of this source.
|
||||
|
||||
:return: The address representing this source.
|
||||
``None`` if there is no producer for this source.
|
||||
It means that this source do not depend anything.
|
||||
"""
|
||||
...
|
||||
@@ -1,35 +0,0 @@
|
||||
from .common import Source
|
||||
from ..address import Address, AddressDomain
|
||||
from ..environment import Environment
|
||||
from ..sink.fdart_sink import FdArtSink
|
||||
from ..utils import FdContext, FD_THUMBNAIL_HWS
|
||||
from ...lowlevel import artio
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
class FdArtSource(Source):
|
||||
__kind: FdContext
|
||||
__name: str
|
||||
|
||||
def __init__(self, kind: FdContext, name: str) -> None:
|
||||
self.__kind = kind
|
||||
self.__name = name
|
||||
|
||||
@property
|
||||
def address(self) -> Address:
|
||||
return Address(AddressDomain.FdOutput, str(self.__kind), self.__name)
|
||||
|
||||
def link_to(self, env: Environment, fd_sink: FdArtSink) -> None:
|
||||
sink_hint = fd_sink.push_hint()
|
||||
LOGGER.info(
|
||||
'Duplicating FreeDesktop artifact from kind "%s" name "%s" to kind "%s" name "%s".',
|
||||
sink_hint.kind,
|
||||
sink_hint.name,
|
||||
self.__kind,
|
||||
self.__name,
|
||||
)
|
||||
for hw in FD_THUMBNAIL_HWS:
|
||||
artio.link_artifact(
|
||||
env.fd_output_artifact(hw, sink_hint.kind, sink_hint.name),
|
||||
env.fd_output_artifact(hw, self.__kind, self.__name),
|
||||
)
|
||||
@@ -1,34 +0,0 @@
|
||||
from .common import Source
|
||||
from ..address import Address, AddressDomain
|
||||
from ..environment import Environment
|
||||
from ..sink.macart_sink import MacArtSink
|
||||
from ..utils import MacCategory
|
||||
from ...lowlevel import artio
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
class MacArtSource(Source):
|
||||
__kind: MacCategory
|
||||
__name: str
|
||||
|
||||
def __init__(self, kind: MacCategory, name: str) -> None:
|
||||
self.__kind = kind
|
||||
self.__name = name
|
||||
|
||||
@property
|
||||
def address(self) -> Address:
|
||||
return Address(AddressDomain.MacOutput, str(self.__kind), self.__name)
|
||||
|
||||
def link_to(self, env: Environment, mac_sink: MacArtSink) -> None:
|
||||
sink_hint = mac_sink.push_hint()
|
||||
LOGGER.info(
|
||||
'Duplicating macOS-only artifact from kind "%s" name "%s" to kind "%s" name "%s".',
|
||||
sink_hint.kind,
|
||||
sink_hint.name,
|
||||
self.__kind,
|
||||
self.__name,
|
||||
)
|
||||
artio.link_artifact(
|
||||
env.mac_output_artifact(sink_hint.kind, sink_hint.name),
|
||||
env.mac_output_artifact(self.__kind, self.__name),
|
||||
)
|
||||
@@ -1,16 +0,0 @@
|
||||
from .common import Source
|
||||
from ..address import Address
|
||||
from ..utils import Artifact, VART_HW
|
||||
from ...lowlevel import artio
|
||||
|
||||
|
||||
class NewArtSource(Source):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def address(self) -> Address | None:
|
||||
return None
|
||||
|
||||
def pull(self) -> Artifact:
|
||||
return artio.new_artifact((VART_HW, VART_HW))
|
||||
@@ -1,52 +0,0 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from .common import Source
|
||||
from ..address import Address
|
||||
from ..environment import Environment
|
||||
from ..sink.tempart_sink import TempArtSink
|
||||
from ..utils import Artifact, VART_HW
|
||||
from ...lowlevel import artio, artrdr
|
||||
from ...lowlevel.artrdr import SvgConfig
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
DEFAULT_SVG_CONFIG = SvgConfig(VART_HW, VART_HW)
|
||||
|
||||
|
||||
class SvgSource(Source):
|
||||
"""The source to SVG located in input directory."""
|
||||
|
||||
__comps: tuple[str, ...]
|
||||
|
||||
def __init__(self, *args: str) -> None:
|
||||
self.__comps = tuple(args)
|
||||
|
||||
@property
|
||||
def address(self) -> Address | None:
|
||||
return None
|
||||
|
||||
def pull(self, env: Environment, cfg: SvgConfig = DEFAULT_SVG_CONFIG) -> Artifact:
|
||||
dst = self.__render(env, None, cfg)
|
||||
return artio.load_artifact(dst)
|
||||
|
||||
def pull_to_temp(
|
||||
self,
|
||||
env: Environment,
|
||||
temp_sink: TempArtSink,
|
||||
cfg: SvgConfig = DEFAULT_SVG_CONFIG,
|
||||
) -> None:
|
||||
self.__render(env, temp_sink.push_hint(env), cfg)
|
||||
|
||||
def __render(self, env: Environment, dst: Optional[Path], cfg: SvgConfig) -> Path:
|
||||
"""
|
||||
Internal used function for rendering SVG.
|
||||
|
||||
:return: The path to destination storing render result.
|
||||
"""
|
||||
src = env.input_artifact(*self.__comps)
|
||||
if dst is None:
|
||||
LOGGER.info("Rendering temporary SVG: %s", src)
|
||||
dst = env.allocate_lib_render_temporary_artifact()
|
||||
else:
|
||||
LOGGER.info("Rendering SVG: %s -> %s", src, dst)
|
||||
artrdr.render_svg(src, dst, env.prologue.svg_render, cfg)
|
||||
return dst
|
||||
@@ -1,24 +0,0 @@
|
||||
from .common import Source
|
||||
from ..address import Address, AddressDomain
|
||||
from ..environment import Environment
|
||||
from ..utils import Artifact
|
||||
from ...lowlevel import artio
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
class TempArtSource(Source):
|
||||
"""The source to artifact (PNG-only) located in temporary directory."""
|
||||
|
||||
__comps: tuple[str, ...]
|
||||
|
||||
def __init__(self, *args: str) -> None:
|
||||
self.__comps = tuple(args)
|
||||
|
||||
@property
|
||||
def address(self) -> Address | None:
|
||||
return Address(AddressDomain.Temporary, *self.__comps)
|
||||
|
||||
def pull(self, env: Environment) -> Artifact:
|
||||
p = env.user_temporary_artifact(*self.__comps)
|
||||
LOGGER.info("Loading temporary artifact: %s", p)
|
||||
return artio.load_artifact(p)
|
||||
@@ -1,34 +0,0 @@
|
||||
from .common import Source
|
||||
from ..address import Address, AddressDomain
|
||||
from ..environment import Environment
|
||||
from ..sink.winart_sink import WinArtSink
|
||||
from ..utils import WinCategory
|
||||
from ...lowlevel import artio
|
||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
class WinArtSource(Source):
|
||||
__kind: WinCategory
|
||||
__name: str
|
||||
|
||||
def __init__(self, kind: WinCategory, name: str) -> None:
|
||||
self.__kind = kind
|
||||
self.__name = name
|
||||
|
||||
@property
|
||||
def address(self) -> Address:
|
||||
return Address(AddressDomain.WinOutput, str(self.__kind), self.__name)
|
||||
|
||||
def link_to(self, env: Environment, win_sink: WinArtSink) -> None:
|
||||
sink_hint = win_sink.push_hint()
|
||||
LOGGER.info(
|
||||
'Duplicating Windows-only artifact from kind "%s" name "%s" to kind "%s" name "%s".',
|
||||
sink_hint.kind,
|
||||
sink_hint.name,
|
||||
self.__kind,
|
||||
self.__name,
|
||||
)
|
||||
artio.link_artifact(
|
||||
env.win_output_artifact(sink_hint.kind, sink_hint.name),
|
||||
env.win_output_artifact(self.__kind, self.__name),
|
||||
)
|
||||
@@ -1,46 +0,0 @@
|
||||
import enum
|
||||
from ..lowlevel.utils import Artifact
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
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"""
|
||||
|
||||
|
||||
# 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)
|
||||
@@ -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,153 +0,0 @@
|
||||
import shutil
|
||||
import os
|
||||
import sys
|
||||
import enum
|
||||
from pathlib import Path
|
||||
import PIL.Image
|
||||
from .utils import Artifact
|
||||
from ..logger import LOWLEVEL_LOGGER as LOGGER
|
||||
|
||||
|
||||
_LINK_MODE_ENV: str = "ASSEMBLICON_LINK_MODE"
|
||||
"""The environment variable selecting how artifacts are linked."""
|
||||
|
||||
|
||||
class LinkMode(enum.StrEnum):
|
||||
"""All valid values of the ``ASSEMBLICON_LINK_MODE`` environment variable."""
|
||||
|
||||
Copy = "copy"
|
||||
Symlink = "symlink"
|
||||
|
||||
|
||||
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) -> Artifact:
|
||||
"""
|
||||
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) -> Artifact:
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
def link_artifact(src: Path, dst: Path) -> None:
|
||||
"""
|
||||
Make given artifact reachable from another path, either via a
|
||||
relative symlink or a content copy.
|
||||
|
||||
The linking mode is controlled by the ``ASSEMBLICON_LINK_MODE``
|
||||
environment variable: ``symlink`` (default) creates a symlink with a
|
||||
path relative to the destination; ``copy`` duplicates the artifact
|
||||
content. An existing destination is only removed after it is proven
|
||||
to be a regular file, or a symlink whose target is a file path
|
||||
(the target is allowed to be missing), and only when it shares the
|
||||
same filename suffix as the source. Symlink creation failure falls
|
||||
back to copying.
|
||||
|
||||
:param src: The path to source artifact file.
|
||||
:param dst: The path to destination which should reach the artifact.
|
||||
:raises ValueError: If the link mode is unknown, the destination
|
||||
exists but is not safely deletable (see above), its suffix
|
||||
differs from the source one, or no common directory exists
|
||||
between source and destination.
|
||||
"""
|
||||
try:
|
||||
mode = LinkMode(os.getenv(_LINK_MODE_ENV, LinkMode.Symlink))
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid link mode for env {_LINK_MODE_ENV}. "
|
||||
f"Valid modes are: {', '.join(m.value for m in LinkMode)}."
|
||||
)
|
||||
|
||||
LOGGER.debug("Linking artifact: %s -> %s (mode: %s)", src, dst, mode)
|
||||
|
||||
# SAFETY: the code below deletes an existing destination file. These
|
||||
# checks are the only guards against accidental data deletion, so
|
||||
# never relax them. A destination is deletable only when it is a
|
||||
# regular file, or a symlink whose target is a file path (the target
|
||||
# itself is allowed to be missing, i.e. a broken symlink is
|
||||
# deletable). Additionally, the source and the destination must
|
||||
# share the same filename suffix.
|
||||
if os.path.lexists(dst):
|
||||
if dst.is_symlink():
|
||||
# a symlink is deletable unless it provably points to a
|
||||
# non-file, e.g. an existing directory
|
||||
if dst.exists() and not dst.is_file():
|
||||
raise ValueError(
|
||||
f'Refusing to remove existing destination "{dst}": '
|
||||
f"it is a symlink not pointing to a file."
|
||||
)
|
||||
elif not dst.is_file():
|
||||
raise ValueError(
|
||||
f'Refusing to remove existing destination "{dst}": '
|
||||
f"it is not a regular file."
|
||||
)
|
||||
if src.suffix != dst.suffix:
|
||||
raise ValueError(
|
||||
f'Refusing to remove existing destination "{dst}": its '
|
||||
f'suffix "{dst.suffix}" differs from source suffix "{src.suffix}".'
|
||||
)
|
||||
dst.unlink()
|
||||
|
||||
if mode == LinkMode.Copy:
|
||||
shutil.copyfile(src, dst)
|
||||
return
|
||||
|
||||
# symlinks are always created with a path relative to the
|
||||
# destination, so that the whole artifact directory stays relocatable
|
||||
try:
|
||||
link_target = src.relative_to(dst.parent, walk_up=True)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f'No common directory between "{src}" and "{dst}", '
|
||||
f"can not create relative symlink."
|
||||
)
|
||||
|
||||
# SAFETY: the symlink target must always be stored with POSIX
|
||||
# separators ("/"). On Windows, "relative_to" yields a
|
||||
# backslash-separated path, and "symlink_to" stores the target string
|
||||
# verbatim into the NTFS reparse point, so a backslash form would
|
||||
# also leak verbatim into tar archives. Whether an unpacker accepts
|
||||
# backslash targets is not guaranteed, and we deliberately do not
|
||||
# rely on the packer normalizing them. Converting to POSIX form here
|
||||
# is the single point guaranteeing that archived symlinks stay
|
||||
# portable, regardless of the platform the build ran on or the
|
||||
# packer used.
|
||||
try:
|
||||
dst.symlink_to(link_target.as_posix())
|
||||
except OSError as e:
|
||||
LOGGER.warning(
|
||||
"Fail to create symlink for artifact %s: %s. Falling back to copy.",
|
||||
dst,
|
||||
e,
|
||||
)
|
||||
if sys.platform == "win32":
|
||||
LOGGER.warning(
|
||||
"Symlink creation on Windows usually requires administrator "
|
||||
"privileges. Consider rerunning as administrator, enabling "
|
||||
"Developer Mode, or setting %s=copy.",
|
||||
_LINK_MODE_ENV,
|
||||
)
|
||||
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,68 +0,0 @@
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from types import TracebackType
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Prologue:
|
||||
input_directory: Path
|
||||
"""The path to input directory."""
|
||||
output_directory: Path
|
||||
"""The path to output directory."""
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
|
||||
class Environment:
|
||||
__prologue: Prologue
|
||||
"""The prologue used for environment."""
|
||||
__temp_dir: tempfile.TemporaryDirectory[str]
|
||||
"""Allocated temporary directory."""
|
||||
|
||||
def __init__(self, prologue: Prologue) -> None:
|
||||
self.__prologue = prologue
|
||||
self.__temp_dir = tempfile.TemporaryDirectory()
|
||||
|
||||
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()
|
||||
|
||||
def input_artifact(self, *args: str) -> Path:
|
||||
"""
|
||||
Get the path to input artifact.
|
||||
|
||||
:return: The built absolute path to input artifact.
|
||||
"""
|
||||
return self.__prologue.input_directory / Path(*args)
|
||||
|
||||
def temporary_artifact(self, *args: str) -> Path:
|
||||
"""
|
||||
Get the path to temporary artifact.
|
||||
|
||||
:return: The built absolute path to temporary artifact.
|
||||
"""
|
||||
return Path(self.__temp_dir.name) / Path(*args)
|
||||
|
||||
def output_artifact(self, *args: str) -> Path:
|
||||
"""
|
||||
Get the path to output artifact.
|
||||
|
||||
:return: The built absolute path to output artifact.
|
||||
"""
|
||||
return self.__prologue.output_directory / Path(*args)
|
||||
@@ -1,3 +0,0 @@
|
||||
from PIL.Image import Image
|
||||
|
||||
type Artifact = Image
|
||||
@@ -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,9 +1,9 @@
|
||||
from ..types import AuroraContext, AuroraEnvironment
|
||||
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 (
|
||||
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,
|
||||
@@ -37,6 +37,19 @@ def register(ctx: AuroraContext) -> None:
|
||||
),
|
||||
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(
|
||||
@@ -145,3 +158,86 @@ def build_image_zoom_original_icon(
|
||||
|
||||
# save it
|
||||
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,13 +1,13 @@
|
||||
from ..types import AuroraContext, AuroraEnvironment
|
||||
from ..assemblicon.highlevel.utils import FdContext, WinCategory, MacCategory
|
||||
from ..assemblicon.highlevel.source 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 (
|
||||
from assemblicon.highlevel.sink import FdArtSink, WinArtSink, MacArtSink, TempArtSink
|
||||
from assemblicon.lowlevel.artproc import (
|
||||
art_anchor_to_offset,
|
||||
default_art_resize,
|
||||
align_paste,
|
||||
@@ -18,12 +18,12 @@ from ..assemblicon.lowlevel.artproc import (
|
||||
|
||||
def register(ctx: AuroraContext) -> None:
|
||||
ctx.register(
|
||||
"nostd/application/org.sarasachipworkshop.picture",
|
||||
"nostd/application/org.sarasaschipworkshop.picture",
|
||||
(
|
||||
SvgSource("app", "sarasacw-picture", "app-frame.svg"),
|
||||
SvgSource("apps", "sarasacw-picture-frame.svg"),
|
||||
TempArtSource("image-jpeg.png"),
|
||||
NewArtSource(),
|
||||
FdArtSink(FdContext.Applications, "org.sarasachipworkshop.picture"),
|
||||
FdArtSink(FdContext.Applications, "org.sarasaschipworkshop.picture"),
|
||||
WinArtSink(WinCategory.Application, "sarasacw-picture"),
|
||||
MacArtSink(MacCategory.Application, "sarasacw-picture"),
|
||||
),
|
||||
@@ -32,7 +32,7 @@ def register(ctx: AuroraContext) -> None:
|
||||
ctx.register(
|
||||
"nostd/application/notepad",
|
||||
(
|
||||
BlenderSource("app", "notepad.blend"),
|
||||
BlenderSource("apps", "notepad.blend"),
|
||||
TempArtSink("notepad.png"),
|
||||
WinArtSink(WinCategory.Application, "notepad"),
|
||||
),
|
||||
|
||||
@@ -1,90 +1,8 @@
|
||||
import PIL.Image
|
||||
import PIL.ImageEnhance
|
||||
from ..types import AuroraContext, AuroraEnvironment
|
||||
from ..assemblicon.highlevel.utils import FdContext, WinCategory, MacCategory
|
||||
from ..assemblicon.highlevel.source import SvgSource, BitmapSource, TempArtSource
|
||||
from ..assemblicon.highlevel.sink import TempArtSink, FdArtSink, WinArtSink, MacArtSink
|
||||
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",
|
||||
(
|
||||
SvgSource("component", "image-base.svg"),
|
||||
TempArtSink("component", "image-base.png"),
|
||||
),
|
||||
build_image_base_icon,
|
||||
)
|
||||
ctx.register(
|
||||
"nostd/mimetype/image-jpeg",
|
||||
(
|
||||
TempArtSource("component", "image-base.png"),
|
||||
BitmapSource("ext", "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,
|
||||
)
|
||||
|
||||
|
||||
IMAGE_INNER_RECT = Rectangle(Point(20 * 4, 52 * 4), Point(236 * 4, 204 * 4))
|
||||
|
||||
|
||||
def build_image_base_icon(
|
||||
env: AuroraEnvironment, src: SvgSource, sink: TempArtSink
|
||||
) -> None:
|
||||
src.pull_to_temp(env, sink)
|
||||
|
||||
|
||||
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)
|
||||
pass
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import PIL.Image
|
||||
from ..types import AuroraContext, AuroraEnvironment
|
||||
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
|
||||
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]",
|
||||
(
|
||||
SvgSource("component", "go-base.svg"),
|
||||
SvgSource("component", "go-inner-shining.svg"),
|
||||
SvgSource("component", "go-inner.svg"),
|
||||
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"),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import PIL.Image
|
||||
from ..types import AuroraContext, AuroraEnvironment
|
||||
from ..assemblicon.highlevel.source import (
|
||||
from assemblicon.highlevel.source import (
|
||||
SvgSource,
|
||||
TempArtSource,
|
||||
NewArtSource,
|
||||
BlenderSource,
|
||||
)
|
||||
from ..assemblicon.highlevel.sink import TempArtSink
|
||||
from assemblicon.highlevel.sink import TempArtSink
|
||||
|
||||
|
||||
def register(ctx: AuroraContext) -> None:
|
||||
@@ -51,6 +51,14 @@ def register(ctx: AuroraContext) -> None:
|
||||
),
|
||||
build_magnifying_glass_icon,
|
||||
)
|
||||
ctx.register(
|
||||
"component/monitor",
|
||||
(
|
||||
SvgSource("component", "monitor.svg"),
|
||||
TempArtSink("component", "monitor.png"),
|
||||
),
|
||||
build_generic_background,
|
||||
)
|
||||
|
||||
|
||||
def build_generic_background(
|
||||
@@ -122,3 +130,9 @@ 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,10 +1,32 @@
|
||||
import PIL.Image
|
||||
import PIL.ImageEnhance
|
||||
from ..types import AuroraContext, AuroraEnvironment
|
||||
from ..assemblicon.highlevel.utils import FdContext
|
||||
from ..assemblicon.highlevel.source import FdArtSource
|
||||
from ..assemblicon.highlevel.sink import FdArtSink
|
||||
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",
|
||||
(
|
||||
@@ -13,6 +35,59 @@ def register(ctx: AuroraContext) -> None:
|
||||
),
|
||||
build_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(
|
||||
@@ -20,3 +95,176 @@ def build_image_x_generic(
|
||||
) -> 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,7 +1,7 @@
|
||||
from ..types import AuroraContext, AuroraEnvironment
|
||||
from ..assemblicon.highlevel.utils import FdContext
|
||||
from ..assemblicon.highlevel.source import SvgSource
|
||||
from ..assemblicon.highlevel.sink import TempArtSink, FdArtSink
|
||||
from assemblicon.highlevel.utils import FdContext
|
||||
from assemblicon.highlevel.source import SvgSource
|
||||
from assemblicon.highlevel.sink import TempArtSink, FdArtSink
|
||||
|
||||
|
||||
def register(ctx: AuroraContext) -> None:
|
||||
@@ -64,6 +64,8 @@ def build_dialog_warning_icon(
|
||||
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:
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user