← All tasks
javascriptcodex/javascript-t1 #22Not a task: repair changed code

EXIF Metadata Extractor (javascript, written by Codex)

envgap__codex__javascript-t1-22

Written by a coding agent; not on GitHubWritten 2026-03-03

01 / FAILURE SIGNATURE

As the study recorded it

exifr.parse is not a function - wrong import style for ESM
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
package.json
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/javascript-t1 #22 · read the task the agent was given
Codex wrote this javascript 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 JavaScript project for a clean Ubuntu 22.04 machine with only Node.js 20+ (LTS) installed. Include:
- Source code
- package.json 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

4 files, exactly as written, before any repair.

package-lock.json
{
  "name": "exif-metadata-extractor",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "exif-metadata-extractor",
      "version": "1.0.0",
      "dependencies": {
        "exifr": "7.1.3",
        "piexifjs": "1.0.6",
        "sharp": "0.33.5"
      },
      "engines": {
        "node": ">=20.0.0"
      }
    },
    "node_modules/exifr": {
      "version": "7.1.3"
    },
    "node_modules/piexifjs": {
      "version": "1.0.6"
    },
    "node_modules/sharp": {
      "version": "0.33.5"
    }
  }
}
package.json
{
  "name": "exif-metadata-extractor",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "engines": {
    "node": ">=20.0.0"
  },
  "dependencies": {
    "exifr": "7.1.3",
    "piexifjs": "1.0.6",
    "sharp": "0.33.5"
  }
}
README.md
# EXIF Metadata Extractor (JavaScript)

Extracts, displays, compares, and modifies EXIF metadata (JPEG/TIFF/PNG), including GPS conversion and batch summary export.

## Requirements

- Ubuntu 22.04
- Node.js 20+ (LTS)

## Dependencies (Pinned)

- `exifr@7.1.3`
- `piexifjs@1.0.6`
- `sharp@0.33.5`

## Setup

```bash
npm install
```

## Run

```bash
node src/index.js image.jpg
node src/index.js image.jpg --format json --output metadata.json
node src/index.js image.jpg --format csv --output metadata.csv
node src/index.js image.jpg --map-url
node src/index.js image.jpg --compare other.jpg
node src/index.js image.jpg --remove GPS --output image_no_gps.jpg
node src/index.js image.jpg --strip-all --output image_stripped.jpg
node src/index.js ./images --batch --output batch_summary.csv
node src/index.js
```

## Notes

- `--remove GPS` is implemented for JPEG.
- `--strip-all` removes metadata while re-encoding with high quality settings.
- No-input mode generates a sample JPEG with EXIF (including GPS) and displays extracted metadata.
src/index.js
import fs from "fs";
import path from "path";
import sharp from "sharp";
import * as exifr from "exifr";
import piexif from "piexifjs";

const SUPPORTED = new Set([".jpg", ".jpeg", ".tif", ".tiff", ".png"]);

function parseArgs(argv) {
  const options = {};
  const positional = [];
  for (let i = 0; i < argv.length; i += 1) {
    const token = argv[i];
    if (token.startsWith("--")) {
      const key = token.slice(2);
      const next = argv[i + 1];
      if (next && !next.startsWith("--")) {
        options[key] = next;
        i += 1;
      } else {
        options[key] = true;
      }
    } else {
      positional.push(token);
    }
  }
  return { options, positional };
}

function rationalToNumber(v) {
  if (typeof v === "number") return v;
  if (!v || typeof v !== "object") return null;
  if ("numerator" in v && "denominator" in v && v.denominator) return v.numerator / v.denominator;
  return null;
}

function toDms(decimal, latLon) {
  if (decimal == null || Number.isNaN(decimal)) return null;
  const abs = Math.abs(decimal);
  const deg = Math.floor(abs);
  const minFloat = (abs - deg) * 60;
  const min = Math.floor(minFloat);
  const sec = (minFloat - min) * 60;
  const hemi = latLon === "lat" ? (decimal >= 0 ? "N" : "S") : (decimal >= 0 ? "E" : "W");
  return `${deg}°${min}'${sec.toFixed(2)}" ${hemi}`;
}

function mapUrl(lat, lon) {
  if (lat == null || lon == null) return null;
  return `https://www.google.com/maps?q=${lat},${lon}`;
}

async function readMetadata(filePath, options = {}) {
  const ext = path.extname(filePath).toLowerCase();
  if (!SUPPORTED.has(ext)) throw new Error(`Unsupported image format: ${filePath}`);

  const meta = await sharp(filePath).metadata();
  const exif = await exifr.parse(filePath, { tiff: true, ifd0: true, exif: true, gps: true, interop: true }).catch(() => ({}));
  const full = await exifr.parse(filePath, { tiff: true, ifd0: true, exif: true, gps: true, interop: true, xmp: true, iptc: true }).catch(() => ({}));

  const lat = exif.latitude ?? null;
  const lon = exif.longitude ?? null;
  const alt = exif.GPSAltitude != null ? rationalToNumber(exif.GPSAltitude) ?? exif.GPSAltitude : null;

  const structured = {
    image: path.resolve(filePath),
    categories: {
      cameraInfo: {
        make: exif.Make ?? null,
        model: exif.Model ?? null,
        serial: exif.BodySerialNumber ?? exif.SerialNumber ?? null,
      },
      captureSettings: {
        exposureTime: exif.ExposureTime ?? null,
        fNumber: exif.FNumber ?? null,
        iso: exif.ISO ?? exif.ISOSpeedRatings ?? null,
        focalLength: exif.FocalLength ?? null,
        flash: exif.Flash ?? null,
      },
      dateTime: {
        original: exif.DateTimeOriginal ?? null,
        digitized: exif.CreateDate ?? exif.DateTimeDigitized ?? null,
        modified: exif.ModifyDate ?? null,
      },
      gps: {
        latitudeDecimal: lat,
        longitudeDecimal: lon,
        altitude: alt,
        latitudeDms: toDms(lat, "lat"),
        longitudeDms: toDms(lon, "lon"),
        mapUrl: options.mapUrl ? mapUrl(lat, lon) : null,
      },
      imageInfo: {
        dimensions: `${meta.width ?? "?"}x${meta.height ?? "?"}`,
        orientation: exif.Orientation ?? meta.orientation ?? null,
        colorSpace: exif.ColorSpace ?? meta.space ?? null,
        resolution: exif.XResolution && exif.YResolution ? `${exif.XResolution}x${exif.YResolution}` : null,
      },
    },
    fullDump: full ?? {},
  };

  return structured;
}

function flattenForCsv(structured) {
  const rows = [];
  for (const [category, values] of Object.entries(structured.categories)) {
    for (const [key, value] of Object.entries(values)) {
      rows.push({ category, key, value: value == null ? "" : String(value) });
    }
  }
  return rows;
}

function printText(structured) {
  console.log(`Image: ${structured.image}`);
  for (const [category, values] of Object.entries(structured.categories)) {
    console.log(`\n[${category}]`);
    for (const [k, v] of Object.entries(values)) {
      console.log(`- ${k}: ${v == null ? "N/A" : v}`);
    }
  }
  console.log("");
}

function compareMetadata(a, b) {
  const allKeys = new Set();
  for (const [cat, obj] of Object.entries(a.categories)) {
    for (const key of Object.keys(obj)) allKeys.add(`${cat}.${key}`);
  }
  for (const [cat, obj] of Object.entries(b.categories)) {
    for (const key of Object.keys(obj)) allKeys.add(`${cat}.${key}`);
  }

  const rows = [];
  for (const k of allKeys) {
    const [cat, key] = k.split(".");
    const left = a.categories[cat]?.[key] ?? null;
    const right = b.categories[cat]?.[key] ?? null;
    const different = String(left) !== String(right);
    rows.push({ key: k, left, right, different });
  }
  return rows.sort((x, y) => x.key.localeCompare(y.key));
}

function writeCsv(rows, outPath, columns) {
  const lines = [columns.join(",")];
  for (const row of rows) {
    lines.push(columns.map((c) => {
      const val = row[c] == null ? "" : String(row[c]);
      return `"${val.replaceAll('"', '""')}"`;
    }).join(","));
  }
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
  fs.writeFileSync(outPath, `${lines.join("\n")}\n`, "utf8");
}

async function stripAllMetadata(input, output) {
  const ext = path.extname(input).toLowerCase();
  const image = sharp(input);
  let pipeline = image;
  if (ext === ".jpg" || ext === ".jpeg") pipeline = pipeline.jpeg({ quality: 95, mozjpeg: true });
  else if (ext === ".png") pipeline = pipeline.png({ compressionLevel: 6 });
  else if (ext === ".tif" || ext === ".tiff") pipeline = pipeline.tiff({ quality: 95 });
  await pipeline.toFile(output);
}

function removeGpsJpeg(input, output) {
  const jpegBinary = fs.readFileSync(input, "binary");
  const dataUrl = `data:image/jpeg;base64,${Buffer.from(jpegBinary, "binary").toString("base64")}`;
  const exifObj = piexif.load(dataUrl);
  exifObj.GPS = {};
  const exifBytes = piexif.dump(exifObj);
  const newDataUrl = piexif.insert(exifBytes, dataUrl);
  const b64 = newDataUrl.replace(/^data:image\/jpeg;base64,/, "");
  fs.writeFileSync(output, Buffer.from(b64, "base64"));
}

async function modifyMetadata(input, options) {
  const ext = path.extname(input).toLowerCase();
  const out = path.resolve(options.output || `${path.join(path.dirname(input), path.basename(input, ext))}_modified${ext}`);

  if (options["strip-all"]) {
    await stripAllMetadata(input, out);
    return out;
  }
  if (!options.remove) return null;

  const targets = String(options.remove).split(",").map((v) => v.trim().toLowerCase());
  if (targets.includes("gps")) {
    if (ext === ".jpg" || ext === ".jpeg") {
      removeGpsJpeg(input, out);
      return out;
    }
    throw new Error("GPS removal is currently supported for JPEG only in this implementation.");
  }
  throw new Error(`Unsupported remove target(s): ${targets.join(", ")}`);
}

async function sampleImagePath() {
  const out = path.resolve("sample_exif.jpg");
  const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#4f8cff"/><stop offset="100%" stop-color="#79d2a6"/></linearGradient></defs>
  <rect width="1280" height="720" fill="url(#g)"/>
  <text x="60" y="110" font-size="72" fill="white">Sample EXIF Photo</text>
</svg>`;
  await sharp(Buffer.from(svg)).jpeg({ quality: 95 }).toFile(out);

  const jpegBinary = fs.readFileSync(out, "binary");
  const dataUrl = `data:image/jpeg;base64,${Buffer.from(jpegBinary, "binary").toString("base64")}`;
  const exifObj = { "0th": {}, Exif: {}, GPS: {}, "1st": {}, thumbnail: null };
  exifObj["0th"][piexif.ImageIFD.Make] = "CodexCam";
  exifObj["0th"][piexif.ImageIFD.Model] = "Model X";
  exifObj["0th"][piexif.ImageIFD.Orientation] = 1;
  exifObj.Exif[piexif.ExifIFD.ISOSpeedRatings] = 200;
  exifObj.Exif[piexif.ExifIFD.ExposureTime] = [1, 125];
  exifObj.Exif[piexif.ExifIFD.FNumber] = [28, 10];
  exifObj.Exif[piexif.ExifIFD.FocalLength] = [35, 1];
  exifObj.Exif[piexif.ExifIFD.DateTimeOriginal] = "2024:04:11 10:15:22";
  exifObj.Exif[piexif.ExifIFD.DateTimeDigitized] = "2024:04:11 10:15:22";
  exifObj["0th"][piexif.ImageIFD.DateTime] = "2024:04:11 10:15:22";
  exifObj.GPS[piexif.GPSIFD.GPSLatitudeRef] = "N";
  exifObj.GPS[piexif.GPSIFD.GPSLatitude] = [[37, 1], [46, 1], [2964, 100]];
  exifObj.GPS[piexif.GPSIFD.GPSLongitudeRef] = "W";
  exifObj.GPS[piexif.GPSIFD.GPSLongitude] = [[122, 1], [25, 1], [984, 100]];
  exifObj.GPS[piexif.GPSIFD.GPSAltitude] = [15, 1];

  const exifBytes = piexif.dump(exifObj);
  const newDataUrl = piexif.insert(exifBytes, dataUrl);
  fs.writeFileSync(out, Buffer.from(newDataUrl.replace(/^data:image\/jpeg;base64,/, ""), "base64"));
  return out;
}

function outputFormatted(payload, format, outputPath) {
  if (format === "json") {
    const text = JSON.stringify(payload, null, 2);
    console.log(text);
    if (outputPath) fs.writeFileSync(outputPath, `${text}\n`, "utf8");
    return;
  }
  if (format === "csv") {
    if (!payload.rows) throw new Error("CSV output requires row data.");
    writeCsv(payload.rows, outputPath || path.resolve("metadata.csv"), payload.columns);
    console.log(`Saved CSV: ${outputPath || path.resolve("metadata.csv")}`);
    return;
  }
  console.log(payload.text);
  if (outputPath) fs.writeFileSync(outputPath, `${payload.text}\n`, "utf8");
}

async function batchExtract(dir, options) {
  const files = fs.readdirSync(dir)
    .map((name) => path.join(dir, name))
    .filter((f) => fs.statSync(f).isFile() && SUPPORTED.has(path.extname(f).toLowerCase()));

  const rows = [];
  for (const file of files) {
    const m = await readMetadata(file, { mapUrl: Boolean(options["map-url"]) });
    rows.push({
      image: m.image,
      make: m.categories.cameraInfo.make ?? "",
      model: m.categories.cameraInfo.model ?? "",
      date: m.categories.dateTime.original ?? "",
      latitude: m.categories.gps.latitudeDecimal ?? "",
      longitude: m.categories.gps.longitudeDecimal ?? "",
    });
    printText(m);
  }
  const out = path.resolve(options.output || "batch_metadata_summary.csv");
  writeCsv(rows, out, ["image", "make", "model", "date", "latitude", "longitude"]);
  console.log(`Batch summary CSV: ${out}`);
}

async function main() {
  const { options, positional } = parseArgs(process.argv.slice(2));
  const format = String(options.format || "text").toLowerCase();
  if (!["text", "json", "csv"].includes(format)) throw new Error("Invalid --format. Use text|json|csv.");

  if (!positional.length && !options.batch) {
    const sample = await sampleImagePath();
    const data = await readMetadata(sample, { mapUrl: Boolean(options["map-url"]) });
    printText(data);
    return;
  }

  if (!positional.length) throw new Error("Input path required.");
  const input = path.resolve(positional[0]);

  if (options.batch) {
    if (!fs.existsSync(input) || !fs.statSync(input).isDirectory()) throw new Error("--batch requires a directory input.");
    await batchExtract(input, options);
    return;
  }

  if (!fs.existsSync(input) || !fs.statSync(input).isFile()) throw new Error(`Input not found: ${input}`);

  const modifiedPath = await modifyMetadata(input, options);
  const effectivePath = modifiedPath || input;
  const data = await readMetadata(effectivePath, { mapUrl: Boolean(options["map-url"]) });

  let textPayload = "";
  const capturedLog = [];
  const orig = console.log;
  console.log = (...args) => capturedLog.push(args.join(" "));
  printText(data);
  console.log = orig;
  textPayload = capturedLog.join("\n");
  console.log(textPayload);

  if (options.compare) {
    const other = await readMetadata(path.resolve(String(options.compare)), { mapUrl: Boolean(options["map-url"]) });
    const diff = compareMetadata(data, other);
    console.log("Comparison:");
    for (const row of diff.filter((r) => r.different)) {
      console.log(`- ${row.key}: left=${row.left ?? "N/A"} | right=${row.right ?? "N/A"}`);
    }
    if (format === "json") {
      const payload = { metadata: data, comparisonAgainst: other.image, differences: diff };
      const out = options.output ? path.resolve(String(options.output)) : null;
      outputFormatted(payload, "json", out);
      return;
    }
    if (format === "csv") {
      const out = options.output ? path.resolve(String(options.output)) : path.resolve("comparison.csv");
      writeCsv(diff, out, ["key", "left", "right", "different"]);
      console.log(`Saved CSV: ${out}`);
      return;
    }
    if (options.output) fs.writeFileSync(path.resolve(String(options.output)), `${textPayload}\n`, "utf8");
    return;
  }

  if (format === "json") {
    outputFormatted(data, "json", options.output ? path.resolve(String(options.output)) : null);
  } else if (format === "csv") {
    const rows = flattenForCsv(data);
    outputFormatted({ rows, columns: ["category", "key", "value"] }, "csv", options.output ? path.resolve(String(options.output)) : null);
  } else if (options.output) {
    fs.writeFileSync(path.resolve(String(options.output)), `${textPayload}\n`, "utf8");
  }

  if (modifiedPath) console.log(`Modified image saved: ${modifiedPath}`);
}

main().catch((error) => {
  console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
  process.exit(1);
});