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

Image Format Converter (python, written by Codex)

envgap__codex__python-t1-18

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 #18 · 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 Format Converter

Write a program that converts images between common formats (PNG, JPEG, BMP, TIFF, WebP) with configurable quality settings, preserving metadata where possible.

FUNCTIONAL REQUIREMENTS:
- Accept an image file path and target format as command-line arguments
- Support conversions between: PNG, JPEG, BMP, TIFF, and WebP formats
- Support a --quality flag for lossy formats (JPEG, WebP) with a value from 1-100 (default: 85)
- Support a --resize flag to resize during conversion using WxH syntax (e.g., --resize 800x600), preserving aspect ratio by default
- Support --maintain-aspect flag (default true) to control whether resizing preserves the original aspect ratio by fitting within the specified dimensions
- Preserve EXIF metadata during conversion where the target format supports it, with a --strip-metadata flag to optionally remove it
- Support alpha channel handling: when converting from formats with transparency (PNG, WebP) to formats without (JPEG, BMP), use a configurable background color via --background flag (default: white)
- Support batch conversion of all images in a directory via --batch flag, optionally filtering by input format with --filter flag
- Print conversion details to console: input format, output format, original dimensions, new dimensions, file size before and after, compression ratio
- Save the converted image to a path specified by --output flag (default: same name with new extension)
- If no input is given, generate a sample 800x600 PNG image with gradients, text, and transparency, then convert it to all other supported formats and display a comparison table
- Handle errors: unsupported formats, corrupted images, insufficient disk space, invalid resize dimensions

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 Format Converter (Python)

Converts images among PNG/JPEG/BMP/TIFF/WebP with quality, resize, metadata options, alpha handling, batch mode, and demo generation.

## 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 webp --quality 90
python src/main.py input.png jpeg --background "#ffffff" --resize 800x600 --maintain-aspect true
python src/main.py input.webp png --strip-metadata
python src/main.py ./images jpeg --batch --filter png --quality 80
python src/main.py
```

## Features

- Formats: PNG, JPEG, BMP, TIFF, WebP
- `--quality` for lossy output (JPEG/WebP)
- `--resize WxH` + `--maintain-aspect` (default true)
- Metadata keep/strip (`--strip-metadata`)
- Alpha flattening with `--background` for non-alpha targets
- Batch conversion (`--batch`) + optional `--filter`
- Conversion details printed per file
- `--output` destination override
- No-args demo generates sample PNG and converts to all other formats

requirements.txt
Pillow==10.4.0

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

import argparse
import os
from pathlib import Path
from typing import Any

from PIL import Image, ImageDraw, ImageFont

SUPPORTED = {"png", "jpeg", "jpg", "bmp", "tiff", "webp"}


def normalize_format(fmt: str) -> str:
    f = fmt.lower()
    if f == "jpg":
        return "jpeg"
    if f not in SUPPORTED:
        raise ValueError(f"Unsupported format: {fmt}")
    return f


def parse_resize(value: str | None) -> tuple[int, int] | None:
    if not value:
        return None
    try:
        w, h = value.lower().split("x", 1)
        return int(w), int(h)
    except Exception as exc:
        raise ValueError("Invalid --resize. Use WxH, e.g., 800x600.") from exc


def output_path(input_path: Path, target: str, explicit: str | None) -> Path:
    if explicit:
        return Path(explicit).resolve()
    return input_path.with_suffix(f".{target}")


def conversion_details(
    in_path: Path,
    out_path: Path,
    in_fmt: str,
    out_fmt: str,
    orig_size: tuple[int, int],
    new_size: tuple[int, int],
) -> dict[str, Any]:
    before = in_path.stat().st_size
    after = out_path.stat().st_size
    ratio = (after / before) if before else 1.0
    return {
        "input": str(in_path),
        "output": str(out_path),
        "input_format": in_fmt,
        "output_format": out_fmt,
        "original_dimensions": f"{orig_size[0]}x{orig_size[1]}",
        "new_dimensions": f"{new_size[0]}x{new_size[1]}",
        "size_before": before,
        "size_after": after,
        "compression_ratio": round(ratio, 4),
    }


def print_details(d: dict[str, Any]) -> None:
    print(f"Input: {d['input']}")
    print(f"Output: {d['output']}")
    print(f"Format: {d['input_format']} -> {d['output_format']}")
    print(f"Dimensions: {d['original_dimensions']} -> {d['new_dimensions']}")
    print(f"Size: {d['size_before']} -> {d['size_after']} bytes")
    print(f"Compression ratio: {d['compression_ratio']}")
    print()


def convert_one(input_path: Path, target: str, args: argparse.Namespace) -> dict[str, Any]:
    target = normalize_format(target)
    quality = int(args.quality)
    if quality < 1 or quality > 100:
        raise ValueError("Quality must be in range 1..100.")
    resize = parse_resize(args.resize)
    maintain_aspect = str(args.maintain_aspect).lower() not in {"false", "0", "no"}

    input_abs = input_path.resolve()
    output_abs = output_path(input_abs, target, args.output)

    with Image.open(input_abs) as img:
        in_fmt = (img.format or input_abs.suffix.lstrip(".")).lower()
        orig_size = img.size
        exif = img.info.get("exif")
        image = img.copy()

    if resize:
        if maintain_aspect:
            image.thumbnail(resize, Image.Resampling.LANCZOS)
        else:
            image = image.resize(resize, Image.Resampling.LANCZOS)

    if target in {"jpeg", "bmp"} and image.mode in {"RGBA", "LA"}:
        bg = Image.new("RGBA", image.size, args.background)
        bg.alpha_composite(image.convert("RGBA"))
        image = bg.convert("RGB")
    elif target in {"jpeg", "bmp"} and image.mode not in {"RGB", "L"}:
        image = image.convert("RGB")

    save_kwargs: dict[str, Any] = {}
    if target in {"jpeg", "webp"}:
        save_kwargs["quality"] = quality
    if not args.strip_metadata and exif and target in {"jpeg", "tiff", "webp"}:
        save_kwargs["exif"] = exif

    output_abs.parent.mkdir(parents=True, exist_ok=True)
    image.save(output_abs, format=target.upper(), **save_kwargs)
    new_size = image.size
    return conversion_details(input_abs, output_abs, in_fmt, target, orig_size, new_size)


def batch_convert(directory: Path, target: str, args: argparse.Namespace) -> list[dict[str, Any]]:
    filter_fmt = normalize_format(args.filter) if args.filter else None
    files = [p for p in directory.iterdir() if p.is_file() and p.suffix.lstrip(".").lower() in SUPPORTED]
    if filter_fmt:
        files = [p for p in files if normalize_format(p.suffix.lstrip(".").lower()) == filter_fmt]
    results = []
    for p in files:
        per_file_args = argparse.Namespace(**vars(args))
        per_file_args.output = None
        results.append(convert_one(p, target, per_file_args))
    return results


def generate_sample_png() -> Path:
    out = Path("sample_800x600.png").resolve()
    w, h = 800, 600
    img = Image.new("RGBA", (w, h), (0, 0, 0, 0))
    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, 120, b, 255))
    draw.ellipse((80, 80, 380, 380), fill=(255, 255, 255, 100))
    draw.rectangle((430, 140, 700, 380), fill=(0, 0, 0, 100))
    font = ImageFont.load_default()
    draw.text((220, 540), "Image Converter Demo", fill=(255, 255, 255, 255), font=font)
    img.save(out, format="PNG")
    return out


def run_demo() -> int:
    sample = generate_sample_png()
    targets = ["jpeg", "bmp", "tiff", "webp"]
    rows = []
    for t in targets:
        args = argparse.Namespace(
            quality=85, resize=None, maintain_aspect="true", strip_metadata=False, background="white", output=None
        )
        rows.append(convert_one(sample, t, args))
    print("format,orig_dim,new_dim,size_before,size_after,ratio")
    for r in rows:
        print(f"{r['output_format']},{r['original_dimensions']},{r['new_dimensions']},{r['size_before']},{r['size_after']},{r['compression_ratio']}")
    return 0


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(description="Image Format Converter")
    p.add_argument("input", nargs="?")
    p.add_argument("target_format", nargs="?")
    p.add_argument("--quality", type=int, default=85)
    p.add_argument("--resize")
    p.add_argument("--maintain-aspect", default="true")
    p.add_argument("--strip-metadata", action="store_true")
    p.add_argument("--background", default="white")
    p.add_argument("--batch", action="store_true")
    p.add_argument("--filter")
    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 args.batch:
        if not args.input or not args.target_format:
            raise ValueError("Batch mode usage: --batch <directory> <target-format>")
        results = batch_convert(Path(args.input).resolve(), args.target_format, args)
        for r in results:
            print_details(r)
        return 0

    if not args.input or not args.target_format:
        raise ValueError("Usage: <input-image> <target-format> [flags]")
    details = convert_one(Path(args.input).resolve(), args.target_format, args)
    print_details(details)
    return 0


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