exiftool-vendored
    Preparing search index...

    exiftool-vendored

    πŸ“Έ exiftool-vendored

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

    npm version Node.js CI GitHub issues

    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();

    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 ExifDateTime classes
    • 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:

    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 β†’

    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 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.

    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;

    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.

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