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