From 86b19d8d645e3231b4729451fa567d0b5b3cc07c Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Thu, 20 Aug 2026 19:15:27 +0800 Subject: [PATCH] feat: use new design --- src/assemblicon/context.py | 18 +-- src/assemblicon/render.py | 121 +++++++++++++++++++++ src/assemblicon/sinks/__init__.py | 7 -- src/assemblicon/sinks/common.py | 12 -- src/assemblicon/sinks/temp_sink.py | 17 --- src/assemblicon/sources/__init__.py | 7 -- src/assemblicon/sources/common.py | 12 -- src/assemblicon/sources/inkscape_source.py | 16 --- src/assemblicon/sources/temp_source.py | 18 --- src/assemblicon/utils.py | 5 +- 10 files changed, 133 insertions(+), 100 deletions(-) create mode 100644 src/assemblicon/render.py delete mode 100644 src/assemblicon/sinks/__init__.py delete mode 100644 src/assemblicon/sinks/common.py delete mode 100644 src/assemblicon/sinks/temp_sink.py delete mode 100644 src/assemblicon/sources/__init__.py delete mode 100644 src/assemblicon/sources/common.py delete mode 100644 src/assemblicon/sources/inkscape_source.py delete mode 100644 src/assemblicon/sources/temp_source.py diff --git a/src/assemblicon/context.py b/src/assemblicon/context.py index 314d5fb..c90d649 100644 --- a/src/assemblicon/context.py +++ b/src/assemblicon/context.py @@ -14,17 +14,17 @@ class Context: self.__output_dir = output_dir self.__temp_dir = tempfile.TemporaryDirectory() - @property - def input_directory(self) -> Path: - return self.__input_dir + def input_artifact(self, *args: str) -> Path: + return self.__input_dir / Path(*args) - @property - def output_directory(self) -> Path: - return self.__output_dir + def output_artifact(self, *args: str) -> Path: + return self.__output_dir / Path(*args) - @property - def temporary_directory(self) -> Path: - return Path(self.__temp_dir.name) + def temporary_artifact(self, *args: str) -> Path: + return Path(self.__temp_dir.name) / "user" / Path(*args) + + def _intern_temporary_artifact(self, *args: str) -> Path: + return Path(self.__temp_dir.name) / "assemblicon" / Path(*args) def __enter__(self) -> "Context": return self diff --git a/src/assemblicon/render.py b/src/assemblicon/render.py new file mode 100644 index 0000000..2fd0fc8 --- /dev/null +++ b/src/assemblicon/render.py @@ -0,0 +1,121 @@ +import logging +import os +import subprocess +from pathlib import Path +from dataclasses import dataclass + +from .context import Context + + +@dataclass +class InkscapeConfig: + width: int + height: int + + def __post_init__(self) -> None: + if self.width <= 0: + raise ValueError( + f"The image width in Inkscape config must be a positive integer" + ) + if self.height <= 0: + raise ValueError( + f"The image height in Inkscape config must be a positive integer" + ) + + +def inkscape_render(src: Path, dst: Path, cfg: InkscapeConfig) -> None: + logging.info("Rendering Inkscape artifact: %s -> %s", src, dst) + + inkscape_bin = os.getenv("ASSEMBLICON_INKSCAPE", "inkscape") + cmd = [ + inkscape_bin, + "--export-area-page", + "--export-width", + str(cfg.width), + "--export-height", + str(cfg.height), + "--export-type", + "png", + "--export-png-color-mode", + "RGBA_8", + "--export-background-opacity", + "0.0", + "--export-png-use-dithering", + "false", + "--export-filename", + str(dst), + str(src), + ] + proc = subprocess.run(cmd, capture_output=False) + if proc.returncode != 0: + raise RuntimeError( + f"Fail to execute Inkscape. Return code is {proc.returncode}" + ) + + +@dataclass +class BlenderConfig: + width: int + height: int + + def __post_init__(self) -> None: + if self.width <= 0: + raise ValueError( + f"The image width in Blender config must be a positive integer" + ) + if self.height <= 0: + raise ValueError( + f"The image height in Blender config must be a positive integer" + ) + + +_BLENDER_RENDER_SCRIPT = """\ +import sys +import traceback + +try: + import bpy + + render = bpy.context.scene.render + render.resolution_x = {width} + render.resolution_y = {height} + render.resolution_percentage = 100 + render.image_settings.file_format = "PNG" + render.image_settings.color_mode = "RGBA" + render.image_settings.color_depth = "8" + render.film_transparent = True + render.use_file_extension = False + render.filepath = {dst} + bpy.ops.render.render(write_still=True) +except BaseException: + traceback.print_exc() + sys.exit(1) +""" + + +def blender_render(ctx: Context, src: Path, dst: Path, cfg: BlenderConfig) -> None: + logging.info("Rendering Blender artifact: %s -> %s", src, dst) + + script = _BLENDER_RENDER_SCRIPT.format( + width=cfg.width, height=cfg.height, dst=repr(str(dst)) + ) + script_path = ctx._intern_temporary_artifact("blender_render.py") + script_path.write_text(script, encoding="utf-8") + + blender_bin = os.getenv("ASSEMBLICON_BLENDER", "blender") + cmd = [ + blender_bin, + "--background", + str(src), + "--python", + str(script_path), + ] + proc = subprocess.run(cmd, capture_output=False) + if proc.returncode != 0: + raise RuntimeError( + f"Fail to execute Blender. Return code is {proc.returncode}" + ) + if not dst.exists(): + raise RuntimeError( + f"Blender exited successfully but output file {dst} is missing" + ) diff --git a/src/assemblicon/sinks/__init__.py b/src/assemblicon/sinks/__init__.py deleted file mode 100644 index 3fcccd1..0000000 --- a/src/assemblicon/sinks/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .common import Sink -from .temp_sink import TempSink - -__all__ = [ - "Sink", - "TempSink" -] diff --git a/src/assemblicon/sinks/common.py b/src/assemblicon/sinks/common.py deleted file mode 100644 index ca3f202..0000000 --- a/src/assemblicon/sinks/common.py +++ /dev/null @@ -1,12 +0,0 @@ -from abc import ABC, abstractmethod -from ..context import Context -from ..utils import Artifact - -class Sink(ABC): - """ - Abstract base class for all sinks. - """ - - @abstractmethod - def push_artifact(self, ctx: Context, art: Artifact) -> None: - pass diff --git a/src/assemblicon/sinks/temp_sink.py b/src/assemblicon/sinks/temp_sink.py deleted file mode 100644 index d1b81cf..0000000 --- a/src/assemblicon/sinks/temp_sink.py +++ /dev/null @@ -1,17 +0,0 @@ -import logging -from pathlib import Path -from ..context import Context -from ..utils import Artifact -from .common import Sink - -class TempSink(Sink): - - __relative_path: Path - - def __init__(self, *args: str) -> None: - self.__relative_path = Path(*args) - - def push_artifact(self, ctx: Context, art: Artifact) -> None: - fp = ctx.temporary_directory / self.__relative_path - logging.info('Saving temporary artifact: %s', fp) - art.save(fp) diff --git a/src/assemblicon/sources/__init__.py b/src/assemblicon/sources/__init__.py deleted file mode 100644 index 3965c97..0000000 --- a/src/assemblicon/sources/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .common import Source -from .temp_source import TempSource - -__all__ = [ - "Source", - "TempSource" -] diff --git a/src/assemblicon/sources/common.py b/src/assemblicon/sources/common.py deleted file mode 100644 index 2389d24..0000000 --- a/src/assemblicon/sources/common.py +++ /dev/null @@ -1,12 +0,0 @@ -from abc import ABC, abstractmethod -from ..context import Context -from ..utils import Artifact - -class Source(ABC): - """ - Abstract base class for all sources. - """ - - @abstractmethod - def pull_artifact(self, ctx: Context) -> Artifact: - pass diff --git a/src/assemblicon/sources/inkscape_source.py b/src/assemblicon/sources/inkscape_source.py deleted file mode 100644 index 852cf4c..0000000 --- a/src/assemblicon/sources/inkscape_source.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging -from pathlib import Path -import PIL.Image -from ..context import Context -from ..utils import Artifact -from .common import Source - -class InkscapeSource(Source): - - __relative_path: Path - - def __init__(self, *args: str) -> None: - self.__relative_path = Path(*args) - - def pull_artifact(self, ctx: Context) -> Artifact: - pass diff --git a/src/assemblicon/sources/temp_source.py b/src/assemblicon/sources/temp_source.py deleted file mode 100644 index 8827567..0000000 --- a/src/assemblicon/sources/temp_source.py +++ /dev/null @@ -1,18 +0,0 @@ -import logging -from pathlib import Path -import PIL.Image -from ..context import Context -from ..utils import Artifact -from .common import Source - -class TempSource(Source): - - __relative_path: Path - - def __init__(self, *args: str) -> None: - self.__relative_path = Path(*args) - - def pull_artifact(self, ctx: Context) -> Artifact: - fp = ctx.temporary_directory / self.__relative_path - logging.info('Loading temporary artifact: %s', fp) - return PIL.Image.open(fp) diff --git a/src/assemblicon/utils.py b/src/assemblicon/utils.py index cd2348a..23b270c 100644 --- a/src/assemblicon/utils.py +++ b/src/assemblicon/utils.py @@ -1,3 +1,4 @@ -from PIL.Image import Image +import logging -type Artifact = Image +def setup_logging() -> None: + logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.INFO)