UNPKG

exiftool-vendored

Version:
518 lines 26.4 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const _fs = __importStar(require("node:fs")); const _path = __importStar(require("node:path")); const BinaryField_1 = require("./BinaryField"); const DefaultMaxProcs_1 = require("./DefaultMaxProcs"); const ExifDate_1 = require("./ExifDate"); const ExifDateTime_1 = require("./ExifDateTime"); const ExifTime_1 = require("./ExifTime"); const ExifTool_1 = require("./ExifTool"); const IsWin32_1 = require("./IsWin32"); const JSON_1 = require("./JSON"); const Object_1 = require("./Object"); const String_1 = require("./String"); const Times_1 = require("./Times"); const _chai_spec_1 = require("./_chai.spec"); function normalize(tagNames) { return tagNames .filter((i) => i !== "FileInodeChangeDate" && i !== "FileCreateDate") .sort(); } function posixPath(path) { return path.split(_path.sep).join("/"); } after(() => (0, _chai_spec_1.end)(ExifTool_1.exiftool)); describe("ExifTool", function () { this.timeout(15000); this.slow(100); const truncated = _path.join(__dirname, "..", "test", "truncated.jpg"); const noexif = _path.join(__dirname, "..", "test", "noexif.jpg"); const img = _path.join(__dirname, "..", "test", "img.jpg"); const img2 = _path.join(__dirname, "..", "test", "ExifTool.jpg"); const img3 = _path.join(__dirname, "..", "test", "with_thumb.jpg"); const nonEnglishImg = _path.join(__dirname, "..", "test", "中文.jpg"); const packageJson = JSON.parse(_fs.readFileSync(_path.join(__dirname, "..", "package.json"), "utf8")); function expectedExiftoolVersion(flavor = "pl") { const vendorVersion = packageJson.optionalDependencies["exiftool-vendored." + flavor]; // Everyone's a monster here: // * semver is unhappy about 0-padded version numbers // * exiftool bumps the major version only when minor hits 99 (!!) // vendorVersion might have a ^ or ~ or something else as a prefix, so get // rid of that: return vendorVersion .replace(/^[^.\d]+/, "") .split(".") .slice(0, 2) .map((ea) => (0, String_1.leftPad)(ea, 2, "0")) .join("."); } it("perl and win32 versions match", () => { const pl = expectedExiftoolVersion("pl"); const exe = expectedExiftoolVersion("exe"); (0, _chai_spec_1.expect)(pl).to.eql(exe); }); it("exports a singleton instance", () => { // don't call any methods that actually results in spinning up a child // proc: (0, _chai_spec_1.expect)(ExifTool_1.exiftool.options.maxProcs).to.eql(DefaultMaxProcs_1.DefaultMaxProcs); }); const ignoreShebangs = []; if (ExifTool_1.exiftool.options.ignoreShebang) { // If the default is true, we can only test true. ignoreShebangs.push(true); } else { ignoreShebangs.push(false); if (!(0, IsWin32_1.isWin32)()) ignoreShebangs.push(true); } // We're running all these with both geolocation on and off to validate // nothing explodes. for (const ignoreShebang of ignoreShebangs) { for (const geolocation of [true, false]) { describe("exiftool(" + JSON.stringify({ ignoreShebang, geolocation }) + ")", () => { let et; before(() => (et = new ExifTool_1.ExifTool({ maxProcs: 2, ignoreShebang, geolocation, }))); after(() => (0, _chai_spec_1.end)(et)); it("returns the correct version", async function () { this.slow(500); return (0, _chai_spec_1.expect)(await et.version()).to.eql(expectedExiftoolVersion()); }); it("returns a reasonable value for MaxProcs", () => { // 64 cpus on my dream laptop (0, _chai_spec_1.expect)(DefaultMaxProcs_1.DefaultMaxProcs).to.be.within(1, 64); }); it("returns expected results for a given file", async function () { this.slow(500); return (0, _chai_spec_1.expect)(et.read(img).then((tags) => tags.Model)).to.eventually.eql("iPhone 7 Plus"); }); it("returns raw tag values", async () => { return (0, _chai_spec_1.expect)(et.readRaw(img, ["-Make", "-Model"])).to.eventually.eql({ Make: "Apple", Model: "iPhone 7 Plus", SourceFile: posixPath(img), errors: [], warnings: [], }); // and nothing else }); it("returns expected results for a given file with non-english filename", async function () { this.slow(500); return (0, _chai_spec_1.expect)(et.read(nonEnglishImg).then((tags) => tags.Model)).to.eventually.eql("iPhone 7 Plus"); }); it("renders Orientation as numbers", async () => { const tags = await et.read(img); (0, _chai_spec_1.expect)(tags.Orientation).to.eql(1); return; }); it("omits OriginalImage{Width,Height} by default", async () => { return (0, _chai_spec_1.expect)(await et.read(img2)).to.containSubset({ Keywords: "jambalaya", ImageHeight: 8, ImageWidth: 8, OriginalImageHeight: undefined, OriginalImageWidth: undefined, }); }); async function readBinaryFieldSizes(filename) { return (0, Object_1.fromEntries)(Object.entries(await et.read(filename)) .filter(([, v]) => v instanceof BinaryField_1.BinaryField) .map(([k, v]) => [k, v.bytes])); } it("parses expected binary fields from " + img2, async () => { // Validate with `node_modules/exiftool-vendored.pl/bin/exiftool -j -struct test/ExifTool.jpg | grep "Binary data" | sed 's/\(.*\): "(Binary data \([0-9]\+\) bytes,.*"/\1: \2/' |sort` (0, _chai_spec_1.expect)(await readBinaryFieldSizes(img2)).to.eql({ BlueTRC: 14, FreeBytes: 12, GreenTRC: 14, PreviewImage: 1039273, RatioImage: 19, RedTRC: 14, SBAExposureRecord: 368, ScreenNail: 5917, ThumbnailImage: 1558, UserAdjSBA_RGBShifts: 5, }); }); it("parses expected binary fields from " + img3, async () => { // Validate with `node_modules/exiftool-vendored.pl/bin/exiftool -j -struct test/with_thumb.jpg | grep "Binary data" | sed 's/\(.*\): "(Binary data \([0-9]\+\) bytes,.*"/\1: \2/' |sort` (0, _chai_spec_1.expect)(await readBinaryFieldSizes(img3)).to.containSubset({ BlueTRC: 32, GreenTRC: 32, RedTRC: 32, ThumbnailImage: 5133, }); }); it("extracts OriginalImage{Width,Height} if deprecated [] is provided to override the -fast option", async () => { return (0, _chai_spec_1.expect)(await et.read(img2, [])).to.containSubset({ Keywords: "jambalaya", ImageHeight: 8, ImageWidth: 8, OriginalImageHeight: 16, OriginalImageWidth: 16, }); }); it("extracts OriginalImage{Width,Height} if [] is provided to override the -fast option", async () => { return (0, _chai_spec_1.expect)(await et.read(img2, { readArgs: [] })).to.containSubset({ Keywords: "jambalaya", ImageHeight: 8, ImageWidth: 8, OriginalImageHeight: 16, OriginalImageWidth: 16, }); }); it("returns warning for a truncated file", async () => { return (0, _chai_spec_1.expect)(await et.read(truncated)).to.containSubset({ FileName: "truncated.jpg", FileSize: "1000 bytes", FileType: "JPEG", FileTypeExtension: "jpg", MIMEType: "image/jpeg", Warning: "JPEG format error", }); }); it("returns no exif metadata for an image with no headers", () => { return (0, _chai_spec_1.expect)(et.read(noexif).then((tags) => normalize(Object.keys(tags)))).to.become(normalize([ "BitsPerSample", "ColorComponents", "Directory", "EncodingProcess", "ExifToolVersion", "FileAccessDate", "FileModifyDate", "FileName", "FilePermissions", "FileSize", "FileType", "FileTypeExtension", "ImageHeight", "ImageSize", "ImageWidth", "Megapixels", "MIMEType", "SourceFile", "YCbCrSubSampling", "errors", "warnings", ])); }); it("returns error for missing file", () => { return (0, _chai_spec_1.expect)(et.read("bogus")).to.eventually.be.rejectedWith(/ENOENT|file not found/i); }); it("works with text files", async () => { return (0, _chai_spec_1.expect)(await et.read(__filename)).to.containSubset({ FileType: "TXT", FileTypeExtension: "txt", MIMEType: "text/plain", // may be utf-8 or us-ascii, but we don't really care. // MIMEEncoding: "us-ascii", errors: [], }); }); function assertReasonableTags(tags) { tags.forEach((tag) => { (0, _chai_spec_1.expect)(tag.errors).to.eql([]); (0, _chai_spec_1.expect)(tag.MIMEType).to.eql("image/jpeg"); (0, _chai_spec_1.expect)(tag.GPSLatitude).to.be.within(-90, 90); (0, _chai_spec_1.expect)(tag.GPSLongitude).to.be.within(-180, 180); }); } it("ends procs when they've run > maxTasksPerProcess", async function () { const maxProcs = 5; const maxTasksPerProcess = 8; const et2 = new ExifTool_1.ExifTool({ maxProcs, maxTasksPerProcess }); const iters = maxProcs * maxTasksPerProcess; // Warm up the children: const promises = (0, Times_1.times)(iters, () => et2.read(img)); const tags = await Promise.all(promises); // I don't want to expose the .batchCluster field as part of the public API: const bc = et2["batchCluster"]; (0, _chai_spec_1.expect)(bc.spawnedProcCount).to.be.gte(maxProcs); (0, _chai_spec_1.expect)(bc.meanTasksPerProc).to.be.within(maxTasksPerProcess / 2, maxTasksPerProcess); assertReasonableTags(tags); await (0, _chai_spec_1.end)(et2); (0, _chai_spec_1.expect)(et2.pids).to.eql([]); return; }); it("ends with multiple procs", async function () { const maxProcs = 4; const et2 = new ExifTool_1.ExifTool({ maxProcs }); try { const tasks = await Promise.all((0, Times_1.times)(maxProcs * 4, () => et2.read(img3))); tasks.forEach((t) => (0, _chai_spec_1.expect)(t).to.include({ // SourceFile: img3, Don't include SourceFile, because it's wonky on windows. :\ MIMEType: "image/jpeg", Model: "Pixel", ImageWidth: 4048, ImageHeight: 3036, })); await (0, _chai_spec_1.end)(et2); (0, _chai_spec_1.expect)(et2.pids).to.eql([]); } finally { await et2.end(); } return; }); it("images with warnings can still be written to", async function () { const img = await (0, _chai_spec_1.testImg)({ srcBasename: "bad-exif-ifd.jpg" }); const s = "2022-11-01T19:56:34.875"; await et.write(img, { AllDates: s }); // Disable inferTimezoneFromDatestamps to get the raw datetime value // (this test isn't about timezone inference) const t = await et.read(img, { inferTimezoneFromDatestamps: false, }); (0, _chai_spec_1.expect)(t.SubSecCreateDate?.toString()).to.eql(s); }); it("invalid images throw errors on write", async function () { const badImg = await (0, _chai_spec_1.testImg)({ srcBasename: "truncated.jpg" }); return (0, _chai_spec_1.expect)(et.write(badImg, { AllDates: new Date().toISOString() })).to.be.rejectedWith(/Corrupted JPEG image/); }); it("reads from a dSLR", async () => { const t = await et.read("./test/oly.jpg"); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(t)).to.containSubset({ Aperture: 5, CreateDate: "2014-07-19T12:05:19-07:00", DateTimeOriginal: "2014-07-19T12:05:19-07:00", ExifImageHeight: 2400, ExifImageWidth: 3200, ExposureProgram: "Program AE", ExposureTime: "1/320", FNumber: 5, ISO: 200, LensInfo: "12-40mm f/2.8", LensModel: "OLYMPUS M.12-40mm F2.8", Make: "OLYMPUS IMAGING CORP.", MaxApertureValue: 2.8, MIMEType: "image/jpeg", Model: "E-M1", ModifyDate: "2014-07-19T12:05:19-07:00", Orientation: 1, SensorTemperature: "80.3 C", tz: "UTC-7", tzSource: "offset between DateTimeOriginal and DateTimeUTC", }); }); it("reads from a smartphone with GPS", async () => { const t = await et.read("./test/pixel.jpg"); (0, _chai_spec_1.expect)(t).to.containSubset({ MIMEType: "image/jpeg", Make: "Google", Model: "Pixel", ExposureTime: "1/831", FNumber: 2, ExposureProgram: "Program AE", ISO: 50, Aperture: 2, MaxApertureValue: 2, ExifImageWidth: 4048, ExifImageHeight: 3036, // dunes beach: GPSLatitude: 37.483667, GPSLongitude: -122.452094, GPSLatitudeRef: "N", GPSLongitudeRef: "W", GPSAltitude: -47, tz: "America/Los_Angeles", }); (0, _chai_spec_1.expect)(t.SubSecDateTimeOriginal?.toString()).to.eql("2016-12-13T09:05:27.120-08:00"); if (geolocation) { (0, _chai_spec_1.expect)(t).to.containSubset({ GeolocationBearing: 318, GeolocationCity: "El Granada", GeolocationCountry: "United States", GeolocationCountryCode: "US", GeolocationDistance: "2.60 km", GeolocationFeatureCode: "PPL", GeolocationPopulation: 5500, GeolocationPosition: "37.5027 -122.4694", GeolocationRegion: "California", GeolocationSubregion: "San Mateo County", GeolocationTimeZone: "America/Los_Angeles", }); } else { for (const ea of [ "GeolocationBearing", "GeolocationCity", "GeolocationCountry", "GeolocationCountryCode", "GeolocationDistance", "GeolocationFeatureCode", "GeolocationPopulation", "GeolocationPosition", "GeolocationRegion", "GeolocationSubregion", "GeolocationTimeZone", ]) { (0, _chai_spec_1.expect)(t).to.not.have.property(ea); } } }); it("reads from a directory with dots", async () => { const dots = await (0, _chai_spec_1.testImg)({ srcBasename: "img.jpg", parentDir: "2019.05.28", }); const tags = await et.read(dots); (0, _chai_spec_1.expect)((0, _chai_spec_1.renderTagsWithISO)(tags)).to.containSubset({ DateTimeCreated: "2016-08-12T13:28:50+08:00", DateTimeOriginal: "2016-08-12T13:28:50.728+08:00", Description: "Prior Title", FNumber: 1.8, GPSLatitudeRef: "N", GPSLongitudeRef: "E", Make: "Apple", MIMEType: "image/jpeg", Model: "iPhone 7 Plus", Subject: ["Test", "examples", "beach"], SubSecCreateDate: "2016-08-12T13:28:50.728+08:00", SubSecDateTimeOriginal: "2016-08-12T13:28:50.728+08:00", SubSecTimeDigitized: 728, SubSecTimeOriginal: 728, tz: "Asia/Hong_Kong", tzSource: et.options.geolocation ? "GeolocationTimeZone" : "GPSLatitude/GPSLongitude", }); }); describe("deleteAllTags", () => { // These tags should be removed: const expectedBeforeTags = [ "ApertureValue", "DateCreated", "DateTimeOriginal", "Flash", "GPSAltitude", "GPSLatitude", "GPSLongitude", "GPSTimeStamp", "LensInfo", "Make", "Model", "ShutterSpeedValue", "TimeCreated", "XPKeywords", ]; // These are intrinsic fields that are expected to remain: const expectedAfterTags = [ "BitsPerSample", "ColorComponents", "Directory", "EncodingProcess", "errors", "ExifToolVersion", "FileAccessDate", "FileModifyDate", "FileName", "FilePermissions", "FileSize", "FileType", "FileTypeExtension", "ImageHeight", "ImageSize", "ImageWidth", "Megapixels", "MIMEType", "SourceFile", "YCbCrSubSampling", ]; function assertMetadataWipe(after) { const afterKeys = (0, Object_1.keys)(after).sort(); expectedBeforeTags.forEach((ea) => (0, _chai_spec_1.expect)(afterKeys).to.not.include(ea)); expectedAfterTags.forEach((ea) => (0, _chai_spec_1.expect)(afterKeys).to.include(ea)); } it("removes all metadata tags", async () => { const f = await (0, _chai_spec_1.testImg)(); const before = await et.read(f); // This is just a sample of additional tags that are expected to be removed: { const beforeKeys = (0, Object_1.keys)(before); [...expectedBeforeTags, ...expectedAfterTags].forEach((ea) => (0, _chai_spec_1.expect)(beforeKeys).to.include(ea)); } await et.deleteAllTags(f); assertMetadataWipe(await et.read(f)); }); it("issue #119", async () => { const f = await (0, _chai_spec_1.testImg)({ srcBasename: "delete-test.jpg" }); await et.deleteAllTags(f); assertMetadataWipe(await et.read(f)); }); }); }); } } describe("parseJSON", () => { it("round-trips", async () => { const t = await ExifTool_1.exiftool.read(img3); function validate(ea) { (0, _chai_spec_1.expect)(ea.SubSecCreateDate.constructor).to.eql(ExifDateTime_1.ExifDateTime); (0, _chai_spec_1.expect)(ea.GPSTimeStamp.constructor).to.eql(ExifTime_1.ExifTime); (0, _chai_spec_1.expect)(ea.GPSDateStamp.constructor).to.eql(ExifDate_1.ExifDate); (0, _chai_spec_1.expect)(ea.ThumbnailImage.constructor).to.eql(BinaryField_1.BinaryField); (0, _chai_spec_1.expect)({ datetime: ea.SubSecCreateDate?.toString(), gpstime: ea.GPSTimeStamp?.toString(), gpsdate: ea.GPSDateStamp?.toString(), thumbBytes: ea.ThumbnailImage?.bytes, }).to.eql({ datetime: "2017-12-22T17:08:35.363-08:00", gpstime: "01:08:22+00:00", gpsdate: "2017-12-23", thumbBytes: 5133, }); // Verify that all primitive types are as expected: (0, _chai_spec_1.expect)(ea.ISO).to.eql(60); (0, _chai_spec_1.expect)(ea.FNumber).to.be.closeTo(2.0, 0.01); (0, _chai_spec_1.expect)(ea.Contrast).to.eql("Normal"); (0, _chai_spec_1.expect)(ea.Keywords).to.eql(["red fish", "blue fish"]); } validate(t); const t2 = (0, JSON_1.parseJSON)(JSON.stringify(t)); validate(t2); (0, _chai_spec_1.expect)(t2.SubSecCreateDate).to.eql(t.SubSecCreateDate); (0, _chai_spec_1.expect)(t2.GPSDateTime).to.eql(t.GPSDateTime); (0, _chai_spec_1.expect)(t2.GPSDateStamp).to.eql(t.GPSDateStamp); (0, _chai_spec_1.expect)(t2.ThumbnailImage).to.eql(t.ThumbnailImage); }); }); }); //# sourceMappingURL=ExifTool.spec.js.map