"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