diff --git a/src/assemblicon/highlevel/sink/tempart_sink.py b/src/assemblicon/highlevel/sink/tempart_sink.py index 4a5831d..6eaba66 100644 --- a/src/assemblicon/highlevel/sink/tempart_sink.py +++ b/src/assemblicon/highlevel/sink/tempart_sink.py @@ -24,9 +24,16 @@ class TempArtSink(Sink): return Address(AddressDomain.Temporary, *self.__comps) def push(self, env: Environment, art: Artifact) -> None: + """ + Save given artifact to temporary directory. + + The artifact is explicitly saved with the intermediary (faster) + encoder configuration, because temporary artifacts are transient + and usually get re-loaded and re-encoded later. + """ p = self.push_hint(env) LOGGER.info("Saving temporary artifact: %s", p) - artio.save_artifact(art, p) + artio.save_artifact(art, p, for_intermediary=True) def repull(self, env: Environment) -> Artifact: """ diff --git a/src/assemblicon/lowlevel/artio.py b/src/assemblicon/lowlevel/artio.py index 040c17d..5c55033 100644 --- a/src/assemblicon/lowlevel/artio.py +++ b/src/assemblicon/lowlevel/artio.py @@ -42,9 +42,65 @@ def load_artifact(p: Path) -> Artifact: return PIL.Image.open(p, "r", ["PNG"]) -def save_artifact(art: Artifact, p: Path) -> None: +def save_artifact(art: Artifact, p: Path, for_intermediary: bool = False) -> None: + """ + Save given artifact as PNG file. + + The PNG encoder settings are selected in tiers by artifact size: + smaller artifacts are saved with lighter compression while larger + ones are saved with stronger compression. This tiered configuration + exists to strike a balance between final artifact file size and + assembly speed, since strong compression is slow and only pays off + on large images. + + :param art: The artifact to be saved. + :param p: The path to saved PNG file. + :param for_intermediary: Whether this artifact is saved as an + intermediary. Intermediaries are transient and will be loaded + and re-encoded later, so they are always saved with the + balanced, faster configuration regardless of their size. + """ LOGGER.debug("Saving artifact: %s", p) - art.save(p, format="PNG") + + # compute optimize flag and compress level based on artifact size. + optimize: bool + compress_level: int + + if for_intermediary: + # If this image is saved for intermediary, + # we use default balanced config. + optimize = False + compress_level = 6 + else: + pixels = art.width * art.height + if pixels >= 1024**2: + # >= 1024x1024, super large icon, + # use best compression config. + optimize = True + compress_level = 9 + elif pixels >= 256**2: + # >= 256x256 and < 1024x1024, large icon, + # use max compress level. + optimize = False + compress_level = 9 + elif pixels >= 64**2: + # >= 64x64 and < 256x256, medium icon, + # use higher compress level. + optimize = False + compress_level = 8 + elif pixels >= 16**2: + # >= 16x16 and < 64x64, small icon, + # use higher compress level. + optimize = False + compress_level = 7 + else: + # < 16x16, very small icon, + # use default balanced config. + optimize = False + compress_level = 6 + + # save as PNG with computed optimize flag and compress level + art.save(p, format="PNG", optimize=optimize, compress_level=compress_level) def dup_artifact(src: Path, dst: Path) -> None: @@ -124,7 +180,7 @@ def link_artifact(src: Path, dst: Path) -> None: 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