← All tasks
javascriptcodex/javascript-t1 #19Not a task: already works

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);
});