npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

exiftool-vendored

v37.1.0

Published

Efficient, cross-platform access to ExifTool

Downloads

682,358

Readme

📸 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