exiftool-vendored
Advanced tools
Comparing version 15.12.1 to 16.0.0
@@ -7,3 +7,3 @@ # Changelog/Versioning | ||
features or bugfixes arise and using ExifTool's version number is at odds with | ||
eachother, so this library follows [Semver](https://semver.org/), and the | ||
each other, so this library follows [Semver](https://semver.org/), and the | ||
vendored versions of ExifTool match the version they vendor. | ||
@@ -29,2 +29,63 @@ | ||
### v16.0.0 | ||
- 💔/🐞 Timezone extraction has been adjusted: if there is a GPS location, we'll | ||
prefer that `tzlookup` as the authoritative timezone offset. If there isn't | ||
GPS lat/lon, we'll use `Timezone`, `OffsetTime`, or `TimeZoneOffset`. If those | ||
are missing, we'll infer the offset from UTC offsets. | ||
Prior builds would defer to the offset in `Timezone`, `OffsetTime`, or | ||
`TimeZoneOffset`, but GPS is more reliable, and results in a proper time zone | ||
(like `America/Los_Angeles`). Zone names work correctly even when times are | ||
adjusted across daylight savings offset boundaries. | ||
- 💔/🐞 Timezone application is now has been improved: if a timezone can be | ||
extracted for a given file, `readTags` will now make all `ExifDateTime` | ||
entries match that timezone. The timestamps should refer to the same | ||
timestamp/seconds-from-common-epoch, but "local time" may be different as | ||
we've adjusted the timezone accordingly. | ||
Metadata sometimes includes a timezone offset, and sometimes it doesn't, and | ||
it's all pretty inconsistent, but worse, prior versions would sometimes | ||
inherit the current system timezone for an arbitrary subset of tags. This | ||
version should remove the system timezone "leaking" into your metadata values. | ||
As an example, if you took a photo with GPS information from Rome (CET, | ||
UTC+1), and your computer is in California with `TZ=America/Los_Angeles`, | ||
prior versions could return `CreateDate: 2022-02-02 02:02:22-07:00`. This | ||
version will translate that time into `CreateDate: 2022-02-02 11:02:22+01:00`. | ||
Note that this fix results in `readTags` rendering different `ExifDateTime` | ||
values from prior versions, so I bumped the major version to highlight this | ||
change. | ||
- 💔 `Tags` is automatically generated by `mktags`, which now has a set of | ||
"required" tags with type and group metadata to ensure a core set of tags | ||
don't disappear or change types. | ||
As a reminder, the `Tags` interface is only a subset of fields returned, due | ||
to TypeScript limitations. `readTags` still returns all values that ExifTool | ||
provides. | ||
- 🐞 Fixed a bunch of broken API links in the README due to `typedoc` changing | ||
URLs. Harumph. | ||
- 🐞 Prior versions of `ExifDateTime.parseISO` would accept just time or date | ||
strings. | ||
- 🐞/📦 `TimeStamp` tags may now be properly parsed as `ExifDateTime`. | ||
- 📦 Added performance section to the README. | ||
- 📦 Timezone offset formatting changed slightly: the hour offset is no longer | ||
zero-padded, which better matches the Luxon implementation we use internally. | ||
- 📦 `ExifDateTime` caches the result of `toDateTime` now, which may save a | ||
couple extra objects fed to the GC. | ||
- 📦 Updated dependencies, including batch-cluster | ||
[v10.3.2](https://github.com/photostructure/batch-cluster.js/blob/main/CHANGELOG.md#v1032)), | ||
which fixed several race conditions and added several process performance | ||
improvements including support for zero-wait multi-process launches. | ||
### v15.12.1 | ||
@@ -31,0 +92,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { DateTime, ToISOTimeOptions } from "luxon"; | ||
import { DateTime, ToISOTimeOptions, Zone, ZoneOptions } from "luxon"; | ||
import { Maybe } from "./Maybe"; | ||
@@ -7,2 +7,3 @@ /** | ||
export declare class ExifDateTime { | ||
#private; | ||
readonly year: number; | ||
@@ -18,3 +19,3 @@ readonly month: number; | ||
readonly zoneName?: string | undefined; | ||
static fromISO(iso: string, zone?: Maybe<string>, rawValue?: string): Maybe<ExifDateTime>; | ||
static fromISO(iso: string, zone?: Maybe<string>): Maybe<ExifDateTime>; | ||
/** | ||
@@ -38,3 +39,5 @@ * Try to parse a date-time string from EXIF. If there is not both a date and | ||
get zone(): Maybe<string>; | ||
setZone(zone?: string | Zone, opts?: ZoneOptions): ExifDateTime; | ||
toDateTime(): DateTime; | ||
toEpochSeconds(): number; | ||
toDate(): Date; | ||
@@ -41,0 +44,0 @@ toISOString(options?: ToISOTimeOptions): Maybe<string>; |
"use strict"; | ||
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); | ||
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); | ||
}; | ||
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
if (kind === "m") throw new TypeError("Private method is not writable"); | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); | ||
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; | ||
}; | ||
var _ExifDateTime_dt; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -24,10 +36,20 @@ exports.ExifDateTime = void 0; | ||
this.zoneName = zoneName; | ||
_ExifDateTime_dt.set(this, void 0); | ||
} | ||
static fromISO(iso, zone, rawValue) { | ||
static fromISO(iso, zone) { | ||
if ((0, String_1.blank)(iso) || null != iso.match(/^\d+$/)) | ||
return undefined; | ||
return this.fromDateTime(luxon_1.DateTime.fromISO(iso, { | ||
setZone: true, | ||
zone: zone !== null && zone !== void 0 ? zone : Timezones_1.UnsetZone, | ||
}), rawValue !== null && rawValue !== void 0 ? rawValue : iso); | ||
// Unfortunately, DateTime.fromISO() is happy to parse a date with no time, | ||
// so we have to do this ourselves: | ||
return this.fromPatterns(iso, [ | ||
// if it specifies a zone, use it: | ||
{ fmt: "y-M-d'T'H:m:s.uZZ" }, | ||
{ fmt: "y-M-d'T'H:m:sZZ" }, | ||
// if it specifies UTC, use it: | ||
{ fmt: "y-M-d'T'H:m:s.u'Z'", zone: "utc" }, | ||
{ fmt: "y-M-d'T'H:m:s'Z'", zone: "utc" }, | ||
// Otherwise use the default zone: | ||
{ fmt: "y-M-d'T'H:m:s.u", zone }, | ||
{ fmt: "y-M-d'T'H:m:s", zone }, | ||
]); | ||
} | ||
@@ -71,5 +93,6 @@ /** | ||
static fromExifStrict(text, zone) { | ||
var _a; | ||
if ((0, String_1.blank)(text)) | ||
return undefined; | ||
return this.fromPatterns(text, [ | ||
return ((_a = this.fromPatterns(text, [ | ||
// if it specifies a zone, use it: | ||
@@ -85,12 +108,3 @@ { fmt: "y:M:d H:m:s.uZZ" }, | ||
// Not found yet? Maybe it's in ISO format? See https://github.com/photostructure/exiftool-vendored.js/issues/71 | ||
// if it specifies a zone, use it: | ||
{ fmt: "y-M-d'T'H:m:s.uZZ" }, | ||
{ fmt: "y-M-d'T'H:m:sZZ" }, | ||
// if it specifies UTC, use it: | ||
{ fmt: "y-M-d'T'H:m:s.u'Z'", zone: "utc" }, | ||
{ fmt: "y-M-d'T'H:m:s'Z'", zone: "utc" }, | ||
// Otherwise use the default zone: | ||
{ fmt: "y-M-d'T'H:m:s.u", zone }, | ||
{ fmt: "y-M-d'T'H:m:s", zone }, | ||
]); | ||
])) !== null && _a !== void 0 ? _a : this.fromISO(text, zone)); | ||
} | ||
@@ -133,4 +147,17 @@ static fromExifLoose(text, defaultZone) { | ||
} | ||
setZone(zone, opts) { | ||
// This is a bit tricky... We want to keep the local time and just _say_ it was in the zone of the image **if we don't already have a zone.** | ||
// If we _do_ have a zone, assume it was already converted by ExifTool into (probably the system) timezone, which means _don't_ keepLocalTime. | ||
const result = ExifDateTime.fromDateTime(this.toDateTime().setZone(zone, { | ||
keepLocalTime: !this.hasZone, | ||
...opts, | ||
}), this.rawValue); | ||
// We know this will be defined: this is valid, so changing the zone will | ||
// also be valid. | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
return result; | ||
} | ||
toDateTime() { | ||
return luxon_1.DateTime.fromObject({ | ||
var _a; | ||
return (__classPrivateFieldSet(this, _ExifDateTime_dt, (_a = __classPrivateFieldGet(this, _ExifDateTime_dt, "f")) !== null && _a !== void 0 ? _a : luxon_1.DateTime.fromObject({ | ||
year: this.year, | ||
@@ -145,4 +172,7 @@ month: this.month, | ||
zone: this.zone, | ||
}); | ||
}), "f")); | ||
} | ||
toEpochSeconds() { | ||
return this.toDateTime().toUnixInteger(); | ||
} | ||
toDate() { | ||
@@ -187,2 +217,3 @@ return this.toDateTime().toJSDate(); | ||
exports.ExifDateTime = ExifDateTime; | ||
_ExifDateTime_dt = new WeakMap(); | ||
//# sourceMappingURL=ExifDateTime.js.map |
/// <reference types="node" /> | ||
import * as bc from "batch-cluster"; | ||
import { ApplicationRecordTags } from "./ApplicationRecordTags"; | ||
import { ExifDate } from "./ExifDate"; | ||
import { ExifDateTime } from "./ExifDateTime"; | ||
import { ExifToolTask } from "./ExifToolTask"; | ||
import { ICCProfileTags } from "./ICCProfileTags"; | ||
import { Maybe } from "./Maybe"; | ||
import { PreviewTag } from "./PreviewTag"; | ||
import { Struct } from "./Struct"; | ||
import { Tags } from "./Tags"; | ||
import { APP12Tags, APP14Tags, APP1Tags, APP4Tags, APP5Tags, APP6Tags, CompositeTags, EXIFTags, ExifToolTags, FileTags, FlashPixTags, IPTCTags, JFIFTags, MakerNotesTags, MetaTags, MPFTags, PanasonicRawTags, PhotoshopTags, PrintIMTags, QuickTimeTags, RAFTags, RIFFTags, Tags, XMPTags } from "./Tags"; | ||
export { ExifDate } from "./ExifDate"; | ||
@@ -16,3 +18,3 @@ export { ExifDateTime } from "./ExifDateTime"; | ||
export { offsetMinutesToZoneName, UnsetZone, UnsetZoneName, UnsetZoneOffsetMinutes, } from "./Timezones"; | ||
export type { AdditionalWriteTags, ExpandedDateTags, Maybe, Omit, Struct, Tags }; | ||
export type { AdditionalWriteTags, APP12Tags, APP14Tags, APP1Tags, APP4Tags, APP5Tags, APP6Tags, ApplicationRecordTags, CompositeTags, EXIFTags, ExifToolTags, ExpandedDateTags, FileTags, FlashPixTags, ICCProfileTags, IPTCTags, JFIFTags, MakerNotesTags, Maybe, MetaTags, MPFTags, Omit, PanasonicRawTags, PhotoshopTags, PrintIMTags, QuickTimeTags, RAFTags, RIFFTags, Struct, Tags, XMPTags, }; | ||
export declare const DefaultExifToolPath: string; | ||
@@ -34,6 +36,2 @@ export declare const DefaultExiftoolArgs: string[]; | ||
"Orientation#"?: number; | ||
/** | ||
* Included because it's so rare, it doesn't always make the Tags build: | ||
*/ | ||
TimeZoneOffset?: number | string; | ||
}; | ||
@@ -43,5 +41,5 @@ declare type ExpandedDateTags = { | ||
}; | ||
declare type NotUndefined<T> = T extends undefined ? never : T; | ||
export declare type Defined<T> = T extends undefined ? never : T; | ||
export declare type DefinedOrNullValued<T> = { | ||
[P in keyof T]: NotUndefined<T[P]> | null; | ||
[P in keyof T]: Defined<T[P]> | null; | ||
}; | ||
@@ -328,2 +326,3 @@ export declare type WriteTags = DefinedOrNullValued<ShortcutTags & AdditionalWriteTags & ExpandedDateTags>; | ||
childEndCounts(): { | ||
startError: number; | ||
broken: number; | ||
@@ -330,0 +329,0 @@ closed: number; |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[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) { | ||
@@ -6,0 +10,0 @@ if (k2 === undefined) k2 = k; |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[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) { | ||
@@ -6,0 +10,0 @@ if (k2 === undefined) k2 = k; |
@@ -0,2 +1,4 @@ | ||
import { Maybe } from "./Maybe"; | ||
export declare function keys<T extends object, K extends string & keyof T>(o: T): K[]; | ||
export declare function isFunction(obj: any): obj is () => any; | ||
export declare function fromEntries(arr: Maybe<[Maybe<string>, any]>[], obj?: any): any; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.isFunction = exports.keys = void 0; | ||
exports.fromEntries = exports.isFunction = exports.keys = void 0; | ||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
@@ -15,2 +15,20 @@ function keys(o) { | ||
exports.isFunction = isFunction; | ||
function fromEntries(arr, obj) { | ||
if (arr == null || arr.length === 0) | ||
return obj; | ||
// don't use Object.create(null), json stringify will break! | ||
for (const ea of arr.filter((ea) => ea != null)) { | ||
if (ea != null && Array.isArray(ea)) { | ||
const [k, v] = ea; | ||
// allow NULL fields: | ||
if (k != null && v !== undefined) { | ||
if (typeof obj !== "object") | ||
obj = {}; | ||
obj[k] = v; | ||
} | ||
} | ||
} | ||
return obj; | ||
} | ||
exports.fromEntries = fromEntries; | ||
//# sourceMappingURL=Object.js.map |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[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) { | ||
@@ -6,0 +10,0 @@ if (k2 === undefined) k2 = k; |
@@ -27,3 +27,4 @@ import { ExifToolTask } from "./ExifToolTask"; | ||
private extractTzOffset; | ||
private normalizeDateTime; | ||
private parseTag; | ||
} |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[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) { | ||
@@ -117,5 +121,8 @@ if (k2 === undefined) k2 = k; | ||
this.extractTzOffset(); | ||
Object.keys(this._raw).forEach((key) => (this.tags[key] = this.parseTag(key, this._raw[key]))); | ||
(0, Maybe_1.map)(this.tz, (ea) => (this.tags.tz = ea)); | ||
(0, Maybe_1.map)(this.tzSource, (ea) => (this.tags.tzSource = ea)); | ||
const t = this.tags; // tsc hack :( | ||
for (const key of Object.keys(this._raw)) { | ||
t[key] = this.parseTag(key, this._raw[key]); | ||
} | ||
if (this.errors.length > 0) | ||
@@ -159,4 +166,5 @@ this.tags.errors = this.errors; | ||
extractTzOffset() { | ||
// tzlookup will be the "best" tz, as it will be a proper Zone name (like | ||
// "America/New_York"), rather than just an hour offset. | ||
(0, Maybe_1.map)((0, Maybe_1.firstDefinedThunk)([ | ||
() => (0, Timezones_1.extractTzOffsetFromTags)(this._tags), | ||
() => { | ||
@@ -176,5 +184,16 @@ if (!this.invalidLatLon && this.lat != null && this.lon != null) { | ||
}, | ||
() => (0, Timezones_1.extractTzOffsetFromTags)(this._tags), | ||
() => (0, Timezones_1.extractTzOffsetFromUTCOffset)(this._tags), | ||
]), (ea) => ({ tz: this.tz, src: this.tzSource } = ea)); | ||
} | ||
normalizeDateTime(tagName, value) { | ||
if (tagName.startsWith("File") || | ||
!(value instanceof ExifDateTime_1.ExifDateTime) || | ||
tagIsInUTC(tagName) || | ||
this.tz == null) { | ||
return value; | ||
} | ||
// ExifTool may have put this in the current system time, instead of the timezone of the file. | ||
return value.zone !== this.tz ? value.setZone(this.tz) : value; | ||
} | ||
parseTag(tagNameWithGroup, value) { | ||
@@ -195,20 +214,23 @@ var _a, _b, _c, _d, _e, _f; | ||
} | ||
const tz = tagName.includes("UTC") || tagName.startsWith("GPS") ? "UTC" : this.tz; | ||
if (typeof value === "string" && tagName.includes("DateTime")) { | ||
const d = (_a = ExifDateTime_1.ExifDateTime.fromExifStrict(value, tz)) !== null && _a !== void 0 ? _a : ExifDateTime_1.ExifDateTime.fromISO(value, tz); | ||
if (d != null) { | ||
return d; | ||
if (typeof value === "string") { | ||
const tz = tagIsInUTC(tagName) ? "UTC" : undefined; | ||
if (tagName.includes("DateTime") || | ||
tagName.toLowerCase().includes("timestamp")) { | ||
const d = (_a = ExifDateTime_1.ExifDateTime.fromExifStrict(value, tz)) !== null && _a !== void 0 ? _a : ExifDateTime_1.ExifDateTime.fromISO(value, tz); | ||
if (d != null) { | ||
return this.normalizeDateTime(tagName, d); | ||
} | ||
} | ||
} | ||
if (typeof value === "string" && tagName.includes("Date")) { | ||
const d = (_f = (_e = (_d = (_c = (_b = ExifDateTime_1.ExifDateTime.fromExifStrict(value, tz)) !== null && _b !== void 0 ? _b : ExifDateTime_1.ExifDateTime.fromISO(value, tz)) !== null && _c !== void 0 ? _c : ExifDateTime_1.ExifDateTime.fromExifLoose(value, tz)) !== null && _d !== void 0 ? _d : ExifDate_1.ExifDate.fromExifStrict(value)) !== null && _e !== void 0 ? _e : ExifDate_1.ExifDate.fromISO(value)) !== null && _f !== void 0 ? _f : ExifDate_1.ExifDate.fromExifLoose(value); | ||
if (d != null) { | ||
return d; | ||
if (tagName.includes("Date")) { | ||
const d = (_f = (_e = (_d = (_c = (_b = ExifDateTime_1.ExifDateTime.fromExifStrict(value, tz)) !== null && _b !== void 0 ? _b : ExifDateTime_1.ExifDateTime.fromISO(value, tz)) !== null && _c !== void 0 ? _c : ExifDateTime_1.ExifDateTime.fromExifLoose(value, tz)) !== null && _d !== void 0 ? _d : ExifDate_1.ExifDate.fromExifStrict(value)) !== null && _e !== void 0 ? _e : ExifDate_1.ExifDate.fromISO(value)) !== null && _f !== void 0 ? _f : ExifDate_1.ExifDate.fromExifLoose(value); | ||
if (d != null) { | ||
return this.normalizeDateTime(tagName, d); | ||
} | ||
} | ||
if (tagName.includes("Time")) { | ||
const t = ExifTime_1.ExifTime.fromEXIF(value); | ||
if (t != null) | ||
return t; | ||
} | ||
} | ||
if (typeof value === "string" && tagName.includes("Time")) { | ||
const t = ExifTime_1.ExifTime.fromEXIF(value); | ||
if (t != null) | ||
return t; | ||
} | ||
// Trust that ExifTool rendered the value with the correct type in JSON: | ||
@@ -224,2 +246,5 @@ return value; | ||
exports.ReadTask = ReadTask; | ||
function tagIsInUTC(tagName) { | ||
return tagName.includes("UTC") || tagName.startsWith("GPS"); | ||
} | ||
//# sourceMappingURL=ReadTask.js.map |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[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) { | ||
@@ -6,0 +10,0 @@ if (k2 === undefined) k2 = k; |
@@ -34,3 +34,3 @@ import { FixedOffsetZone } from "luxon"; | ||
}): Maybe<TzSrc>; | ||
export declare function inferLikelyOffsetMinutes(deltaMs: number): number; | ||
export declare function inferLikelyOffsetMinutes(deltaMinutes: number): number; | ||
export declare function extractTzOffsetFromUTCOffset(t: { | ||
@@ -41,2 +41,3 @@ DateTimeUTC?: string; | ||
GPSTimeStamp?: string; | ||
GPSDateTimeStamp?: string; | ||
SubSecDateTimeOriginal?: string; | ||
@@ -43,0 +44,0 @@ DateTimeOriginal?: string; |
@@ -6,3 +6,2 @@ "use strict"; | ||
const Array_1 = require("./Array"); | ||
const DateTime_1 = require("./DateTime"); | ||
const ExifDateTime_1 = require("./ExifDateTime"); | ||
@@ -40,12 +39,6 @@ const Maybe_1 = require("./Maybe"); | ||
const minutes = Math.abs(absMinutes % 60); | ||
return (`UTC${sign}` + | ||
(minutes === 0 ? `${(0, String_1.pad2)(hours)}` : `${(0, String_1.pad2)(hours)}:${(0, String_1.pad2)(minutes)}`)); | ||
// luxon now renders simple hour offsets without padding: | ||
return `UTC${sign}` + hours + (minutes === 0 ? "" : `:${(0, String_1.pad2)(minutes)}`); | ||
} | ||
exports.offsetMinutesToZoneName = offsetMinutesToZoneName; | ||
function dtToMs(s, defaultZone) { | ||
return (0, Maybe_1.map)(ExifDateTime_1.ExifDateTime.fromExifStrict(s, defaultZone), (dt) => dt.toDate().getTime()); | ||
} | ||
function utcToMs(s) { | ||
return dtToMs(s, "UTC"); | ||
} | ||
function tzHourToOffset(n) { | ||
@@ -94,5 +87,2 @@ return (0, Number_1.isNumber)(n) && reasonableTzOffsetMinutes(n * 60) | ||
exports.extractTzOffsetFromTags = extractTzOffsetFromTags; | ||
function firstUtcMs(tags, tagNames) { | ||
return (0, Maybe_1.first)(tagNames, (tagName) => (0, Maybe_1.map)(utcToMs(tags[tagName]), (utcMs) => ({ tagName, utcMs }))); | ||
} | ||
// timezone offsets may be on a 15 minute boundary, but if GPS acquisition is | ||
@@ -103,15 +93,27 @@ // old, this can be spurious. We get less mistakes with a larger multiple, so | ||
const TzBoundaryMinutes = 30; | ||
function inferLikelyOffsetMinutes(deltaMs) { | ||
return TzBoundaryMinutes * Math.floor(deltaMs / DateTime_1.MinuteMs / TzBoundaryMinutes); | ||
function inferLikelyOffsetMinutes(deltaMinutes) { | ||
return TzBoundaryMinutes * Math.floor(deltaMinutes / TzBoundaryMinutes); | ||
} | ||
exports.inferLikelyOffsetMinutes = inferLikelyOffsetMinutes; | ||
function extractTzOffsetFromUTCOffset(t) { | ||
var _a; | ||
const gpsStamps = (0, Array_1.compact)([t.GPSDateStamp, t.GPSTimeStamp]); | ||
const GPSDateTimeStamp = gpsStamps.length === 2 ? gpsStamps.join(" ") : undefined; | ||
const utc = firstUtcMs({ ...t, GPSDateTimeStamp }, [ | ||
"GPSDateTime", | ||
"DateTimeUTC", | ||
"GPSDateTimeStamp", | ||
]); | ||
const dt = firstUtcMs(t, [ | ||
if (gpsStamps.length === 2) { | ||
(_a = t.GPSDateTimeStamp) !== null && _a !== void 0 ? _a : (t.GPSDateTimeStamp = gpsStamps.join(" ")); | ||
} | ||
// We can always assume these are in UTC: | ||
const utc = (0, Maybe_1.first)(["GPSDateTime", "DateTimeUTC", "GPSDateTimeStamp"], (tagName) => { | ||
const edt = ExifDateTime_1.ExifDateTime.fromExifStrict(t[tagName]); | ||
return edt != null && (edt.zone == null || edt.zone === "UTC") | ||
? { | ||
tagName, | ||
s: edt.setZone("UTC", { keepLocalTime: true }).toEpochSeconds(), | ||
} | ||
: undefined; | ||
}); | ||
if (utc == null) | ||
return; | ||
// If we can find any of these without a zone, the timezone should be the | ||
// offset between this time and the GPS time. | ||
const dt = (0, Maybe_1.first)([ | ||
"SubSecDateTimeOriginal", | ||
@@ -124,7 +126,16 @@ "DateTimeOriginal", | ||
"DateTimeCreated", | ||
]); | ||
if (utc == null || dt == null) | ||
], (tagName) => { | ||
const edt = ExifDateTime_1.ExifDateTime.fromExifStrict(t[tagName]); | ||
return edt != null && edt.zone == null | ||
? { | ||
tagName, | ||
s: edt.setZone("UTC", { keepLocalTime: true }).toEpochSeconds(), | ||
} | ||
: undefined; | ||
}); | ||
if (dt == null) | ||
return; | ||
// By flooring | ||
const offsetMinutes = inferLikelyOffsetMinutes(dt.utcMs - utc.utcMs); | ||
const diffSeconds = dt.s - utc.s; | ||
const offsetMinutes = inferLikelyOffsetMinutes(diffSeconds / 60); | ||
return (0, Maybe_1.map)(offsetMinutesToZoneName(offsetMinutes), (tz) => ({ | ||
@@ -131,0 +142,0 @@ tz, |
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[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) { | ||
@@ -6,0 +10,0 @@ if (k2 === undefined) k2 = k; |
{ | ||
"name": "exiftool-vendored", | ||
"version": "15.12.1", | ||
"version": "16.0.0", | ||
"description": "Efficient, cross-platform access to ExifTool", | ||
@@ -95,8 +95,8 @@ "main": "./dist/ExifTool.js", | ||
"@types/xmldom": "^0.1.31", | ||
"@typescript-eslint/eslint-plugin": "^5.12.1", | ||
"@typescript-eslint/parser": "^5.12.1", | ||
"@typescript-eslint/eslint-plugin": "^5.14.0", | ||
"@typescript-eslint/parser": "^5.14.0", | ||
"chai": "^4.3.6", | ||
"chai-as-promised": "^7.1.1", | ||
"chai-subset": "^1.6.0", | ||
"eslint": "^8.9.0", | ||
"eslint": "^8.10.0", | ||
"eslint-plugin-import": "^2.25.4", | ||
@@ -116,4 +116,4 @@ "eslint-plugin-node": "^11.1.0", | ||
"tmp": "^0.2.1", | ||
"typedoc": "^0.22.12", | ||
"typescript": "^4.5.5", | ||
"typedoc": "^0.22.13", | ||
"typescript": "~4.6.2", | ||
"@xmldom/xmldom": "^0.8.1", | ||
@@ -123,4 +123,4 @@ "xpath": "^0.0.32" | ||
"dependencies": { | ||
"@types/luxon": "^2.0.9", | ||
"batch-cluster": "^10.3.0", | ||
"@types/luxon": "^2.3.0", | ||
"batch-cluster": "^10.3.2", | ||
"he": "^1.2.0", | ||
@@ -127,0 +127,0 @@ "luxon": "^2.3.1", |
114
README.md
@@ -24,15 +24,12 @@ # exiftool-vendored | ||
- [reading tags](https://photostructure.github.io/exiftool-vendored.js/classes/exiftool.html#read) | ||
- extracting embedded binaries, like [thumbnail](https://photostructure.github.io/exiftool-vendored.js/classes/exiftool.html#extractthumbnail) and [preview](https://photostructure.github.io/exiftool-vendored.js/classes/exiftool.html#extractpreview) images | ||
- [writing tags](https://photostructure.github.io/exiftool-vendored.js/classes/exiftool.html#write) | ||
- [rescuing metadata](https://photostructure.github.io/exiftool-vendored.js/classes/exiftool.html#rewritealltags) | ||
- [reading tags](https://photostructure.github.io/exiftool-vendored.js/classes/ExifTool.html#read) | ||
- extracting embedded binaries, like [thumbnail](https://photostructure.github.io/exiftool-vendored.js/classes/ExifTool.html#extractThumbnail) and [preview](https://photostructure.github.io/exiftool-vendored.js/classes/ExifTool.html#extractPreview) images | ||
- [writing tags](https://photostructure.github.io/exiftool-vendored.js/classes/ExifTool.html#write) | ||
- [rescuing metadata](https://photostructure.github.io/exiftool-vendored.js/classes/ExifTool.html#rewriteAllTags) | ||
1. **[Robust type definitions](#tags)** of the top 99.5% tags used by over 6,000 | ||
different camera makes and models (see an [example](interfaces/exiftags.html)) | ||
different camera makes and models (see an [example](https://photostructure.github.io/exiftool-vendored.js/interfaces/EXIFTags.html)) | ||
1. **Auditable ExifTool source code** (the vendored code is | ||
[checksum verified](http://owl.phy.queensu.ca/~phil/exiftool/checksums.txt)) | ||
1. **Automated updates** to ExifTool ([as new versions come out | ||
monthly](http://www.sno.phy.queensu.ca/~phil/exiftool/history.html)) | ||
monthly](https://exiftool.org/history.html)) | ||
@@ -63,3 +60,3 @@ 1. **Robust test coverage**, performed with on [macOS, Linux, and | ||
See the | ||
[CHANGELOG](https://github.com/mceachen/exiftool-vendored.js/blob/main/CHANGELOG.md) | ||
[CHANGELOG](https://github.com/photostructure/exiftool-vendored.js/blob/main/CHANGELOG.md) | ||
for breaking changes since you last updated. | ||
@@ -101,3 +98,3 @@ | ||
If the default [ExifTool constructor | ||
parameters](https://photostructure.github.io/exiftool-vendored.js/interfaces/exiftooloptions.html) | ||
parameters](https://photostructure.github.io/exiftool-vendored.js/interfaces/ExifToolOptions.html) | ||
wont' work for you, it's just a class that takes an options hash: | ||
@@ -116,3 +113,3 @@ | ||
`ExifTool.read()` returns a Promise to a [Tags](https://photostructure.github.io/exiftool-vendored.js/interfaces/tags.html) instance. Note | ||
`ExifTool.read()` returns a Promise to a [Tags](https://photostructure.github.io/exiftool-vendored.js/interfaces/Tags.html) instance. Note | ||
that errors may be returned either by rejecting the promise, or for less | ||
@@ -130,5 +127,5 @@ severe problems, via the `errors` field. | ||
Instead, we build a corpus of "commonly seen" tags from over 5,000 different | ||
Instead, we build a corpus of "commonly seen" tags from over 10,000 different | ||
digital camera makes and models, many from the [ExifTool metadata | ||
repository](https://exiftool.org/sample_images.html). | ||
repository](https://exiftool.org/sample_images.html) and <raw.pixls.us>. | ||
@@ -164,3 +161,3 @@ Here are some example fields: | ||
unknown fields, in other words. It's up to you and your code to look for other | ||
fields you expect, and cast to a more relevant interface. | ||
fields you expect and cast to a more relevant interface. | ||
@@ -179,3 +176,3 @@ ### Errors and Warnings | ||
[`rejectTaskOnStderr`](interfaces/exiftooloptions.html#rejecttaskonstderr). | ||
Either of these parameters are provided to the `ExifTool` constructor. | ||
Either of these parameters is provided to the `ExifTool` constructor. | ||
@@ -185,10 +182,5 @@ ### Logging and events | ||
To enable trace, debug, info, warning, or error logging from this library and | ||
the underlying `batch-cluster` library, | ||
use[`setLogger`](globals.html#setlogger). Example | ||
code can be found | ||
[here](https://github.com/photostructure/batch-cluster.js/blob/main/src/_chai.spec.ts#L20). | ||
the underlying `batch-cluster` library, provide a [Logger](https://photostructure.github.io/batch-cluster.js/interfaces/Logger.html) instance to the `ExifTool` constructor options. | ||
ExifTool instances emits events for "startError", "taskError", "endError", | ||
"beforeEnd", and "end" that you can register listeners for, using | ||
[on](https://batch-cluster.js.org/classes/batchcluster.html#on). | ||
ExifTool instances emits [many lifecycle and error events](https://photostructure.github.io/batch-cluster.js/interfaces/BatchClusterEvents.html#beforeEnd) via `batch-cluster`. | ||
@@ -240,4 +232,4 @@ ### Reading tags | ||
Note that only a portion of tags are writable. Refer to [the | ||
documentation](https://sno.phy.queensu.ca/~phil/exiftool/TagNames/index.html) | ||
Note that only a portion of tags is writable. Refer to [the | ||
documentation](https://exiftool.org/TagNames/index.html) | ||
and look under the "Writable" column. | ||
@@ -248,3 +240,3 @@ | ||
Only string and numeric primitive are supported as values to the object. | ||
Only string and numeric primitives are supported as values to the object. | ||
@@ -259,3 +251,3 @@ To write a comment to the given file so it shows up in the Windows Explorer | ||
To change the DateTimeOriginal, CreateDate and ModifyDate tags (using the | ||
[AllDates](https://sno.phy.queensu.ca/~phil/exiftool/TagNames/Shortcuts.html) | ||
[AllDates](https://exiftool.org/TagNames/Shortcuts.html) | ||
shortcut) to 4:56pm UTC on February 6, 2016: | ||
@@ -282,3 +274,3 @@ | ||
The above example removes any value associated to the `UserComment` tag. | ||
The above example removes any value associated with the `UserComment` tag. | ||
@@ -297,7 +289,7 @@ ### Always Beware: Timezones | ||
You may find that some of your images have corrupt metadata, and that writing | ||
You may find that some of your images have corrupt metadata and that writing | ||
new dates, or editing the rotation information, for example, fails. ExifTool can | ||
try to repair these images by rewriting all the metadata into a new file, along | ||
with the original image content. See the | ||
[documentation](http://owl.phy.queensu.ca/~phil/exiftool/faq.html#Q20) for more | ||
[documentation](https://exiftool.org/faq.html#Q20) for more | ||
details about this functionality. | ||
@@ -317,6 +309,6 @@ | ||
1. Place your [user configuration | ||
file](http://owl.phy.queensu.ca/~phil/exiftool/config.html) in your `HOME` | ||
file](https://exiftool.org/config.html) in your `HOME` | ||
directory | ||
1. Set the `EXIFTOOL_HOME` environment variable to the fully-qualified path that | ||
contains your user config. | ||
contains your user configuration. | ||
1. Specify the in the ExifTool constructor options: | ||
@@ -333,3 +325,3 @@ | ||
If you run this in a docker image based off Alpine or Debian Slim, **this won't work properly unless you install the `procps` package.** | ||
If you run this in a docker image based on Alpine or Debian Slim, **this won't work properly unless you install the `procps` package.** | ||
@@ -347,3 +339,3 @@ [See `batch-cluster` for details.](https://github.com/photostructure/batch-cluster.js/issues/13) | ||
You must explicitly call `.end()` on any used instance of `ExifTool` for `node` | ||
to exit gracefully. | ||
to exit gracefully. | ||
@@ -375,5 +367,5 @@ This call cannot be in a `process.on("exit")` hook, as the `stdio` streams | ||
the world, **this assumption will not be correct**. Parsing the same file in | ||
different parts of the world results in a different times for the same file. | ||
different parts of the world result in different times for the same file. | ||
Prior to version 7, heuristic 1 and 3 was applied. | ||
Prior to version 7, heuristic 1 and 3 were applied. | ||
@@ -386,3 +378,3 @@ As of version 7.0.0, `exiftool-vendored` uses the following heuristics. The | ||
If the [EXIF](https://sno.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html) | ||
If the [EXIF](https://exiftool.org/TagNames/EXIF.html) | ||
`TimeZoneOffset` tag is present it will be applied as per the spec to | ||
@@ -407,3 +399,3 @@ `DateTimeOriginal`, and if there are two values, the `ModifyDate` tag as well. | ||
Because datetimes have this optionally-set timezone, and some tags only specify | ||
Because date-times have this optionally-set timezone, and some tags only specify | ||
the date, this library returns classes that encode the date, the time of day, or | ||
@@ -415,3 +407,3 @@ both, **with an optional timezone and an optional tzoffset**: `ExifDateTime` and | ||
Note also that some smartphones record timestamps with microsecond precision | ||
(not just millis!), and both `ExifDateTime` and `ExifTime` have floating point | ||
(not just milliseconds!), and both `ExifDateTime` and `ExifTime` have floating point | ||
milliseconds. | ||
@@ -433,8 +425,8 @@ | ||
Tags marked with "★★★★", like | ||
[MIMEType](https://photostructure.github.io/exiftool-vendored.js/interfaces/tags.html#mimetype), | ||
[MIMEType](https://photostructure.github.io/exiftool-vendored.js/interfaces/FileTags.html#MIMEType), | ||
should be found in most files. Of the several thousand metadata tags, realize | ||
less than 50 are found generally. You'll need to do your own research to | ||
less than 50 are found generally. You'll need to do your research to | ||
determine which tags are valid for your uses. | ||
Note that if parsing fails (for, example, a datetime string), the raw string | ||
Note that if parsing fails (for, example, a date-time string), the raw string | ||
will be returned. Consuming code should verify both existence and type as | ||
@@ -463,25 +455,29 @@ reasonable for safety. | ||
The `npm run mktags` target reads all tags found in a batch of sample images and | ||
parses the results. | ||
The default [exiftool]() singleton is intentionally throttled. If full system | ||
utilization is acceptable: | ||
Using `exiftool-vendored`: | ||
1. set | ||
[`maxProcs`](https://photostructure.github.io/batch-cluster.js/classes/BatchClusterOptions.html#maxProcs) | ||
higher | ||
```sh | ||
Read 2236 unique tags from 3011 files. | ||
Parsing took 16s (5.4ms / file) # windows 10, core i7, maxProcs 4 | ||
Parsing took 27s (9.0ms / file) # ubuntu 18.04, core i3, maxProcs 1 | ||
Parsing took 13s (4.2ms / file) # ubuntu 18.04, core i3, maxProcs 4 | ||
2. consider setting | ||
[`minDelayBetweenSpawnMillis`](https://photostructure.github.io/batch-cluster.js/classes/BatchClusterOptions.html#minDelayBetweenSpawnMillis) | ||
to 0 | ||
# September 2020 update with > 2x more files and faster CPU: | ||
Read 3100 unique tags from 8028 files. | ||
Parsing took 16s (2.0ms / file) # ubuntu 20.04, AMD Ryzen 9 3900X, maxProcs 24 | ||
``` | ||
3. On a performant linux box, a smaller value of `streamFlushMillis` may work as | ||
well: if you see [`noTaskData` | ||
events](https://photostructure.github.io/batch-cluster.js/interfaces/BatchClusterEvents.html#noTaskData), | ||
you need to bump the value up. | ||
Using the `exiftool` npm package takes 7-10x longer, and doesn't work on Windows. | ||
## Benchmarking | ||
```sh | ||
Reading 3011 files... | ||
Parsing took 86s (28.4ms / file) # ubuntu, core i3 | ||
``` | ||
The `yarn mktags ../path/to/examples` target reads all tags found in a directory | ||
hierarchy of sample images and videos, and parses the results. | ||
`exiftool-vendored` v16.0.0 on a 2019 AMD Ryzen 3900X running Ubuntu 20.04 on an | ||
SSD can process 20+ files per second, per thread, or 500+ files per second | ||
utilizing all CPU threads. | ||
It can read, parse, | ||
### Batch mode | ||
@@ -488,0 +484,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
429203
7860
492
Updated@types/luxon@^2.3.0
Updatedbatch-cluster@^10.3.2