UNPKG

exiftool-vendored

Version:
400 lines 16.6 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ReadTask = exports.DefaultReadTaskOptions = exports.ReadTaskOptionFields = void 0; exports.nullish = nullish; const batch_cluster_1 = require("batch-cluster"); const _path = __importStar(require("node:path")); const Array_1 = require("./Array"); const BinaryField_1 = require("./BinaryField"); const Boolean_1 = require("./Boolean"); const DefaultExifToolOptions_1 = require("./DefaultExifToolOptions"); const ErrorsAndWarnings_1 = require("./ErrorsAndWarnings"); const ExifDate_1 = require("./ExifDate"); const ExifDateTime_1 = require("./ExifDateTime"); const ExifTime_1 = require("./ExifTime"); const ExifToolOptions_1 = require("./ExifToolOptions"); const ExifToolTask_1 = require("./ExifToolTask"); const File_1 = require("./File"); const FilenameCharsetArgs_1 = require("./FilenameCharsetArgs"); const GPS_1 = require("./GPS"); const Lazy_1 = require("./Lazy"); const Number_1 = require("./Number"); const Object_1 = require("./Object"); const OnlyZerosRE_1 = require("./OnlyZerosRE"); const Pick_1 = require("./Pick"); const String_1 = require("./String"); const Timezones_1 = require("./Timezones"); /** * tag names we don't need to muck with, but name conventions (like including * "date") suggest they might be date/time tags */ const PassthroughTags = [ "ExifToolVersion", "DateStampMode", "Sharpness", "Firmware", "DateDisplayFormat", ]; exports.ReadTaskOptionFields = [ "adjustTimeZoneIfDaylightSavings", "backfillTimezones", "defaultVideosToUTC", "geolocation", "geoTz", "ignoreMinorErrors", "ignoreZeroZeroLatLon", "imageHashType", "includeImageDataMD5", "inferTimezoneFromDatestamps", "inferTimezoneFromDatestampTags", "inferTimezoneFromTimeStamp", "keepUTCTime", "numericTags", "preferTimezoneInferenceFromGps", "readArgs", "struct", "useMWG", ]; const NullIsh = ["undef", "null", "undefined"]; function nullish(s) { return s == null || ((0, String_1.isString)(s) && NullIsh.includes(s.trim())); } exports.DefaultReadTaskOptions = { ...(0, Pick_1.pick)(DefaultExifToolOptions_1.DefaultExifToolOptions, ...exports.ReadTaskOptionFields), }; const MaybeDateOrTimeRe = /when|date|time|subsec|creat|modif/i; class ReadTask extends ExifToolTask_1.ExifToolTask { sourceFile; args; options; degroup; #raw = {}; #rawDegrouped = {}; #tags = {}; /** * @param sourceFile the file to read * @param args the full arguments to pass to exiftool that take into account * the flags in `options` */ constructor(sourceFile, args, options) { super(args, options); this.sourceFile = sourceFile; this.args = args; this.options = options; // See https://github.com/photostructure/exiftool-vendored.js/issues/147#issuecomment-1642580118 this.degroup = this.args.includes("-G"); this.#tags = { SourceFile: sourceFile }; this.#tags.errors = this.errors; } static for(filename, options) { const opts = (0, ExifToolOptions_1.handleDeprecatedOptions)({ ...exports.DefaultReadTaskOptions, ...options, }); const sourceFile = _path.resolve(filename); const args = [ ...FilenameCharsetArgs_1.Utf8FilenameCharsetArgs, "-json", ...(0, Array_1.toArray)(opts.readArgs), ]; // "-api struct=undef" doesn't work: but it's the same as struct=0: args.push("-api", "struct=" + ((0, Number_1.isNumber)(opts.struct) ? opts.struct : "0")); if (opts.useMWG) { args.push("-use", "MWG"); } if (opts.imageHashType != null && opts.imageHashType !== false) { // See https://exiftool.org/forum/index.php?topic=14706.msg79218#msg79218 args.push("-api", "requesttags=imagedatahash"); args.push("-api", "imagehashtype=" + opts.imageHashType); } if (true === opts.geolocation) { args.push("-api", "geolocation"); } if (true === opts.keepUTCTime) { args.push("-api", "keepUTCTime"); } // IMPORTANT: "-all" must be after numeric tag references, as the first // reference in wins args.push(...opts.numericTags.map((ea) => "-" + ea + "#")); // We have to add a -all or else we'll only get the numericTags. sad. // TODO: Do you need -xmp:all, -all, or -all:all? Is -* better? args.push("-all", sourceFile); return new ReadTask(sourceFile, args, opts); } toString() { return "ReadTask" + this.sourceFile + ")"; } // only exposed for tests parse(data, err) { try { // Fix ExifToolVersion to be a string to preserve version distinctions like 12.3 vs 12.30 const versionFixedData = data.replace(/"ExifToolVersion"\s*:\s*(\d+(?:\.\d+)?)/, '"ExifToolVersion":"$1"'); this.#raw = JSON.parse(versionFixedData)[0]; } catch (jsonError) { // TODO: should restart exiftool? (0, batch_cluster_1.logger)().warn("ExifTool.ReadTask(): Invalid JSON", { data, err, jsonError, }); throw err ?? jsonError; } // ExifTool does "humorous" things to paths, like flip path separators. resolve() undoes that. if ((0, String_1.notBlank)(this.#raw.SourceFile)) { if (!(0, File_1.compareFilePaths)(this.#raw.SourceFile, this.sourceFile)) { // this would indicate a bug in batch-cluster: throw new Error(`Internal error: unexpected SourceFile of ${this.#raw.SourceFile} for file ${this.sourceFile}`); } } return this.#parseTags(); } #isVideo() { return String(this.#rawDegrouped?.MIMEType).startsWith("video/"); } #defaultToUTC() { return this.#isVideo() && this.options.defaultVideosToUTC; } #tagName(k) { return this.degroup ? (k.split(":")[1] ?? k) : k; } #parseTags() { if (this.degroup) { this.#rawDegrouped = {}; for (const [key, value] of Object.entries(this.#raw)) { const k = this.#tagName(key); this.#rawDegrouped[k] = value; } } else { this.#rawDegrouped = this.#raw; } // avoid casting `this.tags as any` for the rest of the function: const tags = this.#tags; // Must be run before extracting tz offset, to repair possibly-invalid // GeolocationTimeZone this.#extractGpsMetadata(); const tzSrc = this.#extractTzOffset(); if (tzSrc) { tags.zone = tzSrc.zone; tags.tz = tzSrc.tz; tags.tzSource = tzSrc.src; } for (const [key, value] of Object.entries(this.#raw)) { const k = this.#tagName(key); // Did something already set this? (like GPS tags) if (key in tags) continue; const v = this.#parseTag(k, value); // Note that we set `key` (which may include a group prefix): if (v == null) { // Use Reflect.deleteProperty for dynamic keys Reflect.deleteProperty(tags, key); } else { tags[key] = v; } } // we could `return {...tags, ...errorsAndWarnings(this, tags)}` but tags is // a chonky monster, and we don't want to double the work for the poor // garbage collector. const { errors, warnings } = (0, ErrorsAndWarnings_1.errorsAndWarnings)(this, tags); tags.errors = errors; tags.warnings = warnings; return tags; } #extractGpsMetadata = (0, Lazy_1.lazy)(() => { const result = (0, GPS_1.parseGPSLocation)(this.#rawDegrouped, this.options); if (result?.warnings != null && (result.warnings.length ?? 0) > 0) { this.warnings.push(...result.warnings); } if (result?.invalid !== true) { for (const [k, v] of Object.entries(result?.result ?? {})) { this.#tags[k] = v; } } return result; }); #gpsIsInvalid = (0, Lazy_1.lazy)(() => this.#extractGpsMetadata()?.invalid ?? false); #gpsResults = (0, Lazy_1.lazy)(() => this.#gpsIsInvalid() ? {} : (this.#extractGpsMetadata()?.result ?? {})); #extractTzOffsetFromGps = (0, Lazy_1.lazy)(() => { const gps = this.#extractGpsMetadata(); const lat = gps?.result?.GPSLatitude; const lon = gps?.result?.GPSLongitude; if (gps == null || gps.invalid === true || lat == null || lon == null) return; // First try GeolocationTimeZone: const geolocZone = (0, Timezones_1.normalizeZone)(this.#rawDegrouped.GeolocationTimeZone); if (geolocZone != null) { return { zone: geolocZone.name, tz: geolocZone.name, src: "GeolocationTimeZone", }; } try { const geoTz = this.options.geoTz(lat, lon); const zone = (0, Timezones_1.normalizeZone)(geoTz); if (zone != null) { return { zone: zone.name, tz: zone.name, src: "GPSLatitude/GPSLongitude", }; } } catch (error) { this.warnings.push("Failed to determine timezone from GPS coordinates: " + error); } return; }); #tz = (0, Lazy_1.lazy)(() => this.#extractTzOffset()?.tz); #extractTzOffset = (0, Lazy_1.lazy)(() => { if (true === this.options.preferTimezoneInferenceFromGps) { const fromGps = this.#extractTzOffsetFromGps(); if (fromGps != null) { return fromGps; } } return ((0, Timezones_1.extractTzOffsetFromTags)(this.#rawDegrouped, this.options) ?? this.#extractTzOffsetFromGps() ?? (0, Timezones_1.extractTzOffsetFromDatestamps)(this.#rawDegrouped, this.options) ?? // See https://github.com/photostructure/exiftool-vendored.js/issues/113 // and https://github.com/photostructure/exiftool-vendored.js/issues/156 // Videos are frequently encoded in UTC, but don't include the // timezone offset in their datetime stamps. (this.#defaultToUTC() ? { zone: "UTC", tz: "UTC", src: "defaultVideosToUTC", } : // not applicable: undefined) ?? // This is a last-ditch estimation heuristic: (0, Timezones_1.extractTzOffsetFromUTCOffset)(this.#rawDegrouped) ?? // No, really, this is the even worse than UTC offset heuristics: (0, Timezones_1.extractTzOffsetFromTimeStamp)(this.#rawDegrouped, this.options)); }); #parseTag(tagName, value) { if (nullish(value)) return undefined; try { if (PassthroughTags.indexOf(tagName) >= 0) { return value; } if (tagName.startsWith("GPS") || tagName.startsWith("Geolocation")) { if (this.#gpsIsInvalid()) return undefined; // If we parsed out a better value, use that: const parsed = this.#gpsResults()[tagName]; if (parsed != null) return parsed; // Otherwise, parse the raw value like any other tag: (It might be // something like "GPSTimeStamp"): } if (Array.isArray(value)) { return value.map((ea) => this.#parseTag(tagName, ea)); } if ((0, Object_1.isObject)(value)) { const result = {}; for (const [k, v] of Object.entries(value)) { result[k] = this.#parseTag(tagName + "." + k, v); } return result; } if (typeof value === "string") { const b = BinaryField_1.BinaryField.fromRawValue(value); if (b != null) return b; if (/Valid$/.test(tagName)) { const b = (0, Boolean_1.toBoolean)(value); if (b != null) return b; } if (MaybeDateOrTimeRe.test(tagName) && // Reject date/time keys that are "0" or "00" (found in Canon // SubSecTime values) !OnlyZerosRE_1.OnlyZerosRE.test(value)) { // if #defaultToUTC() is true, _we actually think zoneless // datestamps are all in UTC_, rather than being in `this.tz` (which // may be from GPS or other heuristics). See issue #153. const tz = isUtcTagName(tagName) || this.#defaultToUTC() ? "UTC" : this.options.backfillTimezones ? this.#tz() : undefined; // Time-only tags have "time" but not "date" in their name: const keyIncludesTime = /subsec|time/i.test(tagName); const keyIncludesDate = /date/i.test(tagName); const keyIncludesWhen = /when/i.test(tagName); // < ResourceEvent.When const result = (keyIncludesTime || keyIncludesDate || keyIncludesWhen ? ExifDateTime_1.ExifDateTime.from(value, tz) : undefined) ?? (keyIncludesTime || keyIncludesWhen ? ExifTime_1.ExifTime.fromEXIF(value, tz) : undefined) ?? (keyIncludesDate || keyIncludesWhen ? ExifDate_1.ExifDate.from(value) : undefined) ?? value; const defaultTz = this.#tz(); if (this.options.backfillTimezones && result != null && defaultTz != null && result instanceof ExifDateTime_1.ExifDateTime && this.#defaultToUTC() && !isUtcTagName(tagName) && true === result.inferredZone) { return result.setZone(defaultTz); } return result; } } // Trust that ExifTool rendered the value with the correct type in JSON: return value; } catch (e) { this.warnings.push(`Failed to parse ${tagName} with value ${JSON.stringify(value)}: ${e}`); return value; } } } exports.ReadTask = ReadTask; function isUtcTagName(tagName) { return tagName.includes("UTC") || tagName.startsWith("GPS"); } //# sourceMappingURL=ReadTask.js.map