Complete examples for common exiftool-vendored use cases.
For detailed configuration options, see the Configuration Guide.
import { exiftool } from "exiftool-vendored";
// or: const { exiftool } = require("exiftool-vendored");
// Verify installation
console.log(`ExifTool v${await exiftool.version()}`);
// Optional here; await this when cleanup must finish before continuing or exiting
await exiftool.end();
const tags = await exiftool.read("photo.jpg");
console.log("Camera:", tags.Make, tags.Model);
console.log("Size:", tags.ImageWidth, "x", tags.ImageHeight);
console.log("Taken:", tags.DateTimeOriginal);
console.log("Location:", tags.GPSLatitude, tags.GPSLongitude);
const tags = await exiftool.read("photo.jpg");
// Handle optional values safely
const camera = tags.Make ? `${tags.Make} ${tags.Model}` : "Unknown camera";
const dimensions =
tags.ImageWidth && tags.ImageHeight
? `${tags.ImageWidth}x${tags.ImageHeight}`
: "Unknown size";
// Use nullish coalescing for fallbacks
const timestamp = tags.DateTimeOriginal ?? tags.DateTime ?? tags.FileModifyDate;
const title = tags.Title ?? tags.DocumentName ?? tags.FileName;
try {
const tags = await exiftool.read("photo.jpg");
// Check for parsing warnings
if (tags.errors && tags.errors.length > 0) {
console.warn("Metadata warnings:", tags.errors);
}
console.log("Successfully read metadata");
} catch (error) {
console.error("Failed to read file:", error.message);
}
// Add comment and copyright
await exiftool.write("photo.jpg", {
XPComment: "Beautiful sunset",
Copyright: "© 2024 Your Name",
});
// Update capture date
await exiftool.write("photo.jpg", {
DateTimeOriginal: "2024:03:15 14:30:00",
});
// Write to specific metadata groups
await exiftool.write("photo.jpg", {
"IPTC:Keywords": "sunset, landscape, nature",
"IPTC:CopyrightNotice": "© 2024 Photographer Name",
"XMP:Title": "Sunset Over Mountains",
"XMP:Description": "A stunning sunset captured in the mountains",
});
// Delete specific tags by setting to null
await exiftool.write("photo.jpg", {
UserComment: null,
ImageDescription: null,
"IPTC:Keywords": null,
});
Use editTags() to add or remove individual list values without replacing
unrelated metadata. Tag names must be canonical entries in the exported
TagEditTagNames allowlist. TagEditValueTagNames contains the primitive
remove-capable subset; TagEditAddTagNames contains the add-capable list tags;
and TagEditRemoveOnlyTagNames contains audited scalar and flattened structure
fields. XMP-mwg-coll:Collections is the one supported structured tag. Each
primitive operation accepts one value. Arguments are emitted in the provided
order, but ExifTool applies removals before additions for the same tag:
await exiftool.editTags("photo.jpg", [
{
tag: "XMP-dc:Subject",
operation: "remove",
value: "beach",
},
{
tag: "XMP-dc:Subject",
operation: "add",
value: "forest",
},
]);
remove deletes every exact matching value. ExifTool Bags are unordered, and
add does not deduplicate values. Repeat an operation when duplicate additions
are intentional. Remove-then-add for the same tag and value is supported as a
one-write normalization that removes duplicates and leaves one value.
Add-then-remove is rejected because ExifTool still applies the removal first
and would leave the value present. Primitive values must be non-empty and
contain only Unicode that ExifTool can preserve exactly. Leading spaces and
literal HTML entities are preserved. Edit values are always literal text: for
example, "A & B" stores those exact nine characters. write() retains
its older behavior and may interpret valid HTML entity sequences, so read the
stored value before moving an entity-containing value between the two APIs.
Structured edits are intentionally limited to schemas validated by this package. Adding an MWG Collection requires both fields, while removal accepts a non-empty predicate containing either or both fields:
await exiftool.editTags("photo.jpg", [
{
tag: "XMP-mwg-coll:Collections",
operation: "add",
value: {
CollectionName: "Portfolio",
CollectionURI: "urn:portfolio",
},
},
{
tag: "XMP-mwg-coll:Collections",
operation: "remove",
predicate: { CollectionName: "Vacation" },
},
]);
The entire edit array is snapshotted and validated before ExifTool is invoked. If any operation is invalid, no metadata is written.
The allowlist covers audited, qualified Subject, HierarchicalSubject,
Keywords, CatalogSets, People, PersonInImage, TagsList, LastKeywordXMP, and
region/person-name fields, plus the XMP Dynamic Media Album scalar. Album and
the flattened structure fields are remove-only. Other tags and aliases are
rejected before ExifTool runs; this prevents += or -= from unexpectedly
incrementing numbers, shifting dates, or editing multiple physical properties.
Most WriteTaskOptions remain available. writeArgs entries for -api and
-sep/-separator are rejected because they can change exact matching, list
splitting, or duplicate behavior.
For MWG and ACDSee face labels, removing a flattened name preserves the face and its detection geometry. Because ExifTool removes every matching name, first verify that the same name is not used by an unrelated non-face region:
await exiftool.editTags("photo.jpg", [
{
tag: "XMP-mwg-rs:RegionName",
operation: "remove",
value: "Jane",
},
]);
The other audited remove-only fields are
XMP-acdsee-rs:ACDSeeRegionName, XMP-MP:RegionPersonDisplayName, and
XMP-xmpDM:Album. Flattened-name removals preserve remaining sibling fields;
ExifTool may prune a region and its container if the removed name was their
only field. Album removal matches the scalar value exactly.
XMP-iptcExt:PersonInImageName remains unsupported because removing its
x-default value also removes nonmatching alternate-language values.
editTags() does not replace write(): whole-tag deletion still uses
null, and ordered set/clear/forced-empty operations are not supported.
// Update all date fields at once
await exiftool.write("photo.jpg", {
AllDates: "2024:03:15 14:30:00",
});
// This is equivalent to setting:
// - DateTimeOriginal
// - CreateDate
// - ModifyDate
// Set GPS location (decimal degrees)
await exiftool.write("photo.jpg", {
GPSLatitude: 40.7128,
GPSLongitude: -74.006,
GPSAltitude: 10, // meters above sea level
});
// Extract EXIF thumbnail
try {
await exiftool.extractThumbnail("photo.jpg", "thumbnail.jpg");
console.log("Thumbnail extracted successfully");
} catch (error) {
console.log("No thumbnail found or extraction failed");
}
// Extract preview image (larger than thumbnail)
try {
await exiftool.extractPreview("photo.jpg", "preview.jpg");
console.log("Preview extracted successfully");
} catch (error) {
console.log("No preview found or extraction failed");
}
import { parseJSON, ExifDateTime } from "exiftool-vendored";
import { readFile, writeFile } from "node:fs/promises";
// Read and serialize
const tags = await exiftool.read("photo.jpg");
const jsonString = JSON.stringify(tags);
// Save to file or send over network
await writeFile("metadata.json", jsonString);
// Later, deserialize
const savedJson = await readFile("metadata.json", "utf8");
const restoredTags = parseJSON(savedJson);
// restoredTags has proper ExifDateTime objects restored
console.log(restoredTags.DateTimeOriginal instanceof ExifDateTime); // true
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. Startup time varies widely with the OS, hardware, and
security software, so avoid repeatedly creating and disposing instances.
import { ExifTool } from "exiftool-vendored";
const exiftool = new ExifTool();
try {
const tags = await exiftool.read("photo.jpg");
console.log(tags.Make, tags.Model);
} finally {
// Optional: graceful shutdown (recommended for long-running apps)
await exiftool.end();
}
For TypeScript 5.2+ projects with proper tsconfig.json configuration:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "ESNext.Disposable", "DOM"]
}
}
import { ExifTool } from "exiftool-vendored";
// Block scope with automatic cleanup initiation
{
using et = new ExifTool();
const tags = await et.read("photo.jpg");
console.log(`Camera: ${tags.Make} ${tags.Model}`);
// Graceful cleanup starts when the block exits, but is not awaited
}
import { ExifTool } from "exiftool-vendored";
// Graceful cleanup that is awaited when the scope exits
{
await using et = new ExifTool();
const tags = await et.read("photo.jpg");
await et.write("photo.jpg", {
XPComment: "Processed with exiftool-vendored, golly gee whiz it's neato",
Copyright: "© 2024",
});
// Graceful cleanup is awaited when the block exits
}
// Function with automatic cleanup
async function batchProcessPhotos(filePaths) {
await using et = new ExifTool({
maxProcs: 8,
taskTimeoutMillis: 30000,
});
const results = [];
for (const file of filePaths) {
try {
const tags = await et.read(file);
// Add copyright
await et.write(file, {
Copyright: "© 2025 Your Company",
});
results.push({ file, success: true, camera: tags.Make });
} catch (error) {
results.push({ file, success: false, error: error.message });
}
}
return results;
// Cleanup is awaited here, including when an exception leaves the scope
}
import { ExifTool } from "exiftool-vendored";
async function robustProcessing(file) {
try {
await using et = new ExifTool();
const tags = await et.read(file);
if (tags.errors?.length > 0) {
console.warn(`Metadata warnings for ${file}:`, tags.errors);
}
return tags;
} catch (error) {
if (error.message.includes("ENOENT")) {
throw new Error(`File not found: ${file}`);
}
throw error;
}
// Async disposal is awaited before an exception leaves this scope
}
These settings control when the library requests fallback cleanup. They are not a hard guarantee that cleanup will complete within the configured duration.
import { ExifTool } from "exiftool-vendored";
// Custom timeout configuration
{
await using et = new ExifTool({
disposalTimeoutMs: 2000, // 2 seconds for sync disposal
asyncDisposalTimeoutMs: 30_000, // 30 seconds for async disposal
});
// Your processing here
const tags = await et.read("large-file.tiff");
}
await using waits for asynchronous disposaltry/finally blockusing: Initiates cleanup without waiting for completionawait using: Waits for graceful cleanup (recommended).end(): Pre-TypeScript 5.2 environments or fine-grained control