agentbrush 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentbrush/__init__.py +38 -0
- agentbrush/__main__.py +4 -0
- agentbrush/background/__init__.py +3 -0
- agentbrush/background/cli.py +51 -0
- agentbrush/background/ops.py +77 -0
- agentbrush/border/__init__.py +3 -0
- agentbrush/border/cli.py +50 -0
- agentbrush/border/ops.py +150 -0
- agentbrush/cli.py +65 -0
- agentbrush/composite/__init__.py +3 -0
- agentbrush/composite/cli.py +169 -0
- agentbrush/composite/ops.py +129 -0
- agentbrush/convert/__init__.py +3 -0
- agentbrush/convert/cli.py +44 -0
- agentbrush/convert/ops.py +104 -0
- agentbrush/core/__init__.py +17 -0
- agentbrush/core/alpha.py +65 -0
- agentbrush/core/color.py +70 -0
- agentbrush/core/connectivity.py +102 -0
- agentbrush/core/flood_fill.py +82 -0
- agentbrush/core/fonts.py +121 -0
- agentbrush/core/geometry.py +84 -0
- agentbrush/core/result.py +63 -0
- agentbrush/generate/__init__.py +3 -0
- agentbrush/generate/cli.py +40 -0
- agentbrush/generate/ops.py +179 -0
- agentbrush/greenscreen/__init__.py +3 -0
- agentbrush/greenscreen/cli.py +50 -0
- agentbrush/greenscreen/ops.py +132 -0
- agentbrush/resize/__init__.py +3 -0
- agentbrush/resize/cli.py +42 -0
- agentbrush/resize/ops.py +101 -0
- agentbrush/text/__init__.py +3 -0
- agentbrush/text/cli.py +70 -0
- agentbrush/text/ops.py +173 -0
- agentbrush/validate/__init__.py +3 -0
- agentbrush/validate/cli.py +50 -0
- agentbrush/validate/ops.py +379 -0
- agentbrush-0.1.0.dist-info/METADATA +251 -0
- agentbrush-0.1.0.dist-info/RECORD +43 -0
- agentbrush-0.1.0.dist-info/WHEEL +4 -0
- agentbrush-0.1.0.dist-info/entry_points.txt +2 -0
- agentbrush-0.1.0.dist-info/licenses/LICENSE +21 -0
agentbrush/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""AgentBrush — Image editing toolkit for AI agents."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from agentbrush.core.result import Result
|
|
6
|
+
from agentbrush.background.ops import remove_background
|
|
7
|
+
from agentbrush.greenscreen.ops import remove_greenscreen
|
|
8
|
+
from agentbrush.border.ops import cleanup_border
|
|
9
|
+
from agentbrush.text.ops import add_text, render_text
|
|
10
|
+
from agentbrush.composite.ops import composite, paste_centered
|
|
11
|
+
from agentbrush.resize.ops import resize_image
|
|
12
|
+
from agentbrush.validate.ops import validate_design, compare_images
|
|
13
|
+
from agentbrush.convert.ops import convert_image
|
|
14
|
+
|
|
15
|
+
# generate is optional (requires openai package)
|
|
16
|
+
try:
|
|
17
|
+
from agentbrush.generate.ops import generate_image
|
|
18
|
+
_has_generate = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
_has_generate = False
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Result",
|
|
24
|
+
"remove_background",
|
|
25
|
+
"remove_greenscreen",
|
|
26
|
+
"cleanup_border",
|
|
27
|
+
"add_text",
|
|
28
|
+
"render_text",
|
|
29
|
+
"composite",
|
|
30
|
+
"paste_centered",
|
|
31
|
+
"resize_image",
|
|
32
|
+
"validate_design",
|
|
33
|
+
"compare_images",
|
|
34
|
+
"convert_image",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
if _has_generate:
|
|
38
|
+
__all__.append("generate_image")
|
agentbrush/__main__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""CLI for background removal."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from agentbrush.background.ops import remove_background
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_parser(subparsers):
|
|
11
|
+
"""Register the remove-bg subcommand."""
|
|
12
|
+
p = subparsers.add_parser(
|
|
13
|
+
"remove-bg",
|
|
14
|
+
help="Remove solid-color background via edge-based flood fill",
|
|
15
|
+
)
|
|
16
|
+
p.add_argument("input", help="Input image path")
|
|
17
|
+
p.add_argument("output", help="Output image path")
|
|
18
|
+
p.add_argument(
|
|
19
|
+
"--color", default="black",
|
|
20
|
+
help="Background color: black, white, or R,G,B (default: black)",
|
|
21
|
+
)
|
|
22
|
+
p.add_argument(
|
|
23
|
+
"--threshold", type=int, default=25,
|
|
24
|
+
help="Color match threshold 0-255 (default: 25)",
|
|
25
|
+
)
|
|
26
|
+
p.add_argument(
|
|
27
|
+
"--smooth", action="store_true",
|
|
28
|
+
help="Apply 1px edge feathering",
|
|
29
|
+
)
|
|
30
|
+
p.add_argument(
|
|
31
|
+
"--resize",
|
|
32
|
+
help="Resize output: WxH (e.g. 1664x1664)",
|
|
33
|
+
)
|
|
34
|
+
p.set_defaults(func=run)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(args):
|
|
38
|
+
resize = None
|
|
39
|
+
if args.resize:
|
|
40
|
+
w, h = args.resize.lower().split("x")
|
|
41
|
+
resize = (int(w), int(h))
|
|
42
|
+
|
|
43
|
+
result = remove_background(
|
|
44
|
+
args.input, args.output,
|
|
45
|
+
color=args.color,
|
|
46
|
+
threshold=args.threshold,
|
|
47
|
+
smooth=args.smooth,
|
|
48
|
+
resize=resize,
|
|
49
|
+
)
|
|
50
|
+
print(result.summary())
|
|
51
|
+
return 0 if result.success else 1
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Background removal via edge-based flood fill.
|
|
2
|
+
|
|
3
|
+
NEVER use threshold-based removal — it destroys internal outlines/details.
|
|
4
|
+
This module starts flood fill from image edges only, preserving artwork.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
from agentbrush.core.alpha import smooth_edges
|
|
15
|
+
from agentbrush.core.color import ColorTuple, parse_color
|
|
16
|
+
from agentbrush.core.flood_fill import flood_fill_from_edges
|
|
17
|
+
from agentbrush.core.result import Result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def remove_background(
|
|
21
|
+
input_path: Union[str, Path],
|
|
22
|
+
output_path: Union[str, Path],
|
|
23
|
+
color: str = "black",
|
|
24
|
+
threshold: int = 25,
|
|
25
|
+
smooth: bool = False,
|
|
26
|
+
resize: Optional[Tuple[int, int]] = None,
|
|
27
|
+
) -> Result:
|
|
28
|
+
"""Remove solid-color background using edge-based flood fill.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
input_path: Source image path.
|
|
32
|
+
output_path: Destination path for processed image.
|
|
33
|
+
color: Background color name or 'R,G,B' string.
|
|
34
|
+
threshold: Color match threshold 0-255.
|
|
35
|
+
smooth: Apply 1px edge feathering for cleaner cutlines.
|
|
36
|
+
resize: Optional (width, height) to resize output.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Result with stats about the operation.
|
|
40
|
+
"""
|
|
41
|
+
input_path = Path(input_path)
|
|
42
|
+
output_path = Path(output_path)
|
|
43
|
+
|
|
44
|
+
if not input_path.exists():
|
|
45
|
+
return Result(errors=[f"File not found: {input_path}"])
|
|
46
|
+
|
|
47
|
+
target_color = parse_color(color)
|
|
48
|
+
img = Image.open(input_path).convert("RGBA")
|
|
49
|
+
|
|
50
|
+
# Check if already transparent (no-op)
|
|
51
|
+
data = list(img.get_flattened_data())
|
|
52
|
+
initial_transparent = sum(1 for p in data if p[3] == 0)
|
|
53
|
+
if initial_transparent == len(data):
|
|
54
|
+
result = Result.from_image(img, output_path)
|
|
55
|
+
result.warnings.append("Image is already fully transparent")
|
|
56
|
+
os.makedirs(output_path.parent, exist_ok=True)
|
|
57
|
+
img.save(output_path, "PNG")
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
img, removed = flood_fill_from_edges(
|
|
61
|
+
img, target_color=target_color, threshold=threshold
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if smooth:
|
|
65
|
+
img = smooth_edges(img, radius=1)
|
|
66
|
+
|
|
67
|
+
if resize:
|
|
68
|
+
img = img.resize(resize, Image.LANCZOS)
|
|
69
|
+
|
|
70
|
+
os.makedirs(output_path.parent, exist_ok=True)
|
|
71
|
+
img.save(output_path, "PNG")
|
|
72
|
+
|
|
73
|
+
result = Result.from_image(img, output_path)
|
|
74
|
+
result.metadata["pixels_removed"] = removed
|
|
75
|
+
result.metadata["color"] = color
|
|
76
|
+
result.metadata["threshold"] = threshold
|
|
77
|
+
return result
|
agentbrush/border/cli.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""CLI for border cleanup."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
from agentbrush.border.ops import cleanup_border
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def add_parser(subparsers):
|
|
10
|
+
"""Register the border-cleanup subcommand."""
|
|
11
|
+
p = subparsers.add_parser(
|
|
12
|
+
"border-cleanup",
|
|
13
|
+
help="Remove white sticker border + optional green halo",
|
|
14
|
+
)
|
|
15
|
+
p.add_argument("input", help="Input image path")
|
|
16
|
+
p.add_argument("output", help="Output image path")
|
|
17
|
+
p.add_argument(
|
|
18
|
+
"--passes", type=int, default=15,
|
|
19
|
+
help="White border erosion passes (default: 15)",
|
|
20
|
+
)
|
|
21
|
+
p.add_argument(
|
|
22
|
+
"--threshold", type=int, default=185,
|
|
23
|
+
help="White pixel threshold (default: 185)",
|
|
24
|
+
)
|
|
25
|
+
p.add_argument(
|
|
26
|
+
"--green-halo-passes", type=int, default=0,
|
|
27
|
+
help="Green halo erosion passes (default: 0, disabled)",
|
|
28
|
+
)
|
|
29
|
+
p.add_argument(
|
|
30
|
+
"--alpha-smooth", action="store_true",
|
|
31
|
+
help="Apply Gaussian alpha edge smoothing",
|
|
32
|
+
)
|
|
33
|
+
p.add_argument(
|
|
34
|
+
"--alpha-blur-radius", type=float, default=1.5,
|
|
35
|
+
help="Alpha blur radius (default: 1.5)",
|
|
36
|
+
)
|
|
37
|
+
p.set_defaults(func=run)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run(args):
|
|
41
|
+
result = cleanup_border(
|
|
42
|
+
args.input, args.output,
|
|
43
|
+
passes=args.passes,
|
|
44
|
+
threshold=args.threshold,
|
|
45
|
+
green_halo_passes=args.green_halo_passes,
|
|
46
|
+
alpha_smooth=args.alpha_smooth,
|
|
47
|
+
alpha_blur_radius=args.alpha_blur_radius,
|
|
48
|
+
)
|
|
49
|
+
print(result.summary())
|
|
50
|
+
return 0 if result.success else 1
|
agentbrush/border/ops.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Border erosion and halo removal for sticker cleanup.
|
|
2
|
+
|
|
3
|
+
Handles:
|
|
4
|
+
- White sticker border erosion (AI generators add a white outline)
|
|
5
|
+
- Green halo erosion (anti-aliased fringe after green screen removal)
|
|
6
|
+
- Alpha edge smoothing (Gaussian blur on edges, interior preserved)
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, Union
|
|
13
|
+
|
|
14
|
+
from PIL import Image
|
|
15
|
+
|
|
16
|
+
from agentbrush.core.alpha import smooth_alpha_edges
|
|
17
|
+
from agentbrush.core.result import Result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _erode_white_border(
|
|
21
|
+
img: Image.Image,
|
|
22
|
+
passes: int = 15,
|
|
23
|
+
threshold: int = 185,
|
|
24
|
+
) -> tuple:
|
|
25
|
+
"""Iteratively remove light pixels adjacent to transparent (white sticker border).
|
|
26
|
+
|
|
27
|
+
Threshold < 170 eats into colored artwork. 185 is the safe default.
|
|
28
|
+
"""
|
|
29
|
+
w, h = img.size
|
|
30
|
+
pixels = img.load()
|
|
31
|
+
total = 0
|
|
32
|
+
|
|
33
|
+
for _ in range(passes):
|
|
34
|
+
to_remove = []
|
|
35
|
+
for y in range(h):
|
|
36
|
+
for x in range(w):
|
|
37
|
+
r, g, b, a = pixels[x, y]
|
|
38
|
+
if a == 0:
|
|
39
|
+
continue
|
|
40
|
+
if r > threshold and g > threshold and b > threshold:
|
|
41
|
+
adj_transparent = False
|
|
42
|
+
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
|
43
|
+
nx, ny = x + dx, y + dy
|
|
44
|
+
if 0 <= nx < w and 0 <= ny < h:
|
|
45
|
+
if pixels[nx, ny][3] == 0:
|
|
46
|
+
adj_transparent = True
|
|
47
|
+
break
|
|
48
|
+
else:
|
|
49
|
+
adj_transparent = True
|
|
50
|
+
break
|
|
51
|
+
if adj_transparent:
|
|
52
|
+
to_remove.append((x, y))
|
|
53
|
+
for x, y in to_remove:
|
|
54
|
+
pixels[x, y] = (0, 0, 0, 0)
|
|
55
|
+
total += len(to_remove)
|
|
56
|
+
if len(to_remove) == 0:
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
return img, total
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _erode_green_halo(
|
|
63
|
+
img: Image.Image,
|
|
64
|
+
passes: int = 20,
|
|
65
|
+
) -> tuple:
|
|
66
|
+
"""Remove green-tinted pixels adjacent to transparent (anti-alias fringe)."""
|
|
67
|
+
w, h = img.size
|
|
68
|
+
pixels = img.load()
|
|
69
|
+
total = 0
|
|
70
|
+
|
|
71
|
+
for _ in range(passes):
|
|
72
|
+
to_remove = []
|
|
73
|
+
for y in range(h):
|
|
74
|
+
for x in range(w):
|
|
75
|
+
r, g, b, a = pixels[x, y]
|
|
76
|
+
if a == 0:
|
|
77
|
+
continue
|
|
78
|
+
is_greenish = g > 80 and g > r * 1.2 and g > b * 1.2
|
|
79
|
+
if not is_greenish:
|
|
80
|
+
continue
|
|
81
|
+
adj_transparent = False
|
|
82
|
+
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
|
83
|
+
nx, ny = x + dx, y + dy
|
|
84
|
+
if 0 <= nx < w and 0 <= ny < h:
|
|
85
|
+
if pixels[nx, ny][3] == 0:
|
|
86
|
+
adj_transparent = True
|
|
87
|
+
break
|
|
88
|
+
else:
|
|
89
|
+
adj_transparent = True
|
|
90
|
+
break
|
|
91
|
+
if adj_transparent:
|
|
92
|
+
to_remove.append((x, y))
|
|
93
|
+
for x, y in to_remove:
|
|
94
|
+
pixels[x, y] = (0, 0, 0, 0)
|
|
95
|
+
total += len(to_remove)
|
|
96
|
+
if len(to_remove) == 0:
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
return img, total
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cleanup_border(
|
|
103
|
+
input_path: Union[str, Path],
|
|
104
|
+
output_path: Union[str, Path],
|
|
105
|
+
passes: int = 15,
|
|
106
|
+
threshold: int = 185,
|
|
107
|
+
green_halo_passes: int = 0,
|
|
108
|
+
alpha_smooth: bool = False,
|
|
109
|
+
alpha_blur_radius: float = 1.5,
|
|
110
|
+
) -> Result:
|
|
111
|
+
"""Clean up sticker borders: white border erosion + optional green halo removal.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
input_path: Source image path.
|
|
115
|
+
output_path: Destination path.
|
|
116
|
+
passes: Number of white border erosion passes.
|
|
117
|
+
threshold: White pixel threshold (R,G,B all above this = white).
|
|
118
|
+
green_halo_passes: Number of green halo erosion passes (0 to skip).
|
|
119
|
+
alpha_smooth: Apply Gaussian alpha smoothing for die-cut outline.
|
|
120
|
+
alpha_blur_radius: Blur radius for alpha smoothing.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Result with operation stats.
|
|
124
|
+
"""
|
|
125
|
+
input_path = Path(input_path)
|
|
126
|
+
output_path = Path(output_path)
|
|
127
|
+
|
|
128
|
+
if not input_path.exists():
|
|
129
|
+
return Result(errors=[f"File not found: {input_path}"])
|
|
130
|
+
|
|
131
|
+
img = Image.open(input_path).convert("RGBA")
|
|
132
|
+
metadata = {}
|
|
133
|
+
|
|
134
|
+
img, white_removed = _erode_white_border(img, passes=passes, threshold=threshold)
|
|
135
|
+
metadata["white_border_removed"] = white_removed
|
|
136
|
+
|
|
137
|
+
if green_halo_passes > 0:
|
|
138
|
+
img, green_removed = _erode_green_halo(img, passes=green_halo_passes)
|
|
139
|
+
metadata["green_halo_removed"] = green_removed
|
|
140
|
+
|
|
141
|
+
if alpha_smooth:
|
|
142
|
+
img = smooth_alpha_edges(img, blur_radius=alpha_blur_radius)
|
|
143
|
+
metadata["alpha_smoothed"] = True
|
|
144
|
+
|
|
145
|
+
os.makedirs(output_path.parent, exist_ok=True)
|
|
146
|
+
img.save(output_path, "PNG")
|
|
147
|
+
|
|
148
|
+
result = Result.from_image(img, output_path)
|
|
149
|
+
result.metadata = metadata
|
|
150
|
+
return result
|
agentbrush/cli.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Top-level CLI dispatcher for agentbrush.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
agentbrush <command> [options]
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
remove-bg Remove solid-color background via edge-based flood fill
|
|
8
|
+
greenscreen Remove green screen background (multi-pass pipeline)
|
|
9
|
+
border-cleanup Remove white sticker border + optional green halo
|
|
10
|
+
text Render text onto an image or new canvas
|
|
11
|
+
composite Alpha-composite overlay onto base image
|
|
12
|
+
resize Resize image (exact, fit, pad, or scale)
|
|
13
|
+
validate Validate design against product specs
|
|
14
|
+
convert Convert image format (PNG, JPEG, WEBP, etc.)
|
|
15
|
+
generate Generate image from text prompt (OpenAI/Pollinations)
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
from agentbrush import __version__
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(argv=None):
|
|
26
|
+
parser = argparse.ArgumentParser(
|
|
27
|
+
prog="agentbrush",
|
|
28
|
+
description="Image editing toolkit for AI agents",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--version", action="version",
|
|
32
|
+
version=f"agentbrush {__version__}",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
36
|
+
|
|
37
|
+
# Register subcommands
|
|
38
|
+
from agentbrush.background.cli import add_parser as add_bg
|
|
39
|
+
from agentbrush.greenscreen.cli import add_parser as add_gs
|
|
40
|
+
from agentbrush.border.cli import add_parser as add_border
|
|
41
|
+
from agentbrush.text.cli import add_parser as add_text
|
|
42
|
+
from agentbrush.composite.cli import add_parser as add_composite
|
|
43
|
+
from agentbrush.resize.cli import add_parser as add_resize
|
|
44
|
+
from agentbrush.validate.cli import add_parser as add_validate
|
|
45
|
+
from agentbrush.convert.cli import add_parser as add_convert
|
|
46
|
+
from agentbrush.generate.cli import add_parser as add_generate
|
|
47
|
+
|
|
48
|
+
add_bg(subparsers)
|
|
49
|
+
add_gs(subparsers)
|
|
50
|
+
add_border(subparsers)
|
|
51
|
+
add_text(subparsers)
|
|
52
|
+
add_composite(subparsers)
|
|
53
|
+
add_resize(subparsers)
|
|
54
|
+
add_validate(subparsers)
|
|
55
|
+
add_convert(subparsers)
|
|
56
|
+
add_generate(subparsers)
|
|
57
|
+
|
|
58
|
+
args = parser.parse_args(argv)
|
|
59
|
+
|
|
60
|
+
if args.command is None:
|
|
61
|
+
parser.print_help()
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
|
|
64
|
+
exit_code = args.func(args)
|
|
65
|
+
sys.exit(exit_code or 0)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""CLI for image compositing.
|
|
2
|
+
|
|
3
|
+
Supports two modes:
|
|
4
|
+
agentbrush composite <base> <overlay> <output> [--position X,Y] [--opacity N]
|
|
5
|
+
agentbrush composite paste-centered <output> --overlay <img> --canvas WxH [--fit] [--bg-color R,G,B,A]
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
|
|
11
|
+
from agentbrush.composite.ops import composite, paste_centered
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_parser(subparsers):
|
|
15
|
+
"""Register the composite subcommand."""
|
|
16
|
+
p = subparsers.add_parser(
|
|
17
|
+
"composite",
|
|
18
|
+
help="Composite images: overlay onto base, or center on new canvas",
|
|
19
|
+
usage=(
|
|
20
|
+
"agentbrush composite <base> <overlay> <output> [options]\n"
|
|
21
|
+
" agentbrush composite paste-centered <output> "
|
|
22
|
+
"--overlay <img> --canvas WxH [--fit]"
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
p.add_argument("rest", nargs=argparse.REMAINDER)
|
|
26
|
+
p.set_defaults(func=run)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _overlay_parser():
|
|
30
|
+
"""Parser for overlay mode (default)."""
|
|
31
|
+
p = argparse.ArgumentParser(
|
|
32
|
+
prog="agentbrush composite",
|
|
33
|
+
description="Alpha-composite overlay onto base image",
|
|
34
|
+
)
|
|
35
|
+
p.add_argument("base", help="Base (background) image path")
|
|
36
|
+
p.add_argument("overlay", help="Overlay (foreground) image path")
|
|
37
|
+
p.add_argument("output", help="Output image path")
|
|
38
|
+
p.add_argument(
|
|
39
|
+
"--position", default="0,0",
|
|
40
|
+
help="Position as X,Y (default: 0,0). Use 'center' to auto-center.",
|
|
41
|
+
)
|
|
42
|
+
p.add_argument(
|
|
43
|
+
"--resize-overlay",
|
|
44
|
+
help="Resize overlay: WxH (e.g. 400x400)",
|
|
45
|
+
)
|
|
46
|
+
p.add_argument(
|
|
47
|
+
"--opacity", type=float, default=1.0,
|
|
48
|
+
help="Overlay opacity 0.0-1.0 (default: 1.0)",
|
|
49
|
+
)
|
|
50
|
+
return p
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _paste_centered_parser():
|
|
54
|
+
"""Parser for paste-centered mode."""
|
|
55
|
+
p = argparse.ArgumentParser(
|
|
56
|
+
prog="agentbrush composite paste-centered",
|
|
57
|
+
description="Center image on a new canvas",
|
|
58
|
+
)
|
|
59
|
+
p.add_argument("output", help="Output image path")
|
|
60
|
+
p.add_argument(
|
|
61
|
+
"--overlay", required=True,
|
|
62
|
+
help="Image to center on canvas",
|
|
63
|
+
)
|
|
64
|
+
p.add_argument(
|
|
65
|
+
"--canvas", required=True,
|
|
66
|
+
help="Canvas size as WxH (e.g. 4500x5400)",
|
|
67
|
+
)
|
|
68
|
+
p.add_argument(
|
|
69
|
+
"--bg-color", default="0,0,0,0",
|
|
70
|
+
help="Background color as R,G,B,A (default: transparent)",
|
|
71
|
+
)
|
|
72
|
+
p.add_argument(
|
|
73
|
+
"--resize-overlay",
|
|
74
|
+
help="Resize overlay before centering: WxH",
|
|
75
|
+
)
|
|
76
|
+
p.add_argument(
|
|
77
|
+
"--fit", action="store_true",
|
|
78
|
+
help="Scale overlay to fit canvas while preserving aspect ratio",
|
|
79
|
+
)
|
|
80
|
+
return p
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _parse_color(s):
|
|
84
|
+
parts = [int(x) for x in s.split(",")]
|
|
85
|
+
if len(parts) == 3:
|
|
86
|
+
parts.append(255)
|
|
87
|
+
return tuple(parts)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run(args):
|
|
91
|
+
"""Dispatch composite command based on mode."""
|
|
92
|
+
remaining = args.rest
|
|
93
|
+
|
|
94
|
+
# Strip leading '--' if argparse inserted it
|
|
95
|
+
if remaining and remaining[0] == "--":
|
|
96
|
+
remaining = remaining[1:]
|
|
97
|
+
|
|
98
|
+
if not remaining or remaining[0] in ("-h", "--help"):
|
|
99
|
+
print(
|
|
100
|
+
"Usage:\n"
|
|
101
|
+
" agentbrush composite <base> <overlay> <output> [--position X,Y]\n"
|
|
102
|
+
" agentbrush composite paste-centered <output> "
|
|
103
|
+
"--overlay <img> --canvas WxH [--fit]\n"
|
|
104
|
+
"\n"
|
|
105
|
+
"Modes:\n"
|
|
106
|
+
" (default) Alpha-composite overlay onto base image\n"
|
|
107
|
+
" paste-centered Center image on a new canvas\n"
|
|
108
|
+
"\n"
|
|
109
|
+
"Run 'agentbrush composite paste-centered --help' for mode-specific help."
|
|
110
|
+
)
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
if remaining[0] == "paste-centered":
|
|
114
|
+
return _run_paste_centered(remaining[1:])
|
|
115
|
+
else:
|
|
116
|
+
return _run_overlay(remaining)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _run_overlay(argv):
|
|
120
|
+
"""Run overlay composite mode."""
|
|
121
|
+
parser = _overlay_parser()
|
|
122
|
+
ov_args = parser.parse_args(argv)
|
|
123
|
+
|
|
124
|
+
resize = None
|
|
125
|
+
if ov_args.resize_overlay:
|
|
126
|
+
w, h = ov_args.resize_overlay.lower().split("x")
|
|
127
|
+
resize = (int(w), int(h))
|
|
128
|
+
|
|
129
|
+
if ov_args.position == "center":
|
|
130
|
+
from PIL import Image
|
|
131
|
+
base = Image.open(ov_args.base)
|
|
132
|
+
result = paste_centered(
|
|
133
|
+
base.width, base.height,
|
|
134
|
+
ov_args.overlay, ov_args.output,
|
|
135
|
+
resize_overlay=resize,
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
pos = tuple(int(x) for x in ov_args.position.split(","))
|
|
139
|
+
result = composite(
|
|
140
|
+
ov_args.base, ov_args.overlay, ov_args.output,
|
|
141
|
+
position=pos, resize_overlay=resize,
|
|
142
|
+
opacity=ov_args.opacity,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
print(result.summary())
|
|
146
|
+
return 0 if result.success else 1
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _run_paste_centered(argv):
|
|
150
|
+
"""Run paste-centered composite mode."""
|
|
151
|
+
parser = _paste_centered_parser()
|
|
152
|
+
pc_args = parser.parse_args(argv)
|
|
153
|
+
|
|
154
|
+
cw, ch = [int(v) for v in pc_args.canvas.lower().split("x")]
|
|
155
|
+
|
|
156
|
+
resize = None
|
|
157
|
+
if pc_args.resize_overlay:
|
|
158
|
+
w, h = pc_args.resize_overlay.lower().split("x")
|
|
159
|
+
resize = (int(w), int(h))
|
|
160
|
+
|
|
161
|
+
result = paste_centered(
|
|
162
|
+
cw, ch, pc_args.overlay, pc_args.output,
|
|
163
|
+
bg_color=_parse_color(pc_args.bg_color),
|
|
164
|
+
resize_overlay=resize,
|
|
165
|
+
fit=pc_args.fit,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
print(result.summary())
|
|
169
|
+
return 0 if result.success else 1
|