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

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("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
  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);
});