exiftool-vendored
Version:
Efficient, cross-platform access to ExifTool
1,073 lines (1,072 loc) • 80.7 kB
JavaScript
"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",