Image Format Converter (javascript, written by Codex)
envgap__codex__javascript-t1-18
Written by a coding agent; not on GitHubWritten 2026-03-03
01 / FAILURE SIGNATURE
As the study recorded it
pipeline.bmp is not a function - sharp has no .bmp() method
Not a benchmark task.
- In a clean container the reported failure did not reproduce, or the known fix did not make the project run.
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
codex/javascript-t1 #18 · read the task the agent was given
Codex wrote this javascript 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 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
4 files, exactly as written, before any repair.
package-lock.json
{
"name": "image-format-converter",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "image-format-converter",
"version": "1.0.0",
"dependencies": {
"sharp": "0.33.5"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/sharp": {
"version": "0.33.5"
}
}
}
package.json
{
"name": "image-format-converter",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"sharp": "0.33.5"
}
}
README.md
# Image Format Converter (JavaScript) Converts images among PNG/JPEG/BMP/TIFF/WebP with quality, resize, metadata, alpha handling, batch mode, and demo generation. ## Requirements - Ubuntu 22.04 - Node.js 20+ (LTS) ## Dependencies - `sharp@0.33.5` (pinned) ## Setup ```bash npm install ``` ## Run ```bash node src/index.js input.png webp --quality 90 node src/index.js input.png jpeg --background "#ffffff" --resize 800x600 --maintain-aspect true node src/index.js input.webp png --strip-metadata node src/index.js ./images jpeg --batch --filter png --quality 80 node src/index.js ``` ## Features - Formats: PNG, JPEG, BMP, TIFF, WebP - `--quality` for lossy output (JPEG/WebP) - `--resize WxH` + `--maintain-aspect` (default true) - Metadata keep/strip via `--strip-metadata` - Alpha flattening with `--background` for non-alpha targets - Batch conversion via `--batch` and optional `--filter` - Conversion detail output (dimensions, sizes, ratio) - `--output` to choose destination path - No-args demo generates sample PNG and converts to other formats
src/index.js
import fs from "fs";
import path from "path";
import sharp from "sharp";
const SUPPORTED = new Set(["png", "jpeg", "jpg", "bmp", "tiff", "webp"]);
function parseArgs(argv) {
const options = {};
const positional = [];
for (let i = 0; i < argv.length; i += 1) {
const t = argv[i];
if (t.startsWith("--")) {
const k = t.slice(2);
const n = argv[i + 1];
if (n && !n.startsWith("--")) {
options[k] = n;
i += 1;
} else options[k] = true;
} else positional.push(t);
}
return { options, positional };
}
function normalizeFormat(fmt) {
const f = String(fmt).toLowerCase();
if (f === "jpg") return "jpeg";
if (!SUPPORTED.has(f)) throw new Error(`Unsupported format: ${fmt}`);
return f;
}
function parseResize(value) {
if (!value) return null;
const m = /^(\d+)x(\d+)$/i.exec(value.trim());
if (!m) throw new Error("Invalid --resize format. Use WxH (e.g., 800x600).");
return { width: Number.parseInt(m[1], 10), height: Number.parseInt(m[2], 10) };
}
function parseBool(value, defaultValue) {
if (value === undefined) return defaultValue;
const s = String(value).toLowerCase();
if (["true", "1", "yes"].includes(s)) return true;
if (["false", "0", "no"].includes(s)) return false;
return defaultValue;
}
function outputPath(inputPath, targetFmt, explicitOutput) {
if (explicitOutput) return path.resolve(explicitOutput);
const dir = path.dirname(inputPath);
const base = path.basename(inputPath, path.extname(inputPath));
return path.join(dir, `${base}.${targetFmt}`);
}
function compressionRatio(before, after) {
if (before <= 0) return 1;
return after / before;
}
async function convertOne(inputPath, targetFmt, options) {
const format = normalizeFormat(targetFmt);
const resize = parseResize(options.resize || "");
const maintainAspect = parseBool(options["maintain-aspect"], true);
const quality = Number.parseInt(String(options.quality || "85"), 10);
if (quality < 1 || quality > 100) throw new Error("Quality must be in range 1..100.");
const stripMetadata = Boolean(options["strip-metadata"]);
const background = options.background || "white";
const inputAbs = path.resolve(inputPath);
const outputAbs = outputPath(inputAbs, format, options.output);
const before = fs.statSync(inputAbs).size;
const meta = await sharp(inputAbs).metadata();
let pipeline = sharp(inputAbs, { failOn: "error" });
if (!stripMetadata) pipeline = pipeline.withMetadata();
if (resize) {
pipeline = pipeline.resize({
width: resize.width,
height: resize.height,
fit: maintainAspect ? "inside" : "fill",
withoutEnlargement: false,
});
}
if ((format === "jpeg" || format === "bmp") && meta.hasAlpha) {
pipeline = pipeline.flatten({ background });
}
switch (format) {
case "jpeg": pipeline = pipeline.jpeg({ quality }); break;
case "webp": pipeline = pipeline.webp({ quality }); break;
case "png": pipeline = pipeline.png(); break;
case "bmp": pipeline = pipeline.bmp(); break;
case "tiff": pipeline = pipeline.tiff({ quality }); break;
default: break;
}
fs.mkdirSync(path.dirname(outputAbs), { recursive: true });
await pipeline.toFile(outputAbs);
const newMeta = await sharp(outputAbs).metadata();
const after = fs.statSync(outputAbs).size;
return {
input: inputAbs,
output: outputAbs,
input_format: meta.format,
output_format: format,
original_dimensions: `${meta.width}x${meta.height}`,
new_dimensions: `${newMeta.width}x${newMeta.height}`,
size_before: before,
size_after: after,
compression_ratio: Number(compressionRatio(before, after).toFixed(4)),
};
}
function printDetails(d) {
console.log(`Input: ${d.input}`);
console.log(`Output: ${d.output}`);
console.log(`Format: ${d.input_format} -> ${d.output_format}`);
console.log(`Dimensions: ${d.original_dimensions} -> ${d.new_dimensions}`);
console.log(`Size: ${d.size_before} -> ${d.size_after} bytes`);
console.log(`Compression ratio: ${d.compression_ratio}`);
console.log("");
}
async function batchConvert(dir, targetFmt, options) {
const filter = options.filter ? normalizeFormat(options.filter) : null;
const entries = fs.readdirSync(path.resolve(dir), { withFileTypes: true });
const files = entries
.filter((e) => e.isFile())
.map((e) => path.join(path.resolve(dir), e.name))
.filter((p) => SUPPORTED.has(normalizeFormat(path.extname(p).slice(1) || "")))
.filter((p) => (filter ? normalizeFormat(path.extname(p).slice(1)) === filter : true));
const results = [];
for (const f of files) results.push(await convertOne(f, targetFmt, { ...options, output: undefined }));
return results;
}
async function generateSamplePng() {
const samplePath = path.resolve("sample_800x600.png");
const svg = `
<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ff8a00"/>
<stop offset="100%" stop-color="#2f5bea"/>
</linearGradient>
</defs>
<rect width="800" height="600" fill="url(#g)"/>
<circle cx="250" cy="240" r="170" fill="rgba(255,255,255,0.35)"/>
<rect x="420" y="140" width="220" height="220" rx="24" fill="rgba(0,0,0,0.35)"/>
<text x="50%" y="88%" text-anchor="middle" font-size="46" font-family="Arial" fill="white">Image Converter Demo</text>
</svg>`;
await sharp(Buffer.from(svg)).png().toFile(samplePath);
return samplePath;
}
async function runDemo() {
const sample = await generateSamplePng();
const targets = ["jpeg", "bmp", "tiff", "webp"];
const results = [];
for (const t of targets) {
results.push(await convertOne(sample, t, {
quality: "85",
resize: "",
"maintain-aspect": "true",
background: "white",
}));
}
console.log("Demo comparison table:");
console.log("format,orig_dim,new_dim,size_before,size_after,ratio");
for (const r of results) {
console.log(`${r.output_format},${r.original_dimensions},${r.new_dimensions},${r.size_before},${r.size_after},${r.compression_ratio}`);
}
}
async function main() {
const { options, positional } = parseArgs(process.argv.slice(2));
if (positional.length === 0 && !options.batch) {
await runDemo();
return;
}
if (options.batch) {
if (positional.length < 2) throw new Error("Batch mode usage: --batch <directory> <target-format>");
const [dir, targetFmt] = [positional[0], positional[1]];
const results = await batchConvert(dir, targetFmt, options);
results.forEach(printDetails);
return;
}
if (positional.length < 2) throw new Error("Usage: <input-image> <target-format> [flags]");
const details = await convertOne(positional[0], positional[1], options);
printDetails(details);
}
main().catch((e) => {
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
process.exit(1);
});