← All tasks
cppcodex/cpp-t1 #20Not a task: repair not recorded

Image Watermarking Tool (cpp, written by Codex)

envgap__codex__cpp-t1-20

Written by a coding agent; not on GitHubWritten 2026-03-03

01 / FAILURE SIGNATURE

As the study recorded it

OpenCV headers not found - missing target_include_directories
Not a benchmark task.
  • It was made to work, but its repair cannot be rebuilt from the saved files (the saved copy shows no change, or not all of the changes the study's notes describe), so there is no fix to score against.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
CMakeLists.txt
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/cpp-t1 #20 · read the task the agent was given
Codex wrote this cpp 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 C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include:
- Source code
- CMakeLists.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 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.

CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(image_watermarking_tool_cpp VERSION 1.0.0 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

include(FetchContent)

FetchContent_Declare(
  opencv
  URL https://github.com/opencv/opencv/archive/refs/tags/4.10.0.zip
)
set(BUILD_LIST "core,imgproc,imgcodecs" CACHE STRING "" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_PERF_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(opencv)

add_executable(image_watermarking_tool src/main.cpp)
target_link_libraries(image_watermarking_tool PRIVATE opencv_core opencv_imgproc opencv_imgcodecs)
README.md
# Image Watermarking Tool (C++)

Adds text or PNG image watermarks with configurable position, opacity, tiling, and batch directory processing.

## Requirements

- Ubuntu 22.04
- G++ 12+
- CMake 3.22+

## Dependency (Pinned)

- OpenCV `4.10.0` (fetched by `FetchContent`)

## Build

```bash
cmake -S . -B build
cmake --build build -j
```

## Run

```bash
./build/image_watermarking_tool input.jpg --text "CONFIDENTIAL" --font-size 36 --color "#FFFFFF" --position bottom-right --opacity 0.3
./build/image_watermarking_tool input.jpg --watermark overlay.png --position center --opacity 0.5
./build/image_watermarking_tool input.jpg --text "PREVIEW" --tile --spacing 120 --angle -30 --opacity 0.2
./build/image_watermarking_tool ./images --batch --text "ARCHIVE" --position top-left --output ./watermarked
./build/image_watermarking_tool
```

## Notes

- Default output is `<original>_watermarked.<ext>`.
- `--position` supports named values and custom `x,y`.
- `--background-band` adds a semi-transparent strip under text.
- No-arg mode generates `sample_photo_1024x768.png` and writes text/tiled watermark demo outputs.
src/main.cpp
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>

#include <chrono>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <map>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

namespace fs = std::filesystem;

struct CliOptions {
    std::map<std::string, std::string> kv;
    std::vector<std::string> positional;
};

struct Result {
    std::string input;
    std::string watermarkType;
    std::string position;
    double opacity;
    std::string output;
    double ms;
};

const std::set<std::string> kSupported = {
    ".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"
};

CliOptions parseArgs(int argc, char** argv) {
    CliOptions out;
    for (int i = 1; i < argc; ++i) {
        std::string token = argv[i];
        if (token.rfind("--", 0) == 0) {
            std::string key = token.substr(2);
            if (i + 1 < argc) {
                std::string next = argv[i + 1];
                if (next.rfind("--", 0) != 0) {
                    out.kv[key] = next;
                    ++i;
                    continue;
                }
            }
            out.kv[key] = "true";
        } else {
            out.positional.push_back(token);
        }
    }
    return out;
}

std::string lower(std::string v) {
    for (char& c : v) {
        if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
    }
    return v;
}

bool isSupported(const fs::path& p) {
    if (!p.has_extension()) return false;
    return kSupported.count(lower(p.extension().string())) > 0;
}

double parseOpacity(const CliOptions& options) {
    double opacity = 0.3;
    if (options.kv.count("opacity")) opacity = std::stod(options.kv.at("opacity"));
    if (opacity < 0.0 || opacity > 1.0) {
        throw std::invalid_argument("Invalid --opacity. Use 0.0 to 1.0.");
    }
    return opacity;
}

cv::Scalar parseHexColor(const std::string& raw) {
    std::string v = raw.empty() ? "#FFFFFF" : raw;
    if (v[0] == '#') v = v.substr(1);
    if (v.size() != 6) throw std::invalid_argument("Invalid --color. Use #RRGGBB.");
    int r = std::stoi(v.substr(0, 2), nullptr, 16);
    int g = std::stoi(v.substr(2, 2), nullptr, 16);
    int b = std::stoi(v.substr(4, 2), nullptr, 16);
    return cv::Scalar(b, g, r);
}

cv::Mat toBgra(const cv::Mat& input) {
    if (input.channels() == 4) return input.clone();
    cv::Mat out;
    if (input.channels() == 3) {
        cv::cvtColor(input, out, cv::COLOR_BGR2BGRA);
    } else if (input.channels() == 1) {
        cv::cvtColor(input, out, cv::COLOR_GRAY2BGRA);
    } else {
        throw std::runtime_error("Unsupported channel layout.");
    }
    return out;
}

void blendAt(cv::Mat& baseBgra, const cv::Mat& wmBgra, int left, int top, double opacity) {
    if (opacity <= 0.0) return;
    int x0 = std::max(0, left);
    int y0 = std::max(0, top);
    int x1 = std::min(baseBgra.cols, left + wmBgra.cols);
    int y1 = std::min(baseBgra.rows, top + wmBgra.rows);
    if (x0 >= x1 || y0 >= y1) return;

    for (int y = y0; y < y1; ++y) {
        const int wy = y - top;
        for (int x = x0; x < x1; ++x) {
            const int wx = x - left;
            cv::Vec4b wmPx = wmBgra.at<cv::Vec4b>(wy, wx);
            cv::Vec4b basePx = baseBgra.at<cv::Vec4b>(y, x);
            double wa = (wmPx[3] / 255.0) * opacity;
            if (wa <= 0.0) continue;
            for (int c = 0; c < 3; ++c) {
                basePx[c] = static_cast<uchar>(wmPx[c] * wa + basePx[c] * (1.0 - wa));
            }
            basePx[3] = 255;
            baseBgra.at<cv::Vec4b>(y, x) = basePx;
        }
    }
}

std::pair<int, int> positionXY(const std::string& raw, int baseW, int baseH, int wmW, int wmH) {
    std::string value = lower(raw.empty() ? "bottom-right" : raw);
    int pad = 24;
    if (value == "center") return {std::max(0, (baseW - wmW) / 2), std::max(0, (baseH - wmH) / 2)};
    if (value == "top-left") return {pad, pad};
    if (value == "top-right") return {std::max(0, baseW - wmW - pad), pad};
    if (value == "bottom-left") return {pad, std::max(0, baseH - wmH - pad)};
    if (value == "bottom-right") return {std::max(0, baseW - wmW - pad), std::max(0, baseH - wmH - pad)};

    auto comma = value.find(',');
    if (comma == std::string::npos) {
        throw std::invalid_argument("Invalid --position. Use named position or x,y.");
    }
    int x = std::stoi(value.substr(0, comma));
    int y = std::stoi(value.substr(comma + 1));
    return {x, y};
}

cv::Mat textWatermark(const CliOptions& options) {
    auto it = options.kv.find("text");
    if (it == options.kv.end() || it->second.empty()) {
        throw std::invalid_argument("Missing --text value.");
    }
    std::string text = it->second;

    int fontSize = options.kv.count("font-size") ? std::stoi(options.kv.at("font-size")) : 24;
    if (fontSize <= 0) throw std::invalid_argument("Invalid --font-size.");

    if (options.kv.count("font")) {
        std::string font = options.kv.at("font");
        if (font.find('/') != std::string::npos || font.find('\\') != std::string::npos) {
            fs::path fp = fs::absolute(font);
            if (!fs::exists(fp)) {
                throw std::invalid_argument("Missing font file: " + fp.string());
            }
        }
    }

    cv::Scalar color = parseHexColor(options.kv.count("color") ? options.kv.at("color") : "#FFFFFF");
    double fontScale = std::max(0.5, fontSize / 30.0);
    int thickness = std::max(1, static_cast<int>(fontSize / 16.0));
    int baseline = 0;
    cv::Size textSize = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, fontScale, thickness, &baseline);
    int padX = std::max(12, fontSize / 2);
    int padY = std::max(10, fontSize / 3);
    int width = textSize.width + padX * 2;
    int height = textSize.height + padY * 2 + baseline;

    cv::Mat wm(height, width, CV_8UC4, cv::Scalar(0, 0, 0, 0));
    if (options.kv.count("background-band")) {
        int bandY = std::max(0, (height - textSize.height) / 2 - 6);
        int bandH = std::min(height - bandY, textSize.height + 12);
        cv::rectangle(wm, cv::Rect(0, bandY, width, bandH), cv::Scalar(0, 0, 0, 110), cv::FILLED);
    }
    cv::putText(
        wm,
        text,
        cv::Point(padX, padY + textSize.height),
        cv::FONT_HERSHEY_SIMPLEX,
        fontScale,
        cv::Scalar(color[0], color[1], color[2], 255),
        thickness,
        cv::LINE_AA
    );
    return wm;
}

cv::Mat imageWatermark(const CliOptions& options) {
    auto it = options.kv.find("watermark");
    if (it == options.kv.end() || it->second.empty()) {
        throw std::invalid_argument("Missing --watermark value.");
    }
    fs::path wmPath = fs::absolute(it->second);
    if (!fs::exists(wmPath)) throw std::invalid_argument("Watermark image not found: " + wmPath.string());
    if (lower(wmPath.extension().string()) != ".png") {
        throw std::invalid_argument("Image watermark must be a PNG with transparency.");
    }
    cv::Mat wm = cv::imread(wmPath.string(), cv::IMREAD_UNCHANGED);
    if (wm.empty()) throw std::invalid_argument("Invalid watermark image: " + wmPath.string());
    return toBgra(wm);
}

cv::Mat rotateImage(const cv::Mat& src, double angle) {
    cv::Point2f center(src.cols / 2.0f, src.rows / 2.0f);
    cv::Mat rot = cv::getRotationMatrix2D(center, angle, 1.0);
    cv::Rect2f bbox = cv::RotatedRect(center, src.size(), angle).boundingRect2f();
    rot.at<double>(0, 2) += bbox.width / 2.0 - center.x;
    rot.at<double>(1, 2) += bbox.height / 2.0 - center.y;
    cv::Mat out;
    cv::warpAffine(src, out, rot, bbox.size(), cv::INTER_CUBIC, cv::BORDER_CONSTANT, cv::Scalar(0, 0, 0, 0));
    return out;
}

fs::path outputPathFor(const fs::path& input, const CliOptions& options, bool batchMode) {
    const std::string stem = input.stem().string();
    const std::string ext = input.extension().string();
    if (!options.kv.count("output")) {
        return input.parent_path() / (stem + "_watermarked" + ext);
    }
    fs::path out = fs::absolute(options.kv.at("output"));
    if (batchMode) {
        fs::create_directories(out);
        return out / (stem + "_watermarked" + ext);
    }
    if (fs::exists(out) && fs::is_directory(out)) {
        fs::create_directories(out);
        return out / (stem + "_watermarked" + ext);
    }
    return out;
}

bool writeImagePreserveQuality(const fs::path& outPath, const cv::Mat& imageBgra) {
    std::vector<int> params;
    std::string ext = lower(outPath.extension().string());

    cv::Mat writeMat;
    if (ext == ".jpg" || ext == ".jpeg") {
        cv::Mat bgr;
        cv::cvtColor(imageBgra, bgr, cv::COLOR_BGRA2BGR);
        writeMat = bgr;
        params = {cv::IMWRITE_JPEG_QUALITY, 95};
    } else if (ext == ".png") {
        writeMat = imageBgra;
        params = {cv::IMWRITE_PNG_COMPRESSION, 3};
    } else if (ext == ".webp") {
        writeMat = imageBgra;
        params = {cv::IMWRITE_WEBP_QUALITY, 95};
    } else {
        writeMat = imageBgra;
    }
    return cv::imwrite(outPath.string(), writeMat, params);
}

Result watermarkOne(const fs::path& inputPath, const CliOptions& options, bool batchMode) {
    auto start = std::chrono::high_resolution_clock::now();
    if (!isSupported(inputPath)) {
        throw std::invalid_argument("Unsupported image format: " + inputPath.string());
    }

    cv::Mat input = cv::imread(inputPath.string(), cv::IMREAD_UNCHANGED);
    if (input.empty()) throw std::runtime_error("Invalid image: " + inputPath.string());
    if (static_cast<long long>(input.cols) * input.rows > 120000000LL) {
        throw std::runtime_error("Image too large for safe processing: " + inputPath.string());
    }
    cv::Mat base = toBgra(input);

    cv::Mat watermark;
    std::string type;
    if (options.kv.count("text")) {
        watermark = textWatermark(options);
        type = "text";
    } else if (options.kv.count("watermark")) {
        watermark = imageWatermark(options);
        type = "image";
    } else {
        throw std::invalid_argument("Provide either --text or --watermark.");
    }

    bool tile = options.kv.count("tile") > 0;
    if (!tile && (watermark.cols > base.cols || watermark.rows > base.rows)) {
        throw std::invalid_argument("Image too small for watermark.");
    }

    double opacity = parseOpacity(options);
    std::string positionDesc = options.kv.count("position") ? options.kv.at("position") : "bottom-right";

    if (tile) {
        int spacing = options.kv.count("spacing") ? std::stoi(options.kv.at("spacing")) : 100;
        if (spacing < 0) throw std::invalid_argument("Invalid --spacing. Must be >= 0.");
        double angle = options.kv.count("angle") ? std::stod(options.kv.at("angle")) : -30.0;
        cv::Mat rotated = rotateImage(watermark, angle);
        int stepX = std::max(1, rotated.cols + spacing);
        int stepY = std::max(1, rotated.rows + spacing);
        for (int y = -rotated.rows; y < base.rows + rotated.rows; y += stepY) {
            for (int x = -rotated.cols; x < base.cols + rotated.cols; x += stepX) {
                blendAt(base, rotated, x, y, opacity);
            }
        }
        positionDesc = "tile";
    } else {
        auto [x, y] = positionXY(positionDesc, base.cols, base.rows, watermark.cols, watermark.rows);
        blendAt(base, watermark, x, y, opacity);
    }

    fs::path outPath = outputPathFor(inputPath, options, batchMode);
    fs::create_directories(outPath.parent_path());
    if (!writeImagePreserveQuality(outPath, base)) {
        throw std::runtime_error("Failed to write output: " + outPath.string());
    }

    auto end = std::chrono::high_resolution_clock::now();
    double ms = std::chrono::duration<double, std::milli>(end - start).count();

    return {
        fs::absolute(inputPath).string(),
        type,
        positionDesc,
        opacity,
        outPath.string(),
        ms
    };
}

void printResult(const Result& r) {
    std::cout << "Input file: " << r.input << "\n";
    std::cout << "Watermark type: " << r.watermarkType << "\n";
    std::cout << "Position: " << r.position << "\n";
    std::cout << "Opacity: " << r.opacity << "\n";
    std::cout << "Output file: " << r.output << "\n";
    std::cout << "Processing time: " << std::fixed << std::setprecision(2) << r.ms << " ms\n\n";
}

std::vector<fs::path> listImages(const fs::path& dir) {
    if (!fs::exists(dir) || !fs::is_directory(dir)) {
        throw std::invalid_argument("--batch requires a directory input.");
    }
    std::vector<fs::path> out;
    for (const auto& entry : fs::directory_iterator(dir)) {
        if (entry.is_regular_file() && isSupported(entry.path())) {
            out.push_back(entry.path());
        }
    }
    return out;
}

fs::path createSample() {
    int width = 1024;
    int height = 768;
    cv::Mat image(height, width, CV_8UC3);

    for (int y = 0; y < height; ++y) {
        double t = static_cast<double>(y) / std::max(1, height - 1);
        int r = static_cast<int>((1.0 - t) * 111 + t * 33);
        int g = static_cast<int>((1.0 - t) * 177 + t * 95);
        int b = static_cast<int>((1.0 - t) * 252 + t * 145);
        cv::line(image, cv::Point(0, y), cv::Point(width - 1, y), cv::Scalar(b, g, r), 1);
    }
    for (int x = 0; x < width; x += 28) {
        cv::line(image, cv::Point(x, 0), cv::Point(x, height - 1), cv::Scalar(255, 255, 255), 1);
    }
    for (int y = 0; y < height; y += 28) {
        cv::line(image, cv::Point(0, y), cv::Point(width - 1, y), cv::Scalar(0, 0, 0), 1);
    }
    cv::circle(image, cv::Point(845, 135), 75, cv::Scalar(190, 245, 255), cv::FILLED);
    cv::rectangle(image, cv::Rect(0, 500, width, height - 500), cv::Scalar(95, 121, 43), cv::FILLED);

    fs::path out = fs::absolute("sample_photo_1024x768.png");
    if (!cv::imwrite(out.string(), image)) {
        throw std::runtime_error("Failed to write sample image.");
    }
    return out;
}

void runDemo() {
    fs::path sample = createSample();

    CliOptions textOpts;
    textOpts.kv["text"] = "CONFIDENTIAL";
    textOpts.kv["font-size"] = "42";
    textOpts.kv["color"] = "#FFFFFF";
    textOpts.kv["position"] = "bottom-right";
    textOpts.kv["opacity"] = "0.35";
    textOpts.kv["background-band"] = "true";
    textOpts.kv["output"] = fs::absolute("sample_text_watermarked.png").string();
    printResult(watermarkOne(sample, textOpts, false));

    CliOptions tiledOpts;
    tiledOpts.kv["text"] = "PREVIEW";
    tiledOpts.kv["font-size"] = "36";
    tiledOpts.kv["color"] = "#FFFFFF";
    tiledOpts.kv["opacity"] = "0.2";
    tiledOpts.kv["tile"] = "true";
    tiledOpts.kv["spacing"] = "120";
    tiledOpts.kv["angle"] = "-30";
    tiledOpts.kv["output"] = fs::absolute("sample_tiled_watermarked.png").string();
    printResult(watermarkOne(sample, tiledOpts, false));
}

int main(int argc, char** argv) {
    try {
        CliOptions options = parseArgs(argc, argv);
        bool batch = options.kv.count("batch") > 0;

        if (!batch && options.positional.empty()) {
            runDemo();
            return 0;
        }
        if (options.positional.empty()) {
            throw std::invalid_argument("Input path required.");
        }

        fs::path input = fs::absolute(options.positional.front());
        if (batch) {
            for (const auto& file : listImages(input)) {
                printResult(watermarkOne(file, options, true));
            }
            return 0;
        }

        if (!fs::exists(input) || !fs::is_regular_file(input)) {
            throw std::invalid_argument("Input file not found: " + input.string());
        }
        printResult(watermarkOne(input, options, false));
        return 0;
    } catch (const std::exception& ex) {
        std::cerr << "Error: " << ex.what() << "\n";
        return 1;
    }
}