exiftool-vendored 33.2.0 → 33.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/ExifTool.d.ts +2 -1
- package/dist/ExifTool.js +13 -1
- package/dist/ExifTool.js.map +1 -1
- package/dist/Settings.d.ts +16 -0
- package/dist/Settings.js +16 -0
- package/dist/Settings.js.map +1 -1
- package/dist/Timezones.d.ts +470 -11
- package/dist/Timezones.js +485 -21
- package/dist/Timezones.js.map +1 -1
- package/package.json +2 -2
package/dist/Timezones.js
CHANGED
|
@@ -161,6 +161,29 @@ const Zulus = [
|
|
|
161
161
|
"UTC+00:00",
|
|
162
162
|
"GMT+00:00",
|
|
163
163
|
];
|
|
164
|
+
/**
|
|
165
|
+
* Check if a timezone value represents UTC.
|
|
166
|
+
*
|
|
167
|
+
* Handles multiple UTC representations including Zone instances, strings, and
|
|
168
|
+
* numeric offsets. Recognizes common UTC aliases like "GMT", "Z", "Zulu",
|
|
169
|
+
* "+0", "+00:00", etc.
|
|
170
|
+
*
|
|
171
|
+
* @param zone - Timezone to check (Zone, string, or number)
|
|
172
|
+
* @returns true if the zone represents UTC/GMT/Zulu
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* isUTC("UTC") // true
|
|
177
|
+
* isUTC("GMT") // true
|
|
178
|
+
* isUTC("Z") // true
|
|
179
|
+
* isUTC("Zulu") // true
|
|
180
|
+
* isUTC(0) // true
|
|
181
|
+
* isUTC("+00:00") // true
|
|
182
|
+
* isUTC("UTC+0") // true
|
|
183
|
+
* isUTC("America/New_York") // false
|
|
184
|
+
* isUTC("+08:00") // false
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
164
187
|
function isUTC(zone) {
|
|
165
188
|
if (zone == null) {
|
|
166
189
|
return false;
|
|
@@ -173,15 +196,88 @@ function isUTC(zone) {
|
|
|
173
196
|
}
|
|
174
197
|
return false;
|
|
175
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Check if a Zone is the library's sentinel "unset" value.
|
|
201
|
+
*
|
|
202
|
+
* The library uses a special Zone instance to represent unknown/unset
|
|
203
|
+
* timezones since Luxon doesn't officially support unset zones.
|
|
204
|
+
*
|
|
205
|
+
* @param zone - Zone instance to check
|
|
206
|
+
* @returns true if the zone is the UnsetZone sentinel value
|
|
207
|
+
*
|
|
208
|
+
* @see {@link UnsetZone}
|
|
209
|
+
* @see {@link UnsetZoneName}
|
|
210
|
+
* @see {@link UnsetZoneOffsetMinutes}
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```typescript
|
|
214
|
+
* isZoneUnset(UnsetZone) // true
|
|
215
|
+
* isZoneUnset(Info.normalizeZone("UTC")) // false
|
|
216
|
+
* isZoneUnset(Info.normalizeZone("UTC+8")) // false
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
176
219
|
function isZoneUnset(zone) {
|
|
177
220
|
return zone.isUniversal && zone.offset(0) === exports.UnsetZoneOffsetMinutes;
|
|
178
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Type guard to check if a Zone is valid and usable.
|
|
224
|
+
*
|
|
225
|
+
* A zone is considered valid if it:
|
|
226
|
+
* - Is not null/undefined
|
|
227
|
+
* - Has `isValid === true` (Luxon requirement)
|
|
228
|
+
* - Is not the library's UnsetZone sentinel
|
|
229
|
+
* - Has an offset within ±14 hours (the valid range for real-world timezones)
|
|
230
|
+
*
|
|
231
|
+
* This is the canonical validation check used throughout the library.
|
|
232
|
+
*
|
|
233
|
+
* @param zone - Zone to validate
|
|
234
|
+
* @returns true if the zone is valid and usable (type guard)
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const zone = Info.normalizeZone("America/Los_Angeles")
|
|
239
|
+
* if (isZoneValid(zone)) {
|
|
240
|
+
* // TypeScript knows zone is Zone (not Zone | undefined)
|
|
241
|
+
* console.log(zone.name)
|
|
242
|
+
* }
|
|
243
|
+
*
|
|
244
|
+
* isZoneValid(Info.normalizeZone("invalid")) // false
|
|
245
|
+
* isZoneValid(Info.normalizeZone("UTC+8")) // true
|
|
246
|
+
* isZoneValid(UnsetZone) // false
|
|
247
|
+
* isZoneValid(Info.normalizeZone("UTC+20")) // false (beyond ±14 hours)
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
179
250
|
function isZoneValid(zone) {
|
|
180
251
|
return (zone != null &&
|
|
181
252
|
zone.isValid &&
|
|
182
253
|
!isZoneUnset(zone) &&
|
|
183
254
|
Math.abs(zone.offset(Date.now())) < 14 * 60);
|
|
184
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Type guard to check if a value is a Luxon Zone instance.
|
|
258
|
+
*
|
|
259
|
+
* Checks both `instanceof Zone` and constructor name to handle cross-module
|
|
260
|
+
* Zone instances that may not pass instanceof checks.
|
|
261
|
+
*
|
|
262
|
+
* @param zone - Value to check
|
|
263
|
+
* @returns true if the value is a Zone instance (type guard)
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* import { Info } from "luxon"
|
|
268
|
+
*
|
|
269
|
+
* const zone = Info.normalizeZone("UTC+8")
|
|
270
|
+
* if (isZone(zone)) {
|
|
271
|
+
* // TypeScript knows zone is Zone (not unknown)
|
|
272
|
+
* console.log(zone.offset(Date.now()))
|
|
273
|
+
* }
|
|
274
|
+
*
|
|
275
|
+
* isZone(Info.normalizeZone("UTC")) // true
|
|
276
|
+
* isZone("UTC") // false
|
|
277
|
+
* isZone(480) // false
|
|
278
|
+
* isZone(null) // false
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
185
281
|
function isZone(zone) {
|
|
186
282
|
return ((0, Object_1.isObject)(zone) && (zone instanceof luxon_1.Zone || zone.constructor.name === "Zone"));
|
|
187
283
|
}
|
|
@@ -197,7 +293,9 @@ exports.defaultVideosToUTC = "defaultVideosToUTC";
|
|
|
197
293
|
const IanaFormatRE = /^\w{2,15}(?:\/\w{3,15}){0,2}$/;
|
|
198
294
|
// Luxon requires fixed-offset zones to look like "UTC+H", "UTC-H",
|
|
199
295
|
// "UTC+H:mm", "UTC-H:mm":
|
|
200
|
-
|
|
296
|
+
// Also handles Unicode minus (−, U+2212)
|
|
297
|
+
const FixedFormatRE = /^UTC(?<sign>[+−-])(?<hours>\d+)(?::(?<minutes>\d{2}))?$/;
|
|
298
|
+
const MinusRE = /[−-]/;
|
|
201
299
|
function parseFixedOffset(str) {
|
|
202
300
|
const match = FixedFormatRE.exec(str)?.groups;
|
|
203
301
|
if (match == null)
|
|
@@ -206,12 +304,56 @@ function parseFixedOffset(str) {
|
|
|
206
304
|
const m = (0, Number_1.toInt)(match.minutes) ?? 0;
|
|
207
305
|
if (h == null || h < 0 || h > 14 || m < 0 || m >= 60)
|
|
208
306
|
return;
|
|
209
|
-
|
|
307
|
+
// Handle both ASCII minus (-) and Unicode minus (−, U+2212)
|
|
308
|
+
const result = (MinusRE.test(match.sign ?? "") ? -1 : 1) * (h * 60 + m);
|
|
210
309
|
return (validOffsetMinutes().has(result) ? result : undefined) ?? undefined;
|
|
211
310
|
}
|
|
212
311
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
312
|
+
* Normalize a timezone input to a valid Luxon Zone.
|
|
313
|
+
*
|
|
314
|
+
* Accepts multiple input formats and returns a validated Zone instance, or
|
|
315
|
+
* undefined if the input cannot be normalized to a valid timezone.
|
|
316
|
+
*
|
|
317
|
+
* Supported input formats:
|
|
318
|
+
* - **Numbers**: Timezone offset in minutes (e.g., 480 = UTC+8, -300 = UTC-5)
|
|
319
|
+
* - **Strings**: ISO offsets ("+08:00", "-05:00"), IANA zones
|
|
320
|
+
* ("America/Los_Angeles"), UTC variants ("UTC", "GMT", "Z", "Zulu")
|
|
321
|
+
* - **Zone instances**: Validated and returned if valid
|
|
322
|
+
*
|
|
323
|
+
* The function respects Settings:
|
|
324
|
+
* - {@link Settings.allowArchaicTimezoneOffsets} for pre-1982 offsets
|
|
325
|
+
* - {@link Settings.allowBakerIslandTime} for UTC-12:00
|
|
326
|
+
*
|
|
327
|
+
* @param input - Timezone in various formats
|
|
328
|
+
* @returns Valid Zone instance, or undefined if invalid
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```typescript
|
|
332
|
+
* // Numbers (offset in minutes)
|
|
333
|
+
* normalizeZone(480)?.name // "UTC+8"
|
|
334
|
+
* normalizeZone(-300)?.name // "UTC-5"
|
|
335
|
+
* normalizeZone(0)?.name // "UTC"
|
|
336
|
+
*
|
|
337
|
+
* // ISO offset strings
|
|
338
|
+
* normalizeZone("+08:00")?.name // "UTC+8"
|
|
339
|
+
* normalizeZone("-05:30")?.name // "UTC-5:30"
|
|
340
|
+
* normalizeZone("UTC+7")?.name // "UTC+7"
|
|
341
|
+
*
|
|
342
|
+
* // IANA timezone names
|
|
343
|
+
* normalizeZone("America/Los_Angeles")?.name // "America/Los_Angeles"
|
|
344
|
+
* normalizeZone("Asia/Tokyo")?.name // "Asia/Tokyo"
|
|
345
|
+
*
|
|
346
|
+
* // UTC aliases
|
|
347
|
+
* normalizeZone("UTC")?.name // "UTC"
|
|
348
|
+
* normalizeZone("GMT")?.name // "UTC"
|
|
349
|
+
* normalizeZone("Z")?.name // "UTC"
|
|
350
|
+
* normalizeZone("Zulu")?.name // "UTC"
|
|
351
|
+
*
|
|
352
|
+
* // Invalid inputs return undefined
|
|
353
|
+
* normalizeZone("invalid") // undefined
|
|
354
|
+
* normalizeZone("+25:00") // undefined (beyond ±14 hours)
|
|
355
|
+
* normalizeZone(null) // undefined
|
|
356
|
+
* ```
|
|
215
357
|
*/
|
|
216
358
|
function normalizeZone(input) {
|
|
217
359
|
if (input == null ||
|
|
@@ -251,13 +393,73 @@ function normalizeZone(input) {
|
|
|
251
393
|
}
|
|
252
394
|
}
|
|
253
395
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
396
|
+
* Convert a timezone to its short offset format (e.g., "+08:00", "-05:00").
|
|
397
|
+
*
|
|
398
|
+
* Useful for displaying timezone offsets in a standardized format. For IANA
|
|
399
|
+
* zones with daylight saving time, provide a timestamp to get the correct
|
|
400
|
+
* offset for that moment.
|
|
401
|
+
*
|
|
402
|
+
* @param zone - Timezone as Zone, string, or offset in minutes
|
|
403
|
+
* @param ts - Optional timestamp (milliseconds) for IANA zone offset calculation.
|
|
404
|
+
* Defaults to current time if not provided.
|
|
405
|
+
* @returns Zone offset in "+HH:MM" format, or "" if zone is invalid
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```typescript
|
|
409
|
+
* // Fixed offsets
|
|
410
|
+
* zoneToShortOffset("UTC+8") // "+08:00"
|
|
411
|
+
* zoneToShortOffset(480) // "+08:00"
|
|
412
|
+
* zoneToShortOffset("UTC-5:30") // "-05:30"
|
|
413
|
+
*
|
|
414
|
+
* // IANA zones (offset depends on DST)
|
|
415
|
+
* const winter = new Date("2023-01-15").getTime()
|
|
416
|
+
* const summer = new Date("2023-07-15").getTime()
|
|
417
|
+
* zoneToShortOffset("America/Los_Angeles", winter) // "-08:00" (PST)
|
|
418
|
+
* zoneToShortOffset("America/Los_Angeles", summer) // "-07:00" (PDT)
|
|
419
|
+
*
|
|
420
|
+
* // Invalid zones return empty string
|
|
421
|
+
* zoneToShortOffset("invalid") // ""
|
|
422
|
+
* zoneToShortOffset(null) // ""
|
|
423
|
+
* ```
|
|
257
424
|
*/
|
|
258
425
|
function zoneToShortOffset(zone, ts) {
|
|
259
426
|
return normalizeZone(zone)?.formatOffset(ts ?? Date.now(), "short") ?? "";
|
|
260
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Type guard to check if a numeric offset (in minutes) represents a valid timezone.
|
|
430
|
+
*
|
|
431
|
+
* Validates that the offset:
|
|
432
|
+
* - Is a number (not null/undefined)
|
|
433
|
+
* - Is not the UnsetZone sentinel value (-1)
|
|
434
|
+
* - Matches a real-world timezone offset (respects Settings for archaic offsets)
|
|
435
|
+
*
|
|
436
|
+
* Use this for exact validation without rounding. For error-tolerant rounding to
|
|
437
|
+
* the nearest valid offset, use {@link inferLikelyOffsetMinutes} instead.
|
|
438
|
+
*
|
|
439
|
+
* @param tzOffsetMinutes - Offset in minutes to validate (e.g., 480 for UTC+8)
|
|
440
|
+
* @returns true if the offset is exactly valid (type guard)
|
|
441
|
+
*
|
|
442
|
+
* @see {@link inferLikelyOffsetMinutes} for error-tolerant rounding
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* ```typescript
|
|
446
|
+
* validTzOffsetMinutes(480) // true (UTC+8)
|
|
447
|
+
* validTzOffsetMinutes(-300) // true (UTC-5)
|
|
448
|
+
* validTzOffsetMinutes(330) // true (UTC+5:30, India)
|
|
449
|
+
* validTzOffsetMinutes(345) // true (UTC+5:45, Nepal)
|
|
450
|
+
*
|
|
451
|
+
* validTzOffsetMinutes(481) // false (not a valid timezone)
|
|
452
|
+
* validTzOffsetMinutes(-1) // false (UnsetZone sentinel)
|
|
453
|
+
* validTzOffsetMinutes(null) // false
|
|
454
|
+
*
|
|
455
|
+
* // Archaic offsets require Settings
|
|
456
|
+
* Settings.allowArchaicTimezoneOffsets.value = false
|
|
457
|
+
* validTzOffsetMinutes(-630) // false (Hawaii -10:30, archaic)
|
|
458
|
+
*
|
|
459
|
+
* Settings.allowArchaicTimezoneOffsets.value = true
|
|
460
|
+
* validTzOffsetMinutes(-630) // true (Hawaii -10:30, archaic)
|
|
461
|
+
* ```
|
|
462
|
+
*/
|
|
261
463
|
function validTzOffsetMinutes(tzOffsetMinutes) {
|
|
262
464
|
return (tzOffsetMinutes != null &&
|
|
263
465
|
(0, Number_1.isNumber)(tzOffsetMinutes) &&
|
|
@@ -287,8 +489,9 @@ function tzHourToOffset(n) {
|
|
|
287
489
|
}
|
|
288
490
|
// Accept "Z", "UTC+2", "UTC+02", "UTC+2:00", "UTC+02:00", "+2", "+02", and
|
|
289
491
|
// "+02:00". Also accepts seconds like "-00:25:21" for archaic offsets.
|
|
492
|
+
// Handles Unicode minus (−, U+2212)
|
|
290
493
|
// Require the sign (+ or -) and a ":" separator if there are minutes.
|
|
291
|
-
const tzRe = /(?<Z>Z)|((UTC)?(?<sign>[
|
|
494
|
+
const tzRe = /(?<Z>Z)|((UTC)?(?<sign>[+−-])(?<hours>\d\d?)(?::(?<minutes>\d\d)(?::(?<seconds>\d\d))?)?)$/;
|
|
292
495
|
function extractOffsetFromHours(hourOffset) {
|
|
293
496
|
return (0, Number_1.isNumber)(hourOffset)
|
|
294
497
|
? (0, Maybe_1.map)(tzHourToOffset(hourOffset), (zone) => ({
|
|
@@ -301,12 +504,66 @@ function extractOffsetFromHours(hourOffset) {
|
|
|
301
504
|
: undefined;
|
|
302
505
|
}
|
|
303
506
|
/**
|
|
304
|
-
*
|
|
507
|
+
* Extract timezone information from various value types.
|
|
508
|
+
*
|
|
509
|
+
* Handles multiple input formats and performs intelligent parsing:
|
|
510
|
+
* - **Strings**: ISO offsets ("+08:00"), IANA zones, UTC variants, timestamps
|
|
511
|
+
* with embedded timezones ("2023:01:15 10:30:00-08:00")
|
|
512
|
+
* - **Numbers**: Hour offsets (e.g., -8 for UTC-8)
|
|
513
|
+
* - **Arrays**: Uses first non-null value
|
|
514
|
+
* - **ExifDateTime/ExifTime instances**: Extracts their zone property
|
|
515
|
+
*
|
|
516
|
+
* By default, strips timezone abbreviations (PST, PDT, etc.) as they are
|
|
517
|
+
* ambiguous. Returns provenance information indicating which parsing method
|
|
518
|
+
* succeeded.
|
|
519
|
+
*
|
|
520
|
+
* Supports Unicode minus signs (−, U+2212) and plus-minus signs (±, U+00B1)
|
|
521
|
+
* in addition to ASCII +/-.
|
|
522
|
+
*
|
|
523
|
+
* @param value - Value to extract timezone from
|
|
524
|
+
* @param opts.stripTZA - Whether to strip timezone abbreviations (default: true).
|
|
525
|
+
* TZAs like "PST" are ambiguous and usually stripped.
|
|
526
|
+
* @returns TzSrc with zone name and provenance, or undefined if no timezone found
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```typescript
|
|
530
|
+
* // ISO offset strings
|
|
531
|
+
* extractZone("+08:00")
|
|
532
|
+
* // { zone: "UTC+8", tz: "UTC+8", src: "offsetMinutesToZoneName" }
|
|
305
533
|
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
534
|
+
* extractZone("UTC-5:30")
|
|
535
|
+
* // { zone: "UTC-5:30", tz: "UTC-5:30", src: "normalizeZone" }
|
|
308
536
|
*
|
|
309
|
-
*
|
|
537
|
+
* // IANA zone names
|
|
538
|
+
* extractZone("America/Los_Angeles")
|
|
539
|
+
* // { zone: "America/Los_Angeles", tz: "America/Los_Angeles", src: "normalizeZone" }
|
|
540
|
+
*
|
|
541
|
+
* // Timestamps with embedded timezones
|
|
542
|
+
* extractZone("2023:01:15 10:30:00-08:00")
|
|
543
|
+
* // { zone: "UTC-8", tz: "UTC-8", src: "offsetMinutesToZoneName",
|
|
544
|
+
* // leftovers: "2023:01:15 10:30:00" }
|
|
545
|
+
*
|
|
546
|
+
* // Unicode minus signs
|
|
547
|
+
* extractZone("−08:00") // Unicode minus (U+2212)
|
|
548
|
+
* // { zone: "UTC-8", tz: "UTC-8", src: "offsetMinutesToZoneName" }
|
|
549
|
+
*
|
|
550
|
+
* // Numeric hour offsets
|
|
551
|
+
* extractZone(-8)
|
|
552
|
+
* // { zone: "UTC-8", tz: "UTC-8", src: "hourOffset" }
|
|
553
|
+
*
|
|
554
|
+
* // Arrays (uses first non-null)
|
|
555
|
+
* extractZone([null, "+05:30", undefined])
|
|
556
|
+
* // { zone: "UTC+5:30", tz: "UTC+5:30", src: "offsetMinutesToZoneName" }
|
|
557
|
+
*
|
|
558
|
+
* // Strips timezone abbreviations by default
|
|
559
|
+
* extractZone("2023:01:15 10:30:00-08:00 PST")
|
|
560
|
+
* // { zone: "UTC-8", tz: "UTC-8", src: "offsetMinutesToZoneName",
|
|
561
|
+
* // leftovers: "2023:01:15 10:30:00" }
|
|
562
|
+
*
|
|
563
|
+
* // Invalid inputs return undefined
|
|
564
|
+
* extractZone("invalid") // undefined
|
|
565
|
+
* extractZone(null) // undefined
|
|
566
|
+
* ```
|
|
310
567
|
*/
|
|
311
568
|
function extractZone(value, opts) {
|
|
312
569
|
if (value == null ||
|
|
@@ -375,7 +632,8 @@ function extractZone(value, opts) {
|
|
|
375
632
|
const hours = parseInt(capturedGroups.hours ?? "0");
|
|
376
633
|
const minutes = parseInt(capturedGroups.minutes ?? "0");
|
|
377
634
|
const seconds = parseInt(capturedGroups.seconds ?? "0");
|
|
378
|
-
|
|
635
|
+
// Handle both ASCII minus (-) and Unicode minus (−, U+2212)
|
|
636
|
+
const sign = MinusRE.test(capturedGroups.sign ?? "") ? -1 : 1;
|
|
379
637
|
const offsetMinutes = sign * (hours * 60 + minutes + seconds / 60);
|
|
380
638
|
const zone = offsetMinutesToZoneName(offsetMinutes);
|
|
381
639
|
if (zone != null) {
|
|
@@ -417,6 +675,47 @@ function incrementZone(z, minutes) {
|
|
|
417
675
|
? luxon_1.FixedOffsetZone.instance(fixed + minutes)
|
|
418
676
|
: undefined;
|
|
419
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* Extract timezone offset from standard EXIF timezone tags.
|
|
680
|
+
*
|
|
681
|
+
* Checks timezone tags in priority order:
|
|
682
|
+
* 1. TimeZone
|
|
683
|
+
* 2. OffsetTimeOriginal (for DateTimeOriginal)
|
|
684
|
+
* 3. OffsetTimeDigitized (for CreateDate)
|
|
685
|
+
* 4. TimeZoneOffset
|
|
686
|
+
*
|
|
687
|
+
* Handles camera-specific quirks like Nikon's DaylightSavings tag, which
|
|
688
|
+
* requires adjusting the TimeZone offset forward by one hour during DST.
|
|
689
|
+
*
|
|
690
|
+
* @param t - EXIF tags object
|
|
691
|
+
* @param opts.adjustTimeZoneIfDaylightSavings - Optional function to adjust
|
|
692
|
+
* timezone for DST. Defaults to handling Nikon's DaylightSavings quirk.
|
|
693
|
+
* @returns TzSrc with zone and provenance, or undefined if no timezone found
|
|
694
|
+
*
|
|
695
|
+
* @see {@link TimezoneOffsetTagnames} for the list of tags checked
|
|
696
|
+
* @see https://github.com/photostructure/exiftool-vendored.js/issues/215
|
|
697
|
+
*
|
|
698
|
+
* @example
|
|
699
|
+
* ```typescript
|
|
700
|
+
* const tags = await exiftool.read("photo.jpg")
|
|
701
|
+
*
|
|
702
|
+
* const tzSrc = extractTzOffsetFromTags(tags)
|
|
703
|
+
* if (tzSrc) {
|
|
704
|
+
* console.log(`Timezone: ${tzSrc.zone}`)
|
|
705
|
+
* console.log(`Source: ${tzSrc.src}`) // e.g., "OffsetTimeOriginal"
|
|
706
|
+
* }
|
|
707
|
+
*
|
|
708
|
+
* // Nikon DST handling
|
|
709
|
+
* const nikonTags = {
|
|
710
|
+
* TimeZone: "-08:00",
|
|
711
|
+
* DaylightSavings: "Yes",
|
|
712
|
+
* Make: "NIKON CORPORATION"
|
|
713
|
+
* }
|
|
714
|
+
* extractTzOffsetFromTags(nikonTags)
|
|
715
|
+
* // { zone: "UTC-7", tz: "UTC-7",
|
|
716
|
+
* // src: "TimeZone (adjusted for DaylightSavings)" }
|
|
717
|
+
* ```
|
|
718
|
+
*/
|
|
420
719
|
function extractTzOffsetFromTags(t, opts) {
|
|
421
720
|
const adjustFn = opts?.adjustTimeZoneIfDaylightSavings ??
|
|
422
721
|
DefaultExifToolOptions_1.defaultAdjustTimeZoneIfDaylightSavings;
|
|
@@ -506,12 +805,67 @@ Settings_1.Settings.allowArchaicTimezoneOffsets.onChange(() => {
|
|
|
506
805
|
Settings_1.Settings.allowBakerIslandTime.onChange(() => {
|
|
507
806
|
likelyOffsetMinutes.clear();
|
|
508
807
|
});
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
808
|
+
/**
|
|
809
|
+
* Round an arbitrary offset to the nearest valid timezone offset.
|
|
810
|
+
*
|
|
811
|
+
* This is error-tolerant timezone inference, useful for:
|
|
812
|
+
* - GPS-based timezone calculation (where GPS time drift may cause errors)
|
|
813
|
+
* - Handling clock drift in timestamp comparisons
|
|
814
|
+
* - Fuzzy timezone matching
|
|
815
|
+
*
|
|
816
|
+
* By default, uses {@link Settings.maxValidOffsetMinutes} (30 minutes) as the
|
|
817
|
+
* maximum distance from a valid timezone. This threshold handles GPS acquisition
|
|
818
|
+
* lag and clock drift while preventing false matches.
|
|
819
|
+
*
|
|
820
|
+
* Respects Settings for archaic offsets, Baker Island time, and max offset tolerance.
|
|
821
|
+
*
|
|
822
|
+
* @param deltaMinutes - Offset in minutes to round (can be fractional)
|
|
823
|
+
* @param maxValidOffsetMinutes - Maximum distance (in minutes) from a valid
|
|
824
|
+
* timezone to accept. Defaults to {@link Settings.maxValidOffsetMinutes}.
|
|
825
|
+
* @returns Nearest valid offset in minutes, or undefined if too far from any
|
|
826
|
+
* valid timezone
|
|
827
|
+
*
|
|
828
|
+
* @see {@link validTzOffsetMinutes} for exact validation without rounding
|
|
829
|
+
* @see {@link Settings.maxValidOffsetMinutes} to configure the default threshold
|
|
830
|
+
*
|
|
831
|
+
* @example
|
|
832
|
+
* ```typescript
|
|
833
|
+
* // Exact matches
|
|
834
|
+
* inferLikelyOffsetMinutes(480) // 480 (UTC+8, exact)
|
|
835
|
+
* inferLikelyOffsetMinutes(-300) // -300 (UTC-5, exact)
|
|
836
|
+
*
|
|
837
|
+
* // Rounding within default threshold (30 minutes)
|
|
838
|
+
* inferLikelyOffsetMinutes(485) // 480 (UTC+8, rounded from 485)
|
|
839
|
+
* inferLikelyOffsetMinutes(-295) // -300 (UTC-5, rounded from -295)
|
|
840
|
+
* inferLikelyOffsetMinutes(330.5) // 330 (UTC+5:30, rounded)
|
|
841
|
+
*
|
|
842
|
+
* // GPS-based inference with clock drift (within 30 min default)
|
|
843
|
+
* const gpsTime = "2023:01:15 19:30:45" // UTC
|
|
844
|
+
* const localTime = "2023:01:15 11:32:12" // Local with 1.5min drift
|
|
845
|
+
* const deltaMinutes = 480 + 1.5 // ~481.5 minutes
|
|
846
|
+
* inferLikelyOffsetMinutes(deltaMinutes) // 480 (UTC+8)
|
|
847
|
+
*
|
|
848
|
+
* // GPS lag up to 23 minutes still works (within 30 min threshold)
|
|
849
|
+
* inferLikelyOffsetMinutes(443) // 420 (UTC-7, ~23 min from actual)
|
|
850
|
+
*
|
|
851
|
+
* // Beyond threshold returns undefined
|
|
852
|
+
* inferLikelyOffsetMinutes(100) // undefined (not near any valid offset)
|
|
853
|
+
*
|
|
854
|
+
* // Custom threshold
|
|
855
|
+
* inferLikelyOffsetMinutes(495, 30) // 480 (UTC+8 with 30min threshold)
|
|
856
|
+
* inferLikelyOffsetMinutes(495, 15) // undefined (beyond 15min threshold)
|
|
857
|
+
*
|
|
858
|
+
* // Adjust global default
|
|
859
|
+
* Settings.maxValidOffsetMinutes.value = 15 // Stricter matching
|
|
860
|
+
* inferLikelyOffsetMinutes(443) // undefined (beyond 15min threshold)
|
|
861
|
+
* ```
|
|
862
|
+
*/
|
|
863
|
+
function inferLikelyOffsetMinutes(deltaMinutes, maxValidOffsetMinutes = Settings_1.Settings.maxValidOffsetMinutes.value) {
|
|
864
|
+
return (0, Array_1.leastBy)(likelyOffsetMinutes(), (ea) => {
|
|
865
|
+
const diff = Math.abs(ea - deltaMinutes);
|
|
866
|
+
// Reject timezone offsets more than maxValidOffsetMinutes minutes away:
|
|
867
|
+
return diff > maxValidOffsetMinutes ? undefined : diff;
|
|
868
|
+
});
|
|
515
869
|
}
|
|
516
870
|
/**
|
|
517
871
|
* Convert blank strings to undefined.
|
|
@@ -519,6 +873,62 @@ function inferLikelyOffsetMinutes(deltaMinutes) {
|
|
|
519
873
|
function toNotBlank(x) {
|
|
520
874
|
return x == null || (typeof x === "string" && (0, String_1.blank)(x)) ? undefined : x;
|
|
521
875
|
}
|
|
876
|
+
/**
|
|
877
|
+
* Infer timezone offset by comparing local time with GPS/UTC time.
|
|
878
|
+
*
|
|
879
|
+
* Calculates the timezone by finding the difference between:
|
|
880
|
+
* - A "captured at" timestamp (DateTimeOriginal, CreateDate, etc.) assumed to
|
|
881
|
+
* be in local time
|
|
882
|
+
* - A UTC timestamp (GPSDateTime, DateTimeUTC, or combined GPSDateStamp +
|
|
883
|
+
* GPSTimeStamp)
|
|
884
|
+
*
|
|
885
|
+
* Uses {@link inferLikelyOffsetMinutes} to handle minor clock drift and round
|
|
886
|
+
* to the nearest valid timezone offset.
|
|
887
|
+
*
|
|
888
|
+
* This is a fallback when explicit timezone tags are not available.
|
|
889
|
+
*
|
|
890
|
+
* @param t - Tags object with timestamp fields
|
|
891
|
+
* @returns TzSrc with inferred timezone and provenance, or undefined if
|
|
892
|
+
* inference is not possible
|
|
893
|
+
*
|
|
894
|
+
* @see {@link extractTzOffsetFromTags} to check explicit timezone tags first
|
|
895
|
+
*
|
|
896
|
+
* @example
|
|
897
|
+
* ```typescript
|
|
898
|
+
* // GPS-based inference
|
|
899
|
+
* const tags = {
|
|
900
|
+
* DateTimeOriginal: "2023:01:15 11:30:00", // Local time (PST)
|
|
901
|
+
* GPSDateTime: "2023:01:15 19:30:00" // UTC
|
|
902
|
+
* }
|
|
903
|
+
* extractTzOffsetFromUTCOffset(tags)
|
|
904
|
+
* // { zone: "UTC-8", tz: "UTC-8",
|
|
905
|
+
* // src: "offset between DateTimeOriginal and GPSDateTime" }
|
|
906
|
+
*
|
|
907
|
+
* // DateTimeUTC-based inference
|
|
908
|
+
* const tags2 = {
|
|
909
|
+
* CreateDate: "2023:07:20 14:15:30", // Local time (JST)
|
|
910
|
+
* DateTimeUTC: "2023:07:20 05:15:30" // UTC
|
|
911
|
+
* }
|
|
912
|
+
* extractTzOffsetFromUTCOffset(tags2)
|
|
913
|
+
* // { zone: "UTC+9", tz: "UTC+9",
|
|
914
|
+
* // src: "offset between CreateDate and DateTimeUTC" }
|
|
915
|
+
*
|
|
916
|
+
* // Handles clock drift
|
|
917
|
+
* const tags3 = {
|
|
918
|
+
* DateTimeOriginal: "2023:01:15 11:30:45", // Local with drift
|
|
919
|
+
* GPSDateTime: "2023:01:15 19:29:58" // UTC (old GPS fix)
|
|
920
|
+
* }
|
|
921
|
+
* extractTzOffsetFromUTCOffset(tags3)
|
|
922
|
+
* // Still infers UTC-8 despite ~1 minute drift
|
|
923
|
+
*
|
|
924
|
+
* // No UTC timestamp available
|
|
925
|
+
* const tags4 = {
|
|
926
|
+
* DateTimeOriginal: "2023:01:15 11:30:00"
|
|
927
|
+
* // No GPS or UTC timestamp
|
|
928
|
+
* }
|
|
929
|
+
* extractTzOffsetFromUTCOffset(tags4) // undefined
|
|
930
|
+
* ```
|
|
931
|
+
*/
|
|
522
932
|
function extractTzOffsetFromUTCOffset(t) {
|
|
523
933
|
const utcSources = {
|
|
524
934
|
...(0, Pick_1.pick)(t, "GPSDateTime", "DateTimeUTC", "SonyDateTime2"),
|
|
@@ -571,12 +981,66 @@ function extractTzOffsetFromUTCOffset(t) {
|
|
|
571
981
|
src: `offset between ${dt.tagName} and ${utc.tagName}`,
|
|
572
982
|
}));
|
|
573
983
|
}
|
|
574
|
-
|
|
984
|
+
/**
|
|
985
|
+
* Check if two timezone values are equivalent at a specific point in time.
|
|
986
|
+
*
|
|
987
|
+
* Two zones are considered equivalent if they:
|
|
988
|
+
* - Are the same zone (via Luxon's Zone.equals()), OR
|
|
989
|
+
* - Have the same offset at the specified timestamp
|
|
990
|
+
*
|
|
991
|
+
* This is useful for:
|
|
992
|
+
* - De-duplicating timezone records
|
|
993
|
+
* - Comparing zones in different formats ("UTC+5" vs "UTC+05:00")
|
|
994
|
+
* - Matching IANA zones to their offset at a specific time
|
|
995
|
+
*
|
|
996
|
+
* For IANA zones with DST, you can specify a timestamp to evaluate equivalence
|
|
997
|
+
* at that moment. This is important when comparing historical records or future
|
|
998
|
+
* events where DST transitions matter.
|
|
999
|
+
*
|
|
1000
|
+
* @param a - First timezone (Zone, string, or offset in minutes)
|
|
1001
|
+
* @param b - Second timezone (Zone, string, or offset in minutes)
|
|
1002
|
+
* @param at - Timestamp in milliseconds to evaluate zone offsets.
|
|
1003
|
+
* Defaults to current time (Date.now()).
|
|
1004
|
+
* @returns true if zones are equivalent at the specified time
|
|
1005
|
+
*
|
|
1006
|
+
* @example
|
|
1007
|
+
* ```typescript
|
|
1008
|
+
* // Same zone, different formats
|
|
1009
|
+
* equivalentZones("UTC+5", "UTC+05:00") // true
|
|
1010
|
+
* equivalentZones("UTC-8", -480) // true (480 minutes = 8 hours)
|
|
1011
|
+
* equivalentZones("GMT", "UTC") // true
|
|
1012
|
+
* equivalentZones("Z", 0) // true
|
|
1013
|
+
*
|
|
1014
|
+
* // IANA zones matched by current offset (default behavior)
|
|
1015
|
+
* equivalentZones("America/New_York", "UTC-5") // true in winter (EST)
|
|
1016
|
+
* equivalentZones("America/New_York", "UTC-4") // true in summer (EDT)
|
|
1017
|
+
*
|
|
1018
|
+
* // IANA zones at specific times
|
|
1019
|
+
* const winter = new Date("2023-01-15").getTime()
|
|
1020
|
+
* const summer = new Date("2023-07-15").getTime()
|
|
1021
|
+
* equivalentZones("America/New_York", "UTC-5", winter) // true (EST)
|
|
1022
|
+
* equivalentZones("America/New_York", "UTC-4", winter) // false (not EDT in winter)
|
|
1023
|
+
* equivalentZones("America/New_York", "UTC-4", summer) // true (EDT)
|
|
1024
|
+
* equivalentZones("America/New_York", "UTC-5", summer) // false (not EST in summer)
|
|
1025
|
+
*
|
|
1026
|
+
* // Compare two IANA zones at a specific time
|
|
1027
|
+
* equivalentZones("America/New_York", "America/Toronto", winter) // true (both EST)
|
|
1028
|
+
* equivalentZones("America/New_York", "America/Los_Angeles", winter) // false (EST vs PST)
|
|
1029
|
+
*
|
|
1030
|
+
* // Different zones
|
|
1031
|
+
* equivalentZones("UTC+8", "UTC+9") // false
|
|
1032
|
+
*
|
|
1033
|
+
* // Invalid zones return false
|
|
1034
|
+
* equivalentZones("invalid", "UTC") // false
|
|
1035
|
+
* equivalentZones(null, "UTC") // false
|
|
1036
|
+
* ```
|
|
1037
|
+
*/
|
|
1038
|
+
function equivalentZones(a, b, at = Date.now()) {
|
|
575
1039
|
const az = normalizeZone(a);
|
|
576
1040
|
const bz = normalizeZone(b);
|
|
577
1041
|
return (az != null &&
|
|
578
1042
|
bz != null &&
|
|
579
|
-
(az.equals(bz) || az.offset(
|
|
1043
|
+
(az.equals(bz) || az.offset(at) === bz.offset(at)));
|
|
580
1044
|
}
|
|
581
1045
|
function getZoneName(args = {}) {
|
|
582
1046
|
const result = normalizeZone(args.zone)?.name ??
|