UNPKG

exiftool-vendored

Version:
1,073 lines (1,072 loc) 80.7 kB
"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; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const geo_tz_1 = __importDefault(require("geo-tz")); const luxon_1 = require("luxon"); const promises_1 = require("node:fs/promises"); const node_path_1 = __importStar(require("node:path")); const _chai_spec_1 = require("./_chai.spec"); const DateTime_1 = require("./DateTime"); const ExifDateTime_1 = require("./ExifDateTime"); const ExifTool_1 = require("./ExifTool"); const GeolocationTags_1 = require("./GeolocationTags"); const Object_1 = require("./Object"); const Pick_1 = require("./Pick"); const ReadTask_1 = require("./ReadTask"); const Timezones_1 = require("./Timezones"); const SourceFile = (0, node_path_1.join)((0, _chai_spec_1.tmpdir)(), "example.jpg"); function parse(args) { const tt = ReadTask_1.ReadTask.for(args.SourceFile ?? SourceFile, { defaultVideosToUTC: true, backfillTimezones: true, includeImageDataMD5: true, ...args, }); const json = JSON.stringify([{ ...args.tags, SourceFile }]); // pretend that ExifTool rendered `json`: return tt.parse(json, args.error); } function geo_tz(lat, lon) { try { return geo_tz_1.default.find(lat, lon)[0]; } catch { return; } } describe("ReadTask", () => { let exiftool; before(() => (exiftool = new ExifTool_1.ExifTool())); after(() => (0, _chai_spec_1.end)(exiftool)); describe("Lat/Lon parsing", () => { /* Example: $ exiftool -j -coordFormat '%.8f' -fast ../test-images/important/Apple_iPhone7Plus.jpg | grep itude "GPSLatitudeRef": "North", "GPSLongitudeRef": "East", "GPSAltitudeRef": "Above Sea Level", "GPSAltitude": "73 m Above Sea Level", "GPSLatitude": 22.33543889, "GPSLongitude": 114.16401667, */ it("N lat is positive", () => { (0, _chai_spec_1.expect)(parse({ tags: { GPSLatitude: 22.33543889, GPSLatitudeRef: "N", GPSLongitude: 1, }, }).GPSLatitude).to.be.closeTo(22.33543889, 0.00001); }); it("S lat is negative", () => { (0, _chai_spec_1.expect)(parse({ tags: { GPSLatitude: -33.84842123, GPSLatitudeRef: "S", GPSLongitude: 1, }, }).GPSLatitude).to.be.closeTo(-33.84842123, 0.00001); }); it("positive E lon is positive", () => { (0, _chai_spec_1.expect)(parse({ tags: { GPSLongitude: 114.16401667, GPSLongitudeRef: "E", GPSLatitude: 1, }, }).GPSLongitude).to.be.closeTo(114.16401667, 0.00001); }); // See https://github.com/photostructure/exiftool-vendored.js/issues/165 it("negative E lon is negative", () => { (0, _chai_spec_1.expect)(parse({ tags: { GPSLongitude: -114, GPSLongitudeRef: "E", GPSLatitude: 1 }, })).to.containSubset({ GPSLongitude: -114, GPSLongitudeRef: "W", }); }); it("positive W lon is negative", () => { (0, _chai_spec_1.expect)(parse({ tags: { GPSLongitude: 122, GPSLongitudeRef: "W", GPSLatitude: 1 }, })).to.containSubset({ GPSLongitude: -122, GPSLongitudeRef: "W", }); }); it("negative W lon is positive", () => { (0, _chai_spec_1.expect)(parse({ tags: { GPSLongitude: -122.4406148, GPSLongitudeRef: "W", GPSLatitude: 1, }, }).GPSLongitude).to.be.closeTo(-122.4406148, 0.00001); }); it("parses lat lon even if timezone is given", () => { (0, _chai_spec_1.expect)(parse({ tags: { GPSLongitude: -122.4406148, GPSLongitudeRef: "West", OffsetTime: "+02:00", GPSLatitude: 1, }, }).GPSLongitude).to.be.closeTo(-122.4406148, 0.00001); }); for (const geolocation of [true, false]) { for (const preferTimezoneInferenceFromGps of [true, false]) { describe(JSON.stringify({ geolocation, preferTimezoneInferenceFromGps }), () => { it("extracts problematic GPSDateTime", async () => { const t = await exiftool.read((0, node_path_1.join)(_chai_spec_1.testDir, "nexus5x.jpg"), { geolocation, preferTimezoneInferenceFromGps, }); (0, _chai_spec_1.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; (0, _chai_spec_1.expect)(gpsdt.toString()).to.eql("2016-07-19T10:00:24Z"); (0, _chai_spec_1.expect)(gpsdt.rawValue).to.eql("2016:07:19 10:00:24Z"); (0, _chai_spec_1.expect)(gpsdt.zoneName).to.eql("UTC"); if (geolocation) { const actualGeoKeys = Object.keys(t) .filter((ea) => ea.startsWith("Geolocation") && ea !== "GeolocationWarning") .sort(); (0, _chai_spec_1.expect)(actualGeoKeys).to.have.members(GeolocationTags_1.GeolocationTagNames.values.filter((ea) => ea !== "GeolocationWarning")); (0, _chai_spec_1.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, }); } }); }); } } it("omits all GPS tags if invalid lat/lon", () => { (0, _chai_spec_1.expect)(parse({ tags: { SourceFile, GPSLatitude: 0, GPSLongitude: 0, GeolocationCity: "Takoradi", GeolocationRegion: "Western", GeolocationSubregion: "Secondi Takoradi", GeolocationCountryCode: "GH", GeolocationCountry: "Ghana", GeolocationTimeZone: "Africa/Accra", GeolocationFeatureCode: "PPL", GeolocationFeatureType: "Populated Place", GeolocationPopulation: 390000, GeolocationPosition: "4.8982, -1.7602", GeolocationDistance: "578.67 km", GeolocationBearing: 340, GPSLatitudeRef: "North", GPSLongitudeRef: "East", GPSPosition: "0 deg 0' 0.00\" N, 0 deg 0' 0.00\" E", }, ignoreZeroZeroLatLon: true, geolocation: true, })).to.eql({ SourceFile, errors: [], warnings: ["Ignoring zero coordinates from GPSLatitude/GPSLongitude"], }); }); it("extracts GPS tags if valid lat/lon", () => { const tags = { SourceFile, GPSLatitude: "37 deg 48' 3.45\" N", GPSLongitude: "122 deg 23' 55.67\" W", GPSLatitudeRef: "North", GPSLongitudeRef: "West", GPSPosition: "37 deg 48' 3.45\" N, 122 deg 23' 55.67\" W", GeolocationCity: "Chinatown", GeolocationRegion: "California", GeolocationSubregion: "City and County of San Francisco", GeolocationCountryCode: "US", GeolocationCountry: "United States", GeolocationTimeZone: "America/Los_Angeles", GeolocationFeatureCode: "PPLX", GeolocationFeatureType: "Section of Populated Place", GeolocationPopulation: 100000, GeolocationPosition: "37.7966, -122.4086", GeolocationDistance: "1.01 km", GeolocationBearing: 246, }; (0, _chai_spec_1.expect)(parse({ tags, ignoreZeroZeroLatLon: true, geolocation: true, })).to.eql({ ...tags, GPSLatitude: 37.800958, GPSLatitudeRef: "N", GPSLongitude: -122.398797, GPSLongitudeRef: "W", zone: "America/Los_Angeles", tz: "America/Los_Angeles", tzSource: "GeolocationTimeZone", errors: [], warnings: [], }); }); describe("without *Ref fields", () => { for (const latSign of [1, -1]) { for (const lonSign of [1, -1]) { const input = { GPSLatitude: latSign * 34.4, GPSLongitude: lonSign * 119.8, }; it(`extracts (${JSON.stringify(input)})`, () => { (0, _chai_spec_1.expect)(parse({ tags: input })).to.containSubset(input); }); } } }); }); describe("imageHashType", () => { const file = (0, node_path_1.join)(_chai_spec_1.testDir, "with_thumb.jpg"); for (const { imageHashType, hash } of [ { imageHashType: false, hash: undefined }, { imageHashType: "MD5", hash: "5617def2642dbd90ab6a2d4f185d7850" }, { imageHashType: "SHA256", hash: "5e91b8878501fb77075df22d4056f27e74c4aad8869bcdd2c1cce673b5f23a8d", }, { imageHashType: "SHA512", hash: "12c4205a228bfba8a77e0da0c4b02dcd381eb15eb06f460781999fd7bc3db2f07f16b0e3a145dfbe68868ee2086e7c47e5b3315bab5146b3f49e7db3d65e1178", }, ]) { it(JSON.stringify({ imageHashType }), async () => { const t = await exiftool.read(file, undefined, { imageHashType, }); if (hash == null) { (0, _chai_spec_1.expect)(t).to.not.haveOwnProperty("ImageDataHash"); } else { (0, _chai_spec_1.expect)(t).to.haveOwnProperty("ImageDataHash", hash); } }); } }); describe("date and time parsing", () => { for (const includeMilliseconds of [true, false]) { for (const key of ["Time", "DateTimeStamp"]) { describe(JSON.stringify({ key, includeMilliseconds }), () => { it("extracts a valid timestamp", () => { const exp = (0, DateTime_1.hms)(luxon_1.DateTime.now(), { includeMilliseconds }); const tags = {}; tags[key] = exp; const t = parse({ tags }); // Seems obvi? well, check out NotDateRe and MaybeDateOrTimeRe. (0, _chai_spec_1.expect)(t[key]).to.be.instanceOf(ExifTool_1.ExifTime); const suffix = key.includes("GPS") ? "+00:00" : ""; (0, _chai_spec_1.expect)(t[key]?.toString()).to.eql(exp + suffix); }); it("rejects a numeric timestamp", () => { const exp = 12345678; const tags = {}; tags[key] = exp; const t = parse({ tags }); (0, _chai_spec_1.expect)(t[key]).to.equal(exp); }); it("rejects a string timestamp", () => { const exp = "Off"; const tags = {}; tags[key] = exp; const t = parse({ tags }); (0, _chai_spec_1.expect)(t[key]).to.equal(exp); }); it("rejects a 00 timestamp", () => { const exp = "00"; const tags = { GPSLatitude: 43.7767593, GPSLongitude: 11.2593329, }; tags[key] = exp; const t = parse({ tags }); (0, _chai_spec_1.expect)(t[key]).to.equal(exp); }); }); } } }); describe("Time zone extraction", () => { it("finds singular positive TimeZoneOffset and sets accordingly", () => { const t = parse({ tags: { TimeZoneOffset: 9, DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t).to.containSubset({ tz: "UTC+9", tzSource: "TimeZoneOffset", }); (0, _chai_spec_1.expect)(t.DateTimeOriginal).to.containSubset({ tzoffsetMinutes: 9 * 60, zone: "UTC+9", inferredZone: true, }); }); it("respects zero HH:MM OffsetTime (see #203)", () => { // this is not UTC, but it is used for "Atlantic/Reykjavik" and other zones const t = parse({ tags: { OffsetTimeOriginal: "+00:00", DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t.DateTimeOriginal).to.containSubset({ tzoffsetMinutes: 0, inferredZone: true, }); (0, _chai_spec_1.expect)((0, Timezones_1.isUTC)(ExifDateTime_1.ExifDateTime.from(t.DateTimeOriginal)?.zone)).to.eql(true); }); describe("inferTimezoneFromTimeStamp (see #209)", () => { it("disabled", () => { const t = parse({ tags: { DateTimeOriginal: "2016:10:17 09:40:43", CreateDate: "2016:10:17 09:40:43", TimeStamp: "2016:10:17 07:40:43.891-07:00", }, inferTimezoneFromTimeStamp: false, }); (0, _chai_spec_1.expect)(t.tz).to.eql(undefined); }); it("enabled", () => { const t = parse({ tags: { DateTimeOriginal: "2016:10:17 09:40:43", CreateDate: "2016:10:17 09:40:43", TimeStamp: "2016:10:17 07:40:43.891-07:00", }, inferTimezoneFromTimeStamp: true, }); (0, _chai_spec_1.expect)(t).to.containSubset({ tz: "UTC-5", tzSource: "offset between DateTimeOriginal and TimeStamp", }); }); }); it("finds positive array TimeZoneOffset and sets accordingly", () => { const t = parse({ tags: { TimeZoneOffset: [9, 8], DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t).to.containSubset({ tz: "UTC+9", tzSource: "TimeZoneOffset", }); (0, _chai_spec_1.expect)(t.DateTimeOriginal).to.containSubset({ tzoffsetMinutes: 9 * 60, zone: "UTC+9", inferredZone: true, }); }); it("finds zulu TimeZoneOffset and sets accordingly", () => { const t = parse({ tags: { TimeZoneOffset: 0, DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t).to.containSubset({ tz: "UTC", tzSource: "TimeZoneOffset", }); (0, _chai_spec_1.expect)(t.DateTimeOriginal).to.containSubset({ tzoffsetMinutes: 0, zone: "UTC", inferredZone: true, }); }); it("finds negative TimeZoneOffset in array and sets accordingly", () => { const t = parse({ tags: { TimeZoneOffset: [-4], DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t).to.containSubset({ tz: "UTC-4", tzSource: "TimeZoneOffset", }); (0, _chai_spec_1.expect)(t.DateTimeOriginal).to.containSubset({ tzoffsetMinutes: -4 * 60, zone: "UTC-4", inferredZone: true, }); }); it("respects positive HH:MM OffsetTimeOriginal", () => { const t = parse({ tags: { OffsetTimeOriginal: "+03:30", DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t.DateTimeOriginal).to.containSubset({ tzoffsetMinutes: 3 * 60 + 30, zone: "UTC+3:30", inferredZone: true, }); }); it("respects positive HH OffsetTimeOriginal", () => { const t = parse({ tags: { OffsetTimeOriginal: "+07", DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t.DateTimeOriginal).to.containSubset({ tzoffsetMinutes: 7 * 60, zone: "UTC+7", inferredZone: true, }); }); it("respects negative HH:MM OffsetTimeOriginal", () => { const t = parse({ tags: { OffsetTimeOriginal: "-09:30", DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t).to.containSubset({ tz: "UTC-9:30", tzSource: "OffsetTimeOriginal", }); (0, _chai_spec_1.expect)(t.DateTimeOriginal).to.containSubset({ tzoffsetMinutes: -(9 * 60 + 30), year: 2016, month: 8, day: 12, hour: 13, minute: 28, second: 50, millisecond: undefined, inferredZone: true, zone: "UTC-9:30", rawValue: "2016:08:12 13:28:50", }); }); it("respects negative H OffsetTimeOriginal", () => { const t = parse({ tags: { OffsetTimeOriginal: "-9", DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t.DateTimeOriginal.tzoffsetMinutes).to.eql(-9 * 60); (0, _chai_spec_1.expect)(t.tz).to.eql("UTC-9"); (0, _chai_spec_1.expect)(t.tzSource).to.eql("OffsetTimeOriginal"); }); it("respects negative HH OffsetTimeOriginal", () => { const t = parse({ tags: { OffsetTimeOriginal: "-09", DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t.DateTimeOriginal.tzoffsetMinutes).to.eql(-9 * 60); (0, _chai_spec_1.expect)(t.tz).to.eql("UTC-9"); (0, _chai_spec_1.expect)(t.tzSource).to.eql("OffsetTimeOriginal"); }); it("determines timezone offset from GPS (specifically, Landscape Arch!)", () => { const t = parse({ tags: { GPSLatitude: 38.791121, GPSLatitudeRef: "North", GPSLongitude: -109.606407, GPSLongitudeRef: "West", DateTimeOriginal: "2016:08:12 13:28:50", }, }); (0, _chai_spec_1.expect)(t.DateTimeOriginal.tzoffsetMinutes).to.eql(-6 * 60); (0, _chai_spec_1.expect)(t.tz).to.eql("America/Denver"); (0, _chai_spec_1.expect)(t.tzSource).to.eql("GPSLatitude/GPSLongitude"); }); it("uses GPSDateTime and DateTimeOriginal and sets accordingly for -7", () => { const t = parse({ tags: { DateTimeOriginal: "2016:10:19 11:15:14", GPSDateTime: "2016:10:19 18:15:12", DateTimeCreated: "2016:10:19 11:15:14", }, }); (0, _chai_spec_1.expect)(t.DateTimeOriginal.tzoffsetMinutes).to.eql(-7 * 60); (0, _chai_spec_1.expect)(t.DateTimeCreated.tzoffsetMinutes).to.eql(-7 * 60); (0, _chai_spec_1.expect)(t.tz).to.eql("UTC-7"); (0, _chai_spec_1.expect)(t.tzSource).to.eql("offset between DateTimeOriginal and GPSDateTime"); }); it("uses DateTimeUTC and DateTimeOriginal and sets accordingly for +8", () => { const t = parse({ tags: { DateTimeOriginal: "2016:10:19 11:15:14", DateTimeUTC: "2016:10:19 03:15:12", DateTimeCreated: "2016:10:19 11:15:14", }, }); (0, _chai_spec_1.expect)(t.DateTimeOriginal.tzoffsetMinutes).to.eql(8 * 60); (0, _chai_spec_1.expect)(t.DateTimeCreated.tzoffsetMinutes).to.eql(8 * 60); (0, _chai_spec_1.expect)(t.tz).to.eql("UTC+8"); (0, _chai_spec_1.expect)(t.tzSource).to.eql("offset between DateTimeOriginal and DateTimeUTC"); }); it("uses DateTimeUTC and DateTimeOriginal and sets accordingly for +5:30", () => { const t = parse({ tags: { DateTimeOriginal: "2018:10:19 11:15:14", DateTimeUTC: "2018:10:19 05:45:12", DateTimeCreated: "2018:10:19 11:15:14", }, }); (0, _chai_spec_1.expect)(t.DateTimeOriginal.tzoffsetMinutes).to.eql(5.5 * 60); (0, _chai_spec_1.expect)(t.DateTimeCreated.tzoffsetMinutes).to.eql(5.5 * 60); (0, _chai_spec_1.expect)(t.tz).to.eql("UTC+5:30"); (0, _chai_spec_1.expect)(t.tzSource).to.eql("offset between DateTimeOriginal and DateTimeUTC"); }); it("renders SubSecDateTimeOriginal with no zone if no tz is inferrable", () => { const input = { DateTimeOriginal: "2016:12:13 09:05:27", SubSecDateTimeOriginal: "2016:12:13 09:05:27.12038200", }; const t = parse({ tags: input }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithRawValues)(t)).to.eql(input); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ DateTimeOriginal: "2016-12-13T09:05:27", SubSecDateTimeOriginal: "2016-12-13T09:05:27.120", errors: [], warnings: [], }); }); it("renders SubSecDateTimeOriginal for -8", () => { const input = { DateTimeOriginal: "2016:12:13 09:05:27", GPSDateTime: "2016:12:13 17:05:25Z", SubSecDateTimeOriginal: "2016:12:13 09:05:27.12038200", }; const t = parse({ tags: input }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithRawValues)(t)).to.eql(input); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ DateTimeOriginal: "2016-12-13T09:05:27-08:00", GPSDateTime: "2016-12-13T17:05:25Z", SubSecDateTimeOriginal: "2016-12-13T09:05:27.120-08:00", zone: "UTC-8", tz: "UTC-8", tzSource: "offset between SubSecDateTimeOriginal and GPSDateTime", errors: [], warnings: [], }); }); it("skips invalid timestamps", () => { const expected = { DateTimeOriginal: "2016:08:12 13:28:50", GPSDateTime: "not a timestamp", SubSecTime: "00", SubSecTimeOriginal: "00", SubSecTimeDigitized: "00", }; const t = parse({ tags: expected }); (0, _chai_spec_1.expect)(t).containSubset((0, Object_1.omit)(expected, "DateTimeOriginal")); (0, _chai_spec_1.expect)(t.DateTimeOriginal).to.be.instanceOf(ExifDateTime_1.ExifDateTime); (0, _chai_spec_1.expect)(t.DateTimeOriginal?.toString()).to.eql("2016-08-12T13:28:50"); (0, _chai_spec_1.expect)(t.tz).to.eql(undefined); (0, _chai_spec_1.expect)(t.tzSource).to.eql(undefined); }); describe("try to reproduce issue #118", () => { it("invalid GPSTimeStamp doesn't throw", async () => { const t = parse({ tags: { GPSTimeStamp: "1970:01:01 00:00:00Z", // < INVALID, this field is always a timestamp without a date }, }); (0, _chai_spec_1.expect)(t.GPSTimeStamp.toISOString()).to.eql("1970-01-01T00:00:00Z"); }); it("reads file with GPS tags set to common epoch", async () => { const t = await exiftool.read((0, node_path_1.join)(_chai_spec_1.testDir, "0epoch.jpg")); (0, _chai_spec_1.expect)(t.GPSDateTime.toMillis()).to.eql(0); }); }); // https://github.com/photostructure/exiftool-vendored.js/issues/113 describe("timezone parsing", () => { const input = { TimeZone: "+00:00", CreateDate: "2020:08:03 08:00:19-07:00", SubSecCreateDate: "2020:08:03 15:00:19.01+00:00", DateTimeOriginal: "2020:08:03 15:00:19", TimeStamp: "2020:08:03 15:00:19.01", }; it("handles explicit GMT with explicit offset for image/jpeg", () => { const MIMEType = "image/jpeg"; const t = parse({ tags: { MIMEType, ...input, }, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ MIMEType, CreateDate: "2020-08-03T08:00:19-07:00", SubSecCreateDate: "2020-08-03T15:00:19.010Z", DateTimeOriginal: "2020-08-03T15:00:19Z", TimeStamp: "2020-08-03T15:00:19.010Z", zone: "UTC", tz: "UTC", tzSource: "TimeZone", TimeZone: "+00:00", errors: [], warnings: [], }); }); it("handles explicit GMT with explicit offset for video/mp4", () => { const MIMEType = "video/mp4/jpeg"; const t = parse({ tags: { MIMEType, ...input, }, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.containSubset({ MIMEType, CreateDate: "2020-08-03T08:00:19-07:00", // < because the input had a zone SubSecCreateDate: "2020-08-03T15:00:19.010Z", DateTimeOriginal: "2020-08-03T15:00:19Z", TimeStamp: "2020-08-03T15:00:19.010Z", TimeZone: "+00:00", errors: [], warnings: [], }); }); }); describe("iPhone MOV with only CreationDate offset", () => { // https://github.com/photostructure/exiftool-vendored.js/issues/151 it("Timezone from CreationDate with no GPS", () => { const t = (0, _chai_spec_1.renderTagsWithISO)(parse({ tags: { // exiftool -j -*Zone -MIMEType -*Date -GPS\*\# ~/Downloads/sad.MOV MIMEType: "video/quicktime", FileModifyDate: "2023:07:19 15:38:32-07:00", FileAccessDate: "2023:07:19 15:38:42-07:00", FileInodeChangeDate: "2023:07:19 15:38:42-07:00", CreateDate: "2023:06:11 13:30:35", ModifyDate: "2023:06:11 13:30:46", TrackCreateDate: "2023:06:11 13:30:35", TrackModifyDate: "2023:06:11 13:30:46", MediaCreateDate: "2023:06:11 13:30:35", MediaModifyDate: "2023:06:11 13:30:46", // iPhone 14 Pro renders MOV with only the CreationDate // containing a value offset (!!) -- so the other dates _are // actually in UTC_, not local time (!!!!) CreationDate: "2023:06:11 14:30:35+01:00", }, inferTimezoneFromDatestamps: false, backfillTimezones: false, })); (0, _chai_spec_1.expect)(t).to.eql({ zone: "UTC", tz: "UTC", tzSource: "defaultVideosToUTC", MIMEType: "video/quicktime", FileModifyDate: "2023-07-19T15:38:32-07:00", FileAccessDate: "2023-07-19T15:38:42-07:00", FileInodeChangeDate: "2023-07-19T15:38:42-07:00", CreateDate: "2023-06-11T13:30:35Z", ModifyDate: "2023-06-11T13:30:46Z", TrackCreateDate: "2023-06-11T13:30:35Z", TrackModifyDate: "2023-06-11T13:30:46Z", MediaCreateDate: "2023-06-11T13:30:35Z", MediaModifyDate: "2023-06-11T13:30:46Z", CreationDate: "2023-06-11T14:30:35+01:00", errors: [], warnings: [], }); }); it("Timezone from CreationDate with no GPS and new inferTimezoneFromDatestamps", () => { const t = (0, _chai_spec_1.renderTagsWithISO)(parse({ tags: { // exiftool -j -*Zone -MIMEType -*Date -GPS\*\# ~/Downloads/sad.MOV MIMEType: "video/quicktime", FileModifyDate: "2023:07:19 15:38:32-07:00", FileAccessDate: "2023:07:19 15:38:42-07:00", FileInodeChangeDate: "2023:07:19 15:38:42-07:00", CreateDate: "2023:06:11 13:30:35", ModifyDate: "2023:06:11 13:30:46", TrackCreateDate: "2023:06:11 13:30:35", TrackModifyDate: "2023:06:11 13:30:46", MediaCreateDate: "2023:06:11 13:30:35", MediaModifyDate: "2023:06:11 13:30:46", // iPhone 14 Pro renders MOV with only the CreationDate // containing a value offset (!!) -- so the other dates _are // actually in UTC_, not local time (!!!!) CreationDate: "2023:06:11 14:30:35+01:00", }, inferTimezoneFromDatestamps: true, backfillTimezones: true, })); (0, _chai_spec_1.expect)(t).to.eql({ zone: "UTC+1", tz: "UTC+1", tzSource: "CreationDate", MIMEType: "video/quicktime", FileModifyDate: "2023-07-19T15:38:32-07:00", FileAccessDate: "2023-07-19T15:38:42-07:00", FileInodeChangeDate: "2023-07-19T15:38:42-07:00", CreateDate: "2023-06-11T14:30:35+01:00", ModifyDate: "2023-06-11T14:30:46+01:00", TrackCreateDate: "2023-06-11T14:30:35+01:00", TrackModifyDate: "2023-06-11T14:30:46+01:00", MediaCreateDate: "2023-06-11T14:30:35+01:00", MediaModifyDate: "2023-06-11T14:30:46+01:00", CreationDate: "2023-06-11T14:30:35+01:00", errors: [], warnings: [], }); }); it("Timezone from CreationDate and GPS", () => { const t = (0, _chai_spec_1.renderTagsWithISO)(parse({ tags: { // exiftool -j -*Zone -MIMEType -*Date -GPS\*\# ~/Downloads/sad.MOV MIMEType: "video/quicktime", FileModifyDate: "2023:07:19 15:38:32-07:00", FileAccessDate: "2023:07:19 15:38:42-07:00", FileInodeChangeDate: "2023:07:19 15:38:42-07:00", CreateDate: "2023:06:11 13:30:35", ModifyDate: "2023:06:11 13:30:46", TrackCreateDate: "2023:06:11 13:30:35", TrackModifyDate: "2023:06:11 13:30:46", MediaCreateDate: "2023:06:11 13:30:35", MediaModifyDate: "2023:06:11 13:30:46", CreationDate: "2023:06:11 14:30:35+01:00", GPSAltitude: 99.22, GPSAltitudeRef: 0, GPSLatitude: 51.1037, GPSLongitude: -0.8732, }, inferTimezoneFromDatestamps: true, backfillTimezones: true, })); (0, _chai_spec_1.expect)(t).to.eql({ MIMEType: "video/quicktime", zone: "Europe/London", tz: "Europe/London", tzSource: "GPSLatitude/GPSLongitude", FileModifyDate: "2023-07-19T15:38:32-07:00", FileAccessDate: "2023-07-19T15:38:42-07:00", FileInodeChangeDate: "2023-07-19T15:38:42-07:00", CreateDate: "2023-06-11T14:30:35+01:00", ModifyDate: "2023-06-11T14:30:46+01:00", TrackCreateDate: "2023-06-11T14:30:35+01:00", TrackModifyDate: "2023-06-11T14:30:46+01:00", MediaCreateDate: "2023-06-11T14:30:35+01:00", MediaModifyDate: "2023-06-11T14:30:46+01:00", CreationDate: "2023-06-11T14:30:35+01:00", GPSAltitude: 99.22, GPSAltitudeRef: 0, GPSLatitude: 51.1037, GPSLongitude: -0.8732, GPSLatitudeRef: "N", GPSLongitudeRef: "W", errors: [], warnings: [], }); }); it("defaults video without offset to UTC", () => { const t = parse({ tags: { MIMEType: "video/mp4", CreateDate: "2014:07:17 08:46:27", }, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ // ALL DATES ARE IN ZULU! MIMEType: "video/mp4", CreateDate: "2014-07-17T08:46:27Z", zone: "UTC", tz: "UTC", tzSource: ExifTool_1.defaultVideosToUTC, errors: [], warnings: [], }); }); it("retains tzoffset in video timestamps", () => { const t = parse({ tags: { MIMEType: "video/mp4", CreateDate: "2014:07:17 08:46:27-05:00 DST", }, // Disable inferTimezoneFromDatestamps to test defaultVideosToUTC behavior inferTimezoneFromDatestamps: false, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ // ALL DATES ARE IN ZULU! MIMEType: "video/mp4", CreateDate: "2014-07-17T08:46:27-05:00", zone: "UTC", tz: "UTC", tzSource: ExifTool_1.defaultVideosToUTC, errors: [], warnings: [], }); }); it("handles CET timezone for images", () => { const t = parse({ tags: { TimeZone: "+01:00", TimeZoneCity: "Rome", CreateDate: "2020:08:03 16:00:19", // < different (local system) zone! SubSecCreateDate: "2020:08:03 16:00:19.123+01:00", DateTimeOriginal: "2020:08:03 16:00:19", // < missing zone! TimeStamp: "2020:08:03 16:00:19.01", // < missing zone! }, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ CreateDate: "2020-08-03T16:00:19+01:00", DateTimeOriginal: "2020-08-03T16:00:19+01:00", SubSecCreateDate: "2020-08-03T16:00:19.123+01:00", TimeStamp: "2020-08-03T16:00:19.010+01:00", TimeZone: "+01:00", TimeZoneCity: "Rome", zone: "UTC+1", tz: "UTC+1", tzSource: "TimeZone", errors: [], warnings: [], }); }); it("handles CET timezone for video with TimeZone tag", () => { const t = (0, _chai_spec_1.renderTagsWithISO)(parse({ tags: { MIMEType: "video/mp4", TimeZone: "+01:00", TimeZoneCity: "Rome", CreateDate: "2020:08:03 15:00:19", // < missing zone (assume UTC) DateTimeOriginal: "2020:08:03 15:00:19", // < missing zone (assume UTC) SubSecCreateDate: "2020:08:03 16:00:19.123+01:00", TimeStamp: "2020:08:03 15:00:19.01", // < missing zone (assume UTC) }, })); (0, _chai_spec_1.expect)(t).to.eql({ MIMEType: "video/mp4", CreateDate: "2020-08-03T16:00:19+01:00", DateTimeOriginal: "2020-08-03T16:00:19+01:00", SubSecCreateDate: "2020-08-03T16:00:19.123+01:00", TimeStamp: "2020-08-03T16:00:19.010+01:00", TimeZone: "+01:00", TimeZoneCity: "Rome", zone: "UTC+1", tz: "UTC+1", tzSource: "TimeZone", errors: [], warnings: [], }); }); it("handles CET timezone for video without TimeZone tag", () => { const t = parse({ tags: { MIMEType: "video/mp4", CreateDate: "2020:08:03 15:00:19", // < naughty video in UTC SubSecCreateDate: "2020:08:03 16:00:19.123+01:00", DateTimeOriginal: "2020:08:03 15:00:19", // < missing zone! TimeStamp: "2020:08:03 15:00:19.01", // < missing zone! }, // Disable inferTimezoneFromDatestamps to test defaultVideosToUTC behavior inferTimezoneFromDatestamps: false, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ MIMEType: "video/mp4", CreateDate: "2020-08-03T15:00:19Z", SubSecCreateDate: "2020-08-03T16:00:19.123+01:00", DateTimeOriginal: "2020-08-03T15:00:19Z", TimeStamp: "2020-08-03T15:00:19.010Z", zone: "UTC", tz: "UTC", tzSource: ExifTool_1.defaultVideosToUTC, errors: [], warnings: [], }); }); it("doesn't apply missing timezone", () => { const t = parse({ tags: { // not a video! CreateDate: "2020:08:03 08:00:19-07:00", DateTimeOriginal: "2020:08:03 15:00:19", // < no zone! SubSecCreateDate: "2020:08:03 15:00:19.01+00:00", TimeStamp: "2020:08:03 15:00:19.01", // < no zone! }, backfillTimezones: false, // Disable inferTimezoneFromDatestamps to test no-timezone behavior inferTimezoneFromDatestamps: false, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ // No timezone found, so no normalization: CreateDate: "2020-08-03T08:00:19-07:00", DateTimeOriginal: "2020-08-03T15:00:19", // < no zone! SubSecCreateDate: "2020-08-03T15:00:19.010Z", TimeStamp: "2020-08-03T15:00:19.010", // < no zone! errors: [], warnings: [], }); (0, _chai_spec_1.expect)(t.tz).to.eql(undefined); (0, _chai_spec_1.expect)(t.tzSource).to.eql(undefined); }); it("handles EST", () => { const t = parse({ tags: { CreateDate: "2020:12:29 14:24:45", DateTimeOriginal: "2020:12:29 14:24:45", GPSAltitude: 259.016, GPSDateStamp: "2020:12:29", GPSLatitude: 34.15, GPSLongitude: -84.73, ModifyDate: "2020:12:29 14:24:45", OffsetTime: "-05:00", OffsetTimeDigitized: "-05:00", OffsetTimeOriginal: "-05:00", SubSecCreateDate: "2020:12:29 14:24:45.700-05:00", SubSecDateTimeOriginal: "2020:12:29 14:24:45.700-05:00", SubSecModifyDate: "2020:12:29 14:24:45.789-05:00", SubSecTimeDigitized: 700, SubSecTimeOriginal: 700, }, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ CreateDate: "2020-12-29T14:24:45-05:00", DateTimeOriginal: "2020-12-29T14:24:45-05:00", SubSecCreateDate: "2020-12-29T14:24:45.700-05:00", SubSecDateTimeOriginal: "2020-12-29T14:24:45.700-05:00", SubSecModifyDate: "2020-12-29T14:24:45.789-05:00", ModifyDate: "2020-12-29T14:24:45-05:00", GPSAltitude: 259.016, GPSDateStamp: "2020-12-29", GPSLatitude: 34.15, GPSLongitude: -84.73, GPSLatitudeRef: "N", GPSLongitudeRef: "W", OffsetTime: "-05:00", OffsetTimeDigitized: "-05:00", OffsetTimeOriginal: "-05:00", SubSecTimeDigitized: 700, SubSecTimeOriginal: 700, zone: "UTC-5", tz: "UTC-5", tzSource: "OffsetTimeOriginal", errors: [], warnings: [], }); }); it("handles EST with only GPS and geo-tz", () => { const t = parse({ geoTz: geo_tz, tags: { CreateDate: "2020:12:29 14:24:45", GPSLatitude: 34.15, GPSLongitude: -84.73, }, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ CreateDate: "2020-12-29T14:24:45-05:00", GPSLatitude: 34.15, GPSLongitude: -84.73, GPSLatitudeRef: "N", GPSLongitudeRef: "W", zone: "America/New_York", tz: "America/New_York", tzSource: "GPSLatitude/GPSLongitude", errors: [], warnings: [], }); }); it("{defaultVideosToUTC: true} assumes UTC if video (even if GPS infers EST)", () => { const t = parse({ tags: { MIMEType: "video/mp4", CreateDate: "2022:08:31 00:32:06", // Smartphone videos seem to always encode timestamps in UTC, even // if there is GPS metadata GPSLatitude: 34.15, GPSLongitude: -84.73, }, defaultVideosToUTC: true, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ MIMEType: "video/mp4", CreateDate: "2022-08-30T20:32:06-04:00", GPSLatitude: 34.15, GPSLongitude: -84.73, GPSLatitudeRef: "N", GPSLongitudeRef: "W", zone: "America/New_York", tz: "America/New_York", tzSource: "GPSLatitude/GPSLongitude", errors: [], warnings: [], }); }); it("{defaultVideosToUTC: false} assumes video is in local offset (not UTC)", () => { const t = parse({ tags: { MIMEType: "video/mp4", CreateDate: "2022:08:31 00:32:06", // < FWIW I haven't seen local datestamps in videos in the wild GPSLatitude: 34.15, GPSLongitude: -84.73, }, defaultVideosToUTC: false, }); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.eql({ MIMEType: "video/mp4",