exiftool-vendored
Version:
Efficient, cross-platform access to ExifTool
518 lines • 26.4 kB
JavaScript
;
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