UNPKG

exiftool-vendored

Version:
889 lines 46.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const node_fs_1 = require("node:fs"); const node_os_1 = require("node:os"); const node_path_1 = require("node:path"); const _chai_spec_1 = require("./_chai.spec"); const Array_1 = require("./Array"); const CoordinateParser_1 = require("./CoordinateParser"); const ExifDate_1 = require("./ExifDate"); const ExifDateTime_1 = require("./ExifDateTime"); const ExifTool_1 = require("./ExifTool"); const ExifToolVendoredTags_1 = require("./ExifToolVendoredTags"); const File_1 = require("./File"); const Object_1 = require("./Object"); const Sidecars_1 = require("./Sidecars"); const String_1 = require("./String"); const Tags_1 = require("./Tags"); describe("WriteTask", function () { this.slow(1); // always show timings const ImageExifData = { UserComment: "This is a user comment added by exiftool.", Artist: "Arturo DeImage", Copyright: "© Chuckles McSnortypants, Inc.", Credit: "photo by Jenny Snapsalot", }; for (const opts of [ { maxProcs: 1, maxRetries: 0, useMWG: true }, { maxProcs: 3, maxRetries: 3, useMWG: false }, ]) { describe(`new ExifTool(${JSON.stringify(opts)})`, () => { const exiftool = new ExifTool_1.ExifTool(opts); after(() => (0, _chai_spec_1.end)(exiftool)); describe("with empty WriteTags", () => { it("resolves with no options", async () => { const f = await (0, _chai_spec_1.testImg)(); const result = await exiftool.write(f, {}); (0, _chai_spec_1.expect)((0, Array_1.toArray)(result.warnings)).to.eql([]); (0, _chai_spec_1.expect)(result.unchanged).to.eql(1); }); it("resolves writeArgs: -overwrite_original", async () => { const f = await (0, _chai_spec_1.testImg)(); const result = await exiftool.write(f, {}, { writeArgs: ["-overwrite_original"] }); (0, _chai_spec_1.expect)((0, Array_1.toArray)(result.warnings)).to.eql([]); (0, _chai_spec_1.expect)(result.unchanged).to.eql(1); }); }); describe("MWG composite tags", () => { it("round-trips Creator", async () => { const f = await (0, _chai_spec_1.testImg)(); const Creator = "Ms " + (0, _chai_spec_1.randomChars)(5) + " " + (0, _chai_spec_1.randomChars)(5); await exiftool.write(f, { Creator }); const t = await exiftool.read(f); if (opts.useMWG) { (0, _chai_spec_1.expect)(t.Creator).to.eql(Creator, ".Creator"); (0, _chai_spec_1.expect)(t.Artist).to.eql(Creator, ".Artist"); } else { (0, _chai_spec_1.expect)(t.Creator).to.eql([Creator], ".Creator"); (0, _chai_spec_1.expect)(t.Artist).to.eql(undefined, ".Artist"); } }); it("round-trips Description", async () => { const f = await (0, _chai_spec_1.testImg)(); const Description = "new description " + (0, _chai_spec_1.randomChars)(8); await exiftool.write(f, { Description }); const t = await exiftool.read(f); if (opts.useMWG) { (0, _chai_spec_1.expect)(t.Description).to.eql(Description, ".Description"); (0, _chai_spec_1.expect)(t.ImageDescription).to.eql(Description, ".ImageDescription"); (0, _chai_spec_1.expect)(t["Caption-Abstract"]).to.eql(Description, ".Caption-Abstract"); } else { (0, _chai_spec_1.expect)(t.Description).to.eql(Description, ".Description"); (0, _chai_spec_1.expect)(t.ImageDescription).to.eql("Prior Title", ".ImageDescription"); (0, _chai_spec_1.expect)(t["Caption-Abstract"]).to.eql("Prior Title", ".Caption-Abstract"); } }); }); async function assertRoundTrip({ dest, tagName, inputValue, expectedValue, writeArgs, cmp, }) { const fileExists = (0, node_fs_1.existsSync)(dest); const wt = {}; wt[tagName] = inputValue; const writeResult = await exiftool.write(dest, wt, { writeArgs, }); (0, _chai_spec_1.expect)(writeResult.warnings).to.eql(undefined, JSON.stringify({ writeTags: wt, warnings: writeResult.warnings, writeArgs, })); if (fileExists) { (0, _chai_spec_1.expect)(writeResult).to.containSubset({ created: 0, updated: 1 }); } else { (0, _chai_spec_1.expect)(writeResult).to.containSubset({ created: 1, updated: 0 }); } const result = (await exiftool.read(dest)); const expected = expectedValue ?? inputValue; const cleanTagName = (0, String_1.stripSuffix)(tagName, "#"); const actual = result[cleanTagName]; if (cmp != null) { cmp(actual, result); } else { (0, _chai_spec_1.expect)(actual).to.eql(expected, JSON.stringify({ src: dest, tagName, expected, actual })); } return writeResult; } // Well-supported text tag name: const textTagName = "Description"; // Well-supported multi-value string tag: const multiTagName = "TagsList"; function runRoundTripTests({ withTZ, dest, }) { const tzo = withTZ ? "+08:00" : ""; it("round-trips a comment", async () => { return assertRoundTrip({ dest: await dest(), tagName: textTagName, inputValue: "new comment from " + new Date(), }); }); it("round-trips a comment with many whitespace flavors", async () => { return assertRoundTrip({ dest: await dest(), tagName: textTagName, inputValue: "a\rnew\ncomment\n\r\tfrom\r\n" + new Date(), }); }); it("round-trips a non-latin comment", async () => { return assertRoundTrip({ dest: await dest(), tagName: textTagName, inputValue: "早安晨之美" + new Date(), }); }); it("round-trips a comment with simple and compound codepoint emoji", async () => { return assertRoundTrip({ dest: await dest(), tagName: textTagName, inputValue: "⌚✨💑🏽👰🏽🦏🌈🦍🦄🧑‍🤝‍🧑🚵‍♀️ " + new Date(), }); }); it("round-trips a comment with non-latin filename", async () => { return assertRoundTrip({ dest: await dest("中文.jpg"), tagName: textTagName, inputValue: "new comment from " + new Date(), }); }); it("round-trips a non-latin comment with non-latin filename", async () => { return assertRoundTrip({ dest: await dest("中文.jpg"), tagName: textTagName, inputValue: "早安晨之美" + new Date(), }); }); it("round-trips a rtl comment", async () => { return assertRoundTrip({ dest: await dest(), tagName: textTagName, inputValue: "مرحبا بالعالم " + new Date(), }); }); it("round-trips a numeric Orientation", async () => { return assertRoundTrip({ dest: await dest(), tagName: "Orientation#", inputValue: 1, }); }); it("round-trips a string Orientation 90 CW", async () => { return assertRoundTrip({ dest: await dest(), tagName: "Orientation#", inputValue: 6, }); }); it("round-trips a string Orientation 180 CW", async () => { return assertRoundTrip({ dest: await dest(), tagName: "Orientation#", inputValue: 3, }); }); it("updates ExposureTime to a specific time", async () => { return assertRoundTrip({ dest: await dest(), tagName: "ExposureTime", inputValue: "1/300", }); }); it("updates DateTimeOriginal to a specific time", async () => { return assertRoundTrip({ dest: await dest(), tagName: "DateTimeOriginal", inputValue: "2017-11-15T12:34:56" + tzo, cmp: (actual) => { (0, _chai_spec_1.expect)(actual.toISOString()).to.eql(`2017-11-15T12:34:56${tzo}`); }, }); }); it("round-trips list tag array input", async () => { return assertRoundTrip({ dest: await dest(), tagName: multiTagName, inputValue: [ "one", "two", "three", "commas, and { evil [\t|\r] characters \n }", ], }); }); it("updates DateTimeDigitized with TimeZoneOffset", async () => { const src = await dest(); const wt = { DateTimeDigitized: new ExifDateTime_1.ExifDateTime(2010, 7, 13, 14, 15, 16, 123), TimeZoneOffset: +8, }; await exiftool.write(src, wt); const newTags = await exiftool.read(src); const d = newTags.DateTimeDigitized; (0, _chai_spec_1.expect)(d.toISOString()).to.eql("2010-07-13T14:15:16.123" + tzo, JSON.stringify(d)); return; }); it("updates CreateDate to a time with zeroes and OffsetTimeDigitized", async () => { const src = await dest(); const wt = { CreateDate: new ExifDateTime_1.ExifDateTime(2019, 1, 2, 0, 0, 0), // We have to clear the GPS info to make the OffsetTime be respected: GPSLatitude: null, GPSLongitude: null, OffsetTimeDigitized: "-05:00", }; await exiftool.write(src, wt); const t = await exiftool.read(src); (0, _chai_spec_1.expect)(t.CreateDate?.toString()).to.eql("2019-01-02T00:00:00" + (withTZ ? "-05:00" : "")); return; }); it("updates ReleaseDate to a specific date", async () => { const f = await dest(); const wt = { ReleaseDate: ExifDate_1.ExifDate.fromISO("2019-01-02"), }; await exiftool.write(f, wt); const newTags = await exiftool.read(f); (0, _chai_spec_1.expect)(newTags.ReleaseDate.toISOString()).to.eql("2019-01-02"); return; }); it("writes XMP CreateDate as a year-only partial date", async () => { const f = await dest(); const yearOnlyDate = ExifDate_1.ExifDate.fromYear(1980); (0, _chai_spec_1.expect)(yearOnlyDate.isYearOnly()).to.be.true; const wt = { "XMP:CreateDate": yearOnlyDate, }; const writeResult = await exiftool.write(f, wt); (0, _chai_spec_1.expect)(writeResult.created + writeResult.updated).to.be.greaterThan(0); // Note: ExifTool may convert partial dates to different formats when reading back // The important thing is that we can write them without errors const newTags = await exiftool.read(f); // Check that some date-related field was written (0, _chai_spec_1.expect)(newTags.CreateDate ?? newTags.DateTimeOriginal ?? newTags.ModifyDate).to.not.be.undefined; return; }); it("writes XMP CreateDate as a year-month partial date", async () => { const f = await dest(); const yearMonthDate = ExifDate_1.ExifDate.fromYearMonth("1980:08"); (0, _chai_spec_1.expect)(yearMonthDate.isYearMonth()).to.be.true; const wt = { "XMP:CreateDate": yearMonthDate, }; const writeResult = await exiftool.write(f, wt); (0, _chai_spec_1.expect)(writeResult.created + writeResult.updated).to.be.greaterThan(0); // Note: ExifTool may convert partial dates to different formats when reading back // The important thing is that we can write them without errors const newTags = await exiftool.read(f); // Check that some date-related field was written (0, _chai_spec_1.expect)(newTags.CreateDate ?? newTags.DateTimeOriginal ?? newTags.ModifyDate).to.not.be.undefined; return; }); it("writes XMP CreateDate with a numeric year", async () => { const f = await dest(); const wt = { "XMP:CreateDate": 1980, }; const writeResult = await exiftool.write(f, wt); (0, _chai_spec_1.expect)(writeResult.created + writeResult.updated).to.be.greaterThan(0); // Note: ExifTool may convert numeric years to different formats when reading back // The important thing is that we can write them without errors const newTags = await exiftool.read(f); // Check that some date-related field was written (0, _chai_spec_1.expect)(newTags.CreateDate ?? newTags.DateTimeOriginal ?? newTags.ModifyDate).to.not.be.undefined; return; }); it("round-trips a boolean true value", async () => { const file = await dest(); // Using a native boolean tag return assertRoundTrip({ dest: file, tagName: "AlreadyApplied", inputValue: true, expectedValue: true, }); }); it("round-trips a boolean false value", async () => { const file = await dest(); // Using a native boolean tag return assertRoundTrip({ dest: file, tagName: "AlreadyApplied", inputValue: false, expectedValue: false, }); }); function randomFloat(min, max) { return Math.random() * (max - min) + min; } for (const ignoreZeroZeroLatLon of [false, true]) { describe("round-trips GPS values (attempt to reproduce #131): " + JSON.stringify({ ignoreZeroZeroLatLon }), () => { // Verify there's no shenanigans with negative, zero, or positive // lat/lon combinations: for (const GPSLatitude of [ randomFloat(-89, -1), 0, 39.1132577, randomFloat(1, 89), ]) { for (const GPSLongitude of [ randomFloat(-179, -1), -84.6907715, 0, randomFloat(1, 179), ]) { it(JSON.stringify({ GPSLatitude, GPSLongitude }), async () => { exiftool.options.ignoreZeroZeroLatLon = ignoreZeroZeroLatLon; const f = await dest(); const result = await exiftool.write(f, { GPSLatitude, GPSLongitude, Title: "title", }); (0, _chai_spec_1.expect)(result.created + result.updated).to.eql(1); const tags = await exiftool.read(f, { ignoreZeroZeroLatLon, }); if (ignoreZeroZeroLatLon && GPSLatitude === 0 && GPSLongitude === 0) { (0, _chai_spec_1.expect)(tags.GPSLatitude).to.eql(undefined); (0, _chai_spec_1.expect)(tags.GPSLongitude).to.eql(undefined); } else { (0, _chai_spec_1.expect)(tags.GPSLatitude).to.be.closeTo(GPSLatitude, 0.001); (0, _chai_spec_1.expect)(tags.GPSLongitude).to.be.closeTo(GPSLongitude, 0.001); } }); } } }); } it("round-trips a struct tag", async () => { const struct = [ { RegItemId: "item 1", RegOrgId: "org 1" }, { RegEntryRole: "role 2", RegOrgId: "org 2" }, ]; const f = await dest(); await exiftool.write(f, { RegistryID: struct }); const tags = await exiftool.read(f); (0, _chai_spec_1.expect)(tags.RegistryID).to.eql(struct); }); it("rejects setting to a non-time value", async () => { const src = await dest(); (0, _chai_spec_1.expect)((await exiftool.write(src, { DateTimeOriginal: "this is not a time", })).warnings?.join("\n")).to.match(/Invalid date\/time/); }); it("rejects an invalid numeric Orientation", async () => { const src = await dest(); (0, _chai_spec_1.expect)((await exiftool.write(src, { "Orientation#": -1 })).warnings?.join("\n")).to.match(/Value below int16u minimum/i); }); it("rejects an invalid string Orientation", async () => { const src = await dest(); (0, _chai_spec_1.expect)((await exiftool.write(src, { Orientation: "this isn't a valid orientation", })).warnings?.join("\n")).to.be.match(/Can't convert IFD0:Orientation/i); }); it("tags case-insensitively", async () => { const src = await dest(); await exiftool.write(src, { rating: 12 }, [ "-overwrite_original", ]); const t = (await exiftool.read(src)); // this should compile... (0, _chai_spec_1.expect)(t.rating).to.eql(undefined); // but ExifTool will have done the conversion to "Rating": (0, _chai_spec_1.expect)(t.Rating).to.eql(12); }); it("rejects un-writable tags", async () => { const src = await dest(); (0, _chai_spec_1.expect)((await exiftool.write(src, { ImageOffset: 12345, })).warnings?.join("\n")).to.match(/ImageOffset is not writable/i); }); it("handles deleting tags from empty files", async () => { const src = await dest(); const isSidecar = (0, Sidecars_1.isSidecarExt)(src); // if sidecar, should be empty: (0, _chai_spec_1.expect)(await (0, File_1.isFileEmpty)(src)).to.eql(isSidecar); await exiftool.write(src, { Orientation: null }); // still should be empty: (0, _chai_spec_1.expect)(await (0, File_1.isFileEmpty)(src)).to.eql(isSidecar); if (!isSidecar) { const t = await exiftool.read(src); (0, _chai_spec_1.expect)(t.Orientation).to.eql(undefined); } }); it("removes null values", async () => { const src = await dest(); const ExposureTime = "1/4567"; // NOTE: Neither XPComment nor Comment are supported by .XMP const UserComment = [ "Buenos días", "Schönen Tag", "Добрый день", "良い一日", "יום טוב", ].join(","); await exiftool.write(src, { Title: _chai_spec_1.UnicodeTestMessage, "Orientation#": 3, ExposureTime, UserComment, }); { (0, _chai_spec_1.expect)(await (0, File_1.isFileEmpty)(src)).to.eql(false); const t = await exiftool.read(src); (0, _chai_spec_1.expect)(t).to.containSubset({ Orientation: 3, ExposureTime, UserComment, }); } await exiftool.write(src, { Orientation: null }); { (0, _chai_spec_1.expect)(await (0, File_1.isFileEmpty)(src)).to.eql(false); const t = await exiftool.read(src); (0, _chai_spec_1.expect)(t.Orientation).to.eql(undefined); (0, _chai_spec_1.expect)(t).to.containSubset({ Title: _chai_spec_1.UnicodeTestMessage, ExposureTime, UserComment, }); } await exiftool.write(src, { ExposureTime: null, UserComment: null }); { (0, _chai_spec_1.expect)(await (0, File_1.isFileEmpty)(src)).to.eql(false); const t = await exiftool.read(src); (0, _chai_spec_1.expect)(t.Orientation).to.eql(undefined); (0, _chai_spec_1.expect)(t.ExposureTime).to.eql(undefined); (0, _chai_spec_1.expect)(t.UserComment).to.eql(undefined); (0, _chai_spec_1.expect)(t.Title).to.eql(_chai_spec_1.UnicodeTestMessage); } }); it("Accepts a shortcut tag", async () => { // AllDates doesn't accept millisecond precision: const date = "2018-04-17T12:34:56+08:00"; const src = await dest(); await exiftool.write(src, { AllDates: date }); const tags = await exiftool.read(src); (0, _chai_spec_1.expect)(String(tags.DateTimeOriginal)).to.eql(date); (0, _chai_spec_1.expect)(String(tags.CreateDate)).to.eql(date); (0, _chai_spec_1.expect)(String(tags.ModifyDate)).to.eql(date); return; }); it("rejects unknown files", () => { return (0, _chai_spec_1.expect)(exiftool.write((0, node_path_1.join)((0, node_os_1.tmpdir)(), ".nonexistant-" + Date.now()), { Comment: "boom", })).to.be.rejectedWith(/ENOENT|File not found/i); }); it("rejects unknown tags", async () => { const src = await dest(); return (0, _chai_spec_1.expect)((await exiftool.write(src, { RandomTag: 123 })).warnings?.join("\n")).to.match(/Tag 'RandomTag' is not defined/); }); it("round-trips a struct tag with a ResourceEvent with primitive values", async () => { const inputValue = [ { Action: "testing", Changed: "🤷🏿‍♀️", }, ]; return assertRoundTrip({ dest: await dest(), tagName: "History", inputValue, }); }); it("round-trips a struct tag with a stringified value", async () => { const inputValue = [ { Action: "testing", Changed: "🤷🏿‍♀️", Parameters: JSON.stringify({ numeric: 123, string: "hello", meanString: "\n|\r}\t{][(), ", }), }, ]; return assertRoundTrip({ dest: await dest(), tagName: "History", inputValue, }); }); } describe("round-trip with an image", () => runRoundTripTests({ withTZ: true, dest: (name) => (0, _chai_spec_1.testImg)({ srcBasename: name, destBasename: "ĩmägë.jpg", }), // non-latin characters })); describe("round-trip with an XMP sidecar", () => runRoundTripTests({ withTZ: false, // BOO XMP DOESN'T LIKE TIMEZONES WTH dest: (ea) => (0, _chai_spec_1.testFile)((ea ?? "ïmg") + ".xmp"), // < i with diaeresis/umlaut })); describe("round-trip with an MIE sidecar", () => runRoundTripTests({ withTZ: true, dest: (ea) => (0, _chai_spec_1.testFile)((ea ?? "îmg") + ".mie"), // i with circumflex })); function mkResourceEvent(o) { return { Action: "test", Changed: "rating", InstanceID: "instance-id-" + (0, _chai_spec_1.randomChars)(), Parameters: "value-" + (0, _chai_spec_1.randomChars)(), SoftwareAgent: "PhotoStructure", When: ExifDateTime_1.ExifDateTime.now(), ...o, }; } function assertEqlResourceEvents(a, b) { if (a != null || b != null) { for (let idx = 0; idx < a.length; idx++) { (0, _chai_spec_1.expect)((0, Object_1.omit)(a[idx], "When")).to.eql((0, Object_1.omit)(b[idx], "When")); (0, _chai_spec_1.assertEqlDateish)(a[idx].When, b[idx].When); } } } async function mkXMP(nativePath, t) { const priorContents = { Copyright: "PhotoStructure, Inc. " + (0, _chai_spec_1.randomChars)(), ...t, }; await exiftool.write(nativePath, priorContents); (0, _chai_spec_1.expect)(await exiftool.read(nativePath)).to.containSubset((0, Object_1.omit)(priorContents, "History", "Versions")); } describe("appends History structs", () => { it("from no XMP", async () => { const f = await (0, _chai_spec_1.testFile)("image.xmp"); const re = mkResourceEvent(); await exiftool.write(f, { "History+": re }); // < NOT AN ARRAY // NOTE: This tests ReadTask handles History records properly: const t = (await exiftool.read(f)); assertEqlResourceEvents(t.History, [re]); }); it("from empty XMP", async () => { const f = await (0, _chai_spec_1.testFile)("image.xmp"); const re = mkResourceEvent(); await mkXMP(f); await exiftool.write(f, { "History+": [re] }); const t = (await exiftool.read(f)); assertEqlResourceEvents(t.History[0], [re]); }); it("from XMP with existing History", async () => { const f = await (0, _chai_spec_1.testFile)("image.xmp"); const re1 = mkResourceEvent({ Action: "test-1" }); const re2 = mkResourceEvent({ Action: "test-2" }); await mkXMP(f, { History: [re1] }); await exiftool.write(f, { "History+": [re2] }); const t = (await exiftool.read(f)); assertEqlResourceEvents(t.History, [re1, re2]); }); }); describe("replaces History structs", () => { it("from empty XMP", async () => { const f = await (0, _chai_spec_1.testFile)("image.xmp"); await mkXMP(f); const re = mkResourceEvent(); await exiftool.write(f, { History: [re] }); const t = (await exiftool.read(f)); assertEqlResourceEvents(t.History, [re]); }); it("from XMP with existing History", async () => { const f = await (0, _chai_spec_1.testFile)("image.xmp"); const re1 = mkResourceEvent({ Action: "test-1" }); const re2 = mkResourceEvent({ Action: "test-2" }); await mkXMP(f, { History: [re1] }); await exiftool.write(f, { History: [re2] }); const t = (await exiftool.read(f)); assertEqlResourceEvents(t.History, [re2]); }); }); function mkVersion(v) { return { Comments: "comment " + (0, _chai_spec_1.randomChars)(), Event: mkResourceEvent(), Modifier: "modifier " + (0, _chai_spec_1.randomChars)(), ModifyDate: ExifDateTime_1.ExifDateTime.now(), Version: "version " + (0, _chai_spec_1.randomChars)(), ...v, }; } function assertEqlVersions(a, b) { for (let idx = 0; idx < a.length; idx++) { const av = a[idx]; const bv = b[idx]; (0, _chai_spec_1.expect)((0, Object_1.omit)(av, "ModifyDate", "Event")).to.eql((0, Object_1.omit)(bv, "ModifyDate", "Event")); if (av.Event != null || bv.Event != null) assertEqlResourceEvents([av.Event], [bv.Event]); (0, _chai_spec_1.assertEqlDateish)(a[idx].ModifyDate, b[idx].ModifyDate); } } describe("appends Versions structs", () => { it("from no XMP", async () => { const f = await (0, _chai_spec_1.testFile)("image.xmp"); const v = mkVersion(); await exiftool.write(f, { "Versions+": v }); // < NOT AN ARRAY const t = (await exiftool.read(f)); assertEqlVersions(t.Versions, [v]); }); it("from empty XMP", async () => { const f = await (0, _chai_spec_1.testFile)("image.xmp"); await mkXMP(f); const v = mkVersion(); await exiftool.write(f, { "Versions+": v }); // < NOT AN ARRAY const t = (await exiftool.read(f)); assertEqlVersions(t.Versions, [v]); }); it("from XMP with existing History", async () => { const f = await (0, _chai_spec_1.testFile)("image.xmp"); const v1 = mkVersion({ Modifier: "event-1" }); const v2 = mkVersion({ Modifier: "event-2" }); await mkXMP(f, { Versions: [v1] }); await exiftool.write(f, { "Versions+": [v2] }); const t = (await exiftool.read(f)); assertEqlVersions(t.Versions, [v1, v2]); }); }); describe("replaces Versions structs", () => { it("from XMP with existing History", async () => { const f = await (0, _chai_spec_1.testFile)("image.xmp"); const v1 = mkVersion({ Modifier: "event-1" }); const v2 = mkVersion({ Modifier: "event-2" }); await mkXMP(f, { Versions: [v1] }); await exiftool.write(f, { Versions: v2 }); // < OH SNAP NOT AN ARRAY BUT IT STILL WORKS const t = (await exiftool.read(f)); assertEqlVersions(t.Versions, [v2]); }); }); }); } /** * @see https://github.com/photostructure/exiftool-vendored.js/issues/178 */ describe("deleteAllTags()", () => { const exiftool = new ExifTool_1.ExifTool(); after(() => (0, _chai_spec_1.end)(exiftool)); const expectedDefinedTags = [ "Make", "Model", "Software", "ExposureTime", "FNumber", "ISO", "CreateDate", "DateTimeOriginal", "LightSource", "Flash", "FocalLength", "SerialNumber", "DateTimeUTC", ]; function assertMissingGeneralTags(t) { for (const ea of expectedDefinedTags) { (0, _chai_spec_1.expect)(t).to.not.haveOwnProperty(ea); } } function assertDefinedGeneralTags(t) { for (const ea of expectedDefinedTags) { (0, _chai_spec_1.expect)(t).to.haveOwnProperty(ea); } } function isIntrinsticTag(k) { return (Tags_1.FileTagsNames.includes(k) || Tags_1.ExifToolTagsNames.includes(k) || ExifToolVendoredTags_1.ExifToolVendoredTagNames.includes(k) || ["ImageSize", "Megapixels"].includes(k)); } function expectedChangedTag(k) { return [ "CurrentIPTCDigest", "ExifByteOrder", ...Tags_1.FileTagsNames.values, ...ExifToolVendoredTags_1.ExifToolVendoredTagNames.values, ].includes(k); } it("deletes all tags by default", async () => { const img = await (0, _chai_spec_1.testImg)({ srcBasename: "oly.jpg" }); const before = await exiftool.read(img); (0, _chai_spec_1.expect)(before).to.containSubset(ImageExifData); assertDefinedGeneralTags(before); await exiftool.deleteAllTags(img); const after = await exiftool.read(img); assertMissingGeneralTags(after); (0, _chai_spec_1.expect)(after).to.not.containSubset(ImageExifData); for (const k in ImageExifData) { (0, _chai_spec_1.expect)(after).to.not.haveOwnProperty(k); } // And make sure everything else is gone: for (const k in before) { if (expectedChangedTag(k)) continue; if (isIntrinsticTag(k)) { (0, _chai_spec_1.expect)(after[k]).to.eql(before[k], "intrinsic tag " + k); } else { (0, _chai_spec_1.expect)(after).to.not.haveOwnProperty(k); } } }); for (const key in ImageExifData) { it(`deletes all tags except ${key}`, async () => { const img = await (0, _chai_spec_1.testImg)({ srcBasename: "oly.jpg" }); const before = await exiftool.read(img); (0, _chai_spec_1.expect)(before).to.containSubset(ImageExifData); assertDefinedGeneralTags(before); await exiftool.deleteAllTags(img, { retain: [key] }); const after = await exiftool.read(img); assertMissingGeneralTags(after); (0, _chai_spec_1.expect)(after).to.haveOwnProperty(key); for (const k in Object.keys(ImageExifData)) { if (k !== key) { (0, _chai_spec_1.expect)(after).to.not.haveOwnProperty(k); } } }); } it("supports deleting everything-except (issue #178)", async () => { const img = await (0, _chai_spec_1.testImg)({ srcBasename: "oly.jpg" }); const before = await exiftool.read(img); (0, _chai_spec_1.expect)(before).to.containSubset(ImageExifData); assertDefinedGeneralTags(before); await exiftool.deleteAllTags(img, { retain: Object.keys(ImageExifData) }); const after = await exiftool.read(img); assertMissingGeneralTags(after); (0, _chai_spec_1.expect)(after).to.containSubset(ImageExifData); }); it("supports creating arrays in structs", async () => { const f = await (0, _chai_spec_1.testImg)(); const regionData = { RegionInfo: { AppliedToDimensions: { W: 10, H: 10, Unit: "pixel" }, RegionList: [ { Area: { X: 0.5, Y: 0.5, W: 0.1, H: 0.1, Unit: "normalized" }, Rotation: 0, Type: "Face", Name: "randomName1", }, { Area: { X: 0.5, Y: 0.5, W: 0.1, H: 0.1, Unit: "normalized" }, Rotation: 0, Type: "Face", Name: "randomName2", }, ], }, }; await exiftool.write(f, regionData); const after = await exiftool.read(f); (0, _chai_spec_1.expect)(after).to.containSubset(regionData); }); }); }); describe("WriteTask (simpler)", () => { const exiftool = new ExifTool_1.ExifTool(); after(() => (0, _chai_spec_1.end)(exiftool)); describe("GPS coordinate writing", () => { // Test coordinates covering all hemispheres: const testCoordinates = [ { lat: 37.7749, lon: -122.4194, desc: "NW - San Francisco" }, { lat: 40.7128, lon: -74.006, desc: "NW - New York" }, { lat: -33.8688, lon: 151.2093, desc: "SE - Sydney" }, { lat: -22.9068, lon: -43.1729, desc: "SW - Rio de Janeiro" }, { lat: 35.6762, lon: 139.6503, desc: "NE - Tokyo" }, { lat: 1.3521, lon: 103.8198, desc: "NE near equator - Singapore" }, { lat: -0.1855, lon: -78.4352, desc: "SW near equator - Quito" }, ]; const fileFormats = [ { type: "JPEG", getPath: () => (0, _chai_spec_1.testImg)({ srcBasename: "noexif.jpg" }) }, { type: "MIE", getPath: () => (0, _chai_spec_1.testFile)("test.mie") }, { type: "XMP", getPath: () => (0, _chai_spec_1.testFile)("test.xmp") }, ]; for (const format of fileFormats) { describe(`${format.type} files`, () => { for (const coords of testCoordinates) { const { lat, lon, desc } = coords; const assertTags = (tags) => { // Verify latitude (0, _chai_spec_1.expect)(tags.GPSLatitude).to.be.closeTo(lat, 0.0001); if (format.type !== "MIE") { // .mie files don't get a Ref tag (!?!) (0, _chai_spec_1.expect)(tags.GPSLatitudeRef).to.equal(lat >= 0 ? "N" : "S"); } // Verify longitude (0, _chai_spec_1.expect)(tags.GPSLongitude).to.be.closeTo(lon, 0.0001); if (format.type !== "MIE") { // .mie files don't get a Ref tag (!?!) (0, _chai_spec_1.expect)(tags.GPSLongitudeRef).to.equal(lon >= 0 ? "E" : "W"); } // Verify GPSPosition format const actual = (0, CoordinateParser_1.parseCoordinates)(tags.GPSPosition); (0, _chai_spec_1.expect)(actual.latitude).to.be.closeTo(lat, 0.0001); (0, _chai_spec_1.expect)(actual.longitude).to.be.closeTo(lon, 0.0001); }; it(`writes and reads back coordinates for ${desc}`, async () => { const file = await format.getPath(); await exiftool.write(file, { GPSLatitude: lat, GPSLongitude: lon, }); assertTags(await exiftool.read(file)); }); it(`writes and reads back GPSPosition for ${desc}`, async () => { const file = await format.getPath(); await exiftool.write(file, { GPSPosition: lat + "," + lon, }); assertTags(await exiftool.read(file)); }); it(`handles clearing GPS data for ${desc}`, async () => { const file = await format.getPath(); // First write coordinates await exiftool.write(file, { Title: `Something to avoid "Can't delete all meta information" error`, GPSLatitude: lat, GPSLongitude: lon, }); // Then clear them await exiftool.write(file, { GPSLatitude: null, GPSLongitude: null, }); // Verify they're gone const tags = await exiftool.read(file); (0, _chai_spec_1.expect)(tags.GPSLatitude).to.eql(undefined); (0, _chai_spec_1.expect)(tags.GPSLongitude).to.eql(undefined); (0, _chai_spec_1.expect)(tags.GPSLatitudeRef).to.eql(undefined); (0, _chai_spec_1.expect)(tags.GPSLongitudeRef).to.eql(undefined); (0, _chai_spec_1.expect)(tags.GPSPosition).to.eql(undefined); }); } it("doesn't reject invalid latitude values", async () => { const file = await format.getPath(); await exiftool.write(file, { GPSLatitude: 91 }); // > 90 degrees const tags = await exiftool.readRaw(file); (0, _chai_spec_1.expect)(tags.GPSLatitude).to.eql(`91 deg 0' 0.00" N`); }); it("doesn't reject invalid longitude values", async () => { const file = await format.getPath(); await exiftool.write(file, { GPSLongitude: 181 }); // > 180 degrees const tags = await exiftool.readRaw(file); (0, _chai_spec_1.expect)(tags.GPSLongitude).to.eql(`181 deg 0' 0.00" E`); }); }); } }); }); //# sourceMappingURL=WriteTask.spec.js.map