Files
imagepipeline/imagepipeline/modules/imagemagick_fill.py
T
Frank Schwenk a60a18a253 chore: add Ruff and apply formatting across codebase
Introduce ruff lint/format config, expand .gitignore, and reformat Python sources.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 17:34:14 +02:00

116 lines
3.5 KiB
Python

from __future__ import annotations
from pathlib import Path
from imagepipeline.core.context import ModuleContext
from imagepipeline.core.params import Param
from imagepipeline.modules.base import SubprocessModule
from imagepipeline.modules.registry import register
from imagepipeline.utils.subprocess import run_command
def normalize_color(color: str) -> str:
color = color.strip()
if not color:
raise ValueError("Color must not be empty")
if color.startswith("#"):
return color
if color.replace(".", "", 1).isdigit():
return color
return f"#{color}"
def build_fill_arguments(
width: int,
height: int,
*,
color1: str,
color2: str | None,
gradient: bool,
radial: bool,
angle: float | None,
) -> list[str]:
c1 = normalize_color(color1)
size = f"{width}x{height}"
if not gradient:
return ["-size", size, f"xc:{c1}"]
c2 = normalize_color(color2 or color1)
if radial:
return ["-size", size, f"radial-gradient:{c1}-{c2}"]
args = ["-size", size]
if angle is not None:
args.extend(["-define", f"gradient:angle={angle}"])
args.append(f"gradient:{c1}-{c2}")
return args
@register
class ImageMagickFillModule(SubprocessModule):
name = "imagemagick_fill"
description = "Create solid-color or gradient images sized to match each input image"
command_candidates = ("magick", "convert")
@classmethod
def parameters(cls) -> dict[str, Param]:
return {
"color1": Param(
"string",
required=True,
help="Primary color (hex, e.g. #d7fd00, or ImageMagick color name)",
),
"color2": Param(
"string",
default="",
help="Second gradient color; ignored for solid fills",
),
"gradient": Param(
"bool",
default=False,
help="Create a gradient instead of a solid fill",
),
"radial": Param(
"bool",
default=False,
help="Use a radial gradient (linear when false)",
),
"angle": Param(
"float",
default=None,
help="Linear gradient angle in degrees (ignored for radial/solid)",
),
}
def run(self, ctx: ModuleContext) -> None:
command = self.resolve_command()
color1 = ctx.params["color1"]
color2 = ctx.params["color2"] or None
gradient = ctx.params["gradient"]
radial = ctx.params["radial"]
angle = ctx.params["angle"]
ctx.output_dir.mkdir(parents=True, exist_ok=True)
total = len(ctx.input_paths)
for index, src in enumerate(ctx.input_paths, start=1):
self.log_image(ctx, index, total, src)
width, height = self._image_size(command, src)
fill_args = build_fill_arguments(
width,
height,
color1=color1,
color2=color2,
gradient=gradient,
radial=radial,
angle=angle,
)
dst = ctx.output_dir / src.name
run_command([command, *fill_args, str(dst)])
@staticmethod
def _image_size(command: str, src: Path) -> tuple[int, int]:
result = run_command([command, "-format", "%w %h", str(src), "info:"])
width, height = map(int, result.stdout.strip().split())
return width, height