UNPKG

exiftool-vendored

Version:
500 lines 23.3 kB
"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