Fast, cross-platform Node.js access to ExifTool. Built and supported by PhotoStructure.
Requirements: Node.js Active LTS or Maintenance LTS versions only
npm install exiftool-vendoredimport { 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();Order of magnitude faster than other Node.js ExifTool modules. Powers PhotoStructure and 1,000+ other projects.
- Cross-platform: macOS, Linux, Windows
- Full-featured: Read, write, extract embedded images
- Reliable: Extensive test coverage across most camera manufacturers
- TypeScript: Full type definitions for thousands of metadata fields
- Smart dates: Timezone-aware
ExifDateTimeclasses - Auto-generated tags: Based on 10,000+ real camera samples
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);
}// 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.
// 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");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
The generated Tags interface is a deliberately bounded, best-effort model of
what ExifTool extracts.
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.
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.
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 →
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 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.
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 →
Images rarely specify timezones. This library infers them using several heuristics:
- Explicit metadata (TimeZoneOffset, OffsetTime)
- GPS location → timezone lookup
- UTC timestamps → calculate offset
const dt = tags.DateTimeOriginal;
if (dt instanceof ExifDateTime) {
console.log("Timezone offset:", dt.tzoffset, "minutes");
console.log("Timezone:", dt.zone);
}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));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 usingwaits for asynchronous disposal - Less boilerplate: No manual
try/finallycleanup 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.
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;- Installation Guide - Electron, Docker, platform setup
- Usage Examples - Comprehensive API examples
- Date Handling - Timezone complexities explained
- Tags Reference - Understanding the 2,500+ metadata fields
- Electron Integration - Electron-specific setup
- Debugging Guide - Debug logging and common issues
- Temporal Migration - Future JavaScript Temporal API
- TypeDoc Documentation - Complete API reference
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.
- 📋 Issues: GitHub Issues
- 📖 Changelog: CHANGELOG.md
- 🔒 Security: SECURITY.md
- 📄 License: MIT
Matthew McEachen, Joshua Harris, Anton Mokrushin, Luca Ban, Demiurga, David Randler