node-exiftool
A Node.js interface to the exiftool command-line application.
Exiftool is an amazing tool written by Phil Harvey in Perl which can read and write metadata to a number of file formats. It is very powerful and allows to do such things as extracting orientation from JPEG files uploaded to your server by users to rotate generated previews accordingly, as well as appending copyright information to photos using IPTC standard.
exiftool is not distributed with node-exiftool. The module will try to spawn
exiftool
, therefore you must install it manually. You can also use dist-exiftool package which will install exiftool distribution appropriate for your platform. See below for details about how to usenode-exiftool
withdist-exiftool
.
Usage
The module spawns an exiftool process with -stay_open True -@ -
arguments, so
that there is no overhead related to starting a new process to read every file
or directory. The package creates a process asynchronously and listens for
stdout and stderr data
events and uses promises thus avoiding blocking the
Node's event loop.
Require
By default, the executable is hard-coded to be just exiftool
. You must have it
you path for this method to work.
const exiftool = const ep =
Custom Executable
It is possible to specify a custom executable.
const exiftool = const ep = '/usr/local/exiftool'
dist-exiftool
Or you can install dist-exiftool
, which allows to install exiftool
from
npm.
npm i --save dist-exiftool
const exiftool = const exiftoolBin = const ep = exiftoolBin
Opening and Closing
After creating an instance of ExiftoolProcess
, it must be opened. When
finished working with it, it should be closed, when -stay_open False
will be
written to its stdin to exit the process.
const exiftool = const ep = ep // read and write metadata operations
Passing Options
exiftool
will be open with child_process.spawn
, and you can specify options
object which will passed to the spawn
method.
const exiftool = const options = detached: true env: Objectconst ep = ep
Since passing options is available, a check will be made to make sure that
stderr
and stdout
streams are readable, and stdin
is writable. Therefore,
you cannot pass { stdio: 'ignore' }
as an option.
Reading Metadata
You are required to open the exiftool process first, after which you will be able to read and write metadata.
const exiftool = const ep = ep // display pid
Started exiftool process 29671 data: SourceFile: 'image.jpg' ExifToolVersion: 104 XMPToolkit: 'Image::ExifTool 10.40' CreatorWorkURL: 'https://sobesednik.media' Scene: '011200' Creator: 'Photographer Name' Author: 'Author' ImageSize: '500x333' Megapixels: 0167 error: null data: SourceFile: 'image2.jpg' ExifToolVersion: 104 Orientation: 'Rotate 90 CW' XResolution: 72 YResolution: 72 ResolutionUnit: 'inches' YCbCrPositioning: 'Centered' XMPToolkit: 'Image::ExifTool 10.40' CreatorWorkURL: 'https://sobesednik.media' Scene: '011200' Creator: 'Photographer Name' Author: 'Author' ImageSize: '500x334' Megapixels: 0167 error: null Closed exiftool
Reading Metadata from a Readable Stream
You can read metadata from a stream the same way you read a file metadata.
node-exiftool
will create a temporary file and pipe your Readable
into it,
then pass the path to exiftool
. After the result is received from exiftool
,
the temp file will be removed.
const exiftoolBin = const exiftool = const fs = const path = const ep = exiftoolBin const PHOTO_PATH = pathconst rs = fs ep
{ data: [ { SourceFile: '/var/folders/s0/truth-covered-in-security/T/wrote-44788.data', ExifToolVersion: 10.53, XResolution: 72, YResolution: 72, ResolutionUnit: 'inches', YCbCrPositioning: 'Centered', Copyright: 'sobesednik.media 2017', XMPToolkit: 'Image::ExifTool 10.40', Author: 'Author <author@sobes.io>', ImageSize: '362x250', Megapixels: 0.09 } ], error: null }
Writing Metadata
You can write metadata with node-exiftool
. The API is:
ep.writeMetadata(file:string, data:object, args:array)
,
where file
is a path to the file, data
is metadata to add, e.g.,
const data = all: '' comment: 'Exiftool rules!' // has to come after `all` in order not to be removed 'Keywords+': 'keywordA' 'keywordB'
and args
is an array of any other arguments you wish to pass, e.g,. ['overwrite_original']
.
const exiftool = const ep = ep
data: null error: '1 image files updated'
Reading Directory
const exiftool = const ep = ep // read directory
data: SourceFile: 'DIR/IMG_9859.JPG' ExifToolVersion: 104 Orientation: 'Rotate 90 CW' XResolution: 72 YResolution: 72 ResolutionUnit: 'inches' YCbCrPositioning: 'Centered' XMPToolkit: 'Image::ExifTool 10.40' CreatorWorkURL: 'https://sobesednik.media' Scene: '011200' Creator: 'Photographer Name' Author: 'Author' ImageSize: '500x334' Megapixels: 0167 SourceFile: 'DIR/IMG_9860.JPG' ExifToolVersion: 104 XMPToolkit: 'Image::ExifTool 10.40' CreatorWorkURL: 'https://sobesednik.media' Scene: '011200' Creator: 'Photographer Name' Author: 'Author' ImageSize: '500x334' Megapixels: 0167 error: '1 directories scanned\n 2 image files read'
Reading Non-existent File
const exiftool = const ep = ep // try to read file which does not exist
data: null error: 'File not found: filenotfound.jpg'
Custom Arguments
You can pass arguments which you wish to use in the exiftool command call. They will
be automatically prepended with the -
sign so you don't have to do it manually.
const exiftool = const ep = ep // include only some tags
data: SourceFile: 'photo.jpg' Creator: 'Photographer Name' CreatorWorkURL: 'https://sobesednik.media' Orientation: 'Rotate 90 CW' error: null
const exiftool = const ep = ep // exclude some tags and groups of tags
data: SourceFile: 'photo.jpg' Orientation: 'Rotate 90 CW' XResolution: 72 YResolution: 72 ResolutionUnit: 'inches' YCbCrPositioning: 'Centered' XMPToolkit: 'Image::ExifTool 10.11' CreatorWorkURL: 'https://sobesednik.media' Scene: '011200' Creator: 'Photographer Name' ImageSize: '500x334' Megapixels: 0167 error: null
Reading HTML
const exiftool = const ep = ep
data: SourceFile: 'url.html' ExifToolVersion: 104 Title: 'Some web page' Keywords: 'fire, in, your, eyes, etc.' Description: 'Programming: Official sponsor of Open Source since ever.' error: null
html
:
Some web page Hello world
Events
You can also listen for OPEN
and EXIT
events. For example, if the exiftool process
crashed, you might want to restart it.
const exiftool = const cp = { return { cp }} { return _ep } const ep = ep ep
Started exiftool process 28566
exiftool process exited
Started exiftool process 28569
exiftool process exited
...
Stream Encoding
By default, setEncoding('utf8')
will be called on stdout
and stderr
streams, and stdin
will be written with utf8
encoding (this is Node's
default on a Mac at least). If you wish to use system's default encoding, pass
null
when opening the process. If you want to set some other encoding, specify
it as a string.
Node's supported encodings.
const exiftool = const ep = Promise
Writing Tags for Adobe in UTF8
Some metadata must be written in utf8
encoding, for example to be recognized
by Adobe products. However, IPTC fields are encoded in Latin1, so you need to
explicitly pass codedcharacterset=utf8
argument. For example,
Caption-Abstract
is an
IPTC tag, so
to write it in utf8, do the following:
const exiftool = const ep = const metadata = all: '' // remove all metadata at first Title: 'åäö' LocalCaption: 'local caption' 'Caption-Abstract': 'Câptïön \u00C3bstráct: åäö' Copyright: '2017 ©' 'Keywords+': 'këywôrd \u00C3…' 'keywórdB ©˙µå≥' Creator: 'Mr Author' Rating: 5 const file = 'file.jpg' ep // use codedcharacterset
Using Detached Mode on Windows
You can spawn exiftool with { detached: true }
option if you need to manually
handle its exit independent of your application. On Linux, the new process
will be made a leader of its process group, and will not quit with the Node
app. On Windows, the process will not quit either, however, there will be two
exiftool
processes: one returned by the child_process.spawn
method, and
a second one, started by exiftool.exe
itself. There is also going to appear
conhost.exe
, if the parent node application is not attached to a terminal.
wmic process where "caption='node.exe' or caption='exiftool.exe' or caption='conhost.exe'" get caption,processid,parentprocessid
Caption ParentProcessId ProcessIdnode.exe 3464 5752exiftool.exe 5752 6096exiftool.exe 6096 4588conhost.exe 4588 4016
Because Windows will throw an error when trying to kill a process group by
passing -pid
to process.kill
, you should find the second exiftool process by
its parent pid (returned with ep.open()
), and kill it manually, e.g., with
cp.exec('taskkill /F /T /PID ${pid}')
. Check the
detached-true
test for more insight.
Reading utf8 Encoded Filename on Windows
If you're on Windows and your active page is different from utf8
, you should
pass charset filename=utf8
when trying to read a file. It shouldn't be a
problem on a Mac.
An error you can see is: File not found: Fọto.jpg
or whatever filename you
have. To fix it, set filename charset to utf8
.
const exiftool = const ep = ep
To print code page number on Windows, do
const child_process = { return { child_process }}
Example output: Active code page: 437
. utf8
's number is 65001
(on win)
- Special characters don't display properly in my Windows console
- Passing filenames with Unicode characters to ExifTool
How Does It Work
For example, when trying to write metadata:
const exiftool = const ep = ep
Internally, the following command will be sent to exiftool's stdin
when it's open:
-all=
-comment=Exiftool example
-Keywords+=keywordA
-Keywords+=keywordB
-overwrite_original
-json
-s
destination.jpg
-echo1
{begin529963}
-echo2
{begin529963}
-echo4
{ready529963}
-execute529963
And the write promise will be resolved when the process writes
{begin669103}
{ready669103}
to stdout
, and
{begin513858}
1 image files updated
{ready513858}
to stderr
. There's a regex transform stream which is available for reading
when it sees a block like {begin<N>}...some data...{ready<N>}
. Once both
stderr
and stdout
data have been received, the promise returned by
writeMetadata
function will be resolved.
Benchmark
To start the benchmark, execute npm run bench
. It will scan all files
in the benchmark/photos
directory, and if none was found, will work on test
fixtures. Here are some of our results:
> node benchmark/run
/node-exiftool/benchmark/photos/IMG_3051.JPG: 168ms
/node-exiftool/benchmark/photos/IMG_3052.JPG: 166ms
/node-exiftool/benchmark/photos/IMG_3053.JPG: 168ms
/node-exiftool/benchmark/photos/IMG_3054.JPG: 166ms
/node-exiftool/benchmark/photos/IMG_3055.JPG: 165ms
/node-exiftool/benchmark/photos/IMG_3056.JPG: 158ms
/node-exiftool/benchmark/photos/IMG_3057.JPG: 158ms
/node-exiftool/benchmark/photos/IMG_3058.JPG: 162ms
/node-exiftool/benchmark/photos/IMG_3059.JPG: 158ms
/node-exiftool/benchmark/photos/IMG_3060.JPG: 158ms
/node-exiftool/benchmark/photos/IMG_3061.JPG: 157ms
/node-exiftool/benchmark/photos/IMG_3051.JPG: 65ms
/node-exiftool/benchmark/photos/IMG_3052.JPG: 20ms
/node-exiftool/benchmark/photos/IMG_3053.JPG: 22ms
/node-exiftool/benchmark/photos/IMG_3054.JPG: 23ms
/node-exiftool/benchmark/photos/IMG_3055.JPG: 22ms
/node-exiftool/benchmark/photos/IMG_3056.JPG: 22ms
/node-exiftool/benchmark/photos/IMG_3057.JPG: 22ms
/node-exiftool/benchmark/photos/IMG_3058.JPG: 22ms
/node-exiftool/benchmark/photos/IMG_3059.JPG: 20ms
/node-exiftool/benchmark/photos/IMG_3060.JPG: 21ms
/node-exiftool/benchmark/photos/IMG_3061.JPG: 20ms
Exiftool
Read 11 files
Total time: 1784ms
Average time: 162.18ms
Exiftool Open
Read 11 files
Total time: 378ms
Average time: 34.36ms
Exiftool Open was faster by 471%
Testing
We're using exiftool-context
to test with zoroaster
.
Make sure to do the following in tests, when testing current version:
const context = const exiftool = contextglobalExiftoolConstructor = exiftoolExiftoolProcess
Otherwise, the context will use a stable version which it installs independently.
Metadata
Metadata is awesome and although it can increase the file size, it preserves copyright and allows to find out additional information and the author of an image/movie. Let's all use metadata.