exiftool-vendored: fast Node.js access to ExifTool without installing Perl
Fast, cross-platform Node.js access to ExifTool
At a glance
- What is it?
- exiftool-vendored is a Node.js library that gives access to ExifTool by shipping the binaries for each platform inside the npm package, so a JavaScript project can read, write and extract image metadata without asking the user to install Perl and the ExifTool distribution themselves. It suits a service that processes photos in volume, and the detail to plan around is that it manages a long-lived ExifTool process whose lifetime you have to close yourself.
- Who is it for?
- Adopt exiftool-vendored if you need to read or write image metadata from Node.js and cannot assume that ExifTool or Perl is installed on the machine, since the binaries ship with the package. Do not adopt it if you want to avoid a native binary dependency entirely, because the package is large for that reason, and check first whether you need the group-prefixed output mode, since the recommended path for a new integration keys tags by metadata group rather than by bare name.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 16 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem of shipping a Perl dependency
ExifTool is the reference tool for image metadata, and it is written in Perl. That single fact is the origin of this package.
ExifTool is not distributed as a single binary that a user can drop anywhere. It is a Perl application with a large tree of library files, and installing it means either a system package manager with Perl as a prerequisite, or downloading and unpacking a distribution and making sure the interpreter can find all of it. For a JavaScript application installed with npm, that is an unwelcome extra step, and for a service deployed into a container it is an extra layer to maintain.
exiftool-vendored solves that by shipping the binaries. The package description calls it efficient, cross-platform access to ExifTool, and the vendored name says what happened: the ExifTool distributions for macOS, Linux and Windows are packaged inside the npm tarball, so `npm install` is the whole installation story.
That trade has a cost, and it is worth stating plainly. The npm package is large, because it carries a copy of ExifTool for each supported platform rather than one binary for the machine you happen to be on. In exchange, the dependency tree of a JavaScript project stays a JavaScript dependency tree, and a user does not have to know what Perl is.
The project is built and supported by PhotoStructure, and the performance claim is that it is an order of magnitude faster than other Node.js ExifTool modules. It powers PhotoStructure itself and, according to the README, 1,000 or more other projects.
The licence is MIT, the primary language is TypeScript, the default branch is main, and the releases are 39.0.1 from 2026-10-06, 39.0.0 from 2026-10-04 and 38.3.0 from 2026-09-28.
The API, and the process you have to close
The quick start is short, and the last line of it is the one people forget.
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();The requirement is Node.js Active LTS or Maintenance LTS versions only, and the package manifest pins the engine at node 22 or newer.
That final `await exiftool.end()` is the design point of the whole library. ExifTool is not a library you link into; it is a separate program that the Node client starts and talks to over a pipe. The library manages that process as a long-lived child, which is where the performance comes from. Starting ExifTool for every file would pay the interpreter startup cost per file, and the whole point of a persistent process is that the startup is paid once.
The cost of a persistent process is that you have to close it, and the library gives you the means. A Node program that reads a handful of files and exits will leave the child running until it notices its pipe is closed, and in a container that can hold the process open past the end of your work. Calling `end()` is the correct shutdown path.
The API surface itself is a small set of verbs on a single exported object. `read` gets you the tags, `write` changes them, `extractThumbnail`, `extractPreview` and `extractJpgFromRaw` pull images out of a file, and `editTags` handles the targeted edits that the plain write path does not cover.
Reading and writing, and where errors show up
The reading example is worth reading closely because of one line that is easy to overlook.
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);
}The comment is always check for parsing errors, and the check is on `tags.errors`. ExifTool does not fail loudly on a file it cannot fully parse. It emits warnings and carries on, which is the right behaviour for a tool that has to cope with the many slightly broken files that cameras and editors produce, but it means a read can succeed and return partial data.
The optional chaining on `tags.errors?.length` reflects that the property may be absent on a clean file. The consequences for an application are worth thinking about. If you are building a photo library, a file with a parse error is a file that will be missing some fields, and the difference between a field that is absent and a field that is empty is often not obvious downstream. Checking `errors` and `warnings` and treating them as data rather than as noise is the habit worth forming early.
The writing side shows three separate shapes of edit. Adding fields:
// Add keywords and copyright
await exiftool.write("photo.jpg", {
Keywords: ["sunset", "landscape"],
Copyright: "© 2024 Photographer Name",
"IPTC:CopyrightNotice": "© 2024 Photographer Name",
});Note the group-prefixed key in there, `IPTC:CopyrightNotice`, alongside the bare `Copyright`. The same write call handles both forms, which is what makes it possible to set the EXIF copyright and the IPTC copyright notice in one operation.
Setting all the date fields at once is its own shape:
// 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,
});The date value is a string in ExifTool's own date format, `2024:03:15 14:30:00`, and `AllDates` is an ExifTool-wide alias that expands to every date tag the file has. Deleting a tag is done by passing `null`, which is a nice small design choice: the same call handles setting and removing.
For targeted list-value and supported structured edits, the documentation points to `editTags()` and to a section on editing individual tag values in the usage examples, with examples, supported tags and safety constraints.
Extracting embedded images
Three extraction calls cover the cases you actually hit when working with photographs.
// 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");Thumbnail and preview are two sizes of embedded preview image that cameras write into the file. The preview is larger than the thumbnail, and for a web application the preview is usually the one you want, since a thumbnail is generally too small to display at any useful size.
The RAW case is different in kind. A RAW file, such as the Canon CR2 in the example, does not contain a JPEG in the ordinary sense. The embedded preview in a RAW file is a full-size render of the scene, and `extractJpgFromRaw` produces it as a JPEG so you can display or serve it without decoding the RAW format yourself.
That capability is why this library is useful to a photo application rather than merely convenient. Without it you would need a separate RAW decoder, which is a heavy dependency and a slow one, or you would have no preview at all until you ran a full raw development pipeline.
The ExifTool naming is worth noting: the methods are named after what they extract rather than after the technical operation. `extractJpgFromRaw` says exactly what it does and does not pretend to be a general conversion tool.
Together with the read and write operations, the three extraction calls cover the full round trip for a photo service: read the metadata to index the file, write the metadata back when your application adds fields of its own, and extract a displayable image when the original format is not displayable.
Group-prefixed output, and the output contract
The most consequential design decision in the library is the one about tag names, and it is a recent change that the README presents as the recommended path.
By default, `read()` returns bare tag names, like `Make` or `MeteringMode`. The problem is that many tag names appear in several metadata groups of the same file. `MeteringMode` may be in both `EXIF` and `MakerNotes`, and in bare mode ExifTool silently picks one winner per name.
Setting `groupNames: true` keys each tag by its group instead, exactly as ExifTool's `-G` option renders it. The README is emphatic that this removes that ambiguity and is the recommended path for new integrations, while noting that bare mode remains fully supported but that new features will target group-prefixed output first.
import { exiftool, tag } from "exiftool-vendored";
const t = await exiftool.read("photo.jpg", { groupNames: true });
// returns a GroupedTags object:
// { "EXIF:Make": "Canon", "EXIF:MeteringMode": "Multi-segment",
// "MakerNotes:MeteringMode": "Pattern", zone: "America/Los_Angeles", ... }
t["EXIF:Make"]; // typed via the GroupedTags interface
tag(t, "Make"); // "Canon": tag() looks up bare names in either modeThe `tag()` helper is the escape hatch, looking up a bare name in either mode. That is a deliberate concession, because a library that forces every caller to know about grouping makes migration painful, and a library that does not offer the helper makes every caller write their own lookup.
The output contract then specifies three categories of key. Prefixed keys are ExifTool's output, verbatim: every tag ExifTool emits, except `SourceFile` which ExifTool itself leaves bare, is keyed `Group:TagName` using family-0 group names such as `EXIF:Make`, `ExifTool:Warning` and `Composite:ImageSize`. There are two exceptions, both for data safety, since the group-prefixed GPS position tags carry the library's validated, sign-corrected values, and if the GPS position is invalid then all GPS tags are omitted.
Bare keys are the library's own namespace: `SourceFile`, `errors`, `warnings`, `zone`, `tz`, `tzSource`, `zoneSource`, `invalidUtf8Bytes`, and the parsed, validated GPS quartet of `GPSLatitude`, `GPSLatitudeRef`, `GPSLongitude` and `GPSLongitudeRef`.
Errors and warnings aggregate everything, combining ExifTool's stderr with the JSON-embedded `Error` and `Warning` fields, whether bare or `ExifTool:`-prefixed. And `ExifToolVersion` stays a string, with the note that `12.30` is not the same version as `12.3`, under the `ExifTool:ExifToolVersion` key.
That last detail is the sort of thing that causes real bugs if it is not specified. ExifTool version numbers have trailing zeros that are semantically meaningful, and a version that has been through a numeric round trip changes.
The caveats section is candid about the limits. Heuristics degroup first: timezone inference, video detection and other heuristics look tags up by bare name via a degrouped view where the last group listed for a name wins, so when a tag name appears in several groups with different values, those heuristics are approximations, and in bare mode ExifTool's internal priority picks the winner instead, with neither view able to reproduce the other. The bare-name APIs, `TagDescriptions.get()` and the timezone inference machinery, are keyed by bare tag names, so the `Group:` prefix has to be stripped before passing group-prefixed keys to them. And the `APP0` through `APP15` segment tags are only loosely typed in `GroupedTags`.
Typed tags, dates and the project machinery
The developer-facing features are the typed tag definitions, the timezone-aware dates and the auto-generated tag list.
The `Tags` interface contains thousands of metadata fields from an auto-generated TypeScript file, and each tag carries JSDoc annotations with a frequency rating, the metadata groups it belongs to, and a representative example value. The frequency markers distinguish mainstream devices such as an iPhone, Canon, Nikon or Sony from more obscure camera makes and models, and the star rating runs from found in more than 50 percent of files down to rare, under 1 percent. The groups field names the metadata categories, such as EXIF, GPS, IPTC and XMP.
That generated from 10,000 or more real camera samples, which is the important part. A hand-written list of the common tags would tell you what exists but not how often you can rely on it, and the frequency data is what lets you decide whether a missing field means a particular camera model or means something went wrong.
The dates are timezone-aware through `ExifDateTime` classes, and the output contract above shows the related keys: `zone`, `tz`, `tzSource` and `zoneSource`. A photo's capture timestamp is stored as local time with an offset, and to place it on a timeline you need to know the offset and where it came from, which is why there are two source fields rather than one.
The repository shows how a project of this size is maintained. The `package.json` scripts carry explanatory comments, including one that explains why the ignore-scripts setting in `.npmrc` means lifecycle hooks never run and so every step a script depends on has to be spelled out. There is a `mktags` script that compiles and runs `dist/update/mktags.js` to regenerate the tag definitions, an `update` script chain driven by `ncu` for dependency upgrades, and a `update:actions` step that installs pinact and uses it to update the GitHub Actions.
Testing is Mocha against the compiled output, with the spec files run from the compiled output directory, and coverage is collected with nyc under an `.nyc_output` directory, with a `.mocharc.yaml` for the runner configuration. There is a `data/` directory, which is where the camera samples the tag definitions are derived from live, a `docs/` directory, and a `typedoc.json` for the API documentation build, served locally with `npm run docs`.
There is also an `AGENTS.md` and a `.claude/` directory at the root, plus a `claude.sh` script, which is worth noting as a signal about how the project now expects contributions to be prepared.
Editorial conclusion
Adopt exiftool-vendored if you need to read or write image metadata from Node.js and cannot assume that ExifTool or Perl is installed on the machine, since the binaries ship with the package. Do not adopt it if you want to avoid a native binary dependency entirely, because the package is large for that reason, and check first whether you need the group-prefixed output mode, since the recommended path for a new integration keys tags by metadata group rather than by bare name.
Frequently asked questions
What is exiftool-vendored?
It is a Node.js library giving fast, cross-platform access to ExifTool, with the ExifTool binaries for macOS, Linux and Windows packaged inside the npm module. It is MIT licensed, built and supported by PhotoStructure, and requires Node.js Active LTS or Maintenance LTS versions, with the package manifest pinning node 22 or newer.
How do I read metadata with exiftool-vendored?
Call `exiftool.read("photo.jpg")` and read the fields off the result, for example `tags.Make`, `tags.DateTimeOriginal` and `tags.ImageWidth`. You should always check `tags.errors`, since ExifTool reports parse problems as warnings on the tags object rather than as a thrown error.
How do I write metadata and extract images with exiftool-vendored?
Use `exiftool.write(path, { ... })` to set fields, with a group-prefixed key such as IPTC:CopyrightNotice if you want a specific group, `AllDates` to update every date field at once, and `null` to delete a tag. Use extractThumbnail, extractPreview and extractJpgFromRaw to pull embedded images, the last of which turns a RAW file into a displayable JPEG.
What is group-prefixed output in exiftool-vendored?
Passing `{ groupNames: true }` to read keys each tag by its metadata group, exactly as ExifTool's -G option renders it, so a name that appears in several groups such as MeteringMode in EXIF and MakerNotes is no longer ambiguous. The README calls this the recommended path for new integrations, and the tag() helper looks up bare names in either mode.
Why do I have to call exiftool.end() when I am done?
The library keeps a long-lived ExifTool child process rather than starting one per file, and that is where the performance comes from. Closing it is the correct shutdown path, and the quick start ends with `await exiftool.end()` for that reason.
Is exiftool-vendored still being maintained?
The repository is not archived and the last push was on 2026-10-06. Recent releases are 39.0.1 from 2026-10-06, 39.0.0 from 2026-10-04 and 38.3.0 from 2026-09-28, the tag definitions are auto-generated from 10,000 or more real camera samples, and the test suite runs Mocha against the compiled output with coverage collected by nyc.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/photostructure-exiftool-vendored-js)Was this useful?