Image Format Converter (python, written by Claude Code)
envgap__claude-code__python-t2-18
Written by a coding agent; not on GitHubWritten 2026-02-27
01 / FAILURE SIGNATURE
Captured in a clean container
OSError: cannot find library; tried paths: []
02 / ENVIRONMENT RECIPE
- Base commit
c7fda7bb8d04879f527501fe69a715469be1e6d8- Manifest
requirements.txt- Reproduce
true- Run under trace
rc=0; out=$(timeout 60 python3 converter.py < /dev/null 2>&1 | { head -c 1000000; cat > /dev/null; }; exit ${PIPESTATUS[0]}) || rc=$?; printf '%s\n' "$out"; env_error='(ModuleNotFoundError|ImportError|No module named|cannot open shared object file|DLL load failed|shared library|cannot load library|Library not loaded|Cannot find module|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ERR_REQUIRE_ESM|compiled against a different Node|Could not find or load main class|ClassNotFoundException|NoClassDefFoundError|UnsupportedClassVersionError|UnsatisfiedLinkError|NoSuchMethodError|NoSuchFieldError|AbstractMethodError|IncompatibleClassChangeError|IllegalAccessError|ServiceConfigurationError|error while loading shared libraries|symbol lookup error|version `[^'"'"']*'"'"' not found|command not found)'; asked='(^| )[[:blank:]]*usage:|the following arguments are required|missing (required )?(argument|option|operand|parameter)|eoferror: eof when reading a line|please (provide|specify|enter)|no (input|file|directory|url|command) (specified|given|provided)'; low=${out,,}; if [ $rc -eq 0 ]; then exit 0; fi; if [ $rc -ge 126 ] || [[ $out =~ $env_error ]]; then exit 1; fi; if [ $rc -eq 124 ] || [[ $low =~ $asked ]]; then exit 0; fi; if [[ $low =~ nosuchelementexception ]] && [[ $low =~ java\.util\.scanner ]]; then exit 0; fi; exit 1
Reference environment fix used for admission
--- /dev/null +++ b/setup.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# System packages this project needs on a clean Ubuntu machine. +set -e +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y -qq --no-install-recommends libmagickwand-dev
03 / TASK AND FAILURE
claude-code/python-t2 #18 · read the task the agent was given
Claude Code wrote this python project from the task below. It does not run 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 checked by running the task · needs human review
underspecificationLabel rules and the text that matched
[
{
"category": "underspecification",
"rule": "signature.missing_system_requirement",
"source": "failure_signature",
"excerpt": "OSError: cannot find library; tried paths: []"
},
{
"category": "underspecification",
"rule": "diff.adds_external_environment_requirement",
"source": "manifest_diff:setup.sh",
"excerpt": "export DEBIAN_FRONTEND=noninteractive"
},
{
"category": "underspecification",
"rule": "diff.adds_external_environment_requirement",
"source": "manifest_diff:setup.sh",
"excerpt": "apt-get install -y -qq --no-install-recommends libmagickwand-dev"
}
]Written by Claude Code (study run M1T2P18L1). It failed as written and was repaired by changing only its environment.
Commands install and build the declared environment as the study's tracing scripts did, then run the program with the command the study traced.
Preparation dates registries as the oracle does: PyPI index files filtered by upload_time <= the registry date, and yanks applied only when dated at or before it; alternate indexes, direct URLs, apt and arbitrary setup downloads are not network-enforced.
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
converter.py
"""
Image Format Converter using Wand (ImageMagick) and Pillow.
Converts between PNG, JPEG, BMP, TIFF, and WebP formats with quality settings,
resize capabilities, and metadata preservation.
Uses Wand (ImageMagick binding) as the primary engine with Pillow as a fallback
for certain operations and metadata handling.
"""
import argparse
import os
import sys
from pathlib import Path
from typing import Optional, Tuple, Dict
from wand.image import Image as WandImage
from wand.color import Color
from PIL import Image as PILImage
from PIL.ExifTags import TAGS
SUPPORTED_FORMATS = {
".png": "png",
".jpg": "jpeg",
".jpeg": "jpeg",
".bmp": "bmp",
".tiff": "tiff",
".tif": "tiff",
".webp": "webp",
}
def get_format_from_extension(filepath: str) -> str:
"""Determine the Wand format string from file extension."""
ext = Path(filepath).suffix.lower()
if ext not in SUPPORTED_FORMATS:
raise ValueError(
f"Unsupported format: '{ext}'. Supported: {list(SUPPORTED_FORMATS.keys())}"
)
return SUPPORTED_FORMATS[ext]
def extract_metadata_pillow(filepath: str) -> Dict[str, str]:
"""Extract EXIF metadata from an image using Pillow."""
metadata = {}
try:
pil_image = PILImage.open(filepath)
exif_data = pil_image.getexif()
if exif_data:
for tag_id, value in exif_data.items():
tag_name = TAGS.get(tag_id, str(tag_id))
try:
metadata[tag_name] = str(value)
except Exception:
metadata[tag_name] = repr(value)
except Exception:
pass
return metadata
def extract_metadata_wand(image: WandImage) -> Dict[str, str]:
"""Extract metadata from a Wand image using profiles and properties."""
metadata = {}
# Extract image properties
for key in image.metadata:
try:
metadata[key] = image.metadata[key]
except Exception:
pass
return metadata
def print_metadata(metadata: dict) -> None:
"""Print image metadata in a readable format."""
if not metadata:
print(" No metadata found.")
return
for key, value in sorted(metadata.items()):
# Truncate long values
val_str = str(value)
if len(val_str) > 100:
val_str = val_str[:100] + "..."
print(f" {key}: {val_str}")
def convert_image(
input_path: str,
output_path: str,
quality: int = 85,
resize: Optional[Tuple[int, int]] = None,
preserve_metadata: bool = True,
verbose: bool = False,
) -> str:
"""
Convert an image from one format to another using Wand (ImageMagick).
Args:
input_path: Path to the source image file.
output_path: Path for the output image file.
quality: Quality setting for lossy formats (1-100). Default is 85.
resize: Optional (width, height) tuple for resizing.
preserve_metadata: Whether to preserve EXIF/IPTC metadata. Default is True.
verbose: Whether to print detailed information. Default is False.
Returns:
The absolute path to the converted output file.
"""
if not os.path.isfile(input_path):
raise FileNotFoundError(f"Input file not found: {input_path}")
output_format = get_format_from_extension(output_path)
if verbose:
print(f"Opening: {input_path}")
pil_metadata = extract_metadata_pillow(input_path)
with WandImage(filename=input_path) as img:
if verbose:
print(f"Original size: {img.width}x{img.height}")
print(f"Original format: {img.format}")
print(f"Original depth: {img.depth}")
print(f"Original colorspace: {img.colorspace}")
print("Metadata (Wand):")
wand_meta = extract_metadata_wand(img)
print_metadata(wand_meta)
print("Metadata (EXIF via Pillow):")
print_metadata(pil_metadata)
# Resize if requested
if resize:
width, height = resize
if width <= 0 or height <= 0:
raise ValueError("Resize dimensions must be positive integers.")
img.resize(width, height, filter="lanczos2")
if verbose:
print(f"Resized to: {width}x{height}")
# Handle transparency for JPEG output
if output_format == "jpeg" and img.alpha_channel:
img.background_color = Color("white")
img.alpha_channel = "remove"
if verbose:
print("Removed alpha channel for JPEG compatibility.")
# Set quality
quality_clamped = max(1, min(100, quality))
img.compression_quality = quality_clamped
# Format-specific settings
if output_format == "png":
# PNG compression: higher quality = less compression
pass
elif output_format == "tiff":
img.compression = "lzw"
elif output_format == "webp":
img.compression_quality = quality_clamped
# Strip metadata if not preserving
if not preserve_metadata:
img.strip()
if verbose:
print("Stripped metadata from output.")
# Ensure output directory exists
output_dir = os.path.dirname(output_path)
if output_dir:
os.makedirs(output_dir, exist_ok=True)
# Set output format
img.format = output_format
# Save
img.save(filename=output_path)
output_abs = os.path.abspath(output_path)
output_size = os.path.getsize(output_abs)
if verbose:
print(f"Saved: {output_abs}")
print(f"Output format: {output_format}")
print(f"Output file size: {output_size:,} bytes")
return output_abs
def batch_convert(
input_dir: str,
output_dir: str,
target_format: str,
quality: int = 85,
resize: Optional[Tuple[int, int]] = None,
preserve_metadata: bool = True,
verbose: bool = False,
) -> list:
"""
Convert all supported images in a directory to a target format.
Args:
input_dir: Path to the directory containing source images.
output_dir: Path to the output directory.
target_format: Target file extension (e.g., '.png', '.jpg').
quality: Quality setting for lossy formats.
resize: Optional (width, height) tuple for resizing.
preserve_metadata: Whether to preserve metadata.
verbose: Whether to print detailed information.
Returns:
A list of output file paths.
"""
if not os.path.isdir(input_dir):
raise FileNotFoundError(f"Input directory not found: {input_dir}")
os.makedirs(output_dir, exist_ok=True)
target_ext = target_format if target_format.startswith(".") else f".{target_format}"
if target_ext.lower() not in SUPPORTED_FORMATS:
raise ValueError(f"Unsupported target format: {target_ext}")
results = []
for filename in sorted(os.listdir(input_dir)):
ext = Path(filename).suffix.lower()
if ext in SUPPORTED_FORMATS:
input_path = os.path.join(input_dir, filename)
output_filename = Path(filename).stem + target_ext
output_path = os.path.join(output_dir, output_filename)
try:
result = convert_image(
input_path,
output_path,
quality=quality,
resize=resize,
preserve_metadata=preserve_metadata,
verbose=verbose,
)
results.append(result)
if verbose:
print(f"---")
except Exception as e:
print(f"Error converting {filename}: {e}", file=sys.stderr)
return results
def get_image_info(filepath: str) -> dict:
"""
Get detailed information about an image file.
Uses both Wand and Pillow for comprehensive metadata extraction.
Args:
filepath: Path to the image file.
Returns:
A dictionary containing image information.
"""
if not os.path.isfile(filepath):
raise FileNotFoundError(f"File not found: {filepath}")
info = {}
with WandImage(filename=filepath) as img:
info = {
"path": os.path.abspath(filepath),
"format": img.format,
"width": img.width,
"height": img.height,
"depth": img.depth,
"colorspace": str(img.colorspace),
"has_alpha": img.alpha_channel,
"file_size_bytes": os.path.getsize(filepath),
"wand_metadata": extract_metadata_wand(img),
}
# Also get EXIF via Pillow
info["exif_metadata"] = extract_metadata_pillow(filepath)
return info
def compare_images(path1: str, path2: str) -> dict:
"""
Compare two images and report differences.
Args:
path1: Path to the first image.
path2: Path to the second image.
Returns:
A dictionary with comparison results.
"""
with WandImage(filename=path1) as img1, WandImage(filename=path2) as img2:
result = {
"image1": {
"path": os.path.abspath(path1),
"format": img1.format,
"size": f"{img1.width}x{img1.height}",
"file_size": os.path.getsize(path1),
},
"image2": {
"path": os.path.abspath(path2),
"format": img2.format,
"size": f"{img2.width}x{img2.height}",
"file_size": os.path.getsize(path2),
},
"same_dimensions": (img1.width == img2.width and img1.height == img2.height),
}
# Compute difference if same dimensions
if result["same_dimensions"]:
diff_image, distortion = img1.compare(img2, metric="root_mean_square")
result["rms_distortion"] = distortion
diff_image.close()
return result
def main():
"""Main entry point for the CLI."""
parser = argparse.ArgumentParser(
description="Image Format Converter (Wand + Pillow) - Convert between PNG, JPEG, BMP, TIFF, and WebP."
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Convert command
convert_parser = subparsers.add_parser("convert", help="Convert a single image")
convert_parser.add_argument("input", help="Path to the input image")
convert_parser.add_argument("output", help="Path for the output image")
convert_parser.add_argument(
"-q", "--quality", type=int, default=85,
help="Quality setting (1-100, default: 85)"
)
convert_parser.add_argument(
"-r", "--resize", type=str, default=None,
help="Resize to WIDTHxHEIGHT (e.g., 800x600)"
)
convert_parser.add_argument(
"--no-metadata", action="store_true",
help="Strip metadata from the output"
)
convert_parser.add_argument(
"-v", "--verbose", action="store_true",
help="Print detailed conversion information"
)
# Batch command
batch_parser = subparsers.add_parser("batch", help="Batch convert images in a directory")
batch_parser.add_argument("input_dir", help="Input directory path")
batch_parser.add_argument("output_dir", help="Output directory path")
batch_parser.add_argument(
"-f", "--format", required=True,
help="Target format extension (e.g., .png, .jpg)"
)
batch_parser.add_argument(
"-q", "--quality", type=int, default=85,
help="Quality setting (1-100, default: 85)"
)
batch_parser.add_argument(
"-r", "--resize", type=str, default=None,
help="Resize to WIDTHxHEIGHT (e.g., 800x600)"
)
batch_parser.add_argument(
"--no-metadata", action="store_true",
help="Strip metadata from the output"
)
batch_parser.add_argument(
"-v", "--verbose", action="store_true",
help="Print detailed conversion information"
)
# Info command
info_parser = subparsers.add_parser("info", help="Display image information")
info_parser.add_argument("input", help="Path to the image file")
# Compare command
compare_parser = subparsers.add_parser("compare", help="Compare two images")
compare_parser.add_argument("image1", help="Path to the first image")
compare_parser.add_argument("image2", help="Path to the second image")
args = parser.parse_args()
if args.command is None:
parser.print_help()
sys.exit(1)
# Parse resize argument
resize = None
if hasattr(args, "resize") and args.resize:
try:
parts = args.resize.lower().split("x")
resize = (int(parts[0]), int(parts[1]))
except (ValueError, IndexError):
print("Error: Resize must be in WIDTHxHEIGHT format (e.g., 800x600).")
sys.exit(1)
if args.command == "convert":
try:
output = convert_image(
args.input,
args.output,
quality=args.quality,
resize=resize,
preserve_metadata=not args.no_metadata,
verbose=args.verbose,
)
print(f"Converted successfully: {output}")
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
elif args.command == "batch":
try:
results = batch_convert(
args.input_dir,
args.output_dir,
target_format=args.format,
quality=args.quality,
resize=resize,
preserve_metadata=not args.no_metadata,
verbose=args.verbose,
)
print(f"Batch conversion complete. {len(results)} file(s) converted.")
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
elif args.command == "info":
try:
info = get_image_info(args.input)
print(f"File: {info['path']}")
print(f"Format: {info['format']}")
print(f"Dimensions: {info['width']}x{info['height']}")
print(f"Depth: {info['depth']}")
print(f"Colorspace: {info['colorspace']}")
print(f"Alpha channel: {info['has_alpha']}")
print(f"File size: {info['file_size_bytes']:,} bytes")
print("Wand metadata:")
print_metadata(info["wand_metadata"])
print("EXIF metadata (via Pillow):")
print_metadata(info["exif_metadata"])
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
elif args.command == "compare":
try:
result = compare_images(args.image1, args.image2)
print(f"Image 1: {result['image1']['path']}")
print(f" Format: {result['image1']['format']}, Size: {result['image1']['size']}")
print(f" File size: {result['image1']['file_size']:,} bytes")
print(f"Image 2: {result['image2']['path']}")
print(f" Format: {result['image2']['format']}, Size: {result['image2']['size']}")
print(f" File size: {result['image2']['file_size']:,} bytes")
print(f"Same dimensions: {result['same_dimensions']}")
if "rms_distortion" in result:
print(f"RMS distortion: {result['rms_distortion']:.6f}")
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
README.md
# Image Format Converter (Python - Wand + Pillow) An image format converter that supports PNG, JPEG, BMP, TIFF, and WebP formats with quality settings, resize capabilities, and metadata preservation. ## Dependencies - **Wand 0.6.13** - Python binding for ImageMagick, used as the primary image processing engine - **Pillow 10.2.0** - Python Imaging Library fork, used for EXIF metadata extraction ## Prerequisites Wand requires ImageMagick to be installed on your system: ```bash # Ubuntu/Debian sudo apt-get install libmagickwand-dev # macOS brew install imagemagick # Windows # Download and install from https://imagemagick.org/script/download.php ``` ## Installation ```bash pip install -r requirements.txt ``` ## Usage ### Convert a single image ```bash python converter.py convert input.png output.jpg python converter.py convert input.bmp output.webp -q 90 python converter.py convert input.tiff output.png -r 800x600 python converter.py convert input.jpg output.tiff --no-metadata -v ``` ### Batch convert images ```bash python converter.py batch ./input_dir ./output_dir -f .png python converter.py batch ./photos ./thumbnails -f .jpg -q 75 -r 200x200 ``` ### Display image information ```bash python converter.py info photo.jpg ``` ### Compare two images ```bash python converter.py compare image1.png image2.jpg ``` ## Options | Option | Description | |--------|-------------| | `-q, --quality` | Quality setting (1-100, default: 85) | | `-r, --resize` | Resize to WIDTHxHEIGHT (e.g., 800x600) | | `--no-metadata` | Strip EXIF/IPTC metadata from output | | `-v, --verbose` | Print detailed conversion information | ## Supported Formats | Format | Read | Write | Notes | |--------|------|-------|-------| | PNG | Yes | Yes | Lossless compression | | JPEG | Yes | Yes | Lossy, quality configurable | | BMP | Yes | Yes | Uncompressed | | TIFF | Yes | Yes | LZW compression | | WebP | Yes | Yes | Lossy, quality configurable |
requirements.txt
Wand==0.6.13 Pillow==10.2.0