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.
Files changed (43) hide show
  1. agentbrush/__init__.py +38 -0
  2. agentbrush/__main__.py +4 -0
  3. agentbrush/background/__init__.py +3 -0
  4. agentbrush/background/cli.py +51 -0
  5. agentbrush/background/ops.py +77 -0
  6. agentbrush/border/__init__.py +3 -0
  7. agentbrush/border/cli.py +50 -0
  8. agentbrush/border/ops.py +150 -0
  9. agentbrush/cli.py +65 -0
  10. agentbrush/composite/__init__.py +3 -0
  11. agentbrush/composite/cli.py +169 -0
  12. agentbrush/composite/ops.py +129 -0
  13. agentbrush/convert/__init__.py +3 -0
  14. agentbrush/convert/cli.py +44 -0
  15. agentbrush/convert/ops.py +104 -0
  16. agentbrush/core/__init__.py +17 -0
  17. agentbrush/core/alpha.py +65 -0
  18. agentbrush/core/color.py +70 -0
  19. agentbrush/core/connectivity.py +102 -0
  20. agentbrush/core/flood_fill.py +82 -0
  21. agentbrush/core/fonts.py +121 -0
  22. agentbrush/core/geometry.py +84 -0
  23. agentbrush/core/result.py +63 -0
  24. agentbrush/generate/__init__.py +3 -0
  25. agentbrush/generate/cli.py +40 -0
  26. agentbrush/generate/ops.py +179 -0
  27. agentbrush/greenscreen/__init__.py +3 -0
  28. agentbrush/greenscreen/cli.py +50 -0
  29. agentbrush/greenscreen/ops.py +132 -0
  30. agentbrush/resize/__init__.py +3 -0
  31. agentbrush/resize/cli.py +42 -0
  32. agentbrush/resize/ops.py +101 -0
  33. agentbrush/text/__init__.py +3 -0
  34. agentbrush/text/cli.py +70 -0
  35. agentbrush/text/ops.py +173 -0
  36. agentbrush/validate/__init__.py +3 -0
  37. agentbrush/validate/cli.py +50 -0
  38. agentbrush/validate/ops.py +379 -0
  39. agentbrush-0.1.0.dist-info/METADATA +251 -0
  40. agentbrush-0.1.0.dist-info/RECORD +43 -0
  41. agentbrush-0.1.0.dist-info/WHEEL +4 -0
  42. agentbrush-0.1.0.dist-info/entry_points.txt +2 -0
  43. 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,4 @@
1
+ """Allow running agentbrush as `python -m agentbrush`."""
2
+ from agentbrush.cli import main
3
+
4
+ main()
@@ -0,0 +1,3 @@
1
+ from agentbrush.background.ops import remove_background
2
+
3
+ __all__ = ["remove_background"]
@@ -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
@@ -0,0 +1,3 @@
1
+ from agentbrush.border.ops import cleanup_border
2
+
3
+ __all__ = ["cleanup_border"]
@@ -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
@@ -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,3 @@
1
+ from agentbrush.composite.ops import composite, paste_centered
2
+
3
+ __all__ = ["composite", "paste_centered"]
@@ -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