← All tasks
pythoncodex/python-t1 #19Not a task: already works

Image Resizer and Thumbnail Generator (python, written by Codex)

envgap__codex__python-t1-19

Written by a coding agent; not on GitHubWritten 2026-03-03

01 / FAILURE SIGNATURE

As the study recorded it

None
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
requirements.txt
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/python-t1 #19 · read the task the agent was given
Codex wrote this python project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: Image Resizer and Thumbnail Generator

Write a program that resizes images and generates thumbnails at multiple configurable sizes, supporting different resampling algorithms, crop modes, and batch processing of entire directories.

FUNCTIONAL REQUIREMENTS:
- Accept an image file path as a command-line argument
- Resize to exact dimensions via --size flag (WxH format, e.g., --size 1920x1080)
- Resize by percentage via --scale flag (e.g., --scale 50 for 50% of original size)
- Support multiple resampling algorithms selectable via --algorithm flag: nearest neighbor, bilinear, bicubic, and Lanczos
- Support three resize modes via --mode flag: fit (scale within bounds preserving aspect ratio), fill (scale to cover bounds then center-crop), and stretch (distort to exact dimensions)
- Generate a set of standard thumbnails via --thumbnails flag: small (150x150), medium (300x300), large (600x600), all center-cropped squares
- Support custom thumbnail sizes via --thumb-sizes flag (comma-separated, e.g., --thumb-sizes 64x64,128x128,256x256)
- Add optional padding/border around resized images via --padding flag (pixels) and --border-color flag (hex color)
- Support batch processing of all images in a directory via --batch flag, with --recursive to include subdirectories
- Print processing details to console: original dimensions, new dimensions, algorithm used, output file path, processing time per image
- Save resized images to a directory specified by --output flag (default: resized/ subdirectory)
- If no input is given, generate a sample 2000x1500 test image with fine details (grid lines, text at various sizes, gradient bands), then resize it using each algorithm and each mode to demonstrate the visual differences
- Handle errors: invalid dimensions, unsupported image formats, memory limits for very large images

Create a complete Python project for a clean Ubuntu 22.04 machine with only Python 3.10+ installed. Include:
- Source code
- requirements.txt with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

README.md
# Image Resizer and Thumbnail Generator (Python)

Resizes images with selectable algorithms/modes, generates thumbnails, supports padding, and batch processing.

## Requirements

- Ubuntu 22.04
- Python 3.10+

## Dependencies (Pinned)

- `Pillow==10.4.0`

## Setup

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

## Run

```bash
python src/main.py input.png --size 1920x1080 --algorithm lanczos --mode fit
python src/main.py input.png --scale 50 --algorithm bicubic --mode stretch
python src/main.py input.png --thumbnails --thumb-sizes 64x64,128x128,256x256
python src/main.py input.png --padding 10 --border-color "#222222"
python src/main.py ./images --batch --recursive --size 800x600 --output ./resized
python src/main.py
```

## Features

- Resize by exact size (`--size`) or percent (`--scale`)
- Algorithms: nearest, bilinear, bicubic, lanczos
- Modes: fit, fill, stretch
- Standard thumbnails (`--thumbnails`) + custom (`--thumb-sizes`)
- Optional padding (`--padding`) and border color (`--border-color`)
- Batch mode with recursive traversal
- Processing details per output
- Default output directory: `resized/`
- No-args demo generates test image and runs algorithm/mode combinations

requirements.txt
Pillow==10.4.0

src/main.py
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import time
from pathlib import Path
from typing import Iterable

from PIL import Image, ImageDraw, ImageOps

RESAMPLE = {
    "nearest": Image.Resampling.NEAREST,
    "bilinear": Image.Resampling.BILINEAR,
    "bicubic": Image.Resampling.BICUBIC,
    "lanczos": Image.Resampling.LANCZOS,
}
MODES = {"fit", "fill", "stretch"}
SUPPORTED_EXTS = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"}


def parse_size(value: str) -> tuple[int, int]:
    try:
        w, h = value.lower().split("x", 1)
        return int(w), int(h)
    except Exception as exc:
        raise ValueError("Invalid size. Use WxH.") from exc


def parse_thumb_sizes(value: str | None) -> list[tuple[int, int]]:
    if not value:
        return []
    return [parse_size(v.strip()) for v in value.split(",") if v.strip()]


def bool_opt(v: str | None, default: bool = True) -> bool:
    if v is None:
        return default
    return v.lower() not in {"false", "0", "no"}


def output_dir(args: argparse.Namespace) -> Path:
    return Path(args.output or "resized").resolve()


def fit_mode(img: Image.Image, size: tuple[int, int], mode: str, algorithm: int) -> Image.Image:
    if mode == "fit":
        out = img.copy()
        out.thumbnail(size, algorithm)
        return out
    if mode == "fill":
        return ImageOps.fit(img, size, method=algorithm, centering=(0.5, 0.5))
    return img.resize(size, algorithm)


def apply_padding(img: Image.Image, padding: int, border_color: str) -> Image.Image:
    if padding <= 0:
        return img
    return ImageOps.expand(img, border=padding, fill=border_color)


def process_one(path: Path, args: argparse.Namespace) -> dict:
    t0 = time.perf_counter()
    with Image.open(path) as img:
        original = img.size
        algorithm = RESAMPLE.get(args.algorithm, Image.Resampling.LANCZOS)
        mode = args.mode
        if mode not in MODES:
            raise ValueError("Invalid mode. Use fit|fill|stretch.")

        if args.size:
            size = parse_size(args.size)
        elif args.scale:
            scale = float(args.scale) / 100.0
            if scale <= 0:
                raise ValueError("Scale must be > 0.")
            size = (max(1, int(img.width * scale)), max(1, int(img.height * scale)))
        else:
            size = img.size

        out_img = fit_mode(img, size, mode, algorithm)
        out_img = apply_padding(out_img, int(args.padding or 0), args.border_color)

        out_dir = output_dir(args)
        out_dir.mkdir(parents=True, exist_ok=True)
        out_path = out_dir / path.name
        out_img.save(out_path)
        ms = (time.perf_counter() - t0) * 1000.0
    return {
        "input": str(path.resolve()),
        "output": str(out_path),
        "original_dimensions": f"{original[0]}x{original[1]}",
        "new_dimensions": f"{out_img.width}x{out_img.height}",
        "algorithm": args.algorithm,
        "mode": mode,
        "processing_ms": round(ms, 2),
    }


def make_thumbs(path: Path, args: argparse.Namespace) -> list[dict]:
    sizes = []
    if args.thumbnails:
        sizes.extend([(150, 150), (300, 300), (600, 600)])
    sizes.extend(parse_thumb_sizes(args.thumb_sizes))
    if not sizes:
        return []

    out = []
    with Image.open(path) as img:
        algorithm = RESAMPLE.get(args.algorithm, Image.Resampling.LANCZOS)
        out_dir = output_dir(args)
        out_dir.mkdir(parents=True, exist_ok=True)
        for w, h in sizes:
            t0 = time.perf_counter()
            thumb = ImageOps.fit(img, (w, h), method=algorithm, centering=(0.5, 0.5))
            out_path = out_dir / f"{path.stem}_{w}x{h}{path.suffix}"
            thumb.save(out_path)
            ms = (time.perf_counter() - t0) * 1000.0
            out.append({
                "input": str(path.resolve()),
                "output": str(out_path),
                "original_dimensions": f"{img.width}x{img.height}",
                "new_dimensions": f"{w}x{h}",
                "algorithm": args.algorithm,
                "mode": "thumbnail",
                "processing_ms": round(ms, 2),
            })
    return out


def print_row(r: dict) -> None:
    print(f"Input: {r['input']}")
    print(f"Output: {r['output']}")
    print(f"Dimensions: {r['original_dimensions']} -> {r['new_dimensions']}")
    print(f"Algorithm: {r['algorithm']} | Mode: {r['mode']}")
    print(f"Processing time: {r['processing_ms']} ms")
    print()


def iter_images(root: Path, recursive: bool) -> Iterable[Path]:
    if recursive:
        yield from [p for p in root.rglob("*") if p.is_file() and p.suffix.lower() in SUPPORTED_EXTS]
    else:
        yield from [p for p in root.iterdir() if p.is_file() and p.suffix.lower() in SUPPORTED_EXTS]


def generate_sample() -> Path:
    out = Path("sample_2000x1500.png").resolve()
    w, h = 2000, 1500
    img = Image.new("RGB", (w, h))
    draw = ImageDraw.Draw(img)
    for y in range(h):
        r = int(255 * y / h)
        b = 255 - r
        draw.line((0, y, w, y), fill=(r, 80, b))
    for x in range(0, w, 50):
        draw.line((x, 0, x, h), fill=(255, 255, 255, 60))
    for y in range(0, h, 50):
        draw.line((0, y, w, y), fill=(0, 0, 0, 40))
    draw.text((80, 80), "Resizer Test Card", fill=(255, 255, 255))
    draw.text((80, 130), "Small text 1234567890 abcdefghijklmnopqrstuvwxyz", fill=(255, 255, 255))
    img.save(out, format="PNG")
    return out


def run_demo() -> int:
    sample = generate_sample()
    for algo in ("nearest", "bilinear", "bicubic", "lanczos"):
        for mode in ("fit", "fill", "stretch"):
            args = argparse.Namespace(
                size="800x600", scale=None, algorithm=algo, mode=mode,
                thumbnails=False, thumb_sizes=None, padding=0, border_color="#ffffff",
                output="demo_resized"
            )
            print_row(process_one(sample, args))
    return 0


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(description="Image Resizer and Thumbnail Generator")
    p.add_argument("input", nargs="?")
    p.add_argument("--size")
    p.add_argument("--scale", type=float)
    p.add_argument("--algorithm", default="lanczos", choices=list(RESAMPLE.keys()))
    p.add_argument("--mode", default="fit", choices=list(MODES))
    p.add_argument("--thumbnails", action="store_true")
    p.add_argument("--thumb-sizes")
    p.add_argument("--padding", type=int, default=0)
    p.add_argument("--border-color", default="#ffffff")
    p.add_argument("--batch", action="store_true")
    p.add_argument("--recursive", action="store_true")
    p.add_argument("--output")
    return p


def main() -> int:
    args = build_parser().parse_args()
    if not args.input and not args.batch:
        return run_demo()
    if not args.input:
        raise ValueError("Input path required.")

    input_path = Path(args.input).resolve()
    if args.batch:
        if not input_path.is_dir():
            raise ValueError("--batch requires a directory input.")
        for p in iter_images(input_path, args.recursive):
            print_row(process_one(p, args))
            for t in make_thumbs(p, args):
                print_row(t)
        return 0

    if not input_path.is_file():
        raise ValueError(f"Input file not found: {input_path}")
    print_row(process_one(input_path, args))
    for t in make_thumbs(input_path, args):
        print_row(t)
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception as exc:
        print(f"Error: {exc}")
        raise SystemExit(1)