@janhapke/exiv2
v0.2808.5
Published
A native c++ extension for node.js that provides support for reading & writing image metadata via Exiv2. Fork of @11ways/exiv2 with a fix for interpreted (rather than raw) tag values.
Readme
Why this fork exists
@janhapke/exiv2 is a fork of @11ways/exiv2, maintained by Jan Hapke. It exists to:
- fix
getImageTags()returning Exiv2's raw tag values instead of the interpreted ones — see CHANGELOG.md for the underlying bug and fix (e.g. a Nikon lens ID like"154"now correctly resolves to"Nikon AF-S DX VR Zoom-Nikkor 18-55mm f/3.5-5.6G") - ship hand-written TypeScript declarations (
exiv2.d.ts), which upstream has never had
Versioning scheme
This fork tracks the Exiv2 C++ library version it's built and tested against, since correctness here depends directly on which native Exiv2 release resolves tag interpretation. Versions are 0.XXYY.Z:
XXYY— the tracked Exiv2 version's minor number followed by its patch number, zero-padded to 2 digits, concatenated (Exiv20.28.8→ minor28+ patch08→2808).Z— this fork's own release counter for that Exiv2 version, starting at0and bumped for every fork-only change (a fix, a feature like type declarations, a metadata correction) that doesn't require bumping the tracked Exiv2 version.
For example, 0.2808.2 is this fork's 3rd release (Z=2) built against Exiv2 0.28.8.
The patch number is zero-padded (not left as a bare concatenation) because Exiv2's minor version has already grown from 1 digit to 2 (0.9 → 0.10, and never dropped back), and its patch number has already reached 8 in the current 0.28.x line — one release away from testing a double-digit patch. An unpadded XXY scheme breaks the moment a minor bump happens while the old minor's patch was already double digits: e.g. Exiv2 0.28.10 would concatenate to 2810, but 0.29.0 would concatenate to 290 — and since semver compares these as plain numbers, 2810 > 290 would make the fork version for the older 0.28.10 sort as newer than the fork version for 0.29.0. Zero-padding the patch to a fixed 2-digit width keeps every comparison monotonic no matter how the digit counts change (assuming Exiv2's patch number stays under 100, comfortably true for the foreseeable future).
Note: this fork's very first release (the interpreted-tags fix) was published as plain
0.28.8, before this scheme was adopted. Every release from0.2808.2onward follows the scheme above.
Exiv2
Exiv2 is a native C++ extension for node.js that provides support for reading and writing image metadata via the Exiv2 library.
It was created by Damian Beresford
Dependencies
To build this addon you'll need the Exiv2 library and headers so if you're using a package manager you might need to install an additional "-dev" packages.
Debian / Ubuntu
apt-get install pkg-config exiv2 libexiv2-devmacOS
You'll also need to install pkg-config to help locate the library and headers.
port install pkgconfig exiv2brew install pkg-config exiv2FreeBSD
pkg install pkgconf exiv2Arch Linux
pacman -S exiv2 pkgconfWindows
Install pkg-config using Chocolatey:
choco install pkgconfigliteDownload latest msvc64 exiv2 build from the Exiv2 download page and extract to a folder of your choice.
Add a system variable named PKG_CONFIG_PATH and set it's value to EXIV2ROOTDIR\lib\pkgconfig replacing EXIV2ROOTDIR with the path where you extracted exiv2 from the step before (e.g. D:\src\exiv2msvs\lib\pkgconfig).
You'll also need windows-build-tools to compile this package.
For Electron apps, you'll want to copy exiv2.dll to the root directory of your Electron Windows build. You can automated this using the extraFiles option.
Other systems
See the Exiv2 download page for more information.
Requirements
- Node.js 18 or later
- Exiv2 library and development headers (see Dependencies above)
- pkg-config
Installation Instructions
Once the dependencies are in place, you can build and install the module using npm:
npm install @janhapke/exiv2You can verify that everything is installed and operating correctly by running the tests:
npm testInterpreted vs. raw tag values
Unlike the upstream @11ways/exiv2 package, getImageTags() here returns
interpreted tag values (via Exiv2's Metadatum::print()) instead of
raw ones (via Value::toString()). This matters for tags whose stored
value needs manufacturer-specific decoding to be meaningful, e.g.:
// Exif.NikonLd2.LensIDNumber
// before: "154"
// after: "Nikon AF-S DX VR Zoom-Nikkor 18-55mm f/3.5-5.6G"See CHANGELOG.md for details.
Diagnostics / logging
Exiv2's internal logger (Exiv2::LogMsg) writes its own Warning:/Error:
diagnostics straight to stderr whenever it hits malformed image structure
(e.g. a corrupted or truncated file), independently of whether the call
you made succeeds. For example, getImageTags() can resolve normally with
no tags and no error while Exiv2 has already printed something like:
Error: Directory Image with 572 entries considered invalid; not read.Two functions let you control this:
var ex = require('@janhapke/exiv2');
// Suppress Exiv2's diagnostics entirely.
ex.muteLog();
// ...equivalent to:
ex.setLogLevel('mute');
// Or route them into your own code instead of stderr.
ex.setLogHandler(function(event) {
console.log(event.level, event.message);
});
// Restore the default stderr output.
ex.setLogHandler(null);setLogLevel() accepts 'debug', 'info', 'warn', 'error', or
'mute' (Exiv2's default level is 'warn'); only messages at or above
that severity reach the handler.
Caveat: setLogLevel()/muteLog() control Exiv2's own process-global
log level, not anything scoped to a single call — they affect every
getImageTags()/setImageTags()/deleteImageTags()/getImagePreviews()
call in the process, including ones made from other Node.js environments
(see "Concurrency and worker_threads" below). setLogHandler(), by
contrast, is scoped to the Node.js environment (main thread, or a single
worker_thread) that calls it — a handler installed on the main thread
never receives events from calls made in a worker thread, and vice versa.
Within one environment, a handler receives log events from whichever call
on that environment happens to be running at the time; if multiple calls
from the same environment are in flight concurrently, there is no way to
attribute a given message back to a specific one.
Concurrency
All four calls run off the main thread and are safe to call concurrently from JS. Internally:
getImageTags()andgetImagePreviews()(read-only —readMetadata()only) run truly in parallel with each other, up to Node's worker pool size (UV_THREADPOOL_SIZE, 4 by default).setImageTags()anddeleteImageTags()(callwriteMetadata()) are serialized — only one write, and no concurrent read, runs at a time.setLogLevel()/muteLog()/setLogHandler()are also serialized against everything else, briefly, while they run.
This split follows Exiv2's own documented thread-safety
model: Exif/IPTC
parsing is reentrant, and the XMP toolkit's own encode()/decode() are
documented thread-safe internally — provided XmpParser::initialize() has
already run once, which this addon does automatically at load time, before
any concurrent call is possible. Writes stay serialized because
XmpParser::encode() (the path writeMetadata() takes whenever a file
already carries XMP data) iterates an internal Exiv2 registry with no lock
at all as of Exiv2 0.28.x — a real, upstream, not-yet-released-fixed race,
not something this addon can safely work around from the outside.
Known residual risk on reads: two concurrent reads that register
conflicting XMP namespace prefixes can still overwrite each other's entry
in Exiv2's one shared internal registry. This is memory-safe (Exiv2
internally mutex-protects the write itself) but can produce a wrong XMP
tag value, or occasionally a normal err on the affected call — not a
crash. If your files don't carry unusual/custom XMP namespaces this is
very unlikely to matter in practice.
worker_threads
This addon is a Node-API addon, which
Node.js loads fresh into every environment that require()s it — the
main thread, and independently, any worker_thread — all sharing the same
underlying Exiv2 library and its process-wide state (XmpParser, the log
level, etc.), but each with its own isolated setLogHandler() handler (see
above). This means it's safe to require() this addon and call
setLogHandler() from inside a worker_thread pool (e.g. to parallelize
decoding across CPU cores) without one worker's handler ever being invoked
across another worker's V8 isolate — a real crash
(v8::HandleScope::CreateHandle() Cannot create a handle without a
HandleScope) prior to
this being fixed. getImageTags()/setImageTags()/deleteImageTags()/
getImagePreviews() calls from different worker_threads are subject to
the same read/write locking described above, shared across all threads in
the process (not per-worker) — so, for example, a write from one worker
still blocks a concurrent read from another.
Sample Usage
Read tags:
var ex = require('@janhapke/exiv2');
ex.getImageTags('./photo.jpg', function(err, tags) {
console.log("DateTime: " + tags["Exif.Image.DateTime"]);
console.log("DateTimeOriginal: " + tags["Exif.Photo.DateTimeOriginal"]);
});
var fs = require('fs');
ex.getImageTags(fs.readFileSync('./photo.jpg'), function(err, tags) {
console.log("DateTime: " + tags["Exif.Image.DateTime"]);
console.log("DateTimeOriginal: " + tags["Exif.Photo.DateTimeOriginal"]);
});Load preview images:
var ex = require('@janhapke/exiv2')
, fs = require('fs');
ex.getImagePreviews('./photo.jpg', function(err, previews) {
// Display information about the previews.
console.log(previews);
// Or you can save them--though you'll probably want to check the MIME
// type before picking an extension.
fs.writeFile('preview.jpg', previews[0].data);
});Write tags:
var ex = require('@janhapke/exiv2')
var newTags = {
"Exif.Photo.UserComment" : "Some Comment..",
"Exif.Canon.OwnerName" : "My Camera"
};
ex.setImageTags('./photo.jpg', newTags, function(err){
if (err) {
console.error(err);
} else {
console.log("setImageTags complete..");
}
});Delete tags:
var ex = require('@janhapke/exiv2')
var tagsToDelete = ["Exif.Photo.UserComment", "Exif.Canon.OwnerName"];
ex.deleteImageTags('./photo.jpg', tagsToDelete, function(err){
if (err) {
console.error(err);
} else {
console.log("deleteImageTags complete..");
}
});Take a look at the examples/ and test/ directories for more.
Authors
- Damian Beresford - Original creator
- Jelle De Loecker - Follow me on Github (:octocat:@skerit) and on Mastodon (🐦@[email protected])
- Jan Hapke - Maintainer of this fork (@janhapke)
See also the list of contributors who participated in the upstream project, and AUTHORS for this fork.
The original @11ways/exiv2node is developed at Eleven Ways, a team of IAAP Certified Accessibility Specialists.
License
This project is licensed under the MIT License - see the LICENSE file for details.
