EXIF Metadata Extractor (java, written by Codex)
envgap__codex__java-t1-22
Written by a coding agent; not on GitHubWritten 2026-03-03
01 / FAILURE SIGNATURE
As the study recorded it
RationalNumber.valueOf API mismatch with commons-imaging 1.0.0-alpha5
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
codex/java-t1 #22 · read the task the agent was given
Codex 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.
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>exif-metadata-extractor</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.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>1.0.0-alpha5</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</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>ExifMetadataExtractor</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
README.md
# EXIF Metadata Extractor (Java) Reads, displays, compares, and modifies EXIF metadata for JPEG/TIFF/PNG files, including GPS conversion and batch summary export. ## Requirements - Ubuntu 22.04 - JDK 17+ - Maven 3.8+ ## Dependencies (Pinned) - `com.drewnoakes:metadata-extractor:2.19.0` - `org.apache.commons:commons-imaging:1.0.0-alpha5` - `com.google.code.gson:gson:2.11.0` ## Build ```bash mvn clean package ``` ## Run ```bash java -cp target/exif-metadata-extractor-1.0.0.jar ExifMetadataExtractor image.jpg java -cp target/exif-metadata-extractor-1.0.0.jar ExifMetadataExtractor image.jpg --format json --output metadata.json java -cp target/exif-metadata-extractor-1.0.0.jar ExifMetadataExtractor image.jpg --format csv --output metadata.csv java -cp target/exif-metadata-extractor-1.0.0.jar ExifMetadataExtractor image.jpg --map-url java -cp target/exif-metadata-extractor-1.0.0.jar ExifMetadataExtractor image.jpg --compare other.jpg java -cp target/exif-metadata-extractor-1.0.0.jar ExifMetadataExtractor image.jpg --remove GPS --output image_no_gps.jpg java -cp target/exif-metadata-extractor-1.0.0.jar ExifMetadataExtractor image.jpg --strip-all --output image_stripped.jpg java -cp target/exif-metadata-extractor-1.0.0.jar ExifMetadataExtractor ./images --batch --output batch_summary.csv java -cp target/exif-metadata-extractor-1.0.0.jar ExifMetadataExtractor ``` ## Notes - `--remove GPS` is implemented for JPEG in this version. - `--strip-all` rewrites image pixels without metadata. - No-input mode creates `sample_exif.jpg` with camera/date/GPS metadata and extracts it.
src/main/java/ExifMetadataExtractor.java
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
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 org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.common.RationalNumber;
import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter;
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
public final class ExifMetadataExtractor {
private static final Set<String> SUPPORTED = Set.of("jpg", "jpeg", "tif", "tiff", "png");
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private record ParsedArgs(Map<String, String> options, List<String> positional) {}
private ExifMetadataExtractor() {}
public static void main(String[] args) {
try {
ParsedArgs parsed = parseArgs(args);
String format = parsed.options.getOrDefault("format", "text").toLowerCase(Locale.ROOT);
if (!Set.of("text", "json", "csv").contains(format)) {
throw new IllegalArgumentException("Invalid --format. Use text|json|csv.");
}
if (parsed.positional.isEmpty() && !parsed.options.containsKey("batch")) {
Path sample = createSample(Paths.get("sample_exif.jpg").toAbsolutePath());
Map<String, Object> md = extract(sample, parsed.options.containsKey("map-url"));
printText(md);
return;
}
if (parsed.positional.isEmpty()) throw new IllegalArgumentException("Input path required.");
Path input = Paths.get(parsed.positional.get(0)).toAbsolutePath();
if (parsed.options.containsKey("batch")) {
if (!Files.isDirectory(input)) throw new IllegalArgumentException("--batch requires a directory input.");
runBatch(input, parsed.options);
return;
}
if (!Files.isRegularFile(input)) throw new IllegalArgumentException("Input not found: " + input);
Path effective = maybeModify(input, parsed.options);
Map<String, Object> metadata = extract(effective, parsed.options.containsKey("map-url"));
String text = printText(metadata);
if (parsed.options.containsKey("compare")) {
Path otherPath = Paths.get(parsed.options.get("compare")).toAbsolutePath();
Map<String, Object> other = extract(otherPath, parsed.options.containsKey("map-url"));
List<Map<String, Object>> differences = compare(metadata, other);
System.out.println("Comparison:");
for (Map<String, Object> d : differences) {
if (Boolean.TRUE.equals(d.get("different"))) {
System.out.printf("- %s: left=%s | right=%s%n", d.get("key"), d.get("left"), d.get("right"));
}
}
if ("json".equals(format)) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("metadata", metadata);
payload.put("comparisonAgainst", other.get("image"));
payload.put("differences", differences);
writeJsonOrConsole(payload, parsed.options.get("output"));
return;
}
if ("csv".equals(format)) {
Path out = parsed.options.containsKey("output")
? Paths.get(parsed.options.get("output")).toAbsolutePath()
: Paths.get("comparison.csv").toAbsolutePath();
writeCsv(out, differences, List.of("key", "left", "right", "different"));
System.out.println("Saved CSV: " + out);
return;
}
if (parsed.options.containsKey("output")) {
Files.writeString(Paths.get(parsed.options.get("output")).toAbsolutePath(), text, StandardCharsets.UTF_8);
}
return;
}
if ("json".equals(format)) {
writeJsonOrConsole(metadata, parsed.options.get("output"));
} else if ("csv".equals(format)) {
List<Map<String, Object>> rows = flattenForCsv(metadata);
Path out = parsed.options.containsKey("output")
? Paths.get(parsed.options.get("output")).toAbsolutePath()
: Paths.get("metadata.csv").toAbsolutePath();
writeCsv(out, rows, List.of("category", "key", "value"));
System.out.println("Saved CSV: " + out);
} else if (parsed.options.containsKey("output")) {
Files.writeString(Paths.get(parsed.options.get("output")).toAbsolutePath(), text, StandardCharsets.UTF_8);
}
if (!effective.equals(input)) System.out.println("Modified image saved: " + effective);
} catch (Exception ex) {
System.err.println("Error: " + ex.getMessage());
System.exit(1);
}
}
private static ParsedArgs parseArgs(String[] args) {
Map<String, String> options = new LinkedHashMap<>();
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 ext(Path p) {
String n = p.getFileName().toString();
int i = n.lastIndexOf('.');
return i >= 0 ? n.substring(i + 1).toLowerCase(Locale.ROOT) : "";
}
private static String dms(double decimal, boolean lat) {
double abs = Math.abs(decimal);
int deg = (int) Math.floor(abs);
double mFloat = (abs - deg) * 60;
int min = (int) Math.floor(mFloat);
double sec = (mFloat - min) * 60;
String hemi = lat ? (decimal >= 0 ? "N" : "S") : (decimal >= 0 ? "E" : "W");
return String.format(Locale.ROOT, "%d°%d'%.2f\" %s", deg, min, sec, hemi);
}
private static String mapUrl(Double lat, Double lon) {
if (lat == null || lon == null) return null;
return "https://www.google.com/maps?q=" + lat + "," + lon;
}
private static Map<String, Object> extract(Path file, boolean includeMapUrl) throws Exception {
if (!SUPPORTED.contains(ext(file))) throw new IllegalArgumentException("Unsupported image format: " + file);
Metadata metadata = ImageMetadataReader.readMetadata(file.toFile());
ExifIFD0Directory ifd0 = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
ExifSubIFDDirectory sub = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
GpsDirectory gps = metadata.getFirstDirectoryOfType(GpsDirectory.class);
BufferedImage img = ImageIO.read(file.toFile());
if (img == null) throw new IllegalArgumentException("Corrupted image: " + file);
Double lat = null;
Double lon = null;
Double alt = null;
if (gps != null) {
GeoLocation gl = gps.getGeoLocation();
if (gl != null) {
lat = gl.getLatitude();
lon = gl.getLongitude();
}
RationalNumber altRn = gps.getRational(GpsTagConstants.GPS_TAG_GPS_ALTITUDE);
if (altRn != null) alt = altRn.doubleValue();
}
Map<String, Object> camera = new LinkedHashMap<>();
camera.put("make", ifd0 != null ? ifd0.getString(ExifIFD0Directory.TAG_MAKE) : null);
camera.put("model", ifd0 != null ? ifd0.getString(ExifIFD0Directory.TAG_MODEL) : null);
camera.put("serial", sub != null ? sub.getString(ExifSubIFDDirectory.TAG_BODY_SERIAL_NUMBER) : null);
Map<String, Object> capture = new LinkedHashMap<>();
capture.put("exposure_time", sub != null ? sub.getDescription(ExifSubIFDDirectory.TAG_EXPOSURE_TIME) : null);
capture.put("f_number", sub != null ? sub.getDescription(ExifSubIFDDirectory.TAG_FNUMBER) : null);
capture.put("iso", sub != null ? sub.getInteger(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT) : null);
capture.put("focal_length", sub != null ? sub.getDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH) : null);
capture.put("flash", sub != null ? sub.getDescription(ExifSubIFDDirectory.TAG_FLASH) : null);
Map<String, Object> dt = new LinkedHashMap<>();
dt.put("original", sub != null ? sub.getDateOriginal() : null);
dt.put("digitized", sub != null ? sub.getDateDigitized() : null);
dt.put("modified", ifd0 != null ? ifd0.getDate(ExifIFD0Directory.TAG_DATETIME) : null);
Map<String, Object> gpsMap = new LinkedHashMap<>();
gpsMap.put("latitude_decimal", lat);
gpsMap.put("longitude_decimal", lon);
gpsMap.put("altitude", alt);
gpsMap.put("latitude_dms", lat == null ? null : dms(lat, true));
gpsMap.put("longitude_dms", lon == null ? null : dms(lon, false));
gpsMap.put("map_url", includeMapUrl ? mapUrl(lat, lon) : null);
Map<String, Object> info = new LinkedHashMap<>();
info.put("dimensions", img.getWidth() + "x" + img.getHeight());
info.put("orientation", ifd0 != null ? ifd0.getInteger(ExifIFD0Directory.TAG_ORIENTATION) : null);
info.put("color_space", sub != null ? sub.getDescription(ExifSubIFDDirectory.TAG_COLOR_SPACE) : img.getColorModel().toString());
info.put("resolution", ifd0 != null ? (ifd0.getDescription(ExifIFD0Directory.TAG_X_RESOLUTION) + " / " + ifd0.getDescription(ExifIFD0Directory.TAG_Y_RESOLUTION)) : null);
Map<String, Object> fullDump = new LinkedHashMap<>();
for (Directory d : metadata.getDirectories()) {
Map<String, String> tags = new LinkedHashMap<>();
for (Tag t : d.getTags()) tags.put(t.getTagName(), t.getDescription());
fullDump.put(d.getName(), tags);
}
Map<String, Object> categories = new LinkedHashMap<>();
categories.put("camera_info", camera);
categories.put("capture_settings", capture);
categories.put("date_time", dt);
categories.put("gps", gpsMap);
categories.put("image_info", info);
Map<String, Object> out = new LinkedHashMap<>();
out.put("image", file.toAbsolutePath().toString());
out.put("categories", categories);
out.put("full_dump", fullDump);
return out;
}
@SuppressWarnings("unchecked")
private static List<Map<String, Object>> flattenForCsv(Map<String, Object> metadata) {
List<Map<String, Object>> rows = new ArrayList<>();
Map<String, Object> categories = (Map<String, Object>) metadata.get("categories");
for (Map.Entry<String, Object> cat : categories.entrySet()) {
Map<String, Object> vals = (Map<String, Object>) cat.getValue();
for (Map.Entry<String, Object> kv : vals.entrySet()) {
Map<String, Object> row = new LinkedHashMap<>();
row.put("category", cat.getKey());
row.put("key", kv.getKey());
row.put("value", kv.getValue() == null ? "" : kv.getValue().toString());
rows.add(row);
}
}
return rows;
}
@SuppressWarnings("unchecked")
private static String printText(Map<String, Object> metadata) {
StringBuilder sb = new StringBuilder();
sb.append("Image: ").append(metadata.get("image")).append("\n");
Map<String, Object> categories = (Map<String, Object>) metadata.get("categories");
for (Map.Entry<String, Object> cat : categories.entrySet()) {
sb.append("\n[").append(cat.getKey()).append("]\n");
Map<String, Object> vals = (Map<String, Object>) cat.getValue();
for (Map.Entry<String, Object> kv : vals.entrySet()) {
sb.append("- ").append(kv.getKey()).append(": ").append(kv.getValue() == null ? "N/A" : kv.getValue()).append("\n");
}
}
sb.append("\n");
String out = sb.toString();
System.out.print(out);
return out;
}
@SuppressWarnings("unchecked")
private static List<Map<String, Object>> compare(Map<String, Object> a, Map<String, Object> b) {
Set<String> keys = new TreeSet<>();
Map<String, Object> ca = (Map<String, Object>) a.get("categories");
Map<String, Object> cb = (Map<String, Object>) b.get("categories");
for (Map.Entry<String, Object> c : ca.entrySet()) {
for (String k : ((Map<String, Object>) c.getValue()).keySet()) keys.add(c.getKey() + "." + k);
}
for (Map.Entry<String, Object> c : cb.entrySet()) {
for (String k : ((Map<String, Object>) c.getValue()).keySet()) keys.add(c.getKey() + "." + k);
}
List<Map<String, Object>> out = new ArrayList<>();
for (String key : keys) {
String[] p = key.split("\\.", 2);
Object left = ((Map<String, Object>) ca.getOrDefault(p[0], Map.of())).get(p[1]);
Object right = ((Map<String, Object>) cb.getOrDefault(p[0], Map.of())).get(p[1]);
Map<String, Object> row = new LinkedHashMap<>();
row.put("key", key);
row.put("left", left);
row.put("right", right);
row.put("different", !Objects.toString(left, "").equals(Objects.toString(right, "")));
out.add(row);
}
return out;
}
private static void writeCsv(Path path, List<Map<String, Object>> rows, List<String> columns) throws Exception {
Files.createDirectories(path.toAbsolutePath().getParent());
StringBuilder sb = new StringBuilder();
sb.append(String.join(",", columns)).append("\n");
for (Map<String, Object> row : rows) {
List<String> values = new ArrayList<>();
for (String c : columns) {
String v = Objects.toString(row.get(c), "");
values.add("\"" + v.replace("\"", "\"\"") + "\"");
}
sb.append(String.join(",", values)).append("\n");
}
Files.writeString(path, sb.toString(), StandardCharsets.UTF_8);
}
private static void writeJsonOrConsole(Object payload, String outputPath) throws Exception {
String text = GSON.toJson(payload);
if (outputPath == null) System.out.println(text);
else Files.writeString(Paths.get(outputPath).toAbsolutePath(), text, StandardCharsets.UTF_8);
}
private static void stripAll(Path input, Path output) throws Exception {
BufferedImage img = ImageIO.read(input.toFile());
if (img == null) throw new IllegalArgumentException("Cannot read image: " + input);
Files.createDirectories(output.toAbsolutePath().getParent());
String format = ext(output);
if ("jpg".equals(format)) format = "jpeg";
if (!ImageIO.write(img, format, output.toFile())) throw new IllegalArgumentException("Unsupported output format: " + format);
}
private static void removeGpsJpeg(Path input, Path output) throws Exception {
JpegImageMetadata jpegMetadata = (JpegImageMetadata) Imaging.getMetadata(input.toFile());
TiffOutputSet set = null;
if (jpegMetadata != null && jpegMetadata.getExif() != null) {
set = jpegMetadata.getExif().getOutputSet();
}
if (set == null) {
stripAll(input, output);
return;
}
set.removeField(GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF);
set.removeField(GpsTagConstants.GPS_TAG_GPS_LATITUDE);
set.removeField(GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF);
set.removeField(GpsTagConstants.GPS_TAG_GPS_LONGITUDE);
set.removeField(GpsTagConstants.GPS_TAG_GPS_ALTITUDE);
set.removeField(GpsTagConstants.GPS_TAG_GPS_ALTITUDE_REF);
Files.createDirectories(output.toAbsolutePath().getParent());
try (OutputStream os = Files.newOutputStream(output)) {
new ExifRewriter().updateExifMetadataLossless(input.toFile(), os, set);
}
}
private static Path maybeModify(Path input, Map<String, String> options) throws Exception {
if (!options.containsKey("strip-all") && !options.containsKey("remove")) return input;
String extension = input.getFileName().toString().substring(input.getFileName().toString().lastIndexOf('.'));
Path output = options.containsKey("output")
? Paths.get(options.get("output")).toAbsolutePath()
: input.resolveSibling(input.getFileName().toString().replace(extension, "_modified" + extension));
if (options.containsKey("strip-all")) {
stripAll(input, output);
return output;
}
String remove = options.get("remove").toLowerCase(Locale.ROOT);
if (remove.contains("gps")) {
if (!Set.of("jpg", "jpeg").contains(ext(input))) {
throw new IllegalArgumentException("GPS removal is supported for JPEG only in this implementation.");
}
removeGpsJpeg(input, output);
return output;
}
throw new IllegalArgumentException("Unsupported remove target: " + options.get("remove"));
}
private static Path createSample(Path output) throws Exception {
BufferedImage img = new BufferedImage(1280, 720, BufferedImage.TYPE_INT_RGB);
Graphics2D g = img.createGraphics();
g.setColor(new Color(95, 150, 245));
g.fillRect(0, 0, 1280, 720);
g.setColor(Color.WHITE);
g.drawString("Sample EXIF Photo", 80, 100);
g.dispose();
ImageIO.write(img, "jpeg", output.toFile());
TiffOutputSet set = new TiffOutputSet();
set.getOrCreateRootDirectory().add(TiffTagConstants.TIFF_TAG_MAKE, "CodexCam");
set.getOrCreateRootDirectory().add(TiffTagConstants.TIFF_TAG_MODEL, "Model X");
set.getOrCreateExifDirectory().add(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL, "2024:04:11 10:15:22");
set.getOrCreateExifDirectory().add(ExifTagConstants.EXIF_TAG_DATE_TIME_DIGITIZED, "2024:04:11 10:15:22");
set.getOrCreateExifDirectory().add(ExifTagConstants.EXIF_TAG_EXPOSURE_TIME, RationalNumber.valueOf(1, 125));
set.getOrCreateExifDirectory().add(ExifTagConstants.EXIF_TAG_FNUMBER, RationalNumber.valueOf(28, 10));
set.getOrCreateExifDirectory().add(ExifTagConstants.EXIF_TAG_ISO, (short) 200);
set.getOrCreateExifDirectory().add(ExifTagConstants.EXIF_TAG_FOCAL_LENGTH, RationalNumber.valueOf(35, 1));
set.getOrCreateGPSDirectory().add(GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF, "N");
set.getOrCreateGPSDirectory().add(GpsTagConstants.GPS_TAG_GPS_LATITUDE,
new RationalNumber[]{RationalNumber.valueOf(37, 1), RationalNumber.valueOf(46, 1), RationalNumber.valueOf(2964, 100)});
set.getOrCreateGPSDirectory().add(GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF, "W");
set.getOrCreateGPSDirectory().add(GpsTagConstants.GPS_TAG_GPS_LONGITUDE,
new RationalNumber[]{RationalNumber.valueOf(122, 1), RationalNumber.valueOf(25, 1), RationalNumber.valueOf(984, 100)});
set.getOrCreateGPSDirectory().add(GpsTagConstants.GPS_TAG_GPS_ALTITUDE, RationalNumber.valueOf(15, 1));
try (OutputStream os = Files.newOutputStream(output)) {
new ExifRewriter().updateExifMetadataLossless(output.toFile(), os, set);
}
return output;
}
private static void runBatch(Path folder, Map<String, String> options) throws Exception {
List<Map<String, Object>> summary = new ArrayList<>();
try (var stream = Files.list(folder)) {
for (Path p : (Iterable<Path>) stream.filter(Files::isRegularFile)::iterator) {
if (!SUPPORTED.contains(ext(p))) continue;
Map<String, Object> md = extract(p, options.containsKey("map-url"));
printText(md);
@SuppressWarnings("unchecked")
Map<String, Object> categories = (Map<String, Object>) md.get("categories");
@SuppressWarnings("unchecked")
Map<String, Object> cam = (Map<String, Object>) categories.get("camera_info");
@SuppressWarnings("unchecked")
Map<String, Object> dt = (Map<String, Object>) categories.get("date_time");
@SuppressWarnings("unchecked")
Map<String, Object> gps = (Map<String, Object>) categories.get("gps");
Map<String, Object> row = new LinkedHashMap<>();
row.put("image", p.toAbsolutePath().toString());
row.put("make", cam.get("make"));
row.put("model", cam.get("model"));
row.put("date", dt.get("original"));
row.put("latitude", gps.get("latitude_decimal"));
row.put("longitude", gps.get("longitude_decimal"));
summary.add(row);
}
}
Path out = options.containsKey("output")
? Paths.get(options.get("output")).toAbsolutePath()
: Paths.get("batch_metadata_summary.csv").toAbsolutePath();
writeCsv(out, summary, List.of("image", "make", "model", "date", "latitude", "longitude"));
System.out.println("Batch summary CSV: " + out);
}
}