← All tasks
pythonclaude-code/python-t2 #20Lite task

Image Watermarking Tool (python, written by Claude Code)

envgap__claude-code__python-t2-20

Written by a coding agent; not on GitHubWritten 2026-02-28

01 / FAILURE SIGNATURE

Captured in a clean container

ImportError: libGL.so.1: cannot open shared object file: No such file or directory

02 / ENVIRONMENT RECIPE

Base commit
7f5fa0c22c21efa6382db334c680e5f4983233c3
Manifest
requirements.txt
Reproduce
true
Run under trace
rc=0; out=$(timeout 60 python3 watermark.py < /dev/null 2>&1 | { head -c 1000000; cat > /dev/null; }; exit ${PIPESTATUS[0]}) || rc=$?; printf '%s\n' "$out"; env_error='(ModuleNotFoundError|ImportError|No module named|cannot open shared object file|DLL load failed|shared library|cannot load library|Library not loaded|Cannot find module|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ERR_REQUIRE_ESM|compiled against a different Node|Could not find or load main class|ClassNotFoundException|NoClassDefFoundError|UnsupportedClassVersionError|UnsatisfiedLinkError|NoSuchMethodError|NoSuchFieldError|AbstractMethodError|IncompatibleClassChangeError|IllegalAccessError|ServiceConfigurationError|error while loading shared libraries|symbol lookup error|version `[^'"'"']*'"'"' not found|command not found)'; asked='(^| )[[:blank:]]*usage:|the following arguments are required|missing (required )?(argument|option|operand|parameter)|eoferror: eof when reading a line|please (provide|specify|enter)|no (input|file|directory|url|command) (specified|given|provided)'; low=${out,,}; if [ $rc -eq 0 ]; then exit 0; fi; if [ $rc -ge 126 ] || [[ $out =~ $env_error ]]; then exit 1; fi; if [ $rc -eq 124 ] || [[ $low =~ $asked ]]; then exit 0; fi; if [[ $low =~ nosuchelementexception ]] && [[ $low =~ java\.util\.scanner ]]; then exit 0; fi; exit 1
Reference environment fix used for admission
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+# System packages this project needs on a clean Ubuntu machine.
+set -e
+export DEBIAN_FRONTEND=noninteractive
+apt-get update -qq
+apt-get install -y -qq --no-install-recommends libgl1-mesa-glx libglib2.0-0

03 / TASK AND FAILURE

claude-code/python-t2 #20 · read the task the agent was given
Claude Code wrote this python project from the task below. It does not run 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 Python project for a clean Ubuntu 22.04 machine with only Python 3.10+ installed. Include:
- Source code
- requirements.txt 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 checked by running the task · needs human review

underspecification
Label rules and the text that matched
[
  {
    "category": "underspecification",
    "rule": "signature.missing_system_requirement",
    "source": "failure_signature",
    "excerpt": "ImportError: libGL.so.1: cannot open shared object file: No such file or directory"
  },
  {
    "category": "underspecification",
    "rule": "diff.adds_external_environment_requirement",
    "source": "manifest_diff:setup.sh",
    "excerpt": "export DEBIAN_FRONTEND=noninteractive"
  },
  {
    "category": "underspecification",
    "rule": "diff.adds_external_environment_requirement",
    "source": "manifest_diff:setup.sh",
    "excerpt": "apt-get install -y -qq --no-install-recommends libgl1-mesa-glx libglib2.0-0"
  }
]

Written by Claude Code (study run M1T2P20L1). It failed as written and was repaired by changing only its environment.

Commands install and build the declared environment as the study's tracing scripts did, then run the program with the command the study traced.

Preparation dates registries as the oracle does: PyPI index files filtered by upload_time <= the registry date, and yanks applied only when dated at or before it; alternate indexes, direct URLs, apt and arbitrary setup downloads are not network-enforced.

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

README.md
# Image Watermarking Tool (Python / OpenCV)

A command-line image watermarking tool using OpenCV for image processing. Supports text and image watermarks with positioning, opacity control, tiling, rotation, and batch processing.

## Dependencies

- **opencv-python 4.9.0.80** - OpenCV bindings for Python, providing image I/O, drawing, and transformations
- **numpy 1.26.4** - Numerical computing library for efficient pixel-level array operations

## Installation

```bash
pip install -r requirements.txt
```

## Usage

### Text Watermark

```bash
# Basic text watermark
python watermark.py text input.png output.png --text "Copyright 2024"

# Custom position and styling
python watermark.py text input.png output.png --text "CONFIDENTIAL" \
    --position center --font-scale 2.0 --thickness 3 --color "0,0,255,255" --opacity 0.7

# Rotated tiled watermark
python watermark.py text input.png output.png --text "DRAFT" \
    --tile --rotation 45 --opacity 0.3 --tile-spacing "100,100"

# Batch processing
python watermark.py text ./input_dir/ ./output_dir/ --text "Sample" --batch
```

### Image Watermark

```bash
# Basic image watermark
python watermark.py image input.png output.png --watermark logo.png

# Custom positioning and scale
python watermark.py image input.png output.png --watermark logo.png \
    --position bottom-left --scale 0.15 --opacity 0.8

# Tiled image watermark with rotation
python watermark.py image input.png output.png --watermark logo.png \
    --tile --rotation 30 --scale 0.1 --opacity 0.2

# Batch processing
python watermark.py image ./input_dir/ ./output_dir/ --watermark logo.png --batch
```

### Options

| Option | Description | Default |
|--------|-------------|---------|
| `--position` | Watermark position (e.g., bottom-right, center) | bottom-right |
| `--font-scale` | OpenCV font scale factor | 1.5 |
| `--thickness` | Text stroke thickness | 2 |
| `--color` | Color as B,G,R,A | 255,255,255,255 |
| `--opacity` | Opacity from 0.0 to 1.0 | 0.5 |
| `--scale` | Image watermark scale relative to base width | 0.2 |
| `--rotation` | Rotation angle in degrees | 0.0 |
| `--tile` | Tile watermark across the entire image | off |
| `--tile-spacing` | Spacing between tiles as X,Y | 50,50 |
| `--offset` | Position offset as X,Y | 0,0 |
| `--batch` | Process all images in a directory | off |

## Features

- Text watermarks using OpenCV font rendering with antialiasing
- Image watermarks with proportional scaling
- Nine named positions (top-left through bottom-right)
- Per-pixel alpha-blended opacity via NumPy vectorized operations
- Rotation with affine transformations and canvas expansion
- Tiled watermarks with configurable spacing
- Batch processing of entire directories
- Support for PNG, JPEG, BMP, TIFF, and WebP formats
requirements.txt
opencv-python==4.9.0.80
numpy==1.26.4
watermark.py
"""
Image Watermarking Tool using OpenCV (opencv-python) and NumPy.
Supports text and image watermarks with positioning, opacity, tiling, rotation,
and batch processing.
"""

import os
import sys
import argparse
import math
import numpy as np
import cv2


# Supported image extensions
IMAGE_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.bmp', '.tiff', '.webp')

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


def load_image(path: str) -> np.ndarray:
    """Load an image from the given path as BGRA."""
    if not os.path.isfile(path):
        raise FileNotFoundError(f"Image not found: {path}")
    img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
    if img is None:
        raise IOError(f"Failed to read image: {path}")
    if img.shape[2] == 3:
        img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
    return img


def calculate_position(base_w: int, base_h: int, wm_w: int, wm_h: int,
                       position: str, offset_x: int = 0, offset_y: int = 0) -> tuple:
    """Calculate watermark position based on named position string."""
    positions = {
        'top-left':      (0, 0),
        'top-center':    ((base_w - wm_w) // 2, 0),
        'top-right':     (base_w - wm_w, 0),
        'center-left':   (0, (base_h - wm_h) // 2),
        'center':        ((base_w - wm_w) // 2, (base_h - wm_h) // 2),
        'center-right':  (base_w - wm_w, (base_h - wm_h) // 2),
        'bottom-left':   (0, base_h - wm_h),
        'bottom-center': ((base_w - wm_w) // 2, base_h - wm_h),
        'bottom-right':  (base_w - wm_w, base_h - wm_h),
    }
    pos = positions.get(position, positions['bottom-right'])
    return (pos[0] + offset_x, pos[1] + offset_y)


def overlay_image(base: np.ndarray, overlay: np.ndarray, x: int, y: int,
                  opacity: float) -> None:
    """Overlay a BGRA image onto a BGRA base image at (x, y) with opacity."""
    oh, ow = overlay.shape[:2]
    bh, bw = base.shape[:2]

    # Compute the visible region
    x1 = max(0, x)
    y1 = max(0, y)
    x2 = min(bw, x + ow)
    y2 = min(bh, y + oh)

    if x1 >= x2 or y1 >= y2:
        return

    # Source region in overlay
    sx1 = x1 - x
    sy1 = y1 - y
    sx2 = sx1 + (x2 - x1)
    sy2 = sy1 + (y2 - y1)

    overlay_region = overlay[sy1:sy2, sx1:sx2]
    base_region = base[y1:y2, x1:x2]

    alpha = (overlay_region[:, :, 3].astype(np.float32) / 255.0) * opacity
    alpha_3ch = np.stack([alpha] * 3, axis=-1)

    base_region[:, :, :3] = (
        alpha_3ch * overlay_region[:, :, :3].astype(np.float32) +
        (1.0 - alpha_3ch) * base_region[:, :, :3].astype(np.float32)
    ).astype(np.uint8)

    base_region[:, :, 3] = np.clip(
        alpha * 255.0 + (1.0 - alpha) * base_region[:, :, 3].astype(np.float32),
        0, 255
    ).astype(np.uint8)


def rotate_image(img: np.ndarray, angle: float) -> np.ndarray:
    """Rotate an image by the given angle in degrees, expanding the canvas."""
    h, w = img.shape[:2]
    center = (w / 2.0, h / 2.0)
    rot_mat = cv2.getRotationMatrix2D(center, angle, 1.0)

    # Compute new bounding box size
    cos_a = abs(rot_mat[0, 0])
    sin_a = abs(rot_mat[0, 1])
    new_w = int(h * sin_a + w * cos_a)
    new_h = int(h * cos_a + w * sin_a)

    rot_mat[0, 2] += (new_w - w) / 2.0
    rot_mat[1, 2] += (new_h - h) / 2.0

    return cv2.warpAffine(img, rot_mat, (new_w, new_h),
                          flags=cv2.INTER_LINEAR,
                          borderMode=cv2.BORDER_CONSTANT,
                          borderValue=(0, 0, 0, 0))


def create_text_watermark(text: str, font_face: int = cv2.FONT_HERSHEY_SIMPLEX,
                          font_scale: float = 1.5, thickness: int = 2,
                          color: tuple = (255, 255, 255, 255)) -> np.ndarray:
    """Create a text watermark image with transparent background."""
    baseline = 0
    (tw, th), baseline = cv2.getTextSize(text, font_face, font_scale, thickness)

    padding = 10
    img = np.zeros((th + baseline + padding * 2, tw + padding * 2, 4), dtype=np.uint8)

    cv2.putText(img, text,
                (padding, th + padding),
                font_face, font_scale,
                (color[0], color[1], color[2], 255),
                thickness, cv2.LINE_AA)

    return img


def apply_text_watermark(base_image: np.ndarray, text: str,
                         position: str = 'bottom-right',
                         font_scale: float = 1.5, thickness: int = 2,
                         color: tuple = (255, 255, 255, 255),
                         opacity: float = 0.5, rotation: float = 0.0,
                         tile: bool = False, tile_spacing_x: int = 50,
                         tile_spacing_y: int = 50, offset_x: int = 0,
                         offset_y: int = 0) -> np.ndarray:
    """Apply a text watermark to an image."""
    base = base_image.copy()
    if base.shape[2] == 3:
        base = cv2.cvtColor(base, cv2.COLOR_BGR2BGRA)

    stamp = create_text_watermark(text, cv2.FONT_HERSHEY_SIMPLEX,
                                  font_scale, thickness, color)

    if rotation != 0.0:
        stamp = rotate_image(stamp, rotation)

    if tile:
        sh, sw = stamp.shape[:2]
        bh, bw = base.shape[:2]
        step_x = sw + tile_spacing_x
        step_y = sh + tile_spacing_y
        for ty in range(0, bh + sh, step_y):
            for tx in range(0, bw + sw, step_x):
                overlay_image(base, stamp, tx, ty, opacity)
    else:
        sh, sw = stamp.shape[:2]
        bh, bw = base.shape[:2]
        px, py = calculate_position(bw, bh, sw, sh, position, offset_x, offset_y)
        overlay_image(base, stamp, px, py, opacity)

    return base


def apply_image_watermark(base_image: np.ndarray, watermark_path: str,
                          position: str = 'bottom-right', opacity: float = 0.5,
                          scale: float = 0.2, rotation: float = 0.0,
                          tile: bool = False, tile_spacing_x: int = 50,
                          tile_spacing_y: int = 50, offset_x: int = 0,
                          offset_y: int = 0) -> np.ndarray:
    """Apply an image watermark to an image."""
    base = base_image.copy()
    if base.shape[2] == 3:
        base = cv2.cvtColor(base, cv2.COLOR_BGR2BGRA)

    wm = load_image(watermark_path)

    # Scale the watermark relative to the base image width
    bh, bw = base.shape[:2]
    scaled_w = int(bw * scale)
    aspect = wm.shape[0] / wm.shape[1]
    scaled_h = int(scaled_w * aspect)
    wm = cv2.resize(wm, (scaled_w, scaled_h), interpolation=cv2.INTER_LANCZOS4)

    if rotation != 0.0:
        wm = rotate_image(wm, rotation)

    if tile:
        wh, ww = wm.shape[:2]
        step_x = ww + tile_spacing_x
        step_y = wh + tile_spacing_y
        for ty in range(0, bh + wh, step_y):
            for tx in range(0, bw + ww, step_x):
                overlay_image(base, wm, tx, ty, opacity)
    else:
        wh, ww = wm.shape[:2]
        px, py = calculate_position(bw, bh, ww, wh, position, offset_x, offset_y)
        overlay_image(base, wm, px, py, opacity)

    return base


def batch_process(input_dir: str, output_dir: str, mode: str,
                  watermark_kwargs: dict) -> list:
    """Process all images in a directory with the given watermark settings."""
    os.makedirs(output_dir, exist_ok=True)
    processed = []

    for filename in sorted(os.listdir(input_dir)):
        if not any(filename.lower().endswith(ext) for ext in IMAGE_EXTENSIONS):
            continue

        input_path = os.path.join(input_dir, filename)
        output_path = os.path.join(output_dir, filename)

        try:
            base = load_image(input_path)
            if mode == 'text':
                result = apply_text_watermark(base, **watermark_kwargs)
            else:
                result = apply_image_watermark(base, **watermark_kwargs)

            # Convert to BGR for JPEG output
            if filename.lower().endswith(('.jpg', '.jpeg')):
                if result.shape[2] == 4:
                    result = cv2.cvtColor(result, cv2.COLOR_BGRA2BGR)

            cv2.imwrite(output_path, result)
            processed.append(output_path)
            print(f"  Processed: {filename}")
        except Exception as e:
            print(f"  Error processing {filename}: {e}")

    return processed


def parse_color(color_str: str) -> tuple:
    """Parse a color string like '255,255,255,128' into a BGRA tuple."""
    parts = [int(x.strip()) for x in color_str.split(',')]
    if len(parts) == 3:
        parts.append(255)
    return tuple(parts)


def main():
    parser = argparse.ArgumentParser(
        description="Image Watermarking Tool (OpenCV) - Add text or image watermarks"
    )
    subparsers = parser.add_subparsers(dest='command', help='Watermark type')

    # Text watermark subcommand
    text_parser = subparsers.add_parser('text', help='Add a text watermark')
    text_parser.add_argument('input', help='Input image path or directory for batch')
    text_parser.add_argument('output', help='Output image path or directory for batch')
    text_parser.add_argument('--text', '-t', required=True, help='Watermark text')
    text_parser.add_argument(
        '--position', '-p', default='bottom-right', choices=POSITIONS,
        help='Watermark position (default: bottom-right)')
    text_parser.add_argument(
        '--font-scale', type=float, default=1.5,
        help='OpenCV font scale factor (default: 1.5)')
    text_parser.add_argument(
        '--thickness', type=int, default=2,
        help='Text thickness (default: 2)')
    text_parser.add_argument(
        '--color', default='255,255,255,255',
        help='Text color as B,G,R,A (default: 255,255,255,255)')
    text_parser.add_argument(
        '--opacity', type=float, default=0.5,
        help='Opacity 0.0-1.0 (default: 0.5)')
    text_parser.add_argument(
        '--rotation', type=float, default=0.0,
        help='Rotation angle in degrees')
    text_parser.add_argument(
        '--tile', action='store_true',
        help='Tile watermark across entire image')
    text_parser.add_argument(
        '--tile-spacing', default='50,50',
        help='Tile spacing as X,Y (default: 50,50)')
    text_parser.add_argument(
        '--offset', default='0,0',
        help='Position offset as X,Y (default: 0,0)')
    text_parser.add_argument(
        '--batch', action='store_true',
        help='Process all images in input directory')

    # Image watermark subcommand
    img_parser = subparsers.add_parser('image', help='Add an image watermark')
    img_parser.add_argument('input', help='Input image path or directory for batch')
    img_parser.add_argument('output', help='Output image path or directory for batch')
    img_parser.add_argument(
        '--watermark', '-w', required=True, help='Watermark image path')
    img_parser.add_argument(
        '--position', '-p', default='bottom-right', choices=POSITIONS,
        help='Watermark position (default: bottom-right)')
    img_parser.add_argument(
        '--opacity', type=float, default=0.5,
        help='Opacity 0.0-1.0 (default: 0.5)')
    img_parser.add_argument(
        '--scale', type=float, default=0.2,
        help='Scale relative to base image width (default: 0.2)')
    img_parser.add_argument(
        '--rotation', type=float, default=0.0,
        help='Rotation angle in degrees')
    img_parser.add_argument(
        '--tile', action='store_true',
        help='Tile watermark across entire image')
    img_parser.add_argument(
        '--tile-spacing', default='50,50',
        help='Tile spacing as X,Y (default: 50,50)')
    img_parser.add_argument(
        '--offset', default='0,0',
        help='Position offset as X,Y (default: 0,0)')
    img_parser.add_argument(
        '--batch', action='store_true',
        help='Process all images in input directory')

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    if args.command == 'text':
        color = parse_color(args.color)
        tile_spacing = [int(x) for x in args.tile_spacing.split(',')]
        offset = [int(x) for x in args.offset.split(',')]

        kwargs = dict(
            text=args.text,
            position=args.position,
            font_scale=args.font_scale,
            thickness=args.thickness,
            color=color,
            opacity=args.opacity,
            rotation=args.rotation,
            tile=args.tile,
            tile_spacing_x=tile_spacing[0],
            tile_spacing_y=tile_spacing[1],
            offset_x=offset[0],
            offset_y=offset[1],
        )

        if args.batch:
            print(f"Batch processing directory: {args.input}")
            results = batch_process(args.input, args.output, 'text', kwargs)
            print(f"Processed {len(results)} images.")
        else:
            base = load_image(args.input)
            result = apply_text_watermark(base, **kwargs)
            if args.output.lower().endswith(('.jpg', '.jpeg')):
                result = cv2.cvtColor(result, cv2.COLOR_BGRA2BGR)
            cv2.imwrite(args.output, result)
            print(f"Watermarked image saved to: {args.output}")

    elif args.command == 'image':
        tile_spacing = [int(x) for x in args.tile_spacing.split(',')]
        offset = [int(x) for x in args.offset.split(',')]

        kwargs = dict(
            watermark_path=args.watermark,
            position=args.position,
            opacity=args.opacity,
            scale=args.scale,
            rotation=args.rotation,
            tile=args.tile,
            tile_spacing_x=tile_spacing[0],
            tile_spacing_y=tile_spacing[1],
            offset_x=offset[0],
            offset_y=offset[1],
        )

        if args.batch:
            print(f"Batch processing directory: {args.input}")
            results = batch_process(args.input, args.output, 'image', kwargs)
            print(f"Processed {len(results)} images.")
        else:
            base = load_image(args.input)
            result = apply_image_watermark(base, **kwargs)
            if args.output.lower().endswith(('.jpg', '.jpeg')):
                result = cv2.cvtColor(result, cv2.COLOR_BGRA2BGR)
            cv2.imwrite(args.output, result)
            print(f"Watermarked image saved to: {args.output}")


if __name__ == '__main__':
    main()