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

Image Watermarking Tool (javascript, written by Claude Code)

envgap__claude-code__javascript-t1-20

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 #20 · 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 Watermarking Tool

Write a program that adds text and image watermarks to photographs with configurable positioning, opacity, tiling, and batch processing capabilities.

FUNCTIONAL REQUIREMENTS:
- Accept an image file path as a command-line argument
- Support text watermarks via --text flag with configurable font size (--font-size, default 24), color (--color, hex format, default #FFFFFF), and font family (--font, default: a bundled or system sans-serif font)
- Support image overlay watermarks via --watermark flag (accepts a PNG file path with transparency)
- Support configurable position via --position flag: center, top-left, top-right, bottom-left, bottom-right, or custom coordinates (x,y)
- Support configurable opacity via --opacity flag (0.0 to 1.0, default 0.3)
- Support tiled/repeated watermark mode via --tile flag that repeats the watermark across the entire image with configurable spacing (--spacing, default 100 pixels) and rotation angle (--angle, default -30 degrees)
- Support adding a semi-transparent background band behind text watermarks for better readability via --background-band flag
- Support batch watermarking of all images in a directory via --batch flag
- Preserve original image quality and metadata in the output
- Print processing details to console: input file, watermark type, position, opacity, output file, processing time
- Save watermarked images to a path specified by --output flag (default: original name with _watermarked suffix)
- If no input is given, generate a sample 1024x768 photograph-like image, apply both a text watermark and a tiled watermark, then save both versions to demonstrate the capabilities
- Handle errors: missing font files, invalid watermark images, unsupported image formats, images too small for the watermark

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-watermarking-tool",
    "version": "1.0.0",
    "description": "Image watermarking tool with text/image watermarks, positioning, opacity, tiling, rotation, and batch processing",
    "main": "watermark.js",
    "scripts": {
        "start": "node watermark.js",
        "text": "node watermark.js text",
        "image": "node watermark.js image"
    },
    "keywords": [
        "watermark",
        "image",
        "sharp",
        "text-to-svg",
        "batch"
    ],
    "license": "MIT",
    "dependencies": {
        "sharp": "0.33.2",
        "text-to-svg": "3.1.5"
    }
}
README.md
# Image Watermarking Tool (JavaScript / sharp + text-to-svg)

A Node.js command-line image watermarking tool using sharp for image processing and text-to-svg for text rendering. Supports text and image watermarks with positioning, opacity, tiling, rotation, and batch processing.

## Dependencies

- **sharp 0.33.2** - High-performance image processing library
- **text-to-svg 3.1.5** - Convert text to SVG paths for crisp text watermarks

## Installation

```bash
npm install
```

## Usage

### Text Watermark

```bash
node watermark.js text --input photo.png --output watermarked.png --text "Copyright 2024"

# With rotation and tiling
node watermark.js text --input photo.png --output watermarked.png --text "DRAFT" \
    --tile --rotation 45 --opacity 0.3 --tile-spacing "100,100"

# Batch processing
node watermark.js text --input ./photos/ --output ./watermarked/ --text "Sample" --batch
```

### Image Watermark

```bash
node watermark.js image --input photo.png --output watermarked.png --watermark logo.png

# Custom positioning
node watermark.js image --input photo.png --output watermarked.png --watermark logo.png \
    --position bottom-left --scale 0.15 --opacity 0.8

# Batch processing
node watermark.js image --input ./photos/ --output ./watermarked/ --watermark logo.png --batch
```

### Options

| Option | Description | Default |
|--------|-------------|---------|
| `--position` | Watermark position | bottom-right |
| `--font-size` | Font size for text | 40 |
| `--color` | Text color (CSS color) | rgba(255,255,255,0.5) |
| `--opacity` | Opacity from 0.0 to 1.0 | 0.5 |
| `--scale` | Image watermark scale | 0.2 |
| `--rotation` | Rotation in degrees | 0 |
| `--tile` | Tile watermark | off |
| `--tile-spacing` | Tile spacing as X,Y | 50,50 |
| `--batch` | Batch process directory | off |

## Features

- SVG-based text rendering for crisp watermarks at any size
- High-performance image processing via sharp (libvips)
- Nine named positions for watermark placement
- Adjustable opacity via alpha compositing
- Rotation with transparent background preservation
- Tiled watermark patterns
- Batch directory processing
watermark.js
/**
 * Image Watermarking Tool using sharp and text-to-svg.
 * Supports text and image watermarks with positioning, opacity, tiling, rotation,
 * and batch processing.
 */

const sharp = require('sharp');
const TextToSVG = require('text-to-svg');
const path = require('path');
const fs = require('fs');

const POSITIONS = [
    'top-left', 'top-center', 'top-right',
    'center-left', 'center', 'center-right',
    'bottom-left', 'bottom-center', 'bottom-right'
];

const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.webp'];

/**
 * Calculate position coordinates for watermark placement.
 */
function calculatePosition(baseW, baseH, wmW, wmH, position, offsetX = 0, offsetY = 0) {
    const positions = {
        'top-left':      { left: 0, top: 0 },
        'top-center':    { left: Math.floor((baseW - wmW) / 2), top: 0 },
        'top-right':     { left: baseW - wmW, top: 0 },
        'center-left':   { left: 0, top: Math.floor((baseH - wmH) / 2) },
        'center':        { left: Math.floor((baseW - wmW) / 2), top: Math.floor((baseH - wmH) / 2) },
        'center-right':  { left: baseW - wmW, top: Math.floor((baseH - wmH) / 2) },
        'bottom-left':   { left: 0, top: baseH - wmH },
        'bottom-center': { left: Math.floor((baseW - wmW) / 2), top: baseH - wmH },
        'bottom-right':  { left: baseW - wmW, top: baseH - wmH },
    };
    const pos = positions[position] || positions['bottom-right'];
    return { left: pos.left + offsetX, top: pos.top + offsetY };
}

/**
 * Create an SVG text watermark using text-to-svg.
 */
function createTextSVG(text, fontSize, color, fontPath) {
    const textToSVG = fontPath ? TextToSVG.loadSync(fontPath) : TextToSVG.loadSync();
    const attributes = { fill: color, stroke: 'none' };
    const options = { fontSize, anchor: 'top', attributes };
    const svg = textToSVG.getSVG(text, options);
    const metrics = textToSVG.getMetrics(text, options);
    return { svg, width: Math.ceil(metrics.width), height: Math.ceil(metrics.height) };
}

/**
 * Create a tiled watermark buffer covering the specified dimensions.
 */
async function createTiledLayer(stampBuffer, stampW, stampH, baseW, baseH, spacingX, spacingY) {
    const composites = [];
    const stepX = stampW + spacingX;
    const stepY = stampH + spacingY;

    for (let y = 0; y < baseH + stampH; y += stepY) {
        for (let x = 0; x < baseW + stampW; x += stepX) {
            composites.push({
                input: stampBuffer,
                left: Math.max(0, x),
                top: Math.max(0, y),
            });
        }
    }

    return sharp({
        create: {
            width: baseW,
            height: baseH,
            channels: 4,
            background: { r: 0, g: 0, b: 0, alpha: 0 },
        },
    })
        .composite(composites)
        .png()
        .toBuffer();
}

/**
 * Apply a text watermark to an image.
 */
async function applyTextWatermark(inputPath, outputPath, options = {}) {
    const {
        text = 'Watermark',
        position = 'bottom-right',
        fontSize = 40,
        color = 'rgba(255,255,255,0.5)',
        fontPath = null,
        opacity = 0.5,
        rotation = 0,
        tile = false,
        tileSpacingX = 50,
        tileSpacingY = 50,
        offsetX = 0,
        offsetY = 0,
    } = options;

    const baseImage = sharp(inputPath);
    const metadata = await baseImage.metadata();
    const baseW = metadata.width;
    const baseH = metadata.height;

    // Create text SVG
    const { svg, width: textW, height: textH } = createTextSVG(text, fontSize, color, fontPath);
    let stampBuffer = Buffer.from(svg);

    // Apply rotation to the stamp
    if (rotation !== 0) {
        stampBuffer = await sharp(stampBuffer)
            .rotate(rotation, { background: { r: 0, g: 0, b: 0, alpha: 0 } })
            .png()
            .toBuffer();
    }

    // Apply opacity
    if (opacity < 1.0) {
        const stampMeta = await sharp(stampBuffer).metadata();
        stampBuffer = await sharp(stampBuffer)
            .ensureAlpha()
            .composite([{
                input: Buffer.from(
                    `<svg><rect x="0" y="0" width="${stampMeta.width}" height="${stampMeta.height}" fill="rgba(255,255,255,${opacity})"/></svg>`
                ),
                blend: 'dest-in',
            }])
            .png()
            .toBuffer();
    }

    const stampMeta = await sharp(stampBuffer).metadata();
    let watermarkLayer;

    if (tile) {
        watermarkLayer = await createTiledLayer(
            stampBuffer, stampMeta.width, stampMeta.height,
            baseW, baseH, tileSpacingX, tileSpacingY
        );
    } else {
        watermarkLayer = stampBuffer;
    }

    const layerMeta = await sharp(watermarkLayer).metadata();
    const pos = tile
        ? { left: 0, top: 0 }
        : calculatePosition(baseW, baseH, layerMeta.width, layerMeta.height, position, offsetX, offsetY);

    // Clamp position to valid range
    pos.left = Math.max(0, Math.min(pos.left, baseW - 1));
    pos.top = Math.max(0, Math.min(pos.top, baseH - 1));

    await baseImage
        .composite([{ input: watermarkLayer, left: pos.left, top: pos.top }])
        .toFile(outputPath);

    return outputPath;
}

/**
 * Apply an image watermark to an image.
 */
async function applyImageWatermark(inputPath, outputPath, options = {}) {
    const {
        watermarkPath,
        position = 'bottom-right',
        opacity = 0.5,
        scale = 0.2,
        rotation = 0,
        tile = false,
        tileSpacingX = 50,
        tileSpacingY = 50,
        offsetX = 0,
        offsetY = 0,
    } = options;

    if (!watermarkPath) {
        throw new Error('watermarkPath is required for image watermarks');
    }

    const baseImage = sharp(inputPath);
    const metadata = await baseImage.metadata();
    const baseW = metadata.width;
    const baseH = metadata.height;

    // Scale the watermark
    const scaledW = Math.round(baseW * scale);
    let stampBuffer = await sharp(watermarkPath)
        .resize(scaledW)
        .ensureAlpha()
        .png()
        .toBuffer();

    // Apply rotation
    if (rotation !== 0) {
        stampBuffer = await sharp(stampBuffer)
            .rotate(rotation, { background: { r: 0, g: 0, b: 0, alpha: 0 } })
            .png()
            .toBuffer();
    }

    // Apply opacity
    if (opacity < 1.0) {
        const stampMeta = await sharp(stampBuffer).metadata();
        stampBuffer = await sharp(stampBuffer)
            .ensureAlpha()
            .composite([{
                input: Buffer.from(
                    `<svg><rect x="0" y="0" width="${stampMeta.width}" height="${stampMeta.height}" fill="rgba(255,255,255,${opacity})"/></svg>`
                ),
                blend: 'dest-in',
            }])
            .png()
            .toBuffer();
    }

    const stampMeta = await sharp(stampBuffer).metadata();
    let watermarkLayer;

    if (tile) {
        watermarkLayer = await createTiledLayer(
            stampBuffer, stampMeta.width, stampMeta.height,
            baseW, baseH, tileSpacingX, tileSpacingY
        );
    } else {
        watermarkLayer = stampBuffer;
    }

    const layerMeta = await sharp(watermarkLayer).metadata();
    const pos = tile
        ? { left: 0, top: 0 }
        : calculatePosition(baseW, baseH, layerMeta.width, layerMeta.height, position, offsetX, offsetY);

    pos.left = Math.max(0, Math.min(pos.left, baseW - 1));
    pos.top = Math.max(0, Math.min(pos.top, baseH - 1));

    await baseImage
        .composite([{ input: watermarkLayer, left: pos.left, top: pos.top }])
        .toFile(outputPath);

    return outputPath;
}

/**
 * Batch process all images in a directory.
 */
async function batchProcess(inputDir, outputDir, mode, options) {
    if (!fs.existsSync(outputDir)) {
        fs.mkdirSync(outputDir, { recursive: true });
    }

    const files = fs.readdirSync(inputDir)
        .filter(f => IMAGE_EXTENSIONS.includes(path.extname(f).toLowerCase()))
        .sort();

    const processed = [];

    for (const file of files) {
        const inputPath = path.join(inputDir, file);
        const outputPath = path.join(outputDir, file);

        try {
            if (mode === 'text') {
                await applyTextWatermark(inputPath, outputPath, options);
            } else {
                await applyImageWatermark(inputPath, outputPath, options);
            }
            processed.push(outputPath);
            console.log(`  Processed: ${file}`);
        } catch (err) {
            console.error(`  Error processing ${file}: ${err.message}`);
        }
    }

    return processed;
}

/**
 * Parse command-line arguments and run the tool.
 */
async function main() {
    const args = process.argv.slice(2);

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

    const command = args[0];
    const opts = {};
    let input = null, output = null;
    let batch = false;

    for (let i = 1; i < args.length; i++) {
        switch (args[i]) {
            case '--input': case '-i': input = args[++i]; break;
            case '--output': case '-o': output = args[++i]; break;
            case '--text': case '-t': opts.text = args[++i]; break;
            case '--watermark': case '-w': opts.watermarkPath = args[++i]; break;
            case '--position': case '-p': opts.position = args[++i]; break;
            case '--font-size': opts.fontSize = parseInt(args[++i]); break;
            case '--color': opts.color = args[++i]; break;
            case '--font-path': opts.fontPath = args[++i]; break;
            case '--opacity': opts.opacity = parseFloat(args[++i]); break;
            case '--scale': opts.scale = parseFloat(args[++i]); break;
            case '--rotation': opts.rotation = parseFloat(args[++i]); break;
            case '--tile': opts.tile = true; break;
            case '--tile-spacing': {
                const parts = args[++i].split(',');
                opts.tileSpacingX = parseInt(parts[0]);
                opts.tileSpacingY = parseInt(parts[1]);
                break;
            }
            case '--offset': {
                const parts = args[++i].split(',');
                opts.offsetX = parseInt(parts[0]);
                opts.offsetY = parseInt(parts[1]);
                break;
            }
            case '--batch': batch = true; break;
            default:
                console.error(`Unknown argument: ${args[i]}`);
                break;
        }
    }

    if (!input || !output) {
        console.error('Error: --input and --output are required.');
        process.exit(1);
    }

    try {
        if (batch) {
            console.log(`Batch processing directory: ${input}`);
            const results = await batchProcess(input, output, command, opts);
            console.log(`Processed ${results.length} images.`);
        } else if (command === 'text') {
            await applyTextWatermark(input, output, opts);
            console.log(`Watermarked image saved to: ${output}`);
        } else if (command === 'image') {
            await applyImageWatermark(input, output, opts);
            console.log(`Watermarked image saved to: ${output}`);
        } else {
            console.error(`Unknown command: ${command}`);
            printUsage();
            process.exit(1);
        }
    } catch (err) {
        console.error(`Error: ${err.message}`);
        process.exit(1);
    }
}

function printUsage() {
    console.log('Image Watermarking Tool');
    console.log('Usage:');
    console.log('  node watermark.js text --input <file> --output <file> --text <text> [options]');
    console.log('  node watermark.js image --input <file> --output <file> --watermark <file> [options]');
    console.log('');
    console.log('Options:');
    console.log('  --position <pos>       Position (e.g., bottom-right, center)');
    console.log('  --font-size <n>        Font size (default: 40)');
    console.log('  --color <color>        Text color (default: rgba(255,255,255,0.5))');
    console.log('  --opacity <0-1>        Opacity (default: 0.5)');
    console.log('  --scale <factor>       Watermark image scale (default: 0.2)');
    console.log('  --rotation <degrees>   Rotation angle (default: 0)');
    console.log('  --tile                 Tile watermark across image');
    console.log('  --tile-spacing <x,y>   Tile spacing (default: 50,50)');
    console.log('  --batch                Batch process a directory');
}

module.exports = {
    applyTextWatermark,
    applyImageWatermark,
    batchProcess,
    calculatePosition,
};

main().catch(console.error);