299 lines
10 KiB
Python
299 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from imagepipeline.core.params import validate_params
|
|
from imagepipeline.modules.composite import CompositeModule
|
|
from imagepipeline.modules.crop_square import CropSquareModule
|
|
from imagepipeline.modules.darktable_style import DarktableStyleModule
|
|
from imagepipeline.modules.imagemagick_fill import (
|
|
ImageMagickFillModule,
|
|
build_fill_arguments,
|
|
)
|
|
from imagepipeline.modules.imagemagick_grayscale import ImageMagickGrayscale
|
|
from imagepipeline.modules.gmic_grayscale import GmicGrayscale
|
|
from imagepipeline.modules.registry import get_module, list_modules
|
|
from imagepipeline.modules.rembg import RembgModule
|
|
|
|
has_magick = bool(shutil.which("magick") or shutil.which("convert"))
|
|
|
|
|
|
class TestModuleRegistration:
|
|
def test_workflow_modules_registered(self) -> None:
|
|
names = list_modules()
|
|
for name in (
|
|
"rembg",
|
|
"gmic_grayscale",
|
|
"composite",
|
|
"darktable_style",
|
|
"imagemagick_grayscale",
|
|
"imagemagick_fill",
|
|
"crop_square",
|
|
):
|
|
assert name in names
|
|
|
|
def test_get_module_returns_class(self) -> None:
|
|
assert get_module("rembg") is RembgModule
|
|
assert get_module("gmic_grayscale") is GmicGrayscale
|
|
assert get_module("composite") is CompositeModule
|
|
assert get_module("darktable_style") is DarktableStyleModule
|
|
assert get_module("crop_square") is CropSquareModule
|
|
assert get_module("imagemagick_fill") is ImageMagickFillModule
|
|
|
|
|
|
class TestImageMagickFill:
|
|
def test_solid_fill_arguments(self) -> None:
|
|
args = build_fill_arguments(
|
|
800,
|
|
600,
|
|
color1="#d7fd00",
|
|
color2=None,
|
|
gradient=False,
|
|
radial=False,
|
|
angle=None,
|
|
)
|
|
assert args == ["-size", "800x600", "xc:#d7fd00"]
|
|
|
|
def test_linear_gradient_arguments(self) -> None:
|
|
args = build_fill_arguments(
|
|
100,
|
|
50,
|
|
color1="#d7fd00",
|
|
color2="#fc0ade",
|
|
gradient=True,
|
|
radial=False,
|
|
angle=45.0,
|
|
)
|
|
assert args == [
|
|
"-size",
|
|
"100x50",
|
|
"-define",
|
|
"gradient:angle=45.0",
|
|
"gradient:#d7fd00-#fc0ade",
|
|
]
|
|
|
|
def test_radial_gradient_arguments(self) -> None:
|
|
args = build_fill_arguments(
|
|
100,
|
|
50,
|
|
color1="d7fd00",
|
|
color2="fc0ade",
|
|
gradient=True,
|
|
radial=True,
|
|
angle=90.0,
|
|
)
|
|
assert args == ["-size", "100x50", "radial-gradient:#d7fd00-#fc0ade"]
|
|
|
|
def test_requires_color1(self) -> None:
|
|
with pytest.raises(ValueError, match="required"):
|
|
ImageMagickFillModule.validate_module_params({})
|
|
|
|
@pytest.mark.skipif(not has_magick, reason="ImageMagick not installed")
|
|
def test_output_matches_input_size(self, tmp_path: Path) -> None:
|
|
from imagepipeline.core.context import ModuleContext
|
|
from imagepipeline.utils.subprocess import run_command
|
|
|
|
from tests.conftest import make_png
|
|
|
|
src = tmp_path / "ref.png"
|
|
make_png(src, width=40, height=20)
|
|
output_dir = tmp_path / "out"
|
|
output_dir.mkdir()
|
|
ctx = ModuleContext(
|
|
input_paths=[src],
|
|
matched_groups=[],
|
|
output_dir=output_dir,
|
|
params=ImageMagickFillModule.validate_module_params(
|
|
{
|
|
"color1": "#d7fd00",
|
|
"color2": "#fc0ade",
|
|
"gradient": True,
|
|
"angle": 45,
|
|
}
|
|
),
|
|
pipeline_output_root=tmp_path,
|
|
step_id="imagemagick_fill_01",
|
|
logger=None,
|
|
)
|
|
ImageMagickFillModule().run(ctx)
|
|
|
|
dst = output_dir / "ref.png"
|
|
assert dst.is_file()
|
|
magick = shutil.which("magick") or shutil.which("convert")
|
|
result = run_command([magick, "-format", "%w %h", str(dst), "info:"])
|
|
assert result.stdout.strip() == "40 20"
|
|
|
|
|
|
class TestCropSquare:
|
|
@pytest.mark.skipif(not has_magick, reason="ImageMagick not installed")
|
|
def test_center_crops_to_square(self, tmp_path: Path) -> None:
|
|
from imagepipeline.core.context import ModuleContext
|
|
from imagepipeline.utils.subprocess import run_command
|
|
|
|
from tests.conftest import make_png
|
|
|
|
src = tmp_path / "wide.png"
|
|
make_png(src, width=40, height=20)
|
|
output_dir = tmp_path / "out"
|
|
output_dir.mkdir()
|
|
ctx = ModuleContext(
|
|
input_paths=[src],
|
|
matched_groups=[],
|
|
output_dir=output_dir,
|
|
params={},
|
|
pipeline_output_root=tmp_path,
|
|
step_id="crop_square_01",
|
|
logger=None,
|
|
)
|
|
CropSquareModule().run(ctx)
|
|
|
|
dst = output_dir / "wide.png"
|
|
assert dst.is_file()
|
|
magick = shutil.which("magick") or shutil.which("convert")
|
|
result = run_command(
|
|
[magick, "-format", "%w %h", str(dst), "info:"],
|
|
)
|
|
width, height = map(int, result.stdout.strip().split())
|
|
assert width == height == 20
|
|
|
|
|
|
class TestModuleParameters:
|
|
def test_darktable_style_requires_style(self) -> None:
|
|
with pytest.raises(ValueError, match="required"):
|
|
DarktableStyleModule.validate_module_params({})
|
|
|
|
def test_darktable_style_accepts_style(self) -> None:
|
|
params = DarktableStyleModule.validate_module_params(
|
|
{"style": "Watermark F12.rocks"}
|
|
)
|
|
assert params["style"] == "Watermark F12.rocks"
|
|
assert params["style_overwrite"] is True
|
|
|
|
def test_composite_mode_choices(self) -> None:
|
|
with pytest.raises(ValueError, match="must be one of"):
|
|
CompositeModule.validate_module_params({"mode": "invalid"})
|
|
|
|
def test_rembg_defaults(self) -> None:
|
|
params = RembgModule.validate_module_params({})
|
|
assert params["model"] == "birefnet-general"
|
|
assert params["alpha_matting"] is True
|
|
|
|
def test_darktable_export_conf_jpeg(self) -> None:
|
|
from imagepipeline.modules.darktable_style import export_conf_options
|
|
|
|
assert export_conf_options("jpeg") == [
|
|
"plugins/imageio/format/jpeg/quality=90"
|
|
]
|
|
|
|
def test_darktable_export_conf_png(self) -> None:
|
|
from imagepipeline.modules.darktable_style import export_conf_options
|
|
|
|
assert export_conf_options("png") == [
|
|
"plugins/imageio/format/png/bpp=8",
|
|
"plugins/imageio/format/png/compression=5",
|
|
]
|
|
|
|
def test_darktable_resolve_format_from_input(self) -> None:
|
|
from imagepipeline.modules.darktable_style import resolve_export_format
|
|
|
|
format_name, conf = resolve_export_format(Path("photo.jpg"), "")
|
|
assert format_name == "jpeg"
|
|
assert conf == ["plugins/imageio/format/jpeg/quality=90"]
|
|
|
|
format_name, conf = resolve_export_format(Path("photo.png"), "")
|
|
assert format_name == "png"
|
|
assert "plugins/imageio/format/png/compression=5" in conf
|
|
|
|
def test_darktable_rejects_unsupported_format(self) -> None:
|
|
from imagepipeline.modules.darktable_style import resolve_export_format
|
|
|
|
with pytest.raises(ValueError, match="Unsupported output format"):
|
|
resolve_export_format(Path("photo.tiff"), "tiff")
|
|
|
|
|
|
@pytest.mark.skipif(not has_magick, reason="ImageMagick not installed")
|
|
class TestCompositeColor:
|
|
def test_preserves_color_over_grayscale_background(self, tmp_path: Path) -> None:
|
|
from imagepipeline.core.context import ModuleContext
|
|
from imagepipeline.utils.subprocess import run_command
|
|
|
|
from tests.conftest import make_png
|
|
|
|
src = tmp_path / "src.png"
|
|
make_png(src, width=40, height=40, rgb=(200, 50, 50))
|
|
background = tmp_path / "bg.png"
|
|
foreground = tmp_path / "fg.png"
|
|
output_dir = tmp_path / "composite_out"
|
|
output_dir.mkdir()
|
|
magick = shutil.which("magick") or shutil.which("convert")
|
|
|
|
run_command([magick, str(src), "-colorspace", "Gray", str(background)])
|
|
run_command(
|
|
[
|
|
magick,
|
|
"-size",
|
|
"40x40",
|
|
"xc:none",
|
|
"-fill",
|
|
"rgba(255,0,0,0.8)",
|
|
"-draw",
|
|
"circle 20,20 20,2",
|
|
str(foreground),
|
|
]
|
|
)
|
|
|
|
module = CompositeModule()
|
|
ctx = ModuleContext(
|
|
input_paths=[background, foreground],
|
|
output_dir=output_dir,
|
|
params=CompositeModule.validate_module_params({}),
|
|
pipeline_output_root=tmp_path,
|
|
step_id="composite_01",
|
|
matched_groups=[[background, foreground]],
|
|
)
|
|
module.run(ctx)
|
|
|
|
output = output_dir / "fg.png"
|
|
assert output.is_file()
|
|
result = run_command(
|
|
[magick, "identify", "-format", "%[type]", str(output)]
|
|
)
|
|
assert result.stdout.strip() != "Grayscale"
|
|
|
|
|
|
has_workflow_tools = all(
|
|
shutil.which(name)
|
|
for name in ("rembg", "gmic", "magick", "darktable-cli")
|
|
) or all(shutil.which(name) for name in ("rembg", "gmic", "convert", "darktable-cli"))
|
|
|
|
|
|
@pytest.mark.skipif(not has_workflow_tools, reason="Workflow CLI tools not installed")
|
|
class TestWorkflowIntegration:
|
|
def test_watermark_pipeline(self, input_dir, output_base) -> None:
|
|
from imagepipeline import Pipeline
|
|
|
|
with Pipeline(
|
|
name="workflow_test",
|
|
input_dir=input_dir,
|
|
output_base=output_base,
|
|
verbose=False,
|
|
) as p:
|
|
rembg_out = p.step("rembg", inputs="input")
|
|
grayscale = p.step("gmic_grayscale", inputs="input")
|
|
combined = p.step("composite", inputs=[grayscale, rembg_out])
|
|
p.step(
|
|
"darktable_style",
|
|
inputs=combined,
|
|
style="Watermark F12.rocks",
|
|
)
|
|
root = p.run()
|
|
|
|
assert (root / "rembg_01").is_dir()
|
|
assert (root / "gmic_grayscale_01").is_dir()
|
|
assert (root / "composite_01").is_dir()
|
|
assert (root / "darktable_style_01").is_dir()
|
|
assert list((root / "darktable_style_01").iterdir())
|