exiftool-vendored
Version:
Efficient, cross-platform access to ExifTool
500 lines • 23.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.exiftool = exports.ExifTool = exports.WriteTaskOptionFields = exports.DefaultWriteTaskOptions = exports.offsetMinutesToZoneName = exports.defaultVideosToUTC = exports.UnsetZoneOffsetMinutes = exports.UnsetZoneName = exports.UnsetZone = exports.TimezoneOffsetTagnames = exports.DefaultReadTaskOptions = exports.parseJSON = exports.isGeolocationTag = exports.GeolocationTagNames = exports.exiftoolPath = exports.ExifToolTask = exports.ExifTime = exports.ExifDateTime = exports.ExifDate = exports.DefaultMaxProcs = exports.DefaultExiftoolArgs = exports.DefaultExifToolOptions = exports.CapturedAtTagNames = exports.BinaryField = void 0;
const bc = __importStar(require("batch-cluster"));
const _cp = __importStar(require("node:child_process"));
const _fs = __importStar(require("node:fs"));
const node_process_1 = __importDefault(require("node:process"));
const Array_1 = require("./Array");
const AsyncRetry_1 = require("./AsyncRetry");
const BinaryExtractionTask_1 = require("./BinaryExtractionTask");
const BinaryToBufferTask_1 = require("./BinaryToBufferTask");
const DefaultExifToolOptions_1 = require("./DefaultExifToolOptions");
const DeleteAllTagsArgs_1 = require("./DeleteAllTagsArgs");
const ExifToolOptions_1 = require("./ExifToolOptions");
const ExiftoolPath_1 = require("./ExiftoolPath");
const IsWin32_1 = require("./IsWin32");
const Lazy_1 = require("./Lazy");
const Object_1 = require("./Object");
const Pick_1 = require("./Pick");
const ReadRawTask_1 = require("./ReadRawTask");
const ReadTask_1 = require("./ReadTask");
const RewriteAllTagsTask_1 = require("./RewriteAllTagsTask");
const String_1 = require("./String");
const VersionTask_1 = require("./VersionTask");
const Which_1 = require("./Which");
const WriteTask_1 = require("./WriteTask");
var BinaryField_1 = require("./BinaryField");
Object.defineProperty(exports, "BinaryField", { enumerable: true, get: function () { return BinaryField_1.BinaryField; } });
var CapturedAtTagNames_1 = require("./CapturedAtTagNames");
Object.defineProperty(exports, "CapturedAtTagNames", { enumerable: true, get: function () { return CapturedAtTagNames_1.CapturedAtTagNames; } });
var DefaultExifToolOptions_2 = require("./DefaultExifToolOptions");
Object.defineProperty(exports, "DefaultExifToolOptions", { enumerable: true, get: function () { return DefaultExifToolOptions_2.DefaultExifToolOptions; } });
var DefaultExiftoolArgs_1 = require("./DefaultExiftoolArgs");
Object.defineProperty(exports, "DefaultExiftoolArgs", { enumerable: true, get: function () { return DefaultExiftoolArgs_1.DefaultExiftoolArgs; } });
var DefaultMaxProcs_1 = require("./DefaultMaxProcs");
Object.defineProperty(exports, "DefaultMaxProcs", { enumerable: true, get: function () { return DefaultMaxProcs_1.DefaultMaxProcs; } });
var ExifDate_1 = require("./ExifDate");
Object.defineProperty(exports, "ExifDate", { enumerable: true, get: function () { return ExifDate_1.ExifDate; } });
var ExifDateTime_1 = require("./ExifDateTime");
Object.defineProperty(exports, "ExifDateTime", { enumerable: true, get: function () { return ExifDateTime_1.ExifDateTime; } });
var ExifTime_1 = require("./ExifTime");
Object.defineProperty(exports, "ExifTime", { enumerable: true, get: function () { return ExifTime_1.ExifTime; } });
var ExifToolTask_1 = require("./ExifToolTask");
Object.defineProperty(exports, "ExifToolTask", { enumerable: true, get: function () { return ExifToolTask_1.ExifToolTask; } });
var ExiftoolPath_2 = require("./ExiftoolPath");
Object.defineProperty(exports, "exiftoolPath", { enumerable: true, get: function () { return ExiftoolPath_2.exiftoolPath; } });
var GeolocationTags_1 = require("./GeolocationTags");
Object.defineProperty(exports, "GeolocationTagNames", { enumerable: true, get: function () { return GeolocationTags_1.GeolocationTagNames; } });
Object.defineProperty(exports, "isGeolocationTag", { enumerable: true, get: function () { return GeolocationTags_1.isGeolocationTag; } });
var JSON_1 = require("./JSON");
Object.defineProperty(exports, "parseJSON", { enumerable: true, get: function () { return JSON_1.parseJSON; } });
var ReadTask_2 = require("./ReadTask");
Object.defineProperty(exports, "DefaultReadTaskOptions", { enumerable: true, get: function () { return ReadTask_2.DefaultReadTaskOptions; } });
var Timezones_1 = require("./Timezones");
Object.defineProperty(exports, "TimezoneOffsetTagnames", { enumerable: true, get: function () { return Timezones_1.TimezoneOffsetTagnames; } });
Object.defineProperty(exports, "UnsetZone", { enumerable: true, get: function () { return Timezones_1.UnsetZone; } });
Object.defineProperty(exports, "UnsetZoneName", { enumerable: true, get: function () { return Timezones_1.UnsetZoneName; } });
Object.defineProperty(exports, "UnsetZoneOffsetMinutes", { enumerable: true, get: function () { return Timezones_1.UnsetZoneOffsetMinutes; } });
Object.defineProperty(exports, "defaultVideosToUTC", { enumerable: true, get: function () { return Timezones_1.defaultVideosToUTC; } });
Object.defineProperty(exports, "offsetMinutesToZoneName", { enumerable: true, get: function () { return Timezones_1.offsetMinutesToZoneName; } });
var WriteTask_2 = require("./WriteTask");
Object.defineProperty(exports, "DefaultWriteTaskOptions", { enumerable: true, get: function () { return WriteTask_2.DefaultWriteTaskOptions; } });
Object.defineProperty(exports, "WriteTaskOptionFields", { enumerable: true, get: function () { return WriteTask_2.WriteTaskOptionFields; } });
/**
* This is the hardcoded path in the exiftool shebang line (#!/usr/bin/perl).
*
* ExifTool's vendored Perl script uses this standard shebang path, which works
* on most Unix-like systems. However, this may fail on systems where Perl is
* installed elsewhere (e.g., via Homebrew on macOS: /opt/homebrew/bin/perl).
*
* When this hardcoded path doesn't exist, the library automatically falls back
* to using `which perl` to locate the Perl interpreter and ignores the shebang
* line by explicitly invoking Perl with the script as an argument.
*
* @see _ignoreShebang for the fallback logic
* @see whichPerl for the dynamic Perl detection
*/
const PERL = "/usr/bin/perl";
/**
* Is the #!/usr/bin/perl shebang line in exiftool-vendored.pl going to fail? If
* so, we need to find `perl` ourselves, and ignore the shebang line.
*/
const _ignoreShebang = (0, Lazy_1.lazy)(() => !(0, IsWin32_1.isWin32)() && !_fs.existsSync(PERL));
const whichPerl = (0, Lazy_1.lazy)(async () => {
const result = await (0, Which_1.which)(PERL);
if (result == null) {
throw new Error("Perl must be installed. Please add perl to your $PATH and try again.");
}
return result;
});
/**
* Manages delegating calls to a cluster of ExifTool child processes.
*
* **NOTE: Instances are expensive!**
*
* * use either the default exported singleton instance of this class,
* {@link exiftool}, or your own singleton
*
* * make sure you await {@link ExifTool.end} when you're done with an instance
* to clean up subprocesses
*
* * review the {@link ExifToolOptions} for configuration options--the default
* values are conservative to avoid overwhelming your system.
*
* @see https://photostructure.github.io/exiftool-vendored.js/ for more documentation.
*/
class ExifTool {
options;
batchCluster;
constructor(options = {}) {
if (options != null && typeof options !== "object") {
throw new Error("Please update caller to the new ExifTool constructor API");
}
const o = (0, ExifToolOptions_1.handleDeprecatedOptions)({
...DefaultExifToolOptions_1.DefaultExifToolOptions,
...options,
});
const ignoreShebang = o.ignoreShebang ?? _ignoreShebang();
const env = { ...o.exiftoolEnv, LANG: "C" };
if ((0, String_1.notBlank)(node_process_1.default.env.EXIFTOOL_HOME) && (0, String_1.blank)(env.EXIFTOOL_HOME)) {
env.EXIFTOOL_HOME = node_process_1.default.env.EXIFTOOL_HOME;
}
const spawnOpts = {
stdio: "pipe",
shell: false,
detached: false, // < no orphaned exiftool procs, please
env,
};
const processFactory = async () => ignoreShebang
? _cp.spawn(await whichPerl(), [await this.exiftoolPath(), ...o.exiftoolArgs], spawnOpts)
: _cp.spawn(await this.exiftoolPath(), o.exiftoolArgs, spawnOpts);
this.options = {
...o,
ignoreShebang,
processFactory,
};
this.batchCluster = new bc.BatchCluster(this.options);
}
exiftoolPath = (0, Lazy_1.lazy)(async () => {
const o = await this.options.exiftoolPath;
if ((0, String_1.isString)(o) && (0, String_1.notBlank)(o))
return o;
if ((0, Object_1.isFunction)(o))
return o(this.options.logger());
return (0, ExiftoolPath_1.exiftoolPath)(this.options.logger());
});
#taskOptions = (0, Lazy_1.lazy)(() => (0, Pick_1.pick)(this.options, "ignoreMinorErrors"));
/**
* Register life cycle event listeners. Delegates to BatchProcess.
*/
on = (event, listener) => this.batchCluster.on(event, listener);
/**
* Unregister life cycle event listeners. Delegates to BatchProcess.
*/
off = (event, listener) => this.batchCluster.off(event, listener);
/**
* @return a promise holding the version number of the vendored ExifTool
*/
version() {
return this.enqueueTask(() => new VersionTask_1.VersionTask(this.options));
}
read(file, argsOrOptions, options) {
const opts = {
...(0, Pick_1.pick)(this.options, ...ReadTask_1.ReadTaskOptionFields),
...((0, Object_1.isObject)(argsOrOptions) ? argsOrOptions : options),
};
opts.readArgs =
(0, Array_1.ifArray)(argsOrOptions) ?? (0, Array_1.ifArray)(opts.readArgs) ?? this.options.readArgs;
return this.enqueueTask(() => ReadTask_1.ReadTask.for(file, opts)); // < no way to know at compile time if we're going to get back a T!
}
/**
* Read the tags from `file`, without any post-processing of ExifTool values.
*
* **You probably want `read`, not this method. READ THE REST OF THIS COMMENT
* CAREFULLY.**
*
* If you want to extract specific tag values from a file, you may want to use
* this, but all data validation and inference heuristics provided by `read`
* will be skipped.
*
* Note that performance will be very similar to `read`, and will actually be
* worse if you don't include `-fast` or `-fast2` (as the most expensive bit
* is the perl interpreter and scanning the file on disk).
*
* @param args any additional arguments other than the file path. Note that
* "-json", and the Windows unicode filename handler flags, "-charset
* filename=utf8", will be added automatically.
*
* @return Note that the return value will be similar to `Tags`, but with no
* date, time, or other rich type parsing that you get from `.read()`. The
* field values will be `string | number | string[]`.
*
* @see https://github.com/photostructure/exiftool-vendored.js/issues/44 for
* typing details.
*/
readRaw(file, args = []) {
return this.enqueueTask(() => ReadRawTask_1.ReadRawTask.for(file, args, this.#taskOptions()));
}
/**
* Write the given `tags` to `file`.
*
* **NOTE: no input validation is done by this library.** ExifTool, however,
* is strict about tag names and values in the context of the format of file
* being written to.
*
* **IMPORTANT:** Partial dates (year-only or year-month) are only supported
* for XMP tags. Use group-prefixed tag names like `"XMP:CreateDate"` for
* partial date support. EXIF tags require complete dates.
*
* @param file an existing file to write `tags` to
*
* @param tags the tags to write to `file`.
*
* @param options overrides to the default ExifTool options provided to the
* ExifTool constructor.
*
* @returns Either the promise will be resolved if the tags are written to
* successfully, or the promise will be rejected if there are errors or
* warnings.
*
* @see https://exiftool.org/exiftool_pod.html#overwrite_original
*/
write(file, tags, writeArgsOrOptions, options) {
const opts = {
...(0, Pick_1.pick)(this.options, ...WriteTask_1.WriteTaskOptionFields),
...((0, Object_1.isObject)(writeArgsOrOptions) ? writeArgsOrOptions : options),
};
opts.writeArgs =
(0, Array_1.ifArray)(writeArgsOrOptions) ??
(0, Array_1.ifArray)(opts.writeArgs) ??
this.options.writeArgs;
// don't retry because writes might not be idempotent (e.g. incrementing
// timestamps by an hour)
const retriable = false;
return this.enqueueTask(() => WriteTask_1.WriteTask.for(file, tags, opts), retriable);
}
/**
* This will strip `file` of all metadata tags. The original file (with the
* name `${FILENAME}_original`) will be retained. Note that some tags, like
* stat information and image dimensions, are intrinsic to the file and will
* continue to exist if you re-`read` the file.
*
* @param {string} file the file to strip of metadata
*
* @param {(keyof Tags | string)[]} opts.retain optional. If provided, this is
* a list of metadata keys to **not** delete.
*/
deleteAllTags(file, opts) {
const writeArgs = [...DeleteAllTagsArgs_1.DeleteAllTagsArgs];
for (const ea of opts?.retain ?? []) {
writeArgs.push(`-${ea}<${ea}`);
}
return this.write(file, {}, { ...(0, Object_1.omit)(opts ?? {}, "retain"), writeArgs });
}
/**
* Extract the low-resolution thumbnail in `path/to/image.jpg` and write it to
* `path/to/thumbnail.jpg`.
*
* Note that these images can be less than .1 megapixels in size.
*
* @return a `Promise<void>`
*
* @throws if the file could not be read or the output not written
*/
extractThumbnail(imageFile, thumbnailFile, opts) {
return this.extractBinaryTag("ThumbnailImage", imageFile, thumbnailFile, opts);
}
/**
* Extract the "preview" image in `path/to/image.jpg` and write it to
* `path/to/preview.jpg`.
*
* The size of these images varies widely, and is present in dSLR images.
* Canon, Fuji, Olympus, and Sony use this tag.
*
* @return a `Promise<void>`
*
* @throws if the file could not be read or the output not written
*/
extractPreview(imageFile, previewFile, opts) {
return this.extractBinaryTag("PreviewImage", imageFile, previewFile, opts);
}
/**
* Extract the "JpgFromRaw" image in `path/to/image.jpg` and write it to
* `path/to/fromRaw.jpg`.
*
* This size of these images varies widely, and is not present in all RAW
* images. Nikon and Panasonic use this tag.
*
* @return a `Promise<void>`
*
* @throws if the file could not be read or the output not written.
*/
extractJpgFromRaw(imageFile, outputFile, opts) {
return this.extractBinaryTag("JpgFromRaw", imageFile, outputFile, opts);
}
/**
* Extract a given binary value from "tagname" tag associated to
* `path/to/image.jpg` and write it to `dest` (which cannot exist and whose
* directory must already exist).
*
* @return a `Promise<void>`
*
* @throws if the binary output not be written to `dest`.
*/
async extractBinaryTag(tagname, src, dest, opts) {
// BinaryExtractionTask returns a stringified error if the output indicates
// the task should not be retried.
const maybeError = await this.enqueueTask(() => BinaryExtractionTask_1.BinaryExtractionTask.for(tagname, src, dest, {
...this.#taskOptions(),
...opts,
}));
if (maybeError != null) {
throw new Error(maybeError);
}
}
/**
* Extract a given binary value from "tagname" tag associated to
* `path/to/image.jpg` as a `Buffer`. This has the advantage of not writing to
* a file, but if the payload associated to `tagname` is large, this can cause
* out-of-memory errors.
*
* @return a `Promise<Buffer>`
*
* @throws if the file or tag is missing.
*/
async extractBinaryTagToBuffer(tagname, imageFile, opts) {
const result = await this.enqueueTask(() => BinaryToBufferTask_1.BinaryToBufferTask.for(tagname, imageFile, {
...this.#taskOptions(),
...opts,
}));
if (Buffer.isBuffer(result)) {
return result;
}
else if (result instanceof Error) {
throw result;
}
else {
throw new Error("Unexpected result from BinaryToBufferTask: " + JSON.stringify(result));
}
}
/**
* Attempt to fix metadata problems in JPEG images by deleting all metadata
* and rebuilding from scratch. After repairing an image you should be able to
* write to it without errors, but some metadata from the original image may
* be lost in the process.
*
* This should only be applied as a last resort to images whose metadata is
* not readable via {@link ExifTool.read}.
*
* @see https://exiftool.org/faq.html#Q20
*
* @param {string} inputFile the path to the problematic image
* @param {string} outputFile the path to write the repaired image
* @param {boolean} opts.allowMakerNoteRepair if there are problems with MakerNote
* tags, allow ExifTool to apply heuristics to recover corrupt tags. See
* exiftool's `-F` flag.
* @return {Promise<void>} resolved after the outputFile has been written.
*/
rewriteAllTags(inputFile, outputFile, opts) {
return this.enqueueTask(() => RewriteAllTagsTask_1.RewriteAllTagsTask.for(inputFile, outputFile, {
allowMakerNoteRepair: false,
...this.#taskOptions(),
...opts,
}));
}
/**
* Shut down running ExifTool child processes. No subsequent requests will be
* accepted.
*
* This may need to be called in `after` or `finally` clauses in tests or
* scripts for them to exit cleanly.
*/
end(gracefully = true) {
return this.batchCluster.end(gracefully).promise;
}
/**
* @return true if `.end()` has been invoked
*/
get ended() {
return this.batchCluster.ended;
}
// calling whichPerl through this lazy() means we only do that task once per
// instance.
#checkForPerl = (0, Lazy_1.lazy)(async () => {
if (this.options.checkPerl) {
await whichPerl(); // < throws if perl is missing
}
});
/**
* Most users will not need to use `enqueueTask` directly. This method
* supports submitting custom `BatchCluster` tasks.
*
* @param task is a thunk to support retries by providing new instances on retries.
*
* @see BinaryExtractionTask for an example task implementation
*/
enqueueTask(task, retriable = true) {
const f = async () => {
await this.#checkForPerl();
return this.batchCluster.enqueueTask(task());
};
return retriable ? (0, AsyncRetry_1.retryOnReject)(f, this.options.taskRetries) : f();
}
/**
* @return the currently running ExifTool processes. Note that on Windows,
* these are only the process IDs of the directly-spawned ExifTool wrapper,
* and not the actual perl vm. This should only really be relevant for
* integration tests that verify processes are cleaned up properly.
*/
get pids() {
return this.batchCluster.pids();
}
/**
* @return the number of pending (not currently worked on) tasks
*/
get pendingTasks() {
return this.batchCluster.pendingTaskCount;
}
/**
* @return the total number of child processes created by this instance
*/
get spawnedProcs() {
return this.batchCluster.spawnedProcCount;
}
/**
* @return the current number of child processes currently servicing tasks
*/
get busyProcs() {
return this.batchCluster.busyProcCount;
}
/**
* @return report why child processes were recycled
*/
childEndCounts() {
return this.batchCluster.childEndCounts;
}
/**
* Shut down any currently-running child processes. New child processes will
* be started automatically to handle new tasks.
*/
closeChildProcesses(gracefully = true) {
return this.batchCluster.closeChildProcesses(gracefully);
}
}
exports.ExifTool = ExifTool;
/**
* Use this singleton rather than instantiating new {@link ExifTool} instances
* in order to leverage a single running ExifTool process.
*
* As of v3.0, its {@link ExifToolOptions.maxProcs} is set to the number of
* CPUs on the current system; no more than `maxProcs` instances of `exiftool`
* will be spawned. You may want to experiment with smaller or larger values
* for `maxProcs`, depending on CPU and disk speed of your system and
* performance tradeoffs.
*
* Note that each child process consumes between 10 and 50 MB of RAM. If you
* have limited system resources you may want to use a smaller `maxProcs`
* value.
*
* See the source of {@link DefaultExifToolOptions} for more details about how
* this instance is configured.
*/
exports.exiftool = new ExifTool();
//# sourceMappingURL=ExifTool.js.map