exiftool-vendored
Version:
Efficient, cross-platform access to ExifTool
400 lines • 16.6 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;
};
})();
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