UNPKG

exiftool-vendored

Version:
296 lines (295 loc) 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExifDateTime = void 0; const luxon_1 = require("luxon"); const DateTime_1 = require("./DateTime"); const Maybe_1 = require("./Maybe"); const Object_1 = require("./Object"); const String_1 = require("./String"); const TimeParsing_1 = require("./TimeParsing"); const Timezones_1 = require("./Timezones"); /** * Encapsulates encoding and decoding EXIF date and time strings, * along with timezone handling functionality. * * Key features: * - Parses datetime strings in various formats (EXIF strict/loose, ISO) * - Supports timezone inference, conversion, and matching between dates * - Preserves original string representations when available * - Provides conversion to/from multiple datetime formats (Luxon DateTime, JS * Date, ISO strings) * - Supports serialization/deserialization via JSON (see * {@link ExifDateTime.fromJSON}) * * EXIF datetime strings don't typically include timezone information. This * class provides mechanisms to associate timezone data from other EXIF tags * (like GPS position or timezone offset), and distinguishes between explicitly * set and inferred timezone information. */ class ExifDateTime { year; month; day; hour; minute; second; millisecond; tzoffsetMinutes; rawValue; zoneName; inferredZone; static from(exifOrIso, defaultZone) { return exifOrIso instanceof ExifDateTime ? exifOrIso // already an ExifDateTime : (0, String_1.blank)(exifOrIso) ? undefined // in order of strictness: : (this.fromExifStrict(exifOrIso, defaultZone) ?? this.fromISO(exifOrIso, defaultZone) ?? this.fromExifLoose(exifOrIso, defaultZone)); } static fromISO(iso, defaultZone) { if ((0, String_1.blank)(iso) || null != iso.match(/^\d+$/)) return undefined; // Unfortunately, DateTime.fromISO() is happy to parse a date with no time, // so we have to do this ourselves: return this.#fromPatterns(iso, (0, TimeParsing_1.timeFormats)({ formatPrefixes: ["y-MM-dd'T'", "y-MM-dd ", "y-M-d "], defaultZone, })); } /** * Try to parse a date-time string from EXIF. If there is not both a date * and a time component, returns `undefined`. * * @param text from EXIF metadata * @param defaultZone a "zone name" to use as a backstop, or default, if * `text` doesn't specify a zone. This may be IANA-formatted, like * "America/Los_Angeles", or an offset, like "UTC-3". See * `offsetMinutesToZoneName`. */ static fromEXIF(text, defaultZone) { if ((0, String_1.blank)(text)) return undefined; return ( // .fromExifStrict() uses .fromISO() as a backstop this.fromExifStrict(text, defaultZone) ?? this.fromExifLoose(text, defaultZone)); } static #fromPatterns(text, fmts) { const result = (0, TimeParsing_1.parseDateTime)(text, fmts); return result == null ? undefined : ExifDateTime.fromDateTime(result.dt, { rawValue: text, unsetMilliseconds: result.unsetMilliseconds, inferredZone: result.inferredZone, }); } /** * Parse the given date-time string, EXIF-formatted. * * @param text from EXIF metadata, in `y:M:d H:m:s` format (with optional * sub-seconds and/or timezone) * @param defaultZone a "zone name" to use as a backstop, or default, if * `text` doesn't specify a zone. This may be IANA-formatted, like * "America/Los_Angeles", or an offset, like "UTC-3". See * `offsetMinutesToZoneName`. */ static fromExifStrict(text, defaultZone) { if ((0, String_1.blank)(text) || !(0, String_1.isString)(text)) return undefined; return (this.#fromPatterns(text, (0, TimeParsing_1.timeFormats)({ formatPrefixes: ["y:MM:dd ", "y:M:d "], defaultZone })) ?? // Not found yet? Maybe it's in ISO format? See // https://github.com/photostructure/exiftool-vendored.js/issues/71 this.fromISO(text, defaultZone)); } static *#looseExifFormats(defaultZone) { // The following are from actual datestamps seen in the wild (!!) const formats = [ "MMM d y HH:mm:ss", "MMM d y, HH:mm:ss", // Thu Oct 13 00:12:27 2016: "ccc MMM d HH:mm:ss y", ]; const zone = (0, String_1.notBlank)(defaultZone) ? defaultZone : Timezones_1.UnsetZone; for (const fmt of formats) { yield { fmt: fmt, zone, inferredZone: true }; } } static fromExifLoose(text, defaultZone) { return (0, String_1.blank)(text) || !(0, String_1.isString)(text) ? undefined : this.#fromPatterns(text, this.#looseExifFormats(defaultZone)); } static fromDateTime(dt, opts) { if (dt == null || !dt.isValid || dt.year === 0 || dt.year === 1) { return undefined; } return new ExifDateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.millisecond === 0 && true === opts?.unsetMilliseconds ? undefined : dt.millisecond, dt.offset === Timezones_1.UnsetZoneOffsetMinutes ? undefined : dt.offset, opts?.rawValue, dt.zoneName == null || dt.zone?.name === Timezones_1.UnsetZone.name ? undefined : dt.zoneName, opts?.inferredZone); } /** * Create an ExifDateTime from a number of milliseconds since the epoch * (meaning since 1 January 1970 00:00:00 UTC). Uses the default zone. * * @param millis - a number of milliseconds since 1970 UTC * * @param options.rawValue - the original parsed string input * @param options.zone - the zone to place the DateTime into. Defaults to 'local'. * @param options.locale - a locale to set on the resulting DateTime instance * @param options.outputCalendar - the output calendar to set on the resulting DateTime instance * @param options.numberingSystem - the numbering system to set on the resulting DateTime instance */ static fromMillis(millis, options = {}) { if (options.zone == null || [Timezones_1.UnsetZoneName, Timezones_1.UnsetZone].includes(options.zone)) { delete options.zone; } let dt = luxon_1.DateTime.fromMillis(millis, { ...(0, Object_1.omit)(options, "rawValue"), }); if (options.zone == null) { dt = dt.setZone(Timezones_1.UnsetZone, { keepLocalTime: true }); } // TODO: is there a way to provide an invalid millisecond value? const result = this.fromDateTime(dt, { rawValue: options.rawValue }); if (result == null) { throw new Error(`Failed to create ExifDateTime from millis: ${millis}`); } return result; } static now(opts = {}) { return this.fromMillis(Date.now(), opts); } #dt; zone; constructor(year, month, day, hour, minute, second, millisecond, tzoffsetMinutes, rawValue, zoneName, inferredZone) { this.year = year; this.month = month; this.day = day; this.hour = hour; this.minute = minute; this.second = second; this.millisecond = millisecond; this.tzoffsetMinutes = tzoffsetMinutes; this.rawValue = rawValue; this.zoneName = zoneName; this.inferredZone = inferredZone; this.zone = (0, Timezones_1.getZoneName)({ zoneName, tzoffsetMinutes }); } get millis() { return this.millisecond; } get hasZone() { return this.zone != null; } get unsetMilliseconds() { return this.millisecond == null; } setZone(zone, opts) { const dt = (0, TimeParsing_1.setZone)({ zone, src: this.toDateTime(), srcHasZone: this.hasZone, opts, }); return ExifDateTime.fromDateTime(dt, { rawValue: this.rawValue, unsetMilliseconds: this.millisecond == null, inferredZone: opts?.inferredZone ?? true, }); } /** * CAUTION: This instance will inherit the system timezone if this instance * has an unset zone (as Luxon doesn't support "unset" timezones) */ toDateTime(overrideZone) { return (this.#dt ??= luxon_1.DateTime.fromObject({ year: this.year, month: this.month, day: this.day, hour: this.hour, minute: this.minute, second: this.second, millisecond: this.millisecond, }, { zone: overrideZone ?? this.zone, })); } toEpochSeconds(overrideZone) { return this.toDateTime(overrideZone).toUnixInteger(); } toDate() { return this.toDateTime().toJSDate(); } toISOString(options = {}) { return (0, Maybe_1.denull)(this.toDateTime().toISO({ suppressMilliseconds: options.suppressMilliseconds ?? this.millisecond == null, includeOffset: this.hasZone && options.includeOffset !== false, })); } toExifString() { return (0, DateTime_1.dateTimeToExif)(this.toDateTime(), { includeOffset: this.hasZone, includeMilliseconds: this.millisecond != null, }); } toString() { return this.toISOString(); } /** * @return the epoch milliseconds of this */ toMillis() { return this.toDateTime().toMillis(); } get isValid() { return this.toDateTime().isValid; } toJSON() { return { _ctor: "ExifDateTime", // < magick field used by the JSON parser year: this.year, month: this.month, day: this.day, hour: this.hour, minute: this.minute, second: this.second, millisecond: this.millisecond, tzoffsetMinutes: this.tzoffsetMinutes, rawValue: this.rawValue, zoneName: this.zoneName, inferredZone: this.inferredZone, }; } /** * @return a new ExifDateTime from the given JSON. Note that this instance **may not be valid**. */ static fromJSON(json) { return new ExifDateTime(json.year, json.month, json.day, json.hour, json.minute, json.second, json.millisecond, json.tzoffsetMinutes, json.rawValue, json.zoneName, json.inferredZone); } maybeMatchZone(target, maxDeltaMs = 14 * DateTime_1.MinuteMs) { const targetZone = target.zone; if (targetZone == null || !target.hasZone) return; return (this.setZone(targetZone, { keepLocalTime: false })?.ifClose(target, maxDeltaMs) ?? this.setZone(targetZone, { keepLocalTime: true })?.ifClose(target, maxDeltaMs)); } ifClose(target, maxDeltaMs = 14 * DateTime_1.MinuteMs) { const ts = this.toMillis(); const targetTs = target.toMillis(); return Math.abs(ts - targetTs) <= maxDeltaMs ? this : undefined; } plus(duration) { let dt = this.toDateTime().plus(duration); if (!this.hasZone) { dt = dt.setZone(Timezones_1.UnsetZone, { keepLocalTime: true }); } return ExifDateTime.fromDateTime(dt, this); } } exports.ExifDateTime = ExifDateTime; //# sourceMappingURL=ExifDateTime.js.map