← All tasks
cppclaude-code/cpp-t2 #22Not a task: not reproduced

EXIF Metadata Extractor (cpp, written by Claude Code)

envgap__claude-code__cpp-t2-22

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

01 / FAILURE SIGNATURE

As the study recorded it

libexif not installed
Not a benchmark task.
  • In a clean container the reported failure did not reproduce, or the known fix did not make the project run.

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

claude-code/cpp-t2 #22 · read the task the agent was given
Claude Code 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: 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 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.16)
project(ExifExtractor VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(FetchContent)

# Find libexif (system package)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBEXIF REQUIRED libexif)

# Fetch RapidJSON
FetchContent_Declare(
    rapidjson
    GIT_REPOSITORY https://github.com/Tencent/rapidjson.git
    GIT_TAG v1.1.0
)
FetchContent_MakeAvailable(rapidjson)

# Main executable
add_executable(exif_extractor main.cpp)

target_include_directories(exif_extractor PRIVATE
    ${LIBEXIF_INCLUDE_DIRS}
    ${rapidjson_SOURCE_DIR}/include
)

target_link_libraries(exif_extractor PRIVATE
    ${LIBEXIF_LIBRARIES}
)

# Installation
install(TARGETS exif_extractor DESTINATION bin)
main.cpp
/**
 * EXIF Metadata Extractor - Reads and extracts EXIF metadata
 * from images with GPS coordinate conversion and batch processing support.
 *
 * Dependencies:
 *   - libexif (0.6.24): EXIF metadata reading from JPEG images
 *   - rapidjson (1.1.0): JSON serialization
 */

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <filesystem>
#include <cmath>
#include <algorithm>
#include <stdexcept>
#include <iomanip>
#include <sstream>
#include <cstring>

#include <libexif/exif-data.h>
#include <libexif/exif-content.h>
#include <libexif/exif-entry.h>
#include <libexif/exif-tag.h>

#include <rapidjson/document.h>
#include <rapidjson/writer.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/prettywriter.h>

namespace fs = std::filesystem;
using namespace rapidjson;

// --- GPS Converter ---

class GPSConverter {
public:
    struct DMS {
        int degrees;
        int minutes;
        double seconds;
    };

    static double dmsToDecimal(double degrees, double minutes,
                               double seconds, char direction) {
        double decimal = degrees + minutes / 60.0 + seconds / 3600.0;
        if (direction == 'S' || direction == 'W') {
            decimal = -decimal;
        }
        return decimal;
    }

    static DMS decimalToDms(double decimalDeg) {
        double abs_val = std::abs(decimalDeg);
        int degrees = static_cast<int>(abs_val);
        double minutesFloat = (abs_val - degrees) * 60.0;
        int minutes = static_cast<int>(minutesFloat);
        double seconds = (minutesFloat - minutes) * 60.0;
        return {degrees, minutes, std::round(seconds * 10000.0) / 10000.0};
    }

    static std::string formatCoordinate(double decimal, bool isLatitude) {
        DMS dms = decimalToDms(decimal);
        char direction;
        if (isLatitude) {
            direction = decimal >= 0 ? 'N' : 'S';
        } else {
            direction = decimal >= 0 ? 'E' : 'W';
        }
        std::ostringstream oss;
        oss << dms.degrees << "d " << dms.minutes << "' "
            << std::fixed << std::setprecision(4) << dms.seconds
            << "\" " << direction
            << " (" << std::setprecision(8) << decimal << ")";
        return oss.str();
    }

    static std::string getGoogleMapsUrl(double latitude, double longitude) {
        std::ostringstream oss;
        oss << "https://www.google.com/maps?q="
            << std::fixed << std::setprecision(8)
            << latitude << "," << longitude;
        return oss.str();
    }

    static double rationalToDouble(ExifRational r) {
        if (r.denominator == 0) return 0.0;
        return static_cast<double>(r.numerator) / static_cast<double>(r.denominator);
    }

    static double srationalToDouble(ExifSRational r) {
        if (r.denominator == 0) return 0.0;
        return static_cast<double>(r.numerator) / static_cast<double>(r.denominator);
    }
};

// --- Data Structures ---

struct GPSData {
    double latitude = 0.0;
    double longitude = 0.0;
    std::string latitudeFormatted;
    std::string longitudeFormatted;
    double altitude = 0.0;
    bool hasAltitude = false;
    std::string googleMapsUrl;
    bool valid = false;
};

struct ImageInfo {
    std::string filename;
    std::string absolutePath;
    uintmax_t fileSizeBytes = 0;
    std::map<std::string, std::string> exifTags;
    GPSData gps;
    std::string status = "success";
    std::string error;
};

// --- Helper: get EXIF entry value as string ---

std::string getExifValue(ExifEntry* entry) {
    if (!entry) return "";
    char value[1024];
    exif_entry_get_value(entry, value, sizeof(value));
    return std::string(value);
}

// --- EXIF Extractor ---

class ExifExtractor {
public:
    static const std::vector<std::string> SUPPORTED_EXTENSIONS;

    static bool isSupportedImage(const fs::path& filePath) {
        std::string ext = filePath.extension().string();
        std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
        return std::find(SUPPORTED_EXTENSIONS.begin(),
                         SUPPORTED_EXTENSIONS.end(), ext)
               != SUPPORTED_EXTENSIONS.end();
    }

    static ImageInfo extractMetadata(const fs::path& imagePath) {
        ImageInfo info;
        info.filename = imagePath.filename().string();
        info.absolutePath = fs::absolute(imagePath).string();

        try {
            info.fileSizeBytes = fs::file_size(imagePath);
        } catch (...) {
            info.fileSizeBytes = 0;
        }

        ExifData* exifData = exif_data_new_from_file(imagePath.string().c_str());
        if (!exifData) {
            info.status = "error";
            info.error = "Failed to read EXIF data from file";
            return info;
        }

        // Read all EXIF tags across all IFDs
        for (int ifd = 0; ifd < EXIF_IFD_COUNT; ifd++) {
            ExifContent* content = exifData->ifd[ifd];
            if (!content) continue;

            for (unsigned int i = 0; i < content->count; i++) {
                ExifEntry* entry = content->entries[i];
                if (!entry) continue;

                const char* tagName = exif_tag_get_name_in_ifd(entry->tag,
                    static_cast<ExifIfd>(ifd));
                if (!tagName) continue;

                std::string value = getExifValue(entry);
                if (value.length() > 200) {
                    value = value.substr(0, 197) + "...";
                }

                std::string key = std::string(tagName);
                info.exifTags[key] = value;
            }
        }

        // Extract GPS data
        info.gps = extractGPS(exifData);

        exif_data_unref(exifData);
        return info;
    }

    static GPSData extractGPS(ExifData* exifData) {
        GPSData gps;

        ExifContent* gpsContent = exifData->ifd[EXIF_IFD_GPS];
        if (!gpsContent || gpsContent->count == 0) {
            return gps;
        }

        // Latitude
        ExifEntry* latEntry = exif_content_get_entry(gpsContent,
            static_cast<ExifTag>(EXIF_TAG_GPS_LATITUDE));
        ExifEntry* latRefEntry = exif_content_get_entry(gpsContent,
            static_cast<ExifTag>(EXIF_TAG_GPS_LATITUDE_REF));

        // Longitude
        ExifEntry* lonEntry = exif_content_get_entry(gpsContent,
            static_cast<ExifTag>(EXIF_TAG_GPS_LONGITUDE));
        ExifEntry* lonRefEntry = exif_content_get_entry(gpsContent,
            static_cast<ExifTag>(EXIF_TAG_GPS_LONGITUDE_REF));

        if (!latEntry || !latRefEntry || !lonEntry || !lonRefEntry) {
            return gps;
        }

        ExifByteOrder byteOrder = exif_data_get_byte_order(exifData);

        // Parse latitude (3 rationals: degrees, minutes, seconds)
        ExifRational latDeg = exif_get_rational(latEntry->data, byteOrder);
        ExifRational latMin = exif_get_rational(latEntry->data + 8, byteOrder);
        ExifRational latSec = exif_get_rational(latEntry->data + 16, byteOrder);
        char latRef = latRefEntry->data[0];

        double lat = GPSConverter::dmsToDecimal(
            GPSConverter::rationalToDouble(latDeg),
            GPSConverter::rationalToDouble(latMin),
            GPSConverter::rationalToDouble(latSec),
            latRef
        );

        // Parse longitude (3 rationals: degrees, minutes, seconds)
        ExifRational lonDeg = exif_get_rational(lonEntry->data, byteOrder);
        ExifRational lonMin = exif_get_rational(lonEntry->data + 8, byteOrder);
        ExifRational lonSec = exif_get_rational(lonEntry->data + 16, byteOrder);
        char lonRef = lonRefEntry->data[0];

        double lon = GPSConverter::dmsToDecimal(
            GPSConverter::rationalToDouble(lonDeg),
            GPSConverter::rationalToDouble(lonMin),
            GPSConverter::rationalToDouble(lonSec),
            lonRef
        );

        gps.latitude = lat;
        gps.longitude = lon;
        gps.latitudeFormatted = GPSConverter::formatCoordinate(lat, true);
        gps.longitudeFormatted = GPSConverter::formatCoordinate(lon, false);
        gps.googleMapsUrl = GPSConverter::getGoogleMapsUrl(lat, lon);
        gps.valid = true;

        // Altitude
        ExifEntry* altEntry = exif_content_get_entry(gpsContent,
            static_cast<ExifTag>(EXIF_TAG_GPS_ALTITUDE));
        if (altEntry) {
            ExifRational altRat = exif_get_rational(altEntry->data, byteOrder);
            gps.altitude = GPSConverter::rationalToDouble(altRat);

            ExifEntry* altRefEntry = exif_content_get_entry(gpsContent,
                static_cast<ExifTag>(EXIF_TAG_GPS_ALTITUDE_REF));
            if (altRefEntry && altRefEntry->data[0] == 1) {
                gps.altitude = -gps.altitude;
            }
            gps.hasAltitude = true;
        }

        return gps;
    }

    static GPSData extractGPSOnly(const fs::path& imagePath) {
        ExifData* exifData = exif_data_new_from_file(imagePath.string().c_str());
        if (!exifData) {
            return GPSData{};
        }
        GPSData gps = extractGPS(exifData);
        exif_data_unref(exifData);
        return gps;
    }
};

const std::vector<std::string> ExifExtractor::SUPPORTED_EXTENSIONS = {
    ".jpg", ".jpeg", ".tiff", ".tif"
};

// --- Batch Processor ---

class BatchProcessor {
public:
    BatchProcessor(const fs::path& directory, bool recursive = false)
        : directory_(directory), recursive_(recursive) {
        discoverFiles();
    }

    size_t fileCount() const { return files_.size(); }

    std::vector<ImageInfo> extractAll() const {
        std::vector<ImageInfo> results;
        for (const auto& file : files_) {
            results.push_back(ExifExtractor::extractMetadata(file));
        }
        return results;
    }

    std::vector<GPSData> generateGPSReport() const {
        std::vector<GPSData> report;
        for (const auto& file : files_) {
            GPSData gps = ExifExtractor::extractGPSOnly(file);
            if (gps.valid) {
                report.push_back(gps);
            }
        }
        return report;
    }

    void exportToJson(const std::string& outputPath) const {
        auto results = extractAll();

        Document doc;
        doc.SetArray();
        auto& alloc = doc.GetAllocator();

        for (const auto& info : results) {
            Value obj(kObjectType);
            obj.AddMember("filename", Value(info.filename.c_str(), alloc), alloc);
            obj.AddMember("path", Value(info.absolutePath.c_str(), alloc), alloc);
            obj.AddMember("fileSizeBytes", static_cast<uint64_t>(info.fileSizeBytes), alloc);
            obj.AddMember("status", Value(info.status.c_str(), alloc), alloc);

            if (info.status == "success") {
                Value tagsObj(kObjectType);
                for (const auto& [key, val] : info.exifTags) {
                    tagsObj.AddMember(Value(key.c_str(), alloc),
                                     Value(val.c_str(), alloc), alloc);
                }
                obj.AddMember("exifTags", tagsObj, alloc);

                if (info.gps.valid) {
                    Value gpsObj(kObjectType);
                    gpsObj.AddMember("latitude", info.gps.latitude, alloc);
                    gpsObj.AddMember("longitude", info.gps.longitude, alloc);
                    gpsObj.AddMember("latitudeFormatted",
                        Value(info.gps.latitudeFormatted.c_str(), alloc), alloc);
                    gpsObj.AddMember("longitudeFormatted",
                        Value(info.gps.longitudeFormatted.c_str(), alloc), alloc);
                    gpsObj.AddMember("googleMapsUrl",
                        Value(info.gps.googleMapsUrl.c_str(), alloc), alloc);
                    if (info.gps.hasAltitude) {
                        gpsObj.AddMember("altitudeMeters", info.gps.altitude, alloc);
                    }
                    obj.AddMember("gps", gpsObj, alloc);
                }
            } else {
                obj.AddMember("error", Value(info.error.c_str(), alloc), alloc);
            }

            doc.PushBack(obj, alloc);
        }

        StringBuffer buffer;
        PrettyWriter<StringBuffer> writer(buffer);
        doc.Accept(writer);

        std::ofstream out(outputPath);
        out << buffer.GetString();
    }

    const std::vector<fs::path>& files() const { return files_; }

private:
    fs::path directory_;
    bool recursive_;
    std::vector<fs::path> files_;

    void discoverFiles() {
        if (recursive_) {
            for (const auto& entry : fs::recursive_directory_iterator(directory_)) {
                if (entry.is_regular_file() &&
                    ExifExtractor::isSupportedImage(entry.path())) {
                    files_.push_back(entry.path());
                }
            }
        } else {
            for (const auto& entry : fs::directory_iterator(directory_)) {
                if (entry.is_regular_file() &&
                    ExifExtractor::isSupportedImage(entry.path())) {
                    files_.push_back(entry.path());
                }
            }
        }
        std::sort(files_.begin(), files_.end());
    }
};

// --- CLI ---

void printUsage() {
    std::cout << "EXIF Metadata Extractor\n\n"
              << "Commands:\n"
              << "  read  <image> [--json]              Read EXIF from a single image\n"
              << "  gps   <image-or-dir> [-r] [--json]  Extract GPS data\n"
              << "  batch <dir> [-r] [--json out.json]   Batch process images\n";
}

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printUsage();
        return 0;
    }

    std::string command = argv[1];

    try {
        if (command == "read") {
            if (argc < 3) {
                std::cerr << "Usage: read <image> [--json]\n";
                return 1;
            }
            bool jsonOutput = (argc > 3 && std::string(argv[3]) == "--json");

            ImageInfo info = ExifExtractor::extractMetadata(argv[2]);

            if (jsonOutput) {
                Document doc;
                doc.SetObject();
                auto& alloc = doc.GetAllocator();

                doc.AddMember("filename", Value(info.filename.c_str(), alloc), alloc);
                doc.AddMember("path", Value(info.absolutePath.c_str(), alloc), alloc);
                doc.AddMember("fileSizeBytes",
                    static_cast<uint64_t>(info.fileSizeBytes), alloc);
                doc.AddMember("status", Value(info.status.c_str(), alloc), alloc);

                if (info.status == "success") {
                    Value tagsObj(kObjectType);
                    for (const auto& [key, val] : info.exifTags) {
                        tagsObj.AddMember(Value(key.c_str(), alloc),
                                         Value(val.c_str(), alloc), alloc);
                    }
                    doc.AddMember("exifTags", tagsObj, alloc);

                    if (info.gps.valid) {
                        Value gpsObj(kObjectType);
                        gpsObj.AddMember("latitude", info.gps.latitude, alloc);
                        gpsObj.AddMember("longitude", info.gps.longitude, alloc);
                        gpsObj.AddMember("googleMapsUrl",
                            Value(info.gps.googleMapsUrl.c_str(), alloc), alloc);
                        doc.AddMember("gps", gpsObj, alloc);
                    }
                }

                StringBuffer buffer;
                PrettyWriter<StringBuffer> writer(buffer);
                doc.Accept(writer);
                std::cout << buffer.GetString() << std::endl;
            } else {
                std::cout << "File: " << info.filename << "\n"
                          << "Path: " << info.absolutePath << "\n"
                          << "Size: " << info.fileSizeBytes << " bytes\n";

                if (info.status == "error") {
                    std::cout << "ERROR: " << info.error << "\n";
                    return 1;
                }

                std::cout << "\nEXIF Tags (" << info.exifTags.size() << " found):\n";
                for (const auto& [key, val] : info.exifTags) {
                    std::cout << "  " << key << ": " << val << "\n";
                }

                if (info.gps.valid) {
                    std::cout << "\nGPS Data:\n"
                              << "  Latitude:  " << info.gps.latitudeFormatted << "\n"
                              << "  Longitude: " << info.gps.longitudeFormatted << "\n";
                    if (info.gps.hasAltitude) {
                        std::cout << "  Altitude:  " << info.gps.altitude << " m\n";
                    }
                    std::cout << "  Maps: " << info.gps.googleMapsUrl << "\n";
                }
            }

        } else if (command == "gps") {
            if (argc < 3) {
                std::cerr << "Usage: gps <image-or-dir> [-r] [--json]\n";
                return 1;
            }

            fs::path target(argv[2]);
            bool recursive = false;
            bool jsonOutput = false;
            for (int i = 3; i < argc; i++) {
                std::string arg = argv[i];
                if (arg == "-r" || arg == "--recursive") recursive = true;
                if (arg == "--json") jsonOutput = true;
            }

            if (fs::is_regular_file(target)) {
                GPSData gps = ExifExtractor::extractGPSOnly(target);
                if (gps.valid) {
                    if (jsonOutput) {
                        Document doc;
                        doc.SetObject();
                        auto& alloc = doc.GetAllocator();
                        doc.AddMember("latitude", gps.latitude, alloc);
                        doc.AddMember("longitude", gps.longitude, alloc);
                        doc.AddMember("googleMapsUrl",
                            Value(gps.googleMapsUrl.c_str(), alloc), alloc);
                        if (gps.hasAltitude) {
                            doc.AddMember("altitudeMeters", gps.altitude, alloc);
                        }
                        StringBuffer buffer;
                        PrettyWriter<StringBuffer> writer(buffer);
                        doc.Accept(writer);
                        std::cout << buffer.GetString() << "\n";
                    } else {
                        std::cout << "GPS Data for: " << target.filename() << "\n"
                                  << "  Latitude:  " << gps.latitudeFormatted << "\n"
                                  << "  Longitude: " << gps.longitudeFormatted << "\n";
                        if (gps.hasAltitude) {
                            std::cout << "  Altitude:  " << gps.altitude << " m\n";
                        }
                        std::cout << "  Maps: " << gps.googleMapsUrl << "\n";
                    }
                } else {
                    std::cout << "No GPS data found.\n";
                }
            } else if (fs::is_directory(target)) {
                BatchProcessor processor(target, recursive);
                auto report = processor.generateGPSReport();
                std::cout << "GPS Report (" << report.size()
                          << " images with location):\n";
                for (const auto& gps : report) {
                    std::cout << "  " << std::fixed << std::setprecision(8)
                              << gps.latitude << ", " << gps.longitude << "\n"
                              << "    " << gps.googleMapsUrl << "\n";
                }
            }

        } else if (command == "batch") {
            if (argc < 3) {
                std::cerr << "Usage: batch <dir> [-r] [--json out.json]\n";
                return 1;
            }

            fs::path directory(argv[2]);
            bool recursive = false;
            std::string jsonFile;
            for (int i = 3; i < argc; i++) {
                std::string arg = argv[i];
                if (arg == "-r" || arg == "--recursive") recursive = true;
                if (arg == "--json" && i + 1 < argc) jsonFile = argv[++i];
            }

            BatchProcessor processor(directory, recursive);
            std::cout << "Found " << processor.fileCount() << " image(s)\n";

            if (!jsonFile.empty()) {
                processor.exportToJson(jsonFile);
                std::cout << "Exported to: " << jsonFile << "\n";
            } else {
                auto results = processor.extractAll();
                for (const auto& info : results) {
                    std::cout << "\n--- " << info.filename << " ---\n";
                    if (info.status == "success") {
                        std::cout << "  EXIF tags: " << info.exifTags.size() << "\n"
                                  << "  GPS: " << (info.gps.valid ? "Yes" : "No") << "\n";
                    } else {
                        std::cout << "  Error: " << info.error << "\n";
                    }
                }
            }

        } else {
            printUsage();
        }

    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
        return 1;
    }

    return 0;
}
README.md
# EXIF Metadata Extractor (C++ - Trial 2)

A C++ tool for reading and batch processing EXIF metadata from images, with GPS coordinate extraction and conversion.

## Dependencies

- **libexif** (0.6.24): EXIF metadata reading from JPEG images
- **rapidjson** (1.1.0): JSON serialization

## Build

```bash
# Install libexif (Ubuntu/Debian)
sudo apt-get install libexif-dev

# Build
mkdir build && cd build
cmake ..
make
```

## Usage

### Read EXIF metadata from an image
```bash
./exif_extractor read photo.jpg
./exif_extractor read photo.jpg --json
```

### Extract GPS data
```bash
./exif_extractor gps photo.jpg
./exif_extractor gps ./photos --recursive --json
```

### Batch processing
```bash
./exif_extractor batch ./photos --recursive
./exif_extractor batch ./photos --json metadata.json
```

## Features

- Read all EXIF tags from JPEG and TIFF images
- Extract and convert GPS coordinates (DMS to decimal and vice versa)
- Generate Google Maps links from GPS data
- Batch process entire directories with recursive support
- Export metadata to JSON format using RapidJSON