← All tasks
javaclaude-code/java-t1 #22Not a task: repair changed code

EXIF Metadata Extractor (java, written by Claude Code)

envgap__claude-code__java-t1-22

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

01 / FAILURE SIGNATURE

As the study recorded it

unreported exception MetadataException must be caught + source in project root
Not a benchmark task.
  • Its repair changed source code, so it is not an environment task.

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

claude-code/java-t1 #22 · read the task the agent was given
Claude Code wrote this java project from the task below. It does not run on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: EXIF Metadata Extractor

Write a program that reads, displays, and modifies EXIF metadata embedded in image files, supporting GPS coordinate extraction, metadata comparison between images, and bulk metadata operations.

FUNCTIONAL REQUIREMENTS:
- Accept an image file path as a command-line argument (support JPEG, TIFF, and PNG files)
- Extract and display all EXIF tags organized by category: Camera Info (make, model, serial), Capture Settings (exposure time, f-number, ISO, focal length, flash), Date/Time (original, digitized, modified), GPS (latitude, longitude, altitude with decimal degree conversion), Image Info (dimensions, orientation, color space, resolution)
- Support output in multiple formats via --format flag: text (default human-readable), json, csv
- Extract GPS coordinates and convert to both DMS (degrees/minutes/seconds) and decimal degrees format, with an optional Google Maps URL via --map-url flag
- Compare metadata between two images via --compare flag, showing differences side by side
- Support removing specific EXIF tags via --remove flag (e.g., --remove GPS to strip all GPS data for privacy)
- Support removing all metadata via --strip-all flag while preserving image quality
- Support batch extraction from all images in a directory via --batch flag, with a summary CSV output
- Print the metadata summary to console in a structured, readable format
- Save the full metadata dump to a file via --output flag
- If no input is given, generate a sample JPEG image with comprehensive EXIF metadata including GPS coordinates, camera info, and capture settings, then extract and display all metadata
- Handle errors: images without EXIF data, corrupted metadata, unsupported tag types, and read-only files

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.

ExifExtractor.java
package com.exiftool;

import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.Directory;
import com.drew.metadata.Tag;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.exif.GpsDirectory;
import com.drew.lang.GeoLocation;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;

/**
 * EXIF Metadata Extractor - Reads and extracts EXIF metadata from images
 * with GPS coordinate conversion and batch processing support.
 *
 * Dependencies:
 *   - metadata-extractor (2.19.0): EXIF metadata reading
 *   - Gson (2.10.1): JSON serialization
 */
public class ExifExtractor {

    private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
    private static final Set<String> SUPPORTED_EXTENSIONS = Set.of(
            ".jpg", ".jpeg", ".tiff", ".tif", ".png", ".gif", ".bmp", ".webp"
    );

    // --- GPS Converter ---

    public static class GPSConverter {

        public static double dmsToDecimal(double degrees, double minutes,
                                          double seconds, String direction) {
            double decimal = degrees + minutes / 60.0 + seconds / 3600.0;
            if ("S".equalsIgnoreCase(direction) || "W".equalsIgnoreCase(direction)) {
                decimal = -decimal;
            }
            return Math.round(decimal * 1e8) / 1e8;
        }

        public static double[] decimalToDms(double decimalDeg) {
            double abs = Math.abs(decimalDeg);
            int degrees = (int) abs;
            double minutesFloat = (abs - degrees) * 60;
            int minutes = (int) minutesFloat;
            double seconds = Math.round((minutesFloat - minutes) * 60 * 10000.0) / 10000.0;
            return new double[]{degrees, minutes, seconds};
        }

        public static String formatCoordinate(double decimal, boolean isLatitude) {
            double[] dms = decimalToDms(decimal);
            String direction;
            if (isLatitude) {
                direction = decimal >= 0 ? "N" : "S";
            } else {
                direction = decimal >= 0 ? "E" : "W";
            }
            return String.format("%.0fd %d' %.4f\" %s (%.8f)",
                    dms[0], (int) dms[1], dms[2], direction, decimal);
        }

        public static String getGoogleMapsUrl(double latitude, double longitude) {
            return String.format("https://www.google.com/maps?q=%f,%f",
                    latitude, longitude);
        }
    }

    // --- Data classes ---

    public static class ImageInfo {
        public String filename;
        public String absolutePath;
        public long fileSizeBytes;
        public Map<String, Map<String, String>> metadata = new LinkedHashMap<>();
        public GPSData gpsData;
        public String status = "success";
        public String error;

        public ImageInfo(String filename, String absolutePath) {
            this.filename = filename;
            this.absolutePath = absolutePath;
        }
    }

    public static class GPSData {
        public double latitude;
        public double longitude;
        public String latitudeFormatted;
        public String longitudeFormatted;
        public Double altitudeMeters;
        public String googleMapsUrl;
    }

    public static class GPSReportEntry {
        public String filename;
        public double latitude;
        public double longitude;
        public Double altitude;
        public String mapsUrl;
    }

    public static class BatchResult {
        public int totalFiles;
        public int processed;
        public int errors;
        public int withGps;
        public List<ImageInfo> results = new ArrayList<>();
    }

    // --- Core extraction ---

    public static ImageInfo extractMetadata(File imageFile) {
        ImageInfo info = new ImageInfo(imageFile.getName(), imageFile.getAbsolutePath());
        info.fileSizeBytes = imageFile.length();

        try {
            Metadata metadata = ImageMetadataReader.readMetadata(imageFile);

            for (Directory directory : metadata.getDirectories()) {
                Map<String, String> tags = new LinkedHashMap<>();
                for (Tag tag : directory.getTags()) {
                    String description = tag.getDescription();
                    if (description != null && description.length() > 200) {
                        description = description.substring(0, 197) + "...";
                    }
                    tags.put(tag.getTagName(), description);
                }
                if (!tags.isEmpty()) {
                    info.metadata.put(directory.getName(), tags);
                }
            }

            // Extract GPS data
            GpsDirectory gpsDir = metadata.getFirstDirectoryOfType(GpsDirectory.class);
            if (gpsDir != null) {
                GeoLocation geoLocation = gpsDir.getGeoLocation();
                if (geoLocation != null && !geoLocation.isZero()) {
                    GPSData gps = new GPSData();
                    gps.latitude = geoLocation.getLatitude();
                    gps.longitude = geoLocation.getLongitude();
                    gps.latitudeFormatted = GPSConverter.formatCoordinate(
                            gps.latitude, true);
                    gps.longitudeFormatted = GPSConverter.formatCoordinate(
                            gps.longitude, false);
                    gps.googleMapsUrl = GPSConverter.getGoogleMapsUrl(
                            gps.latitude, gps.longitude);

                    // Altitude
                    if (gpsDir.containsTag(GpsDirectory.TAG_ALTITUDE)) {
                        gps.altitudeMeters = gpsDir.getDouble(GpsDirectory.TAG_ALTITUDE);
                    }

                    info.gpsData = gps;
                }
            }

        } catch (ImageProcessingException | IOException e) {
            info.status = "error";
            info.error = e.getMessage();
        }

        return info;
    }

    public static GPSData extractGPSOnly(File imageFile) {
        try {
            Metadata metadata = ImageMetadataReader.readMetadata(imageFile);
            GpsDirectory gpsDir = metadata.getFirstDirectoryOfType(GpsDirectory.class);
            if (gpsDir != null) {
                GeoLocation geo = gpsDir.getGeoLocation();
                if (geo != null && !geo.isZero()) {
                    GPSData gps = new GPSData();
                    gps.latitude = geo.getLatitude();
                    gps.longitude = geo.getLongitude();
                    gps.latitudeFormatted = GPSConverter.formatCoordinate(
                            gps.latitude, true);
                    gps.longitudeFormatted = GPSConverter.formatCoordinate(
                            gps.longitude, false);
                    gps.googleMapsUrl = GPSConverter.getGoogleMapsUrl(
                            gps.latitude, gps.longitude);
                    if (gpsDir.containsTag(GpsDirectory.TAG_ALTITUDE)) {
                        gps.altitudeMeters = gpsDir.getDouble(GpsDirectory.TAG_ALTITUDE);
                    }
                    return gps;
                }
            }
        } catch (Exception e) {
            System.err.println("Error reading GPS: " + e.getMessage());
        }
        return null;
    }

    // --- Batch processing ---

    public static List<File> discoverImages(Path directory, boolean recursive) throws IOException {
        if (recursive) {
            List<File> files = new ArrayList<>();
            Files.walkFileTree(directory, new SimpleFileVisitor<>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                    if (isSupportedImage(file.toFile())) {
                        files.add(file.toFile());
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
            return files;
        } else {
            try (var stream = Files.list(directory)) {
                return stream
                        .map(Path::toFile)
                        .filter(ExifExtractor::isSupportedImage)
                        .sorted()
                        .collect(Collectors.toList());
            }
        }
    }

    public static boolean isSupportedImage(File file) {
        String name = file.getName().toLowerCase();
        return SUPPORTED_EXTENSIONS.stream().anyMatch(name::endsWith);
    }

    public static BatchResult batchExtract(Path directory, boolean recursive) throws IOException {
        List<File> files = discoverImages(directory, recursive);
        BatchResult result = new BatchResult();
        result.totalFiles = files.size();

        for (File file : files) {
            ImageInfo info = extractMetadata(file);
            result.results.add(info);
            if ("success".equals(info.status)) {
                result.processed++;
            } else {
                result.errors++;
            }
            if (info.gpsData != null) {
                result.withGps++;
            }
        }

        return result;
    }

    public static List<GPSReportEntry> generateGPSReport(Path directory,
                                                          boolean recursive) throws IOException {
        List<File> files = discoverImages(directory, recursive);
        List<GPSReportEntry> report = new ArrayList<>();

        for (File file : files) {
            GPSData gps = extractGPSOnly(file);
            if (gps != null) {
                GPSReportEntry entry = new GPSReportEntry();
                entry.filename = file.getName();
                entry.latitude = gps.latitude;
                entry.longitude = gps.longitude;
                entry.altitude = gps.altitudeMeters;
                entry.mapsUrl = gps.googleMapsUrl;
                report.add(entry);
            }
        }

        return report;
    }

    public static void exportToJson(BatchResult result, String outputPath) throws IOException {
        try (Writer writer = new FileWriter(outputPath)) {
            gson.toJson(result, writer);
        }
    }

    // --- Display helpers ---

    private static void printImageInfo(ImageInfo info) {
        System.out.println("=== " + info.filename + " ===");
        System.out.println("Path: " + info.absolutePath);
        System.out.println("File size: " + info.fileSizeBytes + " bytes");

        if ("error".equals(info.status)) {
            System.out.println("ERROR: " + info.error);
            return;
        }

        for (Map.Entry<String, Map<String, String>> dirEntry : info.metadata.entrySet()) {
            System.out.println("\n[" + dirEntry.getKey() + "]");
            for (Map.Entry<String, String> tagEntry : dirEntry.getValue().entrySet()) {
                System.out.println("  " + tagEntry.getKey() + ": " + tagEntry.getValue());
            }
        }

        if (info.gpsData != null) {
            System.out.println("\n[GPS Coordinates]");
            System.out.println("  Latitude:  " + info.gpsData.latitudeFormatted);
            System.out.println("  Longitude: " + info.gpsData.longitudeFormatted);
            if (info.gpsData.altitudeMeters != null) {
                System.out.println("  Altitude:  " + info.gpsData.altitudeMeters + " m");
            }
            System.out.println("  Maps: " + info.gpsData.googleMapsUrl);
        }
    }

    // --- Main ---

    public static void main(String[] args) {
        if (args.length == 0) {
            printUsage();
            return;
        }

        String command = args[0];

        switch (command) {
            case "read" -> handleRead(args);
            case "batch" -> handleBatch(args);
            case "gps" -> handleGps(args);
            default -> printUsage();
        }
    }

    private static void handleRead(String[] args) {
        if (args.length < 2) {
            System.err.println("Usage: read <image-path> [--json]");
            return;
        }
        File imageFile = new File(args[1]);
        if (!imageFile.exists()) {
            System.err.println("File not found: " + args[1]);
            return;
        }

        boolean jsonOutput = args.length > 2 && "--json".equals(args[2]);
        ImageInfo info = extractMetadata(imageFile);

        if (jsonOutput) {
            System.out.println(gson.toJson(info));
        } else {
            printImageInfo(info);
        }
    }

    private static void handleBatch(String[] args) {
        if (args.length < 2) {
            System.err.println("Usage: batch <directory> [--recursive] [--json <output.json>]");
            return;
        }

        Path directory = Paths.get(args[1]);
        boolean recursive = false;
        String jsonOutput = null;

        for (int i = 2; i < args.length; i++) {
            if ("--recursive".equals(args[i]) || "-r".equals(args[i])) {
                recursive = true;
            } else if ("--json".equals(args[i]) && i + 1 < args.length) {
                jsonOutput = args[++i];
            }
        }

        try {
            BatchResult result = batchExtract(directory, recursive);
            System.out.println("Total files: " + result.totalFiles);
            System.out.println("Processed:   " + result.processed);
            System.out.println("Errors:      " + result.errors);
            System.out.println("With GPS:    " + result.withGps);

            if (jsonOutput != null) {
                exportToJson(result, jsonOutput);
                System.out.println("\nExported to: " + jsonOutput);
            } else {
                for (ImageInfo info : result.results) {
                    System.out.println();
                    printImageInfo(info);
                }
            }
        } catch (IOException e) {
            System.err.println("Error processing directory: " + e.getMessage());
        }
    }

    private static void handleGps(String[] args) {
        if (args.length < 2) {
            System.err.println("Usage: gps <image-or-directory> [--recursive] [--json]");
            return;
        }

        Path target = Paths.get(args[1]);
        boolean recursive = false;
        boolean jsonOutput = false;

        for (int i = 2; i < args.length; i++) {
            if ("--recursive".equals(args[i]) || "-r".equals(args[i])) {
                recursive = true;
            } else if ("--json".equals(args[i])) {
                jsonOutput = true;
            }
        }

        if (Files.isRegularFile(target)) {
            GPSData gps = extractGPSOnly(target.toFile());
            if (gps != null) {
                if (jsonOutput) {
                    System.out.println(gson.toJson(gps));
                } else {
                    System.out.println("GPS Data for: " + target.getFileName());
                    System.out.println("  Latitude:  " + gps.latitudeFormatted);
                    System.out.println("  Longitude: " + gps.longitudeFormatted);
                    if (gps.altitudeMeters != null) {
                        System.out.println("  Altitude:  " + gps.altitudeMeters + " m");
                    }
                    System.out.println("  Maps: " + gps.googleMapsUrl);
                }
            } else {
                System.out.println("No GPS data found.");
            }
        } else if (Files.isDirectory(target)) {
            try {
                List<GPSReportEntry> report = generateGPSReport(target, recursive);
                if (jsonOutput) {
                    System.out.println(gson.toJson(report));
                } else {
                    System.out.println("GPS Report (" + report.size() + " images with location):");
                    for (GPSReportEntry entry : report) {
                        System.out.printf("  %s: %.8f, %.8f%n",
                                entry.filename, entry.latitude, entry.longitude);
                        System.out.println("    " + entry.mapsUrl);
                    }
                }
            } catch (IOException e) {
                System.err.println("Error: " + e.getMessage());
            }
        }
    }

    private static void printUsage() {
        System.out.println("EXIF Metadata Extractor");
        System.out.println();
        System.out.println("Commands:");
        System.out.println("  read  <image> [--json]              Read EXIF from a single image");
        System.out.println("  batch <dir> [-r] [--json out.json]  Batch process a directory");
        System.out.println("  gps   <image-or-dir> [-r] [--json]  Extract GPS data only");
    }
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.exiftool</groupId>
    <artifactId>exif-extractor</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>EXIF Metadata Extractor</name>
    <description>Reads and extracts EXIF metadata from images with GPS coordinate conversion and batch processing</description>

    <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.drewnoakes</groupId>
            <artifactId>metadata-extractor</artifactId>
            <version>2.19.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.10.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.exiftool.ExifExtractor</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.5.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
README.md
# EXIF Metadata Extractor (Java - Trial 1)

A Java tool for reading and extracting EXIF metadata from images with GPS coordinate conversion and batch processing support.

## Dependencies

- **metadata-extractor** (2.19.0): EXIF metadata reading and parsing
- **Gson** (2.10.1): JSON serialization and output formatting

## Build

```bash
mvn clean package
```

## Usage

### Read EXIF metadata from a single image
```bash
java -jar target/exif-extractor-1.0.0.jar read photo.jpg
java -jar target/exif-extractor-1.0.0.jar read photo.jpg --json
```

### Batch process a directory
```bash
java -jar target/exif-extractor-1.0.0.jar batch ./photos
java -jar target/exif-extractor-1.0.0.jar batch ./photos --recursive --json output.json
```

### Extract GPS data
```bash
java -jar target/exif-extractor-1.0.0.jar gps photo.jpg
java -jar target/exif-extractor-1.0.0.jar gps ./photos --recursive --json
```

## Features

- Read all EXIF metadata directories and tags
- Extract GPS coordinates with DMS and decimal degree formatting
- Generate Google Maps links from GPS data
- Batch process directories with recursive support
- Export all metadata to JSON format
- Support for JPEG, TIFF, PNG, GIF, BMP, and WebP formats