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

Image Resizer and Thumbnail Generator (javascript, written by Claude Code)

envgap__claude-code__javascript-t1-19

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 #19 · 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 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

3 files, exactly as written, before any repair.

package.json
{
  "name": "image-resizer",
  "version": "1.0.0",
  "description": "Image Resizer and Thumbnail Generator - resizes images with multiple algorithms, crop modes, and batch processing",
  "main": "resizer.js",
  "bin": {
    "image-resizer": "resizer.js"
  },
  "scripts": {
    "start": "node resizer.js",
    "resize": "node resizer.js resize",
    "thumbnail": "node resizer.js thumbnail",
    "batch": "node resizer.js batch",
    "batch-thumbnails": "node resizer.js batch-thumbnails"
  },
  "keywords": [
    "image",
    "resize",
    "thumbnail",
    "sharp",
    "batch",
    "lanczos",
    "bicubic",
    "bilinear"
  ],
  "license": "MIT",
  "dependencies": {
    "sharp": "0.33.2"
  }
}
README.md
# Image Resizer and Thumbnail Generator (JavaScript / sharp)

A Node.js command-line tool for resizing images and generating thumbnails using
the sharp library with multiple resampling algorithms, crop modes, and batch processing.

## Dependencies

- **sharp 0.33.2** - High-performance image processing library built on libvips.

## Installation

```bash
npm install
```

## Usage

### Resize a single image

```bash
node resizer.js resize input.jpg output.jpg 800 600 -a lanczos
```

### Generate a thumbnail

```bash
node resizer.js thumbnail input.jpg thumb.jpg -s 150
```

### Batch resize

```bash
node resizer.js batch ./input_dir ./output_dir 1024 768 -a bicubic
```

### Batch thumbnails

```bash
node resizer.js batch-thumbnails ./input_dir ./thumbs -s 200
```

## Algorithms

| Algorithm  | Description                        |
|------------|------------------------------------|
| `nearest`  | Nearest-neighbor (fastest)         |
| `bilinear` | Bilinear interpolation             |
| `bicubic`  | Bicubic interpolation              |
| `lanczos`  | Lanczos3 resampling (best quality) |

## Crop Modes

| Mode           | Description                              |
|----------------|------------------------------------------|
| `center`       | Crop from the center                     |
| `top-left`     | Crop from the top-left corner            |
| `top-right`    | Crop from the top-right corner           |
| `bottom-left`  | Crop from the bottom-left corner         |
| `bottom-right` | Crop from the bottom-right corner        |
| `smart`        | Content-aware smart crop                 |

## Programmatic Usage

```javascript
const { resizeImage, generateThumbnail } = require('./resizer');

await resizeImage({
  inputPath: 'photo.jpg',
  outputPath: 'resized.jpg',
  width: 800,
  height: 600,
  algorithm: 'lanczos',
  cropMode: 'center',
});
```
resizer.js
#!/usr/bin/env node

/**
 * Image Resizer and Thumbnail Generator
 *
 * Resizes images using multiple algorithms (bilinear, bicubic, Lanczos),
 * supports crop modes, and batch processing of directories.
 *
 * Dependencies: sharp 0.33.2
 */

const sharp = require("sharp");
const path = require("path");
const fs = require("fs");

// Supported image extensions
const SUPPORTED_EXTENSIONS = new Set([
  ".jpg",
  ".jpeg",
  ".png",
  ".bmp",
  ".tiff",
  ".tif",
  ".webp",
  ".gif",
]);

// Algorithm mapping to sharp kernel options
const ALGORITHMS = {
  nearest: sharp.kernel.nearest,
  bilinear: sharp.kernel.cubic, // sharp uses cubic as bilinear equivalent
  bicubic: sharp.kernel.cubic,
  lanczos: sharp.kernel.lanczos3,
};

// Crop position mapping for sharp gravity
const CROP_POSITIONS = {
  center: sharp.gravity.centre,
  "top-left": sharp.gravity.northwest,
  "top-right": sharp.gravity.northeast,
  "bottom-left": sharp.gravity.southwest,
  "bottom-right": sharp.gravity.southeast,
  smart: sharp.strategy.attention,
};

/**
 * Get all supported image files from a directory.
 * @param {string} dirPath - Directory to scan.
 * @returns {string[]} Sorted array of image file paths.
 */
function getSupportedImages(dirPath) {
  if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
    throw new Error(`Not a directory: ${dirPath}`);
  }

  const files = fs.readdirSync(dirPath);
  return files
    .filter((f) => SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()))
    .sort()
    .map((f) => path.join(dirPath, f));
}

/**
 * Format bytes into human-readable size.
 * @param {number} bytes - File size in bytes.
 * @returns {string} Formatted size string.
 */
function formatSize(bytes) {
  const units = ["B", "KB", "MB", "GB"];
  let size = bytes;
  let unitIndex = 0;
  while (size >= 1024 && unitIndex < units.length - 1) {
    size /= 1024;
    unitIndex++;
  }
  return `${size.toFixed(1)} ${units[unitIndex]}`;
}

/**
 * Resize a single image.
 *
 * @param {Object} options - Resize options.
 * @param {string} options.inputPath - Path to source image.
 * @param {string} options.outputPath - Path for output image.
 * @param {number} options.width - Target width.
 * @param {number} options.height - Target height.
 * @param {string} [options.algorithm='lanczos'] - Resampling algorithm.
 * @param {string} [options.cropMode=null] - Crop mode for exact dimensions.
 * @param {boolean} [options.maintainAspect=true] - Keep aspect ratio.
 * @param {number} [options.quality=90] - Output quality (1-100).
 * @returns {Promise<Object>} Result with dimensions and sizes.
 */
async function resizeImage({
  inputPath,
  outputPath,
  width,
  height,
  algorithm = "lanczos",
  cropMode = null,
  maintainAspect = true,
  quality = 90,
}) {
  if (!ALGORITHMS[algorithm]) {
    throw new Error(
      `Unsupported algorithm '${algorithm}'. Choose from: ${Object.keys(ALGORITHMS).join(", ")}`
    );
  }

  const inputStat = fs.statSync(inputPath);
  const originalSize = inputStat.size;

  const metadata = await sharp(inputPath).metadata();
  const origWidth = metadata.width;
  const origHeight = metadata.height;

  let pipeline = sharp(inputPath);

  if (cropMode) {
    // Resize to cover the target area, then crop
    const position = CROP_POSITIONS[cropMode];
    if (!position) {
      throw new Error(
        `Unknown crop mode '${cropMode}'. Choose from: ${Object.keys(CROP_POSITIONS).join(", ")}`
      );
    }

    pipeline = pipeline.resize(width, height, {
      fit: "cover",
      position: position,
      kernel: ALGORITHMS[algorithm],
    });
  } else if (maintainAspect) {
    pipeline = pipeline.resize(width, height, {
      fit: "inside",
      withoutEnlargement: false,
      kernel: ALGORITHMS[algorithm],
    });
  } else {
    pipeline = pipeline.resize(width, height, {
      fit: "fill",
      kernel: ALGORITHMS[algorithm],
    });
  }

  // Set output quality based on format
  const ext = path.extname(outputPath).toLowerCase();
  if (ext === ".jpg" || ext === ".jpeg") {
    pipeline = pipeline.jpeg({ quality, mozjpeg: true });
  } else if (ext === ".png") {
    pipeline = pipeline.png({ compressionLevel: 6 });
  } else if (ext === ".webp") {
    pipeline = pipeline.webp({ quality });
  }

  // Ensure output directory exists
  const outputDir = path.dirname(outputPath);
  fs.mkdirSync(outputDir, { recursive: true });

  const outputInfo = await pipeline.toFile(outputPath);
  const newSize = fs.statSync(outputPath).size;

  return {
    input: inputPath,
    output: outputPath,
    originalDimensions: { width: origWidth, height: origHeight },
    newDimensions: { width: outputInfo.width, height: outputInfo.height },
    originalSize,
    newSize,
    algorithm,
  };
}

/**
 * Generate a square thumbnail from an image.
 */
async function generateThumbnail({
  inputPath,
  outputPath,
  size = 150,
  algorithm = "lanczos",
  quality = 85,
}) {
  return resizeImage({
    inputPath,
    outputPath,
    width: size,
    height: size,
    algorithm,
    cropMode: "center",
    quality,
  });
}

/**
 * Batch resize all images in a directory.
 */
async function batchResize({
  inputDir,
  outputDir,
  width,
  height,
  algorithm = "lanczos",
  cropMode = null,
  maintainAspect = true,
  quality = 90,
  outputFormat = null,
}) {
  const images = getSupportedImages(inputDir);

  if (images.length === 0) {
    console.log("No supported images found in the input directory.");
    return [];
  }

  console.log(`Found ${images.length} image(s) to process.`);

  const results = [];
  let successCount = 0;
  let errorCount = 0;

  for (const imgPath of images) {
    try {
      let outName = path.basename(imgPath);
      if (outputFormat) {
        outName =
          path.basename(imgPath, path.extname(imgPath)) + "." + outputFormat;
      }
      const outPath = path.join(outputDir, outName);

      const result = await resizeImage({
        inputPath: imgPath,
        outputPath: outPath,
        width,
        height,
        algorithm,
        cropMode,
        maintainAspect,
        quality,
      });

      results.push(result);
      successCount++;
      console.log(
        `  Resized: ${path.basename(imgPath)} ` +
          `(${result.originalDimensions.width}x${result.originalDimensions.height} -> ` +
          `${result.newDimensions.width}x${result.newDimensions.height})`
      );
    } catch (err) {
      errorCount++;
      console.error(`  Error: ${path.basename(imgPath)}: ${err.message}`);
    }
  }

  console.log(`\nDone! ${successCount} succeeded, ${errorCount} failed.`);
  return results;
}

/**
 * Batch generate thumbnails for all images in a directory.
 */
async function batchThumbnails({
  inputDir,
  outputDir,
  size = 150,
  algorithm = "lanczos",
  quality = 85,
}) {
  const images = getSupportedImages(inputDir);

  if (images.length === 0) {
    console.log("No supported images found.");
    return [];
  }

  console.log(`Generating thumbnails for ${images.length} image(s)...`);

  const results = [];
  for (const imgPath of images) {
    try {
      const outPath = path.join(
        outputDir,
        "thumb_" + path.basename(imgPath)
      );

      const result = await generateThumbnail({
        inputPath: imgPath,
        outputPath: outPath,
        size,
        algorithm,
        quality,
      });

      results.push(result);
      console.log(
        `  Thumbnail: ${path.basename(imgPath)} ` +
          `(${result.newDimensions.width}x${result.newDimensions.height})`
      );
    } catch (err) {
      console.error(`  Error: ${path.basename(imgPath)}: ${err.message}`);
    }
  }

  console.log(`\nGenerated ${results.length} thumbnail(s) in ${outputDir}`);
  return results;
}

// ============================================================================
// CLI
// ============================================================================

function printUsage() {
  console.log(`Image Resizer and Thumbnail Generator

Usage:
  node resizer.js resize <input> <output> <width> <height> [options]
  node resizer.js thumbnail <input> <output> [options]
  node resizer.js batch <input-dir> <output-dir> <width> <height> [options]
  node resizer.js batch-thumbnails <input-dir> <output-dir> [options]

Algorithms: nearest, bilinear, bicubic, lanczos (default)
Crop modes: center, top-left, top-right, bottom-left, bottom-right, smart

Options:
  --algorithm, -a   Resampling algorithm (default: lanczos)
  --crop, -c        Crop mode for exact dimensions
  --quality, -q     Output quality 1-100 (default: 90)
  --size, -s        Thumbnail size (default: 150)
  --format, -f      Output format (jpg, png, webp)
  --no-aspect       Stretch to exact dimensions
`);
}

function parseArgs(args) {
  const opts = {};
  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === "--algorithm" || arg === "-a") opts.algorithm = args[++i];
    else if (arg === "--crop" || arg === "-c") opts.cropMode = args[++i];
    else if (arg === "--quality" || arg === "-q")
      opts.quality = parseInt(args[++i]);
    else if (arg === "--size" || arg === "-s") opts.size = parseInt(args[++i]);
    else if (arg === "--format" || arg === "-f")
      opts.outputFormat = args[++i];
    else if (arg === "--no-aspect") opts.maintainAspect = false;
  }
  return opts;
}

async function main() {
  const args = process.argv.slice(2);

  if (args.length < 1) {
    printUsage();
    process.exit(0);
  }

  const command = args[0];
  const positional = args.slice(1).filter((a) => !a.startsWith("-"));
  const opts = parseArgs(args.slice(1));

  try {
    switch (command) {
      case "resize": {
        if (positional.length < 4) {
          console.error(
            "Usage: resize <input> <output> <width> <height> [options]"
          );
          process.exit(1);
        }
        const result = await resizeImage({
          inputPath: positional[0],
          outputPath: positional[1],
          width: parseInt(positional[2]),
          height: parseInt(positional[3]),
          ...opts,
        });
        console.log(`Resized: ${result.input}`);
        console.log(
          `  ${result.originalDimensions.width}x${result.originalDimensions.height} -> ` +
            `${result.newDimensions.width}x${result.newDimensions.height}`
        );
        console.log(
          `  Size: ${formatSize(result.originalSize)} -> ${formatSize(result.newSize)}`
        );
        console.log(`  Algorithm: ${result.algorithm}`);
        break;
      }

      case "thumbnail": {
        if (positional.length < 2) {
          console.error("Usage: thumbnail <input> <output> [options]");
          process.exit(1);
        }
        const result = await generateThumbnail({
          inputPath: positional[0],
          outputPath: positional[1],
          size: opts.size || 150,
          algorithm: opts.algorithm || "lanczos",
          quality: opts.quality || 85,
        });
        console.log(`Thumbnail created: ${result.output}`);
        console.log(
          `  Size: ${result.newDimensions.width}x${result.newDimensions.height}`
        );
        break;
      }

      case "batch": {
        if (positional.length < 4) {
          console.error(
            "Usage: batch <input-dir> <output-dir> <width> <height> [options]"
          );
          process.exit(1);
        }
        await batchResize({
          inputDir: positional[0],
          outputDir: positional[1],
          width: parseInt(positional[2]),
          height: parseInt(positional[3]),
          ...opts,
        });
        break;
      }

      case "batch-thumbnails": {
        if (positional.length < 2) {
          console.error(
            "Usage: batch-thumbnails <input-dir> <output-dir> [options]"
          );
          process.exit(1);
        }
        await batchThumbnails({
          inputDir: positional[0],
          outputDir: positional[1],
          size: opts.size || 150,
          algorithm: opts.algorithm || "lanczos",
          quality: opts.quality || 85,
        });
        break;
      }

      default:
        console.error(`Unknown command: ${command}`);
        printUsage();
        process.exit(1);
    }
  } catch (err) {
    console.error(`Error: ${err.message}`);
    process.exit(1);
  }
}

// Export functions for library use
module.exports = {
  resizeImage,
  generateThumbnail,
  batchResize,
  batchThumbnails,
  getSupportedImages,
};

// Run CLI if executed directly
if (require.main === module) {
  main();
}