Image Watermarking Tool (javascript, written by Codex)
envgap__codex__javascript-t1-20
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 #20 · 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 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
4 files, exactly as written, before any repair.
package-lock.json
{
"name": "image-watermarking-tool",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "image-watermarking-tool",
"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-watermarking-tool",
"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 Watermarking Tool (JavaScript) Adds text or PNG watermarks with position, opacity, tiling, and batch processing. ## Requirements - Ubuntu 22.04 - Node.js 20+ (LTS) ## Dependency (Pinned) - `sharp@0.33.5` ## Setup ```bash npm install ``` ## Run ```bash node src/index.js input.jpg --text "CONFIDENTIAL" --font-size 36 --color "#FFFFFF" --position bottom-right --opacity 0.3 node src/index.js input.jpg --watermark overlay.png --position center --opacity 0.5 node src/index.js input.jpg --text "PREVIEW" --tile --spacing 120 --angle -30 --opacity 0.2 node src/index.js ./images --batch --text "ARCHIVE" --position top-left --output ./watermarked node src/index.js ``` ## Notes - Default output path is `<original>_watermarked.<ext>`. - `--position` supports `center`, `top-left`, `top-right`, `bottom-left`, `bottom-right`, or custom `x,y`. - `--background-band` adds a semi-transparent band behind text. - No-arg mode creates a sample `1024x768` image and writes text and tiled watermark examples.
src/index.js
import fs from "fs";
import path from "path";
import sharp from "sharp";
const SUPPORTED = new Set([".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"]);
const NAMED_POSITIONS = new Set(["center", "top-left", "top-right", "bottom-left", "bottom-right"]);
function parseArgs(argv) {
const options = {};
const positional = [];
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token.startsWith("--")) {
const key = token.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith("--")) {
options[key] = next;
i += 1;
} else {
options[key] = true;
}
} else {
positional.push(token);
}
}
return { options, positional };
}
function parseOpacity(raw) {
const value = raw == null ? 0.3 : Number(raw);
if (!Number.isFinite(value) || value < 0 || value > 1) {
throw new Error("Invalid --opacity. Use a value between 0.0 and 1.0.");
}
return value;
}
function parseHexColor(raw) {
const value = String(raw || "#FFFFFF");
if (!/^#[0-9a-f]{6}$/i.test(value)) {
throw new Error("Invalid --color. Use #RRGGBB.");
}
return value;
}
function parsePosition(raw, baseW, baseH, wmW, wmH) {
const value = String(raw || "bottom-right").toLowerCase();
const pad = 24;
if (NAMED_POSITIONS.has(value)) {
if (value === "center") return { left: Math.max(0, Math.round((baseW - wmW) / 2)), top: Math.max(0, Math.round((baseH - wmH) / 2)) };
if (value === "top-left") return { left: pad, top: pad };
if (value === "top-right") return { left: Math.max(0, baseW - wmW - pad), top: pad };
if (value === "bottom-left") return { left: pad, top: Math.max(0, baseH - wmH - pad) };
return { left: Math.max(0, baseW - wmW - pad), top: Math.max(0, baseH - wmH - pad) };
}
const match = /^(-?\d+),(-?\d+)$/.exec(value);
if (!match) {
throw new Error("Invalid --position. Use center/top-left/top-right/bottom-left/bottom-right or x,y.");
}
return { left: Number(match[1]), top: Number(match[2]) };
}
function parseOutputPath(inputPath, options, batchMode) {
const ext = path.extname(inputPath);
const stem = path.basename(inputPath, ext);
if (!options.output) {
return path.join(path.dirname(inputPath), `${stem}_watermarked${ext}`);
}
const outPath = path.resolve(String(options.output));
if (batchMode) {
fs.mkdirSync(outPath, { recursive: true });
return path.join(outPath, `${stem}_watermarked${ext}`);
}
const exists = fs.existsSync(outPath) && fs.statSync(outPath).isDirectory();
if (exists) {
fs.mkdirSync(outPath, { recursive: true });
return path.join(outPath, `${stem}_watermarked${ext}`);
}
return outPath;
}
function ensureFontOption(fontValue) {
if (!fontValue) return "sans-serif";
if (fontValue.includes("/") || fontValue.includes("\\")) {
const resolved = path.resolve(fontValue);
if (!fs.existsSync(resolved)) {
throw new Error(`Missing font file: ${resolved}`);
}
return "sans-serif";
}
return fontValue;
}
async function createTextWatermark(text, options) {
const fontSize = Number.parseInt(String(options["font-size"] || "24"), 10);
if (!Number.isFinite(fontSize) || fontSize <= 0) {
throw new Error("Invalid --font-size.");
}
const color = parseHexColor(options.color || "#FFFFFF");
const fontFamily = ensureFontOption(options.font || "sans-serif");
const padding = Math.max(12, Math.round(fontSize * 0.5));
const width = Math.max(100, Math.round(text.length * fontSize * 0.62 + padding * 2));
const height = Math.max(40, Math.round(fontSize * 1.8 + padding));
const band = options["background-band"] ? `<rect x="0" y="${Math.round(height * 0.2)}" width="${width}" height="${Math.round(height * 0.65)}" fill="rgba(0,0,0,0.4)" />` : "";
const escaped = text
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">");
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
${band}
<text x="${padding}" y="${Math.round(height * 0.7)}"
font-size="${fontSize}" font-family="${fontFamily}" fill="${color}">${escaped}</text>
</svg>`;
const buffer = await sharp(Buffer.from(svg)).png().toBuffer();
const meta = await sharp(buffer).metadata();
return { buffer, width: meta.width || width, height: meta.height || height, type: "text" };
}
async function createImageWatermark(inputPath) {
const resolved = path.resolve(inputPath);
if (!fs.existsSync(resolved)) {
throw new Error(`Watermark image not found: ${resolved}`);
}
const ext = path.extname(resolved).toLowerCase();
if (ext !== ".png") {
throw new Error("Image watermark must be a PNG with transparency.");
}
const buffer = fs.readFileSync(resolved);
const meta = await sharp(buffer).metadata();
if (!meta.width || !meta.height) {
throw new Error("Invalid watermark image.");
}
return { buffer, width: meta.width, height: meta.height, type: "image" };
}
function imageCandidates(dir) {
return fs
.readdirSync(dir, { withFileTypes: true })
.filter((d) => d.isFile())
.map((d) => path.join(dir, d.name))
.filter((p) => SUPPORTED.has(path.extname(p).toLowerCase()));
}
async function createTiledLayer(baseW, baseH, watermarkBuffer, spacing, angle) {
const rotated = await sharp(watermarkBuffer)
.rotate(angle, { background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toBuffer();
const meta = await sharp(rotated).metadata();
const rw = meta.width || 1;
const rh = meta.height || 1;
const stepX = Math.max(1, rw + spacing);
const stepY = Math.max(1, rh + spacing);
const composites = [];
for (let y = -rh; y < baseH + rh; y += stepY) {
for (let x = -rw; x < baseW + rw; x += stepX) {
composites.push({ input: rotated, left: x, top: y });
}
}
return sharp({
create: {
width: baseW,
height: baseH,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 },
},
})
.composite(composites)
.png()
.toBuffer();
}
async function watermarkOne(inputPath, options) {
const started = Date.now();
const inputAbs = path.resolve(inputPath);
if (!fs.existsSync(inputAbs)) throw new Error(`Input file not found: ${inputAbs}`);
if (!SUPPORTED.has(path.extname(inputAbs).toLowerCase())) throw new Error(`Unsupported image format: ${inputAbs}`);
const base = sharp(inputAbs, { failOn: "error" });
const metadata = await base.metadata();
const width = metadata.width || 0;
const height = metadata.height || 0;
if (width <= 0 || height <= 0) throw new Error(`Invalid image: ${inputAbs}`);
if (width * height > 120_000_000) throw new Error(`Image too large for safe processing: ${inputAbs}`);
let watermark;
if (options.text) watermark = await createTextWatermark(String(options.text), options);
else if (options.watermark) watermark = await createImageWatermark(String(options.watermark));
else throw new Error("Provide either --text or --watermark.");
if (!options.tile && (watermark.width > width || watermark.height > height)) {
throw new Error("Image too small for watermark.");
}
const opacity = parseOpacity(options.opacity);
const tile = Boolean(options.tile);
const spacing = Number.parseInt(String(options.spacing || "100"), 10);
const angle = Number.parseFloat(String(options.angle || "-30"));
if (!Number.isFinite(spacing) || spacing < 0) throw new Error("Invalid --spacing.");
if (!Number.isFinite(angle)) throw new Error("Invalid --angle.");
let pipeline = sharp(inputAbs);
if (tile) {
const tiled = await createTiledLayer(width, height, watermark.buffer, spacing, angle);
pipeline = pipeline.composite([{ input: tiled, blend: "over", opacity }]);
} else {
const pos = parsePosition(options.position || "bottom-right", width, height, watermark.width, watermark.height);
pipeline = pipeline.composite([{ input: watermark.buffer, left: pos.left, top: pos.top, blend: "over", opacity }]);
}
const outputPath = parseOutputPath(inputAbs, options, Boolean(options.batch));
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
await pipeline.withMetadata().toFile(outputPath);
return {
input: inputAbs,
watermarkType: watermark.type,
position: tile ? "tile" : String(options.position || "bottom-right"),
opacity,
output: outputPath,
ms: Date.now() - started,
};
}
function printResult(result) {
console.log(`Input file: ${result.input}`);
console.log(`Watermark type: ${result.watermarkType}`);
console.log(`Position: ${result.position}`);
console.log(`Opacity: ${result.opacity}`);
console.log(`Output file: ${result.output}`);
console.log(`Processing time: ${result.ms} ms`);
console.log("");
}
async function createSampleImage() {
const out = path.resolve("sample_photo_1024x768.png");
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="768">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#6fb1fc"/>
<stop offset="100%" stop-color="#4364f7"/>
</linearGradient>
<linearGradient id="ground" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#2a9d8f"/>
<stop offset="100%" stop-color="#1d3557"/>
</linearGradient>
</defs>
<rect width="1024" height="500" fill="url(#sky)"/>
<rect y="500" width="1024" height="268" fill="url(#ground)"/>
<circle cx="820" cy="120" r="70" fill="#fff6b7" opacity="0.9"/>
<g stroke="rgba(255,255,255,0.18)">
${Array.from({ length: 34 }, (_, i) => `<line x1="${i * 30}" y1="0" x2="${i * 30}" y2="768"/>`).join("")}
${Array.from({ length: 26 }, (_, i) => `<line x1="0" y1="${i * 30}" x2="1024" y2="${i * 30}"/>`).join("")}
</g>
</svg>`;
await sharp(Buffer.from(svg)).png().toFile(out);
return out;
}
async function runDemo() {
const sample = await createSampleImage();
const textResult = await watermarkOne(sample, {
text: "CONFIDENTIAL",
"font-size": "42",
color: "#FFFFFF",
opacity: "0.35",
position: "bottom-right",
"background-band": true,
output: path.resolve("sample_text_watermarked.png"),
});
printResult(textResult);
const tileResult = await watermarkOne(sample, {
text: "PREVIEW",
"font-size": "36",
color: "#FFFFFF",
opacity: "0.2",
tile: true,
spacing: "120",
angle: "-30",
output: path.resolve("sample_tiled_watermarked.png"),
});
printResult(tileResult);
}
async function main() {
const { options, positional } = parseArgs(process.argv.slice(2));
if (!positional.length && !options.batch) {
await runDemo();
return;
}
if (!positional.length) {
throw new Error("Input path required.");
}
const inputPath = path.resolve(positional[0]);
if (options.batch) {
if (!fs.existsSync(inputPath) || !fs.statSync(inputPath).isDirectory()) {
throw new Error("--batch requires a directory input.");
}
for (const file of imageCandidates(inputPath)) {
const result = await watermarkOne(file, { ...options, batch: true });
printResult(result);
}
return;
}
const result = await watermarkOne(inputPath, options);
printResult(result);
}
main().catch((error) => {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});