Image Watermarking Tool (java, written by Codex)
envgap__codex__java-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
pom.xml- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/java-t1 #20 · read the task the agent was given
Codex wrote this java 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 Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include: - Source code - pom.xml 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.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.tmlr</groupId>
<artifactId>image-watermarking-tool</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-bmp</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-tiff</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>org.sejda.imageio</groupId>
<artifactId>webp-imageio</artifactId>
<version>0.1.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>ImageWatermarkingTool</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
README.md
# Image Watermarking Tool (Java) Adds text or PNG-image watermarks with configurable position, opacity, tiling, and batch directory processing. ## Requirements - Ubuntu 22.04 - JDK 17+ - Maven 3.8+ ## Dependencies (Pinned) - `com.twelvemonkeys.imageio:imageio-core:3.11.0` - `com.twelvemonkeys.imageio:imageio-jpeg:3.11.0` - `com.twelvemonkeys.imageio:imageio-bmp:3.11.0` - `com.twelvemonkeys.imageio:imageio-tiff:3.11.0` - `org.sejda.imageio:webp-imageio:0.1.6` ## Build ```bash mvn clean package ``` ## Run ```bash java -cp target/image-watermarking-tool-1.0.0.jar ImageWatermarkingTool input.jpg --text "CONFIDENTIAL" --font-size 36 --color "#FFFFFF" --position bottom-right --opacity 0.3 java -cp target/image-watermarking-tool-1.0.0.jar ImageWatermarkingTool input.jpg --watermark overlay.png --position center --opacity 0.5 java -cp target/image-watermarking-tool-1.0.0.jar ImageWatermarkingTool input.jpg --text "PREVIEW" --tile --spacing 120 --angle -30 --opacity 0.2 java -cp target/image-watermarking-tool-1.0.0.jar ImageWatermarkingTool ./images --batch --text "ARCHIVE" --position top-left --output ./watermarked java -cp target/image-watermarking-tool-1.0.0.jar ImageWatermarkingTool ``` ## Notes - Default output path is `<original>_watermarked.<ext>`. - `--position` accepts named positions or `x,y`. - `--background-band` adds a translucent band under text. - No-arg mode generates `sample_photo_1024x768.png` and writes text/tiled demo outputs.
src/main/java/ImageWatermarkingTool.java
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public final class ImageWatermarkingTool {
private static final Set<String> SUPPORTED = Set.of("png", "jpg", "jpeg", "bmp", "tif", "tiff", "webp");
private static final Set<String> NAMED_POSITIONS = Set.of("center", "top-left", "top-right", "bottom-left", "bottom-right");
private record ParsedArgs(Map<String, String> options, List<String> positional) {}
private record Result(String input, String watermarkType, String position, double opacity, String output, double ms) {}
private ImageWatermarkingTool() {}
public static void main(String[] args) {
try {
ParsedArgs parsed = parseArgs(args);
boolean batch = parsed.options.containsKey("batch");
if (parsed.positional.isEmpty() && !batch) {
runDemo();
return;
}
if (parsed.positional.isEmpty()) {
throw new IllegalArgumentException("Input path required.");
}
Path input = Paths.get(parsed.positional.get(0)).toAbsolutePath();
if (batch) {
if (!Files.isDirectory(input)) {
throw new IllegalArgumentException("--batch requires a directory input.");
}
for (Path file : listImages(input)) {
printResult(watermarkFile(file, parsed.options, true));
}
} else {
if (!Files.isRegularFile(input)) {
throw new IllegalArgumentException("Input file not found: " + input);
}
printResult(watermarkFile(input, parsed.options, false));
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
System.exit(1);
}
}
private static ParsedArgs parseArgs(String[] args) {
Map<String, String> options = new HashMap<>();
List<String> positional = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
String token = args[i];
if (token.startsWith("--")) {
String key = token.substring(2);
if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
options.put(key, args[++i]);
} else {
options.put(key, "true");
}
} else {
positional.add(token);
}
}
return new ParsedArgs(options, positional);
}
private static String extension(Path file) {
String name = file.getFileName().toString();
int dot = name.lastIndexOf('.');
if (dot < 0) return "";
return name.substring(dot + 1).toLowerCase(Locale.ROOT);
}
private static String normalizedFormat(Path file) {
String ext = extension(file);
if ("jpg".equals(ext)) return "jpeg";
return ext;
}
private static List<Path> listImages(Path folder) throws Exception {
List<Path> files = new ArrayList<>();
try (var stream = Files.list(folder)) {
stream.filter(Files::isRegularFile).forEach(p -> {
if (SUPPORTED.contains(extension(p))) {
files.add(p.toAbsolutePath());
}
});
}
return files;
}
private static double parseOpacity(Map<String, String> options) {
double opacity = options.containsKey("opacity") ? Double.parseDouble(options.get("opacity")) : 0.3;
if (opacity < 0.0 || opacity > 1.0) {
throw new IllegalArgumentException("Invalid --opacity. Use 0.0 to 1.0.");
}
return opacity;
}
private static Color parseHexColor(String value) {
String raw = value == null ? "#FFFFFF" : value.trim();
if (!raw.matches("^#[0-9a-fA-F]{6}$")) {
throw new IllegalArgumentException("Invalid --color. Use #RRGGBB.");
}
return Color.decode(raw);
}
private static Font resolveFont(Map<String, String> options, int fontSize) throws Exception {
String fontOpt = options.get("font");
if (fontSize <= 0) throw new IllegalArgumentException("Invalid --font-size.");
if (fontOpt == null || fontOpt.isBlank()) {
return new Font("SansSerif", Font.PLAIN, fontSize);
}
if (fontOpt.contains("/") || fontOpt.contains("\\")) {
Path fontPath = Paths.get(fontOpt).toAbsolutePath();
if (!Files.exists(fontPath)) {
throw new IllegalArgumentException("Missing font file: " + fontPath);
}
Font base = Font.createFont(Font.TRUETYPE_FONT, fontPath.toFile());
return base.deriveFont((float) fontSize);
}
return new Font(fontOpt, Font.PLAIN, fontSize);
}
private static BufferedImage textWatermark(Map<String, String> options) throws Exception {
String text = options.get("text");
if (text == null || text.isBlank()) {
throw new IllegalArgumentException("Missing --text value.");
}
int fontSize = options.containsKey("font-size") ? Integer.parseInt(options.get("font-size")) : 24;
Font font = resolveFont(options, fontSize);
Color color = parseHexColor(options.getOrDefault("color", "#FFFFFF"));
BufferedImage probe = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
Graphics2D pg = probe.createGraphics();
pg.setFont(font);
FontMetrics fm = pg.getFontMetrics();
int textWidth = Math.max(1, fm.stringWidth(text));
int textHeight = Math.max(1, fm.getHeight());
pg.dispose();
int padX = Math.max(12, fontSize / 2);
int padY = Math.max(10, fontSize / 3);
int width = textWidth + padX * 2;
int height = textHeight + padY * 2;
BufferedImage out = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = out.createGraphics();
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setFont(font);
if (options.containsKey("background-band")) {
int y = Math.max(0, (height - textHeight) / 2 - 6);
int bandHeight = Math.min(height, textHeight + 12);
g.setColor(new Color(0, 0, 0, 110));
g.fillRect(0, y, width, bandHeight);
}
g.setColor(color);
g.drawString(text, padX, padY + fm.getAscent());
g.dispose();
return out;
}
private static BufferedImage imageWatermark(Map<String, String> options) throws Exception {
String pathValue = options.get("watermark");
if (pathValue == null || pathValue.isBlank()) {
throw new IllegalArgumentException("Missing --watermark value.");
}
Path wmPath = Paths.get(pathValue).toAbsolutePath();
if (!Files.exists(wmPath)) throw new IllegalArgumentException("Watermark image not found: " + wmPath);
if (!extension(wmPath).equals("png")) throw new IllegalArgumentException("Image watermark must be a PNG with transparency.");
BufferedImage wm = ImageIO.read(wmPath.toFile());
if (wm == null) throw new IllegalArgumentException("Invalid watermark image: " + wmPath);
return toArgb(wm);
}
private static BufferedImage toArgb(BufferedImage input) {
if (input.getType() == BufferedImage.TYPE_INT_ARGB) return input;
BufferedImage out = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = out.createGraphics();
g.drawImage(input, 0, 0, null);
g.dispose();
return out;
}
private static int[] parsePosition(String raw, int baseW, int baseH, int wmW, int wmH) {
String value = raw == null ? "bottom-right" : raw.toLowerCase(Locale.ROOT);
int pad = 24;
if (NAMED_POSITIONS.contains(value)) {
return switch (value) {
case "center" -> new int[]{Math.max(0, (baseW - wmW) / 2), Math.max(0, (baseH - wmH) / 2)};
case "top-left" -> new int[]{pad, pad};
case "top-right" -> new int[]{Math.max(0, baseW - wmW - pad), pad};
case "bottom-left" -> new int[]{pad, Math.max(0, baseH - wmH - pad)};
default -> new int[]{Math.max(0, baseW - wmW - pad), Math.max(0, baseH - wmH - pad)};
};
}
String[] parts = value.split(",", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid --position. Use named position or x,y.");
}
return new int[]{Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim())};
}
private static BufferedImage applyOpacity(BufferedImage watermark, double opacity) {
BufferedImage out = new BufferedImage(watermark.getWidth(), watermark.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = out.createGraphics();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) opacity));
g.drawImage(watermark, 0, 0, null);
g.dispose();
return out;
}
private static BufferedImage composeSingle(BufferedImage base, BufferedImage watermark, String position) {
BufferedImage out = new BufferedImage(base.getWidth(), base.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = out.createGraphics();
g.drawImage(base, 0, 0, null);
int[] xy = parsePosition(position, base.getWidth(), base.getHeight(), watermark.getWidth(), watermark.getHeight());
g.drawImage(watermark, xy[0], xy[1], null);
g.dispose();
return out;
}
private static BufferedImage rotate(BufferedImage input, double angleDegrees) {
double radians = Math.toRadians(angleDegrees);
double sin = Math.abs(Math.sin(radians));
double cos = Math.abs(Math.cos(radians));
int newW = (int) Math.floor(input.getWidth() * cos + input.getHeight() * sin);
int newH = (int) Math.floor(input.getHeight() * cos + input.getWidth() * sin);
BufferedImage out = new BufferedImage(Math.max(1, newW), Math.max(1, newH), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = out.createGraphics();
AffineTransform at = new AffineTransform();
at.translate(newW / 2.0, newH / 2.0);
at.rotate(radians);
at.translate(-input.getWidth() / 2.0, -input.getHeight() / 2.0);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.drawImage(input, at, null);
g.dispose();
return out;
}
private static BufferedImage composeTiled(BufferedImage base, BufferedImage watermark, int spacing, double angle) {
if (spacing < 0) {
throw new IllegalArgumentException("Invalid --spacing. Must be >= 0.");
}
BufferedImage rotated = rotate(watermark, angle);
int stepX = Math.max(1, rotated.getWidth() + spacing);
int stepY = Math.max(1, rotated.getHeight() + spacing);
BufferedImage out = new BufferedImage(base.getWidth(), base.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = out.createGraphics();
g.drawImage(base, 0, 0, null);
for (int y = -rotated.getHeight(); y < base.getHeight() + rotated.getHeight(); y += stepY) {
for (int x = -rotated.getWidth(); x < base.getWidth() + rotated.getWidth(); x += stepX) {
g.drawImage(rotated, x, y, null);
}
}
g.dispose();
return out;
}
private static Path outputPath(Path input, Map<String, String> options, boolean batchMode) throws Exception {
if (!options.containsKey("output")) {
return input.resolveSibling(input.getFileName().toString().replaceFirst("(\\.[^.]+)$", "_watermarked$1"));
}
Path out = Paths.get(options.get("output")).toAbsolutePath();
if (batchMode) {
Files.createDirectories(out);
return out.resolve(input.getFileName().toString().replaceFirst("(\\.[^.]+)$", "_watermarked$1"));
}
if (Files.exists(out) && Files.isDirectory(out)) {
return out.resolve(input.getFileName().toString().replaceFirst("(\\.[^.]+)$", "_watermarked$1"));
}
return out;
}
private static BufferedImage flattenOnWhite(BufferedImage rgba) {
BufferedImage rgb = new BufferedImage(rgba.getWidth(), rgba.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = rgb.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, rgba.getWidth(), rgba.getHeight());
g.drawImage(rgba, 0, 0, null);
g.dispose();
return rgb;
}
private static void writeImage(BufferedImage image, Path output, String format) throws Exception {
Files.createDirectories(output.toAbsolutePath().getParent());
String fmt = "jpg".equals(format) ? "jpeg" : format;
BufferedImage toWrite = image;
if ("jpeg".equals(fmt) && image.getColorModel().hasAlpha()) {
toWrite = flattenOnWhite(image);
}
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(fmt);
if (!writers.hasNext()) throw new IllegalArgumentException("No writer for format: " + fmt);
ImageWriter writer = writers.next();
try (ImageOutputStream ios = ImageIO.createImageOutputStream(output.toFile())) {
writer.setOutput(ios);
ImageWriteParam param = writer.getDefaultWriteParam();
if ("jpeg".equals(fmt) && param.canWriteCompressed()) {
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(0.95f);
}
writer.write(null, new IIOImage(toWrite, null, null), param);
} finally {
writer.dispose();
}
}
private static Result watermarkFile(Path input, Map<String, String> options, boolean batchMode) throws Exception {
String ext = extension(input);
if (!SUPPORTED.contains(ext)) {
throw new IllegalArgumentException("Unsupported image format: " + input);
}
long start = System.nanoTime();
BufferedImage src = ImageIO.read(input.toFile());
if (src == null) throw new IllegalArgumentException("Invalid image: " + input);
if ((long) src.getWidth() * src.getHeight() > 120_000_000L) {
throw new IllegalArgumentException("Image too large for safe processing: " + input);
}
BufferedImage base = toArgb(src);
BufferedImage watermark;
String type;
if (options.containsKey("text")) {
watermark = textWatermark(options);
type = "text";
} else if (options.containsKey("watermark")) {
watermark = imageWatermark(options);
type = "image";
} else {
throw new IllegalArgumentException("Provide either --text or --watermark.");
}
double opacity = parseOpacity(options);
boolean tile = options.containsKey("tile");
if (!tile && (watermark.getWidth() > base.getWidth() || watermark.getHeight() > base.getHeight())) {
throw new IllegalArgumentException("Image too small for watermark.");
}
BufferedImage wm = applyOpacity(watermark, opacity);
BufferedImage outputImage;
String positionValue;
if (tile) {
int spacing = options.containsKey("spacing") ? Integer.parseInt(options.get("spacing")) : 100;
double angle = options.containsKey("angle") ? Double.parseDouble(options.get("angle")) : -30.0;
outputImage = composeTiled(base, wm, spacing, angle);
positionValue = "tile";
} else {
positionValue = options.getOrDefault("position", "bottom-right");
outputImage = composeSingle(base, wm, positionValue);
}
Path outPath = outputPath(input, options, batchMode);
writeImage(outputImage, outPath, normalizedFormat(outPath));
double ms = (System.nanoTime() - start) / 1_000_000.0;
return new Result(input.toString(), type, positionValue, opacity, outPath.toString(), ms);
}
private static void printResult(Result result) {
System.out.println("Input file: " + result.input);
System.out.println("Watermark type: " + result.watermarkType);
System.out.println("Position: " + result.position);
System.out.println("Opacity: " + result.opacity);
System.out.println("Output file: " + result.output);
System.out.printf("Processing time: %.2f ms%n%n", result.ms);
}
private static Path createSample() throws Exception {
int width = 1024;
int height = 768;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
for (int y = 0; y < height; y++) {
float t = (float) y / Math.max(1, height - 1);
int r = (int) ((1f - t) * 111 + t * 33);
int gr = (int) ((1f - t) * 177 + t * 95);
int b = (int) ((1f - t) * 252 + t * 145);
g.setColor(new Color(r, gr, b));
g.drawLine(0, y, width, y);
}
g.setColor(new Color(255, 255, 255, 45));
for (int x = 0; x < width; x += 28) g.drawLine(x, 0, x, height);
g.setColor(new Color(0, 0, 0, 35));
for (int y = 0; y < height; y += 28) g.drawLine(0, y, width, y);
g.setColor(new Color(255, 245, 190));
g.fillOval(770, 60, 150, 150);
g.setColor(new Color(43, 121, 95));
g.fillRect(0, 500, width, height - 500);
g.dispose();
Path out = Paths.get("sample_photo_1024x768.png").toAbsolutePath();
writeImage(image, out, "png");
return out;
}
private static void runDemo() throws Exception {
Path sample = createSample();
Map<String, String> textOpts = new HashMap<>();
textOpts.put("text", "CONFIDENTIAL");
textOpts.put("font-size", "42");
textOpts.put("color", "#FFFFFF");
textOpts.put("position", "bottom-right");
textOpts.put("opacity", "0.35");
textOpts.put("background-band", "true");
textOpts.put("output", Paths.get("sample_text_watermarked.png").toAbsolutePath().toString());
printResult(watermarkFile(sample, textOpts, false));
Map<String, String> tiledOpts = new HashMap<>();
tiledOpts.put("text", "PREVIEW");
tiledOpts.put("font-size", "36");
tiledOpts.put("color", "#FFFFFF");
tiledOpts.put("opacity", "0.2");
tiledOpts.put("tile", "true");
tiledOpts.put("spacing", "120");
tiledOpts.put("angle", "-30");
tiledOpts.put("output", Paths.get("sample_tiled_watermarked.png").toAbsolutePath().toString());
printResult(watermarkFile(sample, tiledOpts, false));
}
}