Skip to content

photostructure/exiftool-vendored.js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2,008 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

📸 exiftool-vendored

Fast, cross-platform Node.js access to ExifTool. Built and supported by PhotoStructure.

npm version Node.js CI GitHub issues

🚀 Installation & Quick Start

Requirements: Node.js Active LTS or Maintenance LTS versions only

npm install exiftool-vendored
import { exiftool } from "exiftool-vendored";

// Read metadata
const tags = await exiftool.read("photo.jpg");
console.log(`Camera: ${tags.Make} ${tags.Model}`);
console.log(`Taken: ${tags.DateTimeOriginal}`);
console.log(`Size: ${tags.ImageWidth}x${tags.ImageHeight}`);

// Write metadata
await exiftool.write("photo.jpg", {
  XPComment: "Amazing sunset!",
  Copyright: "© 2024 Your Name",
});

// Extract thumbnail
await exiftool.extractThumbnail("photo.jpg", "thumb.jpg");

await exiftool.end();

🤔 Why exiftool-vendored?

Performance

Order of magnitude faster than other Node.js ExifTool modules. Powers PhotoStructure and 1,000+ other projects.

🔧 Battle-tested

  • Cross-platform: macOS, Linux, Windows
  • Full-featured: Read, write, extract embedded images
  • Reliable: Extensive test coverage across most camera manufacturers

📚 Developer-Friendly

  • TypeScript: Full type definitions for thousands of metadata fields
  • Smart dates: Timezone-aware ExifDateTime classes
  • Auto-generated tags: Based on 10,000+ real camera samples

✨ Core Features

Reading Metadata

const tags = await exiftool.read("photo.jpg");

// Camera info
console.log(tags.Make, tags.Model, tags.LensModel);

// Capture settings
console.log(tags.ISO, tags.FNumber, tags.ExposureTime);

// Location (if available)
console.log(tags.GPSLatitude, tags.GPSLongitude);

// Always check for parsing errors
if (tags.errors?.length > 0) {
  console.warn("Metadata warnings:", tags.errors);
}

Writing Metadata

// Add keywords and copyright
await exiftool.write("photo.jpg", {
  Keywords: ["sunset", "landscape"],
  Copyright: "© 2024 Photographer Name",
  "IPTC:CopyrightNotice": "© 2024 Photographer Name",
});

// Update all date fields at once
await exiftool.write("photo.jpg", {
  AllDates: "2024:03:15 14:30:00",
});

// Delete tags
await exiftool.write("photo.jpg", {
  UserComment: null,
});

For targeted list-value and supported structured edits, use editTags(). See Editing Individual Tag Values for examples, supported tags, and safety constraints.

Extracting Images

// Extract thumbnail
await exiftool.extractThumbnail("photo.jpg", "thumbnail.jpg");

// Extract preview (larger than thumbnail)
await exiftool.extractPreview("photo.jpg", "preview.jpg");

// Extract JPEG from RAW files
await exiftool.extractJpgFromRaw("photo.cr2", "processed.jpg");

🏷️ Understanding Tags

The Tags interface contains thousands of metadata fields from an auto-generated TypeScript file. Each tag has JSDoc annotations:

/**
 * @frequency 🔥 ★★★★ (85%)
 * @groups EXIF, MakerNotes
 * @example 100
 */
ISO?: number;

/**
 * @frequency 🧊 ★★★☆ (23%)
 * @groups MakerNotes
 * @example "Custom lens data"
 */
LensSpec?: string;
  • 🔥 = Found on mainstream devices (iPhone, Canon, Nikon, Sony)
  • 🧊 = Only found on more obscure camera makes and models
  • ★★★★ = Found in >50% of files, ☆☆☆☆ = rare (<1%)
  • @groups = Metadata categories (EXIF, GPS, IPTC, XMP, etc.)
  • @example = Representative values

🛡️ Code defensively!

The generated Tags interface is a deliberately bounded, best-effort model of what ExifTool extracts.

Declared fields may be missing

Formats, cameras, and editing software write different subsets of metadata, and later tools may strip fields. Treat every property as optional, even when it is common for the files you currently handle.

Undeclared fields may be included

The interface favors the most common and useful fields. Without tag pruning, TypeScript fails with TS2590: Expression produces a union type that is too complex to represent.

Rare, vendor-specific, and custom tags may still appear at runtime. If your app needs arbitrary tags, intersect Tags with Record<string, unknown>; if a tag would be useful to others, please open a PR to add it to the generated interface.

Value types may vary

ExifTool may return unexpected representations for malformed, ambiguous, or format-specific values. Validate values at runtime and handle strings in nominally numeric fields gracefully.

📖 Complete Tags Documentation →

🪤 Parsing gotchas

Timezones may be inferred

Media metadata often omits its timezone, so this library uses several heuristics to infer one. See Dates & Timezones for the inference order and configuration options.

Malformed UTF-8 is marked and preserved

Malformed UTF-8 bytes are marked with the Unicode replacement character U+FFFD, not ASCII ?, so byte corruption stays distinguishable from authored punctuation. The original bytes are preserved without another file read in the sparse invalidUtf8Bytes sidecar:

const tags = await exiftool.read(file);
tags.ImageDescription; // "Arch Enemy\rG�teborg, 19.07.2007"

const bytes = tags.invalidUtf8Bytes?.ImageDescription;
if (bytes instanceof Uint8Array) {
  // Camera/tag-specific evidence identifies this Kodak value as MacRoman:
  const recovered = new TextDecoder("macintosh").decode(bytes);
  recovered; // "Arch Enemy\rGöteborg, 19.07.2007"
}

The macintosh charset is justified by evidence for this specific Kodak value; it is not a default for every damaged field.

Nested metadata mirrors the representation selected by ExifTool's struct option. Extracted XML element text, attribute values, CDATA, and parsed embedded XML values are captured at their returned paths. These are the bytes of ExifTool's extracted value after XML parsing, not the original XML token: entity spellings and CDATA delimiters are not retained. Value filters never receive XML element or attribute names, and ExifTool's JSON output repairs or canonicalizes malformed names before the library sees them, so their exact bytes cannot be exposed by this sidecar.

The library deliberately does not guess a legacy charset: files assembled over many years may contain several encodings, and readable alternatives are not necessarily correct. Consumers can decode only the sidecar entries using their own camera/tag/user context. An explicit custom ExifTool Filter disables both built-in repair and byte capture. To restore the pre-v37 display, guard on the value type first: typeof value === "string" ? value.replace(/\uFFFD/g, "?") : value.

⚠️ Important Notes

Configuration

exiftool-vendored provides two levels of configuration:

Library-wide Settings - Global configuration affecting all instances:

import { Settings } from "exiftool-vendored";

// Enable parsing of archaic timezone offsets for historical photos
Settings.allowArchaicTimezoneOffsets.value = true;

Per-instance Options - Configuration for individual ExifTool instances:

import { ExifTool } from "exiftool-vendored";

const exiftool = new ExifTool({
  maxProcs: 8, // More concurrent processes
  useMWG: true, // Use Metadata Working Group tags
  backfillTimezones: true, // Infer missing timezones
});

📖 Complete Configuration Guide →

Dates & Timezones

Images rarely specify timezones. This library infers them using several heuristics:

  1. Explicit metadata (TimeZoneOffset, OffsetTime)
  2. GPS location → timezone lookup
  3. UTC timestamps → calculate offset
const dt = tags.DateTimeOriginal;
if (dt instanceof ExifDateTime) {
  console.log("Timezone offset:", dt.tzoffset, "minutes");
  console.log("Timezone:", dt.zone);
}

📖 Date & Timezone Guide →

Resource Cleanup

With the default settings, ExifTool workers no longer keep Node.js alive after awaited work finishes. During normal shutdown, the library attempts to clean up workers automatically. Abrupt termination, such as SIGKILL or an operating- system crash, cannot run cleanup handlers.

Call and await .end() when cleanup must finish before your application continues or exits. For servers and daemons, make this one part of the application's complete shutdown procedure:

import { exiftool } from "exiftool-vendored";

async function shutdown(signal) {
  try {
    await closeApplicationResources(); // Server, sockets, database, etc.
    await exiftool.end();
  } finally {
    // A signal listener disables Node's default termination behavior. Re-send
    // the signal after cleanup so the process terminates normally.
    process.kill(process.pid, signal);
  }
}

process.once("SIGINT", (signal) => void shutdown(signal));
process.once("SIGTERM", (signal) => void shutdown(signal));

Automatic Cleanup with Disposable Interfaces

For TypeScript 5.2+ projects configured for explicit resource management, you can bind an instance's lifecycle to a scope:

import { ExifTool } from "exiftool-vendored";

// Starts cleanup when the scope exits, but does not wait for it
{
  using et = new ExifTool();
  const tags = await et.read("photo.jpg");
  // ExifTool cleanup is initiated when the block exits
}

// Waits for cleanup when the scope exits (recommended)
{
  await using et = new ExifTool();
  const tags = await et.read("photo.jpg");
  // ExifTool cleanup is awaited when the block exits
}

Benefits:

  • Scope-bound disposal: Disposal runs on ordinary scope exit, including exceptions
  • Awaited cleanup: await using waits for asynchronous disposal
  • Less boilerplate: No manual try/finally cleanup block

Caution:

  • Operating-system startup lag: Startup time varies widely with the OS, hardware, and security software, and can take several seconds on some systems. Don't dispose an instance until you're really done with it.

Tag Completeness

The Tags interface shows the most common fields, but ExifTool can extract many more. Cast to access unlisted fields:

const tags = await exiftool.read("photo.jpg");
const customField = (tags as any).UncommonTag;

📚 Documentation

Guides

Troubleshooting

API Reference

⚡ Performance

The default singleton is throttled for stability. For high-throughput processing:

import { ExifTool } from "exiftool-vendored";

const exiftool = new ExifTool({
  maxProcs: 8, // More concurrent processes
  minDelayBetweenSpawnMillis: 0, // Faster spawning
  streamFlushMillis: 10, // Faster streaming
});

// Process many files efficiently
const results = await Promise.all(filePaths.map((file) => exiftool.read(file)));

await exiftool.end();

Benchmarks: 20+ files/second/thread, 500+ files/second using all CPU cores.

🤝 Support & Community

Contributors

Matthew McEachen, Joshua Harris, Anton Mokrushin, Luca Ban, Demiurga, David Randler