Image Resizer and Thumbnail Generator (javascript, written by Codex)
envgap__codex__javascript-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
package.json- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/javascript-t1 #19 · read the task the agent was given
Codex 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 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 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-resizer-thumbnail-generator",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "image-resizer-thumbnail-generator",
"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-resizer-thumbnail-generator",
"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 Resizer and Thumbnail Generator (JavaScript) Resizes images with configurable algorithms/modes, generates thumbnails, supports padding/borders, and batch processing. ## 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 --size 1920x1080 --algorithm lanczos --mode fit node src/index.js input.png --scale 50 --algorithm bicubic --mode stretch node src/index.js input.png --thumbnails --thumb-sizes 64x64,128x128 node src/index.js input.png --padding 10 --border-color "#222222" node src/index.js ./images --batch --recursive --size 800x600 --output ./resized node src/index.js ``` ## Features - `--size WxH` and `--scale percent` - Algorithms: nearest, bilinear, bicubic, lanczos - Modes: fit, fill, stretch - Standard thumbnails (`--thumbnails`) and custom (`--thumb-sizes`) - Optional padding (`--padding`) and border color (`--border-color`) - Batch mode (`--batch`) with optional recursive traversal (`--recursive`) - Console processing details per output - Default output directory: `resized/` - No-args mode generates a 2000x1500 sample and demonstrates algorithm/mode differences
src/index.js
import fs from "fs";
import path from "path";
import sharp from "sharp";
const ALGO_MAP = {
nearest: sharp.kernel.nearest,
bilinear: sharp.kernel.linear,
bicubic: sharp.kernel.cubic,
lanczos: sharp.kernel.lanczos3,
};
const MODES = new Set(["fit", "fill", "stretch"]);
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 parseSize(value) {
const m = /^(\d+)x(\d+)$/i.exec(String(value || ""));
if (!m) throw new Error("Invalid size. Use WxH.");
return { width: Number(m[1]), height: Number(m[2]) };
}
function parseThumbSizes(raw) {
if (!raw) return [];
return String(raw).split(",").filter(Boolean).map(parseSize);
}
function resizeConfig(meta, options) {
const mode = String(options.mode || "fit").toLowerCase();
if (!MODES.has(mode)) throw new Error("Invalid --mode. Use fit|fill|stretch.");
const algorithm = String(options.algorithm || "lanczos").toLowerCase();
if (!ALGO_MAP[algorithm]) throw new Error("Invalid --algorithm. Use nearest|bilinear|bicubic|lanczos.");
let width = meta.width;
let height = meta.height;
if (options.size) {
const s = parseSize(options.size);
width = s.width;
height = s.height;
} else if (options.scale) {
const scale = Number(options.scale);
if (!Number.isFinite(scale) || scale <= 0) throw new Error("Invalid --scale value.");
width = Math.max(1, Math.round(meta.width * (scale / 100)));
height = Math.max(1, Math.round(meta.height * (scale / 100)));
}
return { mode, algorithm, width, height };
}
function fitToSharp(mode) {
if (mode === "fit") return "inside";
if (mode === "fill") return "cover";
return "fill";
}
function collectImages(root, recursive) {
const out = [];
function walk(dir) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) {
if (recursive) walk(p);
continue;
}
if (!e.isFile()) continue;
const ext = path.extname(e.name).slice(1).toLowerCase();
if (["png", "jpg", "jpeg", "bmp", "tif", "tiff", "webp"].includes(ext)) out.push(p);
}
}
walk(root);
return out;
}
async function processOne(inputPath, options) {
const t0 = Date.now();
const outDir = path.resolve(options.output || "resized");
fs.mkdirSync(outDir, { recursive: true });
const meta = await sharp(inputPath).metadata();
const rc = resizeConfig(meta, options);
let img = sharp(inputPath).resize({
width: rc.width,
height: rc.height,
fit: fitToSharp(rc.mode),
position: "centre",
kernel: ALGO_MAP[rc.algorithm],
});
const padding = Number.parseInt(String(options.padding || "0"), 10);
if (padding > 0) {
img = img.extend({
top: padding, bottom: padding, left: padding, right: padding,
background: options["border-color"] || "#ffffff",
});
}
const outPath = path.join(outDir, path.basename(inputPath));
await img.toFile(outPath);
const outMeta = await sharp(outPath).metadata();
const ms = Date.now() - t0;
return {
input: inputPath,
output: outPath,
original: `${meta.width}x${meta.height}`,
resized: `${outMeta.width}x${outMeta.height}`,
algorithm: rc.algorithm,
mode: rc.mode,
ms,
};
}
async function generateThumbnails(inputPath, options) {
const sizes = [];
if (options.thumbnails) sizes.push({ width: 150, height: 150 }, { width: 300, height: 300 }, { width: 600, height: 600 });
sizes.push(...parseThumbSizes(options["thumb-sizes"]));
if (sizes.length === 0) return [];
const outDir = path.resolve(options.output || "resized");
fs.mkdirSync(outDir, { recursive: true });
const out = [];
for (const s of sizes) {
const file = path.join(outDir, `${path.basename(inputPath, path.extname(inputPath))}_${s.width}x${s.height}${path.extname(inputPath)}`);
const t0 = Date.now();
await sharp(inputPath)
.resize({ width: s.width, height: s.height, fit: "cover", position: "centre", kernel: ALGO_MAP[String(options.algorithm || "lanczos")] || sharp.kernel.lanczos3 })
.toFile(file);
const ms = Date.now() - t0;
out.push({ input: inputPath, output: file, original: "N/A", resized: `${s.width}x${s.height}`, algorithm: String(options.algorithm || "lanczos"), mode: "thumbnail", ms });
}
return out;
}
function printResult(r) {
console.log(`Input: ${r.input}`);
console.log(`Output: ${r.output}`);
console.log(`Dimensions: ${r.original} -> ${r.resized}`);
console.log(`Algorithm: ${r.algorithm} | Mode: ${r.mode}`);
console.log(`Processing time: ${r.ms} ms`);
console.log("");
}
async function createSample() {
const out = path.resolve("sample_2000x1500.png");
const svg = `
<svg width="2000" height="1500" xmlns="http://www.w3.org/2000/svg">
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#2937f0"/><stop offset="100%" stop-color="#f857a6"/></linearGradient></defs>
<rect width="2000" height="1500" fill="url(#g)"/>
${Array.from({ length: 40 }, (_, i) => `<line x1="${i * 50}" y1="0" x2="${i * 50}" y2="1500" stroke="rgba(255,255,255,0.15)" />`).join("")}
${Array.from({ length: 30 }, (_, i) => `<line x1="0" y1="${i * 50}" x2="2000" y2="${i * 50}" stroke="rgba(0,0,0,0.15)" />`).join("")}
<text x="80" y="180" font-size="96" fill="white">Resizer Test Card</text>
<text x="80" y="260" font-size="48" fill="white">Small text: 1234567890 abcdefghijklmnopqrstuvwxyz</text>
<text x="80" y="320" font-size="24" fill="white">Tiny text for detail check</text>
</svg>`;
await sharp(Buffer.from(svg)).png().toFile(out);
return out;
}
async function runDemo() {
const sample = await createSample();
for (const algorithm of ["nearest", "bilinear", "bicubic", "lanczos"]) {
for (const mode of ["fit", "fill", "stretch"]) {
const r = await processOne(sample, { size: "800x600", algorithm, mode, output: path.resolve("demo_resized") });
printResult(r);
}
}
}
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 < 1) throw new Error("Batch mode: provide input directory.");
const files = collectImages(path.resolve(positional[0]), Boolean(options.recursive));
for (const f of files) {
printResult(await processOne(f, options));
const thumbs = await generateThumbnails(f, options);
thumbs.forEach(printResult);
}
return;
}
const input = path.resolve(positional[0]);
printResult(await processOne(input, options));
const thumbs = await generateThumbnails(input, options);
thumbs.forEach(printResult);
}
main().catch((e) => {
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
process.exit(1);
});