397 lines
16 KiB
JavaScript
397 lines
16 KiB
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 {
|
|
this.#raw = JSON.parse(data)[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) {
|
|
delete 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
|