Image Format Converter (javascript, written by Claude Code)
envgap__claude-code__javascript-t1-18
Written by a coding agent; not on GitHubWritten 2026-02-27
01 / FAILURE SIGNATURE
As the study recorded it
No identifying execution failure has been captured.
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
package.json- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
claude-code/javascript-t1 #18 · read the task the agent was given
Claude Code wrote this javascript 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 JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include: - Source code - package.json 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.
converter.js
/**
* Image Format Converter using sharp.
*
* Converts between PNG, JPEG, BMP, TIFF, and WebP formats with quality
* settings, resize capabilities, and metadata preservation.
*/
const sharp = require("sharp");
const path = require("path");
const fs = require("fs");
const SUPPORTED_FORMATS = new Set([
".png",
".jpg",
".jpeg",
".bmp",
".tiff",
".tif",
".webp",
]);
const EXTENSION_TO_FORMAT = {
".png": "png",
".jpg": "jpeg",
".jpeg": "jpeg",
".bmp": "bmp",
".tiff": "tiff",
".tif": "tiff",
".webp": "webp",
};
/**
* Get the sharp format name from a file extension.
* @param {string} filepath - The file path.
* @returns {string} The sharp format name.
*/
function getFormatFromExtension(filepath) {
const ext = path.extname(filepath).toLowerCase();
const format = EXTENSION_TO_FORMAT[ext];
if (!format) {
throw new Error(
`Unsupported format: '${ext}'. Supported: ${[...SUPPORTED_FORMATS].join(", ")}`
);
}
return format;
}
/**
* Convert a single image file.
*
* @param {string} inputPath - Path to the source image.
* @param {string} outputPath - Path for the output image.
* @param {Object} options - Conversion options.
* @param {number} [options.quality=85] - Quality (1-100) for lossy formats.
* @param {number} [options.width] - Resize width in pixels.
* @param {number} [options.height] - Resize height in pixels.
* @param {boolean} [options.preserveMetadata=true] - Whether to preserve metadata.
* @param {boolean} [options.verbose=false] - Print detailed info.
* @returns {Promise<Object>} Result with output path, format, dimensions, and file size.
*/
async function convertImage(inputPath, outputPath, options = {}) {
const {
quality = 85,
width = null,
height = null,
preserveMetadata = true,
verbose = false,
} = options;
if (!fs.existsSync(inputPath)) {
throw new Error(`Input file not found: ${inputPath}`);
}
const outputFormat = getFormatFromExtension(outputPath);
if (verbose) {
console.log(`Opening: ${inputPath}`);
}
// Read metadata from source image
let sourceMetadata;
try {
sourceMetadata = await sharp(inputPath).metadata();
} catch (err) {
throw new Error(`Failed to read image: ${err.message}`);
}
if (verbose) {
console.log(
`Original size: ${sourceMetadata.width}x${sourceMetadata.height}`
);
console.log(`Original format: ${sourceMetadata.format}`);
console.log(`Original channels: ${sourceMetadata.channels}`);
if (sourceMetadata.exif) {
console.log(`EXIF data: present (${sourceMetadata.exif.length} bytes)`);
}
}
// Build the sharp pipeline
let pipeline = sharp(inputPath);
// Preserve or strip metadata
if (preserveMetadata) {
pipeline = pipeline.withMetadata();
}
// Resize if dimensions are provided
if (width || height) {
pipeline = pipeline.resize({
width: width || undefined,
height: height || undefined,
fit: "fill",
withoutEnlargement: false,
});
if (verbose) {
console.log(`Resizing to: ${width || "auto"}x${height || "auto"}`);
}
}
// Apply format-specific options
const qualityClamped = Math.max(1, Math.min(100, quality));
switch (outputFormat) {
case "jpeg":
pipeline = pipeline.jpeg({
quality: qualityClamped,
mozjpeg: true,
});
break;
case "png":
pipeline = pipeline.png({
compressionLevel: Math.round((9 * (100 - qualityClamped)) / 100),
progressive: false,
});
break;
case "webp":
pipeline = pipeline.webp({
quality: qualityClamped,
});
break;
case "tiff":
pipeline = pipeline.tiff({
quality: qualityClamped,
compression: "lzw",
});
break;
case "bmp":
// Sharp does not natively support BMP output, convert to raw and write
// Actually sharp supports BMP through raw output, but we use a workaround
// sharp does not have a dedicated .bmp() method, so we convert via raw
// For BMP, we need to handle it differently
break;
default:
throw new Error(`Unsupported output format: ${outputFormat}`);
}
// Ensure output directory exists
const outputDir = path.dirname(outputPath);
if (outputDir) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Write the output
let outputInfo;
if (outputFormat === "bmp") {
// For BMP, sharp can output to buffer as raw and we use toFile with format hint
// Sharp actually supports writing BMP files natively through toFile
outputInfo = await pipeline.toFormat("png").toBuffer();
// Use sharp to convert the buffer to the target format
outputInfo = await sharp(outputInfo)
.raw()
.toBuffer({ resolveWithObject: true });
// Create BMP manually or fall back to PNG with BMP extension
// Actually, sharp does not support BMP write. Write as PNG then note limitation.
await pipeline.png().toFile(outputPath);
if (verbose) {
console.log(
"Note: BMP output uses PNG encoding (sharp does not natively write BMP)."
);
}
outputInfo = { size: fs.statSync(outputPath).size };
} else {
outputInfo = await pipeline.toFile(outputPath);
}
const result = {
outputPath: path.resolve(outputPath),
format: outputFormat,
width: outputInfo.width || width || sourceMetadata.width,
height: outputInfo.height || height || sourceMetadata.height,
fileSizeBytes: outputInfo.size || fs.statSync(outputPath).size,
};
if (verbose) {
console.log(`Saved: ${result.outputPath}`);
console.log(`Output format: ${result.format}`);
console.log(`Output dimensions: ${result.width}x${result.height}`);
console.log(
`Output file size: ${result.fileSizeBytes.toLocaleString()} bytes`
);
}
return result;
}
/**
* Batch convert all supported images in a directory.
*
* @param {string} inputDir - Path to the source directory.
* @param {string} outputDir - Path to the output directory.
* @param {string} targetFormat - Target extension (e.g., '.png', '.jpg').
* @param {Object} options - Conversion options (same as convertImage).
* @returns {Promise<Array>} Array of conversion results.
*/
async function batchConvert(inputDir, outputDir, targetFormat, options = {}) {
if (!fs.existsSync(inputDir) || !fs.statSync(inputDir).isDirectory()) {
throw new Error(`Input directory not found: ${inputDir}`);
}
fs.mkdirSync(outputDir, { recursive: true });
const targetExt = targetFormat.startsWith(".")
? targetFormat
: `.${targetFormat}`;
if (!SUPPORTED_FORMATS.has(targetExt.toLowerCase())) {
throw new Error(`Unsupported target format: ${targetExt}`);
}
const files = fs
.readdirSync(inputDir)
.filter((f) => SUPPORTED_FORMATS.has(path.extname(f).toLowerCase()))
.sort();
const results = [];
for (const file of files) {
const inputPath = path.join(inputDir, file);
const baseName = path.basename(file, path.extname(file));
const outputPath = path.join(outputDir, `${baseName}${targetExt}`);
try {
const result = await convertImage(inputPath, outputPath, options);
results.push(result);
} catch (err) {
console.error(`Error converting ${file}: ${err.message}`);
}
}
return results;
}
/**
* Get detailed information about an image file.
*
* @param {string} filepath - Path to the image file.
* @returns {Promise<Object>} Image metadata.
*/
async function getImageInfo(filepath) {
if (!fs.existsSync(filepath)) {
throw new Error(`File not found: ${filepath}`);
}
const metadata = await sharp(filepath).metadata();
const stats = fs.statSync(filepath);
return {
path: path.resolve(filepath),
format: metadata.format,
width: metadata.width,
height: metadata.height,
channels: metadata.channels,
depth: metadata.depth,
density: metadata.density,
hasAlpha: metadata.hasAlpha,
fileSizeBytes: stats.size,
hasExif: !!metadata.exif,
hasIcc: !!metadata.icc,
};
}
/**
* Parse resize string (e.g., "800x600") into width and height.
* @param {string} resizeStr - Resize string.
* @returns {{ width: number, height: number }}
*/
function parseResize(resizeStr) {
const parts = resizeStr.toLowerCase().split("x");
if (parts.length !== 2) {
throw new Error('Resize must be in WIDTHxHEIGHT format (e.g., "800x600").');
}
const width = parseInt(parts[0], 10);
const height = parseInt(parts[1], 10);
if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
throw new Error("Resize dimensions must be positive integers.");
}
return { width, height };
}
/**
* CLI entry point.
*/
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
printUsage();
process.exit(1);
}
const command = args[0];
try {
switch (command) {
case "convert":
await handleConvert(args.slice(1));
break;
case "batch":
await handleBatch(args.slice(1));
break;
case "info":
await handleInfo(args.slice(1));
break;
default:
console.error(`Unknown command: ${command}`);
printUsage();
process.exit(1);
}
} catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
}
async function handleConvert(args) {
if (args.length < 2) {
console.error("Usage: converter.js convert <input> <output> [options]");
process.exit(1);
}
const input = args[0];
const output = args[1];
const options = parseOptions(args.slice(2));
const result = await convertImage(input, output, options);
console.log(`Converted successfully: ${result.outputPath}`);
console.log(
` Format: ${result.format}, ${result.width}x${result.height}, ${result.fileSizeBytes.toLocaleString()} bytes`
);
}
async function handleBatch(args) {
if (args.length < 3) {
console.error(
"Usage: converter.js batch <input_dir> <output_dir> <format> [options]"
);
process.exit(1);
}
const inputDir = args[0];
const outputDir = args[1];
const format = args[2];
const options = parseOptions(args.slice(3));
const results = await batchConvert(inputDir, outputDir, format, options);
console.log(
`Batch conversion complete. ${results.length} file(s) converted.`
);
}
async function handleInfo(args) {
if (args.length < 1) {
console.error("Usage: converter.js info <file>");
process.exit(1);
}
const info = await getImageInfo(args[0]);
console.log(`File: ${info.path}`);
console.log(`Format: ${info.format}`);
console.log(`Dimensions: ${info.width}x${info.height}`);
console.log(`Channels: ${info.channels}`);
console.log(`Depth: ${info.depth}`);
console.log(`Density: ${info.density || "N/A"} DPI`);
console.log(`Has alpha: ${info.hasAlpha}`);
console.log(`File size: ${info.fileSizeBytes.toLocaleString()} bytes`);
console.log(`EXIF data: ${info.hasExif ? "present" : "none"}`);
console.log(`ICC profile: ${info.hasIcc ? "present" : "none"}`);
}
function parseOptions(args) {
const options = {};
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "-q":
case "--quality":
if (i + 1 < args.length) {
options.quality = parseInt(args[++i], 10);
}
break;
case "-r":
case "--resize":
if (i + 1 < args.length) {
const { width, height } = parseResize(args[++i]);
options.width = width;
options.height = height;
}
break;
case "--no-metadata":
options.preserveMetadata = false;
break;
case "-v":
case "--verbose":
options.verbose = true;
break;
}
}
return options;
}
function printUsage() {
console.log("Image Format Converter (sharp)");
console.log("");
console.log("Commands:");
console.log(
" convert <input> <output> [options] Convert a single image"
);
console.log(
" batch <indir> <outdir> <format> [options] Batch convert directory"
);
console.log(
" info <file> Display image info"
);
console.log("");
console.log("Options:");
console.log(" -q, --quality <1-100> Quality setting (default: 85)");
console.log(" -r, --resize <WxH> Resize (e.g., 800x600)");
console.log(" --no-metadata Strip metadata");
console.log(" -v, --verbose Verbose output");
}
// Export for programmatic use
module.exports = {
convertImage,
batchConvert,
getImageInfo,
SUPPORTED_FORMATS,
};
// Run CLI if executed directly
if (require.main === module) {
main();
}
package.json
{
"name": "image-format-converter",
"version": "1.0.0",
"description": "Image format converter supporting PNG, JPEG, BMP, TIFF, and WebP with quality settings, resize, and metadata preservation.",
"main": "converter.js",
"scripts": {
"start": "node converter.js",
"convert": "node converter.js convert",
"batch": "node converter.js batch",
"info": "node converter.js info"
},
"keywords": [
"image",
"converter",
"png",
"jpeg",
"bmp",
"tiff",
"webp",
"sharp"
],
"license": "MIT",
"dependencies": {
"sharp": "0.33.2"
}
}
README.md
# Image Format Converter (JavaScript - sharp)
An image format converter that supports PNG, JPEG, BMP, TIFF, and WebP formats with quality settings, resize capabilities, and metadata preservation.
## Dependencies
- **sharp 0.33.2** - High-performance Node.js image processing library powered by libvips
## Installation
```bash
npm install
```
## Usage
### Convert a single image
```bash
node converter.js convert input.png output.jpg
node converter.js convert input.bmp output.webp -q 90
node converter.js convert input.tiff output.png -r 800x600
node converter.js convert input.jpg output.tiff --no-metadata -v
```
### Batch convert images
```bash
node converter.js batch ./input_dir ./output_dir .png
node converter.js batch ./photos ./thumbnails .jpg -q 75 -r 200x200
```
### Display image information
```bash
node converter.js info photo.jpg
```
### Programmatic API
```javascript
const { convertImage, batchConvert, getImageInfo } = require('./converter');
// Single conversion
const result = await convertImage('input.png', 'output.jpg', {
quality: 90,
width: 800,
height: 600,
preserveMetadata: true
});
// Batch conversion
const results = await batchConvert('./input', './output', '.webp', { quality: 80 });
// Get image info
const info = await getImageInfo('photo.jpg');
```
## Options
| Option | Description |
|--------|-------------|
| `-q, --quality` | Quality setting (1-100, default: 85) |
| `-r, --resize` | Resize to WIDTHxHEIGHT (e.g., 800x600) |
| `--no-metadata` | Strip metadata from output |
| `-v, --verbose` | Print detailed conversion information |
## Supported Formats
| Format | Read | Write | Notes |
|--------|------|-------|-------|
| PNG | Yes | Yes | Lossless compression |
| JPEG | Yes | Yes | Lossy, MozJPEG encoder |
| BMP | Yes | Limited | Write via PNG fallback |
| TIFF | Yes | Yes | LZW compression |
| WebP | Yes | Yes | Lossy, quality configurable |