EXIF Metadata Extractor (cpp, written by Codex)
envgap__codex__cpp-t1-22
Written by a coding agent; not on GitHubWritten 2026-03-03
01 / FAILURE SIGNATURE
As the study recorded it
OpenCV missing include dirs + exiv2 requires brotli system lib
Not a benchmark task.
- It was made to work, but its repair cannot be rebuilt from the saved files (the saved copy shows no change, or not all of the changes the study's notes describe), so there is no fix to score against.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
CMakeLists.txt- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/cpp-t1 #22 · read the task the agent was given
Codex wrote this cpp project from the task below. It does not run on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: 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.22) project(exif_metadata_extractor_cpp VERSION 1.0.0 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) include(FetchContent) FetchContent_Declare( opencv URL https://github.com/opencv/opencv/archive/refs/tags/4.10.0.zip ) set(BUILD_LIST "core,imgproc,imgcodecs" CACHE STRING "" FORCE) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(BUILD_TESTS OFF CACHE BOOL "" FORCE) set(BUILD_PERF_TESTS OFF CACHE BOOL "" FORCE) set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(opencv) FetchContent_Declare( nlohmann_json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz ) FetchContent_MakeAvailable(nlohmann_json) FetchContent_Declare( exiv2 URL https://github.com/Exiv2/exiv2/archive/refs/tags/v0.28.3.zip ) set(EXIV2_ENABLE_WEBREADY OFF CACHE BOOL "" FORCE) set(EXIV2_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) set(EXIV2_BUILD_EXIV2_COMMAND OFF CACHE BOOL "" FORCE) set(EXIV2_BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE) set(EXIV2_BUILD_DOC OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(exiv2) add_executable(exif_metadata_extractor src/main.cpp) target_link_libraries(exif_metadata_extractor PRIVATE opencv_core opencv_imgproc opencv_imgcodecs Exiv2::exiv2lib nlohmann_json::nlohmann_json)
README.md
# EXIF Metadata Extractor (C++) Reads, displays, compares, and modifies EXIF metadata for JPEG/TIFF/PNG files, including GPS conversion and batch summary export. ## Requirements - Ubuntu 22.04 - G++ 12+ - CMake 3.22+ ## Dependencies (Pinned) - OpenCV `4.10.0` - nlohmann/json `3.11.3` - Exiv2 `0.28.3` All dependencies are pinned in `CMakeLists.txt` via `FetchContent`. ## Build ```bash cmake -S . -B build cmake --build build -j ``` ## Run ```bash ./build/exif_metadata_extractor image.jpg ./build/exif_metadata_extractor image.jpg --format json --output metadata.json ./build/exif_metadata_extractor image.jpg --format csv --output metadata.csv ./build/exif_metadata_extractor image.jpg --map-url ./build/exif_metadata_extractor image.jpg --compare other.jpg ./build/exif_metadata_extractor image.jpg --remove GPS --output image_no_gps.jpg ./build/exif_metadata_extractor image.jpg --strip-all --output image_stripped.jpg ./build/exif_metadata_extractor ./images --batch --output batch_summary.csv ./build/exif_metadata_extractor ``` ## Notes - `--remove` supports category-style removal (`GPS`, `camera`, `date`). - `--strip-all` clears EXIF/IPTC/XMP metadata while preserving pixel data. - No-input mode creates `sample_exif.jpg` with camera/date/GPS metadata and extracts it.
src/main.cpp
#include <exiv2/exiv2.hpp>
#include <nlohmann/json.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace fs = std::filesystem;
using json = nlohmann::json;
struct CliOptions {
std::map<std::string, std::string> kv;
std::vector<std::string> positional;
};
const std::set<std::string> kSupported = {".jpg", ".jpeg", ".tif", ".tiff", ".png"};
CliOptions parseArgs(int argc, char** argv) {
CliOptions out;
for (int i = 1; i < argc; ++i) {
std::string token = argv[i];
if (token.rfind("--", 0) == 0) {
std::string key = token.substr(2);
if (i + 1 < argc) {
std::string next = argv[i + 1];
if (next.rfind("--", 0) != 0) {
out.kv[key] = next;
++i;
continue;
}
}
out.kv[key] = "true";
} else {
out.positional.push_back(token);
}
}
return out;
}
std::string lower(std::string s) {
for (char& c : s) {
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
}
return s;
}
bool supported(const fs::path& p) {
return p.has_extension() && kSupported.count(lower(p.extension().string())) > 0;
}
std::string getExifStr(const Exiv2::ExifData& exif, const std::string& key) {
auto it = exif.findKey(Exiv2::ExifKey(key));
if (it == exif.end()) return "";
return it->toString();
}
double rationalToDouble(const Exiv2::Rational& r) {
return r.second == 0 ? 0.0 : static_cast<double>(r.first) / static_cast<double>(r.second);
}
double gpsToDecimal(const Exiv2::ExifData& exif, const std::string& coordKey, const std::string& refKey) {
auto coordIt = exif.findKey(Exiv2::ExifKey(coordKey));
auto refIt = exif.findKey(Exiv2::ExifKey(refKey));
if (coordIt == exif.end() || refIt == exif.end() || coordIt->count() < 3) return std::numeric_limits<double>::quiet_NaN();
Exiv2::Rational d = coordIt->toRational(0);
Exiv2::Rational m = coordIt->toRational(1);
Exiv2::Rational s = coordIt->toRational(2);
double dec = rationalToDouble(d) + rationalToDouble(m) / 60.0 + rationalToDouble(s) / 3600.0;
std::string ref = refIt->toString();
if (ref == "S" || ref == "W") dec = -dec;
return dec;
}
std::string toDms(double decimal, bool lat) {
if (!std::isfinite(decimal)) return "";
double abs = std::abs(decimal);
int deg = static_cast<int>(abs);
double minF = (abs - deg) * 60.0;
int min = static_cast<int>(minF);
double sec = (minF - min) * 60.0;
std::string hemi = lat ? (decimal >= 0 ? "N" : "S") : (decimal >= 0 ? "E" : "W");
std::ostringstream oss;
oss << deg << "°" << min << "'" << std::fixed << std::setprecision(2) << sec << "\" " << hemi;
return oss.str();
}
json extractMetadata(const fs::path& file, bool includeMapUrl) {
if (!supported(file)) throw std::invalid_argument("Unsupported image format: " + file.string());
auto image = Exiv2::ImageFactory::open(file.string());
if (!image) throw std::runtime_error("Unable to open image: " + file.string());
image->readMetadata();
const Exiv2::ExifData& exif = image->exifData();
cv::Mat mat = cv::imread(file.string(), cv::IMREAD_UNCHANGED);
if (mat.empty()) throw std::runtime_error("Corrupted image: " + file.string());
double lat = gpsToDecimal(exif, "Exif.GPSInfo.GPSLatitude", "Exif.GPSInfo.GPSLatitudeRef");
double lon = gpsToDecimal(exif, "Exif.GPSInfo.GPSLongitude", "Exif.GPSInfo.GPSLongitudeRef");
double alt = std::numeric_limits<double>::quiet_NaN();
auto altIt = exif.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSAltitude"));
if (altIt != exif.end() && altIt->count() > 0) alt = rationalToDouble(altIt->toRational(0));
json categories;
categories["camera_info"] = {
{"make", getExifStr(exif, "Exif.Image.Make")},
{"model", getExifStr(exif, "Exif.Image.Model")},
{"serial", getExifStr(exif, "Exif.Photo.BodySerialNumber")}
};
categories["capture_settings"] = {
{"exposure_time", getExifStr(exif, "Exif.Photo.ExposureTime")},
{"f_number", getExifStr(exif, "Exif.Photo.FNumber")},
{"iso", getExifStr(exif, "Exif.Photo.ISOSpeedRatings")},
{"focal_length", getExifStr(exif, "Exif.Photo.FocalLength")},
{"flash", getExifStr(exif, "Exif.Photo.Flash")}
};
categories["date_time"] = {
{"original", getExifStr(exif, "Exif.Photo.DateTimeOriginal")},
{"digitized", getExifStr(exif, "Exif.Photo.DateTimeDigitized")},
{"modified", getExifStr(exif, "Exif.Image.DateTime")}
};
categories["gps"] = {
{"latitude_decimal", std::isfinite(lat) ? json(lat) : json(nullptr)},
{"longitude_decimal", std::isfinite(lon) ? json(lon) : json(nullptr)},
{"altitude", std::isfinite(alt) ? json(alt) : json(nullptr)},
{"latitude_dms", std::isfinite(lat) ? json(toDms(lat, true)) : json(nullptr)},
{"longitude_dms", std::isfinite(lon) ? json(toDms(lon, false)) : json(nullptr)},
{"map_url", (includeMapUrl && std::isfinite(lat) && std::isfinite(lon))
? json("https://www.google.com/maps?q=" + std::to_string(lat) + "," + std::to_string(lon))
: json(nullptr)}
};
categories["image_info"] = {
{"dimensions", std::to_string(mat.cols) + "x" + std::to_string(mat.rows)},
{"orientation", getExifStr(exif, "Exif.Image.Orientation")},
{"color_space", getExifStr(exif, "Exif.Photo.ColorSpace")},
{"resolution", getExifStr(exif, "Exif.Image.XResolution") + " / " + getExifStr(exif, "Exif.Image.YResolution")}
};
json fullDump = json::object();
for (const auto& entry : exif) {
fullDump[entry.key()] = entry.toString();
}
return {
{"image", fs::absolute(file).string()},
{"categories", categories},
{"full_dump", fullDump}
};
}
std::string printText(const json& md) {
std::ostringstream oss;
oss << "Image: " << md["image"].get<std::string>() << "\n";
for (auto it = md["categories"].begin(); it != md["categories"].end(); ++it) {
oss << "\n[" << it.key() << "]\n";
for (auto kv = it.value().begin(); kv != it.value().end(); ++kv) {
oss << "- " << kv.key() << ": ";
if (kv.value().is_null() || (kv.value().is_string() && kv.value().get<std::string>().empty())) oss << "N/A";
else oss << kv.value();
oss << "\n";
}
}
oss << "\n";
std::string text = oss.str();
std::cout << text;
return text;
}
json flattenCsv(const json& md) {
json rows = json::array();
for (auto cat = md["categories"].begin(); cat != md["categories"].end(); ++cat) {
for (auto kv = cat.value().begin(); kv != cat.value().end(); ++kv) {
rows.push_back({
{"category", cat.key()},
{"key", kv.key()},
{"value", kv.value().is_null() ? "" : kv.value().dump()}
});
}
}
return rows;
}
void writeCsv(const fs::path& out, const json& rows, const std::vector<std::string>& cols) {
fs::create_directories(out.parent_path());
std::ofstream ofs(out);
for (size_t i = 0; i < cols.size(); ++i) {
if (i) ofs << ",";
ofs << cols[i];
}
ofs << "\n";
for (const auto& row : rows) {
for (size_t i = 0; i < cols.size(); ++i) {
if (i) ofs << ",";
std::string v = row.contains(cols[i]) ? row[cols[i]].dump() : "";
if (v.size() >= 2 && v.front() == '"' && v.back() == '"') v = v.substr(1, v.size() - 2);
std::string esc;
for (char c : v) {
if (c == '"') esc += "\"\"";
else esc.push_back(c);
}
ofs << "\"" << esc << "\"";
}
ofs << "\n";
}
}
json compareMetadata(const json& a, const json& b) {
std::set<std::string> keys;
for (auto cat = a["categories"].begin(); cat != a["categories"].end(); ++cat) {
for (auto kv = cat.value().begin(); kv != cat.value().end(); ++kv) keys.insert(cat.key() + "." + kv.key());
}
for (auto cat = b["categories"].begin(); cat != b["categories"].end(); ++cat) {
for (auto kv = cat.value().begin(); kv != cat.value().end(); ++kv) keys.insert(cat.key() + "." + kv.key());
}
json out = json::array();
for (const auto& key : keys) {
auto dot = key.find('.');
std::string cat = key.substr(0, dot);
std::string field = key.substr(dot + 1);
json left = a["categories"].contains(cat) && a["categories"][cat].contains(field) ? a["categories"][cat][field] : json(nullptr);
json right = b["categories"].contains(cat) && b["categories"][cat].contains(field) ? b["categories"][cat][field] : json(nullptr);
out.push_back({
{"key", key},
{"left", left},
{"right", right},
{"different", left.dump() != right.dump()}
});
}
return out;
}
void stripAll(const fs::path& in, const fs::path& out) {
fs::copy_file(in, out, fs::copy_options::overwrite_existing);
auto image = Exiv2::ImageFactory::open(out.string());
if (!image) throw std::runtime_error("Failed to open output for stripping.");
image->readMetadata();
image->clearExifData();
image->clearIptcData();
image->clearXmpData();
image->writeMetadata();
}
void removeByCategory(const fs::path& in, const fs::path& out, const std::string& removeArg) {
fs::copy_file(in, out, fs::copy_options::overwrite_existing);
auto image = Exiv2::ImageFactory::open(out.string());
if (!image) throw std::runtime_error("Failed to open output file.");
image->readMetadata();
Exiv2::ExifData& exif = image->exifData();
std::vector<std::string> targets;
std::stringstream ss(removeArg);
std::string token;
while (std::getline(ss, token, ',')) {
targets.push_back(lower(token));
}
for (auto it = exif.begin(); it != exif.end(); ) {
std::string key = it->key();
bool erase = false;
for (const auto& t : targets) {
if (t == "gps" && key.rfind("Exif.GPSInfo.", 0) == 0) erase = true;
if ((t == "camera" || t == "camera_info") &&
(key == "Exif.Image.Make" || key == "Exif.Image.Model" || key == "Exif.Photo.BodySerialNumber")) erase = true;
if ((t == "date" || t == "datetime") &&
(key == "Exif.Photo.DateTimeOriginal" || key == "Exif.Photo.DateTimeDigitized" || key == "Exif.Image.DateTime")) erase = true;
}
if (erase) it = exif.erase(it);
else ++it;
}
image->setExifData(exif);
image->writeMetadata();
}
fs::path maybeModify(const fs::path& input, const CliOptions& options) {
if (!options.kv.count("strip-all") && !options.kv.count("remove")) return input;
fs::path out = options.kv.count("output")
? fs::absolute(options.kv.at("output"))
: input.parent_path() / (input.stem().string() + "_modified" + input.extension().string());
fs::create_directories(out.parent_path());
if (options.kv.count("strip-all")) stripAll(input, out);
else removeByCategory(input, out, options.kv.at("remove"));
return out;
}
fs::path createSample() {
fs::path out = fs::absolute("sample_exif.jpg");
cv::Mat img(720, 1280, CV_8UC3, cv::Scalar(245, 150, 95));
cv::putText(img, "Sample EXIF Photo", cv::Point(80, 120), cv::FONT_HERSHEY_SIMPLEX, 2.0, cv::Scalar(255, 255, 255), 3, cv::LINE_AA);
if (!cv::imwrite(out.string(), img)) throw std::runtime_error("Failed to write sample image.");
auto image = Exiv2::ImageFactory::open(out.string());
image->readMetadata();
Exiv2::ExifData& exif = image->exifData();
exif["Exif.Image.Make"] = "CodexCam";
exif["Exif.Image.Model"] = "Model X";
exif["Exif.Photo.BodySerialNumber"] = "CX-0001";
exif["Exif.Photo.ExposureTime"] = "1/125";
exif["Exif.Photo.FNumber"] = "28/10";
exif["Exif.Photo.ISOSpeedRatings"] = "200";
exif["Exif.Photo.FocalLength"] = "35/1";
exif["Exif.Photo.Flash"] = "0";
exif["Exif.Photo.DateTimeOriginal"] = "2024:04:11 10:15:22";
exif["Exif.Photo.DateTimeDigitized"] = "2024:04:11 10:15:22";
exif["Exif.Image.DateTime"] = "2024:04:11 10:15:22";
exif["Exif.GPSInfo.GPSLatitudeRef"] = "N";
exif["Exif.GPSInfo.GPSLatitude"] = "37/1 46/1 2964/100";
exif["Exif.GPSInfo.GPSLongitudeRef"] = "W";
exif["Exif.GPSInfo.GPSLongitude"] = "122/1 25/1 984/100";
exif["Exif.GPSInfo.GPSAltitude"] = "15/1";
image->setExifData(exif);
image->writeMetadata();
return out;
}
void runBatch(const fs::path& folder, const CliOptions& options) {
if (!fs::is_directory(folder)) throw std::invalid_argument("--batch requires a directory input.");
json summary = json::array();
for (const auto& entry : fs::directory_iterator(folder)) {
if (!entry.is_regular_file() || !supported(entry.path())) continue;
json md = extractMetadata(entry.path(), options.kv.count("map-url") > 0);
printText(md);
summary.push_back({
{"image", md["image"]},
{"make", md["categories"]["camera_info"]["make"]},
{"model", md["categories"]["camera_info"]["model"]},
{"date", md["categories"]["date_time"]["original"]},
{"latitude", md["categories"]["gps"]["latitude_decimal"]},
{"longitude", md["categories"]["gps"]["longitude_decimal"]}
});
}
fs::path out = options.kv.count("output") ? fs::absolute(options.kv.at("output")) : fs::absolute("batch_metadata_summary.csv");
writeCsv(out, summary, {"image", "make", "model", "date", "latitude", "longitude"});
std::cout << "Batch summary CSV: " << out << "\n";
}
int main(int argc, char** argv) {
try {
CliOptions options = parseArgs(argc, argv);
std::string format = lower(options.kv.count("format") ? options.kv.at("format") : "text");
if (format != "text" && format != "json" && format != "csv") {
throw std::invalid_argument("Invalid --format. Use text|json|csv.");
}
if (options.positional.empty() && !options.kv.count("batch")) {
fs::path sample = createSample();
json md = extractMetadata(sample, options.kv.count("map-url") > 0);
printText(md);
return 0;
}
if (options.positional.empty()) throw std::invalid_argument("Input path required.");
fs::path input = fs::absolute(options.positional.front());
if (options.kv.count("batch")) {
runBatch(input, options);
return 0;
}
if (!fs::exists(input) || !fs::is_regular_file(input)) throw std::invalid_argument("Input not found: " + input.string());
fs::path effective = maybeModify(input, options);
json md = extractMetadata(effective, options.kv.count("map-url") > 0);
std::string text = printText(md);
if (options.kv.count("compare")) {
json other = extractMetadata(fs::absolute(options.kv.at("compare")), options.kv.count("map-url") > 0);
json diff = compareMetadata(md, other);
std::cout << "Comparison:\n";
for (const auto& row : diff) {
if (row["different"].get<bool>()) {
std::cout << "- " << row["key"] << ": left=" << row["left"] << " | right=" << row["right"] << "\n";
}
}
if (format == "json") {
json payload = {{"metadata", md}, {"comparison_against", other["image"]}, {"differences", diff}};
if (options.kv.count("output")) {
fs::path out = fs::absolute(options.kv.at("output"));
fs::create_directories(out.parent_path());
std::ofstream ofs(out);
ofs << payload.dump(2) << "\n";
} else {
std::cout << payload.dump(2) << "\n";
}
return 0;
}
if (format == "csv") {
fs::path out = options.kv.count("output") ? fs::absolute(options.kv.at("output")) : fs::absolute("comparison.csv");
writeCsv(out, diff, {"key", "left", "right", "different"});
std::cout << "Saved CSV: " << out << "\n";
return 0;
}
if (options.kv.count("output")) {
fs::path out = fs::absolute(options.kv.at("output"));
fs::create_directories(out.parent_path());
std::ofstream ofs(out);
ofs << text;
}
return 0;
}
if (format == "json") {
if (options.kv.count("output")) {
fs::path out = fs::absolute(options.kv.at("output"));
fs::create_directories(out.parent_path());
std::ofstream ofs(out);
ofs << md.dump(2) << "\n";
} else {
std::cout << md.dump(2) << "\n";
}
} else if (format == "csv") {
json rows = flattenCsv(md);
fs::path out = options.kv.count("output") ? fs::absolute(options.kv.at("output")) : fs::absolute("metadata.csv");
writeCsv(out, rows, {"category", "key", "value"});
std::cout << "Saved CSV: " << out << "\n";
} else if (options.kv.count("output")) {
fs::path out = fs::absolute(options.kv.at("output"));
fs::create_directories(out.parent_path());
std::ofstream ofs(out);
ofs << text;
}
if (effective != input) std::cout << "Modified image saved: " << effective << "\n";
return 0;
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << "\n";
return 1;
}
}