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