Skip to content

Commit ed0fb1b

Browse files
feat: add before/after comparison image functionality
- Implement create_comparison_image function for side-by-side image comparison - Update CLI to include comparison command - Enhance update.md with instructions for creating and opening comparison images
1 parent 6b14a75 commit ed0fb1b

File tree

2 files changed

+163
-2
lines changed

2 files changed

+163
-2
lines changed

agentic/commands/update.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,35 @@ Agents report back via `SendMessage` (auto-delivered to you). Agents may report
9292
- Agent's self-assessment score
9393
- Any spec changes the agent made
9494

95-
**After the summary**, run `eog` to open all previews at once so the user can browse them:
95+
**After the summary**, create before/after comparison images and open them:
96+
97+
a. **Download current GCS images** for each library:
98+
```bash
99+
curl -sL "https://storage.googleapis.com/pyplots-images/plots/{spec_id}/{library}/plot.png" \
100+
-o "plots/{spec_id}/implementations/.update-preview/{library}/before.png"
101+
```
102+
If curl fails (non-zero exit or empty file), the library has no previous version — pass `none` as before_path.
103+
104+
b. **Create comparison images** for each library:
105+
```bash
106+
uv run python -m core.images compare \
107+
plots/{spec_id}/implementations/.update-preview/{library}/before.png \
108+
plots/{spec_id}/implementations/.update-preview/{library}/plot.png \
109+
plots/{spec_id}/implementations/.update-preview/{library}/comparison.png \
110+
{spec_id} {library}
111+
```
112+
If the before image doesn't exist, use `none` instead of the before path:
113+
```bash
114+
uv run python -m core.images compare \
115+
none \
116+
plots/{spec_id}/implementations/.update-preview/{library}/plot.png \
117+
plots/{spec_id}/implementations/.update-preview/{library}/comparison.png \
118+
{spec_id} {library}
119+
```
120+
121+
c. **Open comparisons in eog** (instead of raw plot.png files):
96122
```bash
97-
eog plots/{spec_id}/implementations/.update-preview/*/plot.png &
123+
eog plots/{spec_id}/implementations/.update-preview/*/comparison.png &
98124
```
99125
Run this via `Bash` tool with `run_in_background: true` so it doesn't block the conversation.
100126

core/images.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
- PNG optimization with pngquant
66
- Branded og:image generation for social media
77
- Collage generation for spec overview pages
8+
- Before/after comparison images for update reviews
89
910
Usage as CLI:
1011
python -m core.images thumbnail input.png output.png 400
1112
python -m core.images process input.png output.png thumb.png
1213
python -m core.images brand input.png output.png "scatter-basic" "matplotlib"
1314
python -m core.images collage output.png img1.png img2.png img3.png img4.png
15+
python -m core.images compare before.png after.png output.png [spec_id] [library]
1416
"""
1517

1618
import logging
@@ -150,6 +152,125 @@ def process_plot_image(
150152
return result
151153

152154

155+
# =============================================================================
156+
# Before/After Comparison Images
157+
# =============================================================================
158+
159+
# Comparison image dimensions
160+
COMPARE_WIDTH = 2400
161+
COMPARE_HEIGHT = 800
162+
COMPARE_MARGIN = 30
163+
COMPARE_GAP = 30
164+
COMPARE_HEADER_HEIGHT = 50
165+
COMPARE_LABEL_HEIGHT = 40
166+
167+
168+
def create_comparison_image(
169+
before_path: str | Path | None,
170+
after_path: str | Path,
171+
output_path: str | Path,
172+
spec_id: str = "",
173+
library: str = "",
174+
) -> None:
175+
"""Create a side-by-side before/after comparison image.
176+
177+
Layout: [margin] [BEFORE image] [gap] [AFTER image] [margin]
178+
with a header bar and labels above each image.
179+
180+
Args:
181+
before_path: Path to the "before" image, or None for new implementations.
182+
after_path: Path to the "after" image.
183+
output_path: Path where the comparison image will be saved.
184+
spec_id: Spec ID for the header label.
185+
library: Library name for the header label.
186+
"""
187+
# Canvas
188+
canvas = Image.new("RGB", (COMPARE_WIDTH, COMPARE_HEIGHT), PYPLOTS_BG)
189+
draw = ImageDraw.Draw(canvas)
190+
191+
# Available space for each image panel
192+
panel_width = (COMPARE_WIDTH - 2 * COMPARE_MARGIN - COMPARE_GAP) // 2
193+
panel_top = COMPARE_HEADER_HEIGHT + COMPARE_LABEL_HEIGHT
194+
panel_height = COMPARE_HEIGHT - panel_top - COMPARE_MARGIN
195+
196+
# Header bar
197+
header_text = f"{library} · {spec_id}" if library and spec_id else library or spec_id
198+
if header_text:
199+
header_font = _get_font(28, weight=700)
200+
bbox = draw.textbbox((0, 0), header_text, font=header_font)
201+
text_w = bbox[2] - bbox[0]
202+
text_h = bbox[3] - bbox[1]
203+
draw.text(
204+
((COMPARE_WIDTH - text_w) // 2, (COMPARE_HEADER_HEIGHT - text_h) // 2),
205+
header_text,
206+
fill=PYPLOTS_DARK,
207+
font=header_font,
208+
)
209+
210+
# Labels
211+
label_font = _get_font(22, weight=400)
212+
before_label = "BEFORE (current)"
213+
after_label = "AFTER (updated)"
214+
215+
before_bbox = draw.textbbox((0, 0), before_label, font=label_font)
216+
before_label_w = before_bbox[2] - before_bbox[0]
217+
draw.text(
218+
(COMPARE_MARGIN + (panel_width - before_label_w) // 2, COMPARE_HEADER_HEIGHT + 8),
219+
before_label,
220+
fill="#6b7280",
221+
font=label_font,
222+
)
223+
224+
after_bbox = draw.textbbox((0, 0), after_label, font=label_font)
225+
after_label_w = after_bbox[2] - after_bbox[0]
226+
after_panel_x = COMPARE_MARGIN + panel_width + COMPARE_GAP
227+
draw.text(
228+
(after_panel_x + (panel_width - after_label_w) // 2, COMPARE_HEADER_HEIGHT + 8),
229+
after_label,
230+
fill="#6b7280",
231+
font=label_font,
232+
)
233+
234+
# Load and place BEFORE image (or placeholder)
235+
if before_path and Path(before_path).exists():
236+
before_img = Image.open(before_path)
237+
if before_img.mode in ("RGBA", "P"):
238+
before_img = before_img.convert("RGB")
239+
before_img = _fit_image(before_img, panel_width, panel_height)
240+
bx = COMPARE_MARGIN + (panel_width - before_img.width) // 2
241+
by = panel_top + (panel_height - before_img.height) // 2
242+
canvas.paste(before_img, (bx, by))
243+
else:
244+
# Gray placeholder
245+
placeholder_font = _get_font(20, weight=400)
246+
placeholder_text = "No previous version"
247+
pb = draw.textbbox((0, 0), placeholder_text, font=placeholder_font)
248+
pw = pb[2] - pb[0]
249+
ph = pb[3] - pb[1]
250+
px = COMPARE_MARGIN + (panel_width - pw) // 2
251+
py = panel_top + (panel_height - ph) // 2
252+
draw.text((px, py), placeholder_text, fill="#9ca3af", font=placeholder_font)
253+
254+
# Load and place AFTER image
255+
after_img = Image.open(after_path)
256+
if after_img.mode in ("RGBA", "P"):
257+
after_img = after_img.convert("RGB")
258+
after_img = _fit_image(after_img, panel_width, panel_height)
259+
ax = after_panel_x + (panel_width - after_img.width) // 2
260+
ay = panel_top + (panel_height - after_img.height) // 2
261+
canvas.paste(after_img, (ax, ay))
262+
263+
canvas.save(output_path, "PNG", optimize=True)
264+
265+
266+
def _fit_image(img: Image.Image, max_width: int, max_height: int) -> Image.Image:
267+
"""Scale an image to fit within max_width x max_height, preserving aspect ratio."""
268+
scale = min(max_width / img.width, max_height / img.height, 1.0)
269+
if scale < 1.0:
270+
return img.resize((int(img.width * scale), int(img.height * scale)), Image.Resampling.LANCZOS)
271+
return img
272+
273+
153274
# =============================================================================
154275
# OG Image Branding Functions
155276
# =============================================================================
@@ -597,12 +718,14 @@ def print_usage() -> None:
597718
print(" python -m core.images process <input> <output> [thumb]")
598719
print(" python -m core.images brand <input> <output> [spec_id] [library]")
599720
print(" python -m core.images collage <output> <img1> [img2] [img3] [img4]")
721+
print(" python -m core.images compare <before> <after> <output> [spec_id] [library]")
600722
print("")
601723
print("Examples:")
602724
print(" python -m core.images thumbnail plot.png thumb.png 400")
603725
print(" python -m core.images process plot.png out.png thumb.png")
604726
print(" python -m core.images brand plot.png og.png scatter-basic matplotlib")
605727
print(" python -m core.images collage og.png img1.png img2.png img3.png img4.png")
728+
print(" python -m core.images compare before.png after.png comparison.png area-basic matplotlib")
606729
sys.exit(1)
607730

608731
if len(sys.argv) < 2:
@@ -643,6 +766,18 @@ def print_usage() -> None:
643766
create_og_collage(input_files, output_file)
644767
print(f"Collage: {output_file} ({OG_WIDTH}x{OG_HEIGHT}px, {len(input_files)} images)")
645768

769+
elif command == "compare":
770+
if len(sys.argv) < 5:
771+
print_usage()
772+
before_file = sys.argv[2]
773+
after_file = sys.argv[3]
774+
output_file = sys.argv[4]
775+
spec_id = sys.argv[5] if len(sys.argv) > 5 else ""
776+
library = sys.argv[6] if len(sys.argv) > 6 else ""
777+
before = None if before_file == "none" else before_file
778+
create_comparison_image(before, after_file, output_file, spec_id, library)
779+
print(f"Comparison: {output_file} ({COMPARE_WIDTH}x{COMPARE_HEIGHT}px)")
780+
646781
else:
647782
print(f"Unknown command: {command}")
648783
print_usage()

0 commit comments

Comments
 (0)