Skip to content

Commit

Permalink
add preferTimezoneInferenceFromGps
Browse files Browse the repository at this point in the history
  • Loading branch information
mceachen committed Oct 6, 2024
1 parent 43eb28f commit cbc92c5
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 145 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ vendored versions of ExifTool match the version that they vendor.

## Version history

### v28.5.0

- 🐞/📦 Add new `ExifToolOptions.preferTimezoneInferenceFromGps` to prefer GPS timezones. See the jsdoc for details.

### v28.4.1

- 📦 The warning "Invalid GPSLatitude or GPSLongitude. Deleting geolocation tags" will only be added if `GPSLatitude` or `GPSLongitude` is non-null.
Expand Down
2 changes: 1 addition & 1 deletion docs/serve.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"cleanUrls": false
}
}
2 changes: 2 additions & 0 deletions src/DefaultExifToolOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export const DefaultExifToolOptions: Omit<
writeArgs: [],

adjustTimeZoneIfDaylightSavings: defaultAdjustTimeZoneIfDaylightSavings,

preferTimezoneInferenceFromGps: false, // to retain prior behavior
})

/**
Expand Down
12 changes: 12 additions & 0 deletions src/ExifToolOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,18 @@ const exiftool = new ExifTool({ geoTz: (lat, lon) => geotz.find(lat, lon)[0] })
* @see https://github.com/photostructure/exiftool-vendored.js/issues/215
*/
adjustTimeZoneIfDaylightSavings: (tags: Tags, tz: string) => Maybe<number>

/**
* Timezone parsing requires a bunch of heuristics due to hardware and
* software companies not following metadata specifications similarly.
*
* If GPS metadata is trustworthy, set this to `true` to override explicit
* values assigned to {@link TimezoneOffsetTagnames}.
*
* Note that there **are** regions that have had their IANA timezone change
* over time--this will result incorrect timezones.
*/
preferTimezoneInferenceFromGps: boolean
}

export function handleDeprecatedOptions<
Expand Down
31 changes: 14 additions & 17 deletions src/GeolocationTags.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { keysOf } from "./Object"

export const GeolocationTagNames = keysOf<GeolocationTags>({
GeolocationBearing: true,
GeolocationCity: true,
GeolocationCountry: true,
GeolocationCountryCode: true,
GeolocationDistance: true,
GeolocationFeatureCode: true,
GeolocationFeatureType: true,
GeolocationPopulation: true,
GeolocationPosition: true,
GeolocationRegion: true,
GeolocationSubregion: true,
GeolocationTimeZone: true,
GeolocationWarning: true,
})
export const GeolocationTagNames = [
"GeolocationBearing",
"GeolocationCity",
"GeolocationCountry",
"GeolocationCountryCode",
"GeolocationDistance",
"GeolocationFeatureCode",
"GeolocationFeatureType",
"GeolocationPopulation",
"GeolocationPosition",
"GeolocationRegion",
"GeolocationSubregion",
"GeolocationTimeZone",
] satisfies (keyof GeolocationTags)[]

/**
* Is the given tag name intrinsic to the content of a given file? In other
Expand Down
205 changes: 120 additions & 85 deletions src/ReadTask.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { hms } from "./DateTime"
import { ExifDateTime } from "./ExifDateTime"
import { defaultVideosToUTC, ExifTime, ExifTool } from "./ExifTool"
import { GeolocationTagNames } from "./GeolocationTags"
import { omit } from "./Object"
import { pick } from "./Pick"
import { ReadTask, ReadTaskOptions } from "./ReadTask"
Expand Down Expand Up @@ -150,47 +151,55 @@ describe("ReadTask", () => {
})

for (const geolocation of [true, false]) {
describe(JSON.stringify({ geolocation }), () => {
it("extracts problematic GPSDateTime", async () => {
const t = await exiftool.read(
join(testDir, "nexus5x.jpg"),
undefined,
{ geolocation }
)
expect(t).to.containSubset({
MIMEType: "image/jpeg",
Make: "LGE",
Model: "Nexus 5X",
ImageWidth: 16,
ImageHeight: 16,
tz: "Europe/Zurich",
tzSource: geolocation
? "GeolocationTimeZone"
: "GPSLatitude/GPSLongitude",
})

const gpsdt = t.GPSDateTime as any as ExifDateTime
expect(gpsdt.toString()).to.eql("2016-07-19T10:00:24Z")
expect(gpsdt.rawValue).to.eql("2016:07:19 10:00:24Z")
expect(gpsdt.zoneName).to.eql("UTC")

if (geolocation) {
expect(t).to.containSubset({
GeolocationCity: "Adligenswil",
GeolocationRegion: "Lucerne",
GeolocationSubregion: "Lucerne-Land District",
GeolocationCountryCode: "CH",
GeolocationCountry: "Switzerland",
GeolocationTimeZone: "Europe/Zurich",
GeolocationFeatureCode: "PPL",
GeolocationPopulation: 5600,
GeolocationPosition: "47.0653, 8.3613",
GeolocationDistance: "1.71 km",
GeolocationBearing: 60,
for (const preferTimezoneInferenceFromGps of [true, false]) {
describe.only(
JSON.stringify({ geolocation, preferTimezoneInferenceFromGps }),
() => {
it("extracts problematic GPSDateTime", async () => {
const t = await exiftool.read(join(testDir, "nexus5x.jpg"), {
geolocation,
preferTimezoneInferenceFromGps,
})
expect(t).to.containSubset({
MIMEType: "image/jpeg",
Make: "LGE",
Model: "Nexus 5X",
ImageWidth: 16,
ImageHeight: 16,
tz: "Europe/Zurich",
tzSource: geolocation
? "GeolocationTimeZone"
: "GPSLatitude/GPSLongitude",
})

const gpsdt = t.GPSDateTime as any as ExifDateTime
expect(gpsdt.toString()).to.eql("2016-07-19T10:00:24Z")
expect(gpsdt.rawValue).to.eql("2016:07:19 10:00:24Z")
expect(gpsdt.zoneName).to.eql("UTC")

if (geolocation) {
const actualGeoKeys = Object.keys(t)
.filter((ea) => ea.startsWith("Geolocation"))
.sort()
expect(actualGeoKeys).to.eql(GeolocationTagNames)
expect(t).to.containSubset({
GeolocationCity: "Adligenswil",
GeolocationRegion: "Lucerne",
GeolocationSubregion: "Lucerne-Land District",
GeolocationCountryCode: "CH",
GeolocationCountry: "Switzerland",
GeolocationTimeZone: "Europe/Zurich",
GeolocationFeatureCode: "PPL",
GeolocationPopulation: 5600,
GeolocationPosition: "47.0653, 8.3613",
GeolocationDistance: "1.71 km",
GeolocationBearing: 60,
})
}
})
}
})
})
)
}
}

describe("without *Ref fields", () => {
Expand Down Expand Up @@ -1481,51 +1490,77 @@ describe("ReadTask", () => {
/**
* @see https://github.com/photostructure/exiftool-vendored.js/issues/215
*/
describe("issue 215", () => {
it("adjusts Nikon timezones by an hour if DaylightSavings is truthy", () => {
// From https://github.com/immich-app/immich/issues/13141#issuecomment-2390788790
// exiftool -j -struct -GPS*# ~/src/exiftool-vendored.js/test/nikon-daylight-savings.jpg -\*time\* -\*date\* -Daylight* -\*zone\* -Make -Model
const t = parse({
tags: {
GPSLatitude: -45.8745231666667,
GPSLongitude: 170.503112783333,
GPSLatitudeRef: "S",
GPSLongitudeRef: "E",
GPSPosition: "-45.8745231666667 170.503112783333",
ExposureTime: "1/20",
OffsetTime: "+13:00",
OffsetTimeOriginal: "+13:00",
OffsetTimeDigitized: "+13:00",
TimeZone: "+12:00",
ISOAutoShutterTime: "Auto",
SelfTimerTime: "10 s",
SelfTimerShotCount: 9,
SelfTimerShotInterval: "0.5 s",
PlaybackMonitorOffTime: "10 s",
MenuMonitorOffTime: "1 min",
ShootingInfoMonitorOffTime: "4 s",
ImageReviewMonitorOffTime: "4 s",
LiveViewMonitorOffTime: "10 min",
PowerUpTime: "0000:00:00 00:00:00",
SubSecTime: 45,
SubSecTimeDigitized: 45,
DateTimeOriginal: "2020:02:10 20:24:43",
ProfileDateTime: "2024:10:01 13:21:03",
SubSecDateTimeOriginal: "2020:02:10 20:24:43+13:00",
FileModifyDate: "2024:10:05 11:34:57-07:00",
FileAccessDate: "2024:10:05 11:34:57-07:00",
FileInodeChangeDate: "2024:10:05 11:34:57-07:00",
ModifyDate: "2024:10:03 08:15:25",
CreateDate: "2020:02:10 20:24:43",
DateDisplayFormat: "D/M/Y",
SubSecCreateDate: "2020:02:10 20:24:43.45+13:00",
SubSecModifyDate: "2024:10:03 08:15:25.45+13:00",
DaylightSavings: "Yes",
Make: "NIKON CORPORATION",
Model: "NIKON D7500",
},
})
expect(t.tz).to.eql("UTC+13")
})
describe("issue #215", () => {
for (const geolocation of [true, false]) {
for (const preferTimezoneInferenceFromGps of [true, false]) {
describe.only(
JSON.stringify({ geolocation, preferTimezoneInferenceFromGps }),
() => {
it("adjusts Nikon timezones by an hour if DaylightSavings is truthy", () => {
// From https://github.com/immich-app/immich/issues/13141#issuecomment-2390788790
// exiftool -j -struct -GPS*# ~/src/exiftool-vendored.js/test/nikon-daylight-savings.jpg -\*time\* -\*date\* -Daylight* -\*zone\* -Make -Model
const t = parse({
geolocation,
preferTimezoneInferenceFromGps,
backfillTimezones: true,
tags: {
GPSLatitude: -45.8745231666667,
GPSLongitude: 170.503112783333,
GPSLatitudeRef: "S",
GPSLongitudeRef: "E",
GPSPosition: "-45.8745231666667 170.503112783333",
ExposureTime: "1/20",
OffsetTime: "+13:00",
OffsetTimeOriginal: "+13:00",
OffsetTimeDigitized: "+13:00",
TimeZone: "+12:00",
ISOAutoShutterTime: "Auto",
SelfTimerTime: "10 s",
SelfTimerShotCount: 9,
SelfTimerShotInterval: "0.5 s",
PlaybackMonitorOffTime: "10 s",
MenuMonitorOffTime: "1 min",
ShootingInfoMonitorOffTime: "4 s",
ImageReviewMonitorOffTime: "4 s",
LiveViewMonitorOffTime: "10 min",
PowerUpTime: "0000:00:00 00:00:00",
SubSecTime: 45,
SubSecTimeDigitized: 45,
DateTimeOriginal: "2020:02:10 20:24:43",
ProfileDateTime: "2024:10:01 13:21:03",
SubSecDateTimeOriginal: "2020:02:10 20:24:43+13:00",
FileModifyDate: "2024:10:05 11:34:57-07:00",
FileAccessDate: "2024:10:05 11:34:57-07:00",
FileInodeChangeDate: "2024:10:05 11:34:57-07:00",
ModifyDate: "2024:10:03 08:15:25",
CreateDate: "2020:02:10 20:24:43",
DateDisplayFormat: "D/M/Y",
SubSecCreateDate: "2020:02:10 20:24:43.45", // < needs backfill
SubSecModifyDate: "2024:10:03 08:15:25.45+13:00",
DaylightSavings: "Yes",
Make: "NIKON CORPORATION",
Model: "NIKON D7500",
},
})
expect(renderTagsWithISO(t)).to.containSubset({
DateTimeOriginal: "2020-02-10T20:24:43+13:00",
SubSecDateTimeOriginal: "2020-02-10T20:24:43+13:00",
SubSecCreateDate: "2020-02-10T20:24:43.450+13:00",
SubSecModifyDate: "2024-10-03T08:15:25.450+13:00",
...(preferTimezoneInferenceFromGps
? {
tz: "Pacific/Auckland",
tzSource: "GPSLatitude/GPSLongitude",
}
: {
tz: "UTC+13",
tzSource: "TimeZone (adjusted for DaylightSavings)",
}),
})
})
}
)
}
}
})
})
Loading

0 comments on commit cbc92c5

Please sign in to comment.