diff --git a/binding.gyp b/binding.gyp index f20036a28..2624ed62a 100644 --- a/binding.gyp +++ b/binding.gyp @@ -86,6 +86,7 @@ 'sources': [ 'src/common.cc', 'src/metadata.cc', + 'src/stats.cc', 'src/operations.cc', 'src/pipeline.cc', 'src/sharp.cc', diff --git a/docs/api-input.md b/docs/api-input.md index 98ebb0159..9006c8a0a 100644 --- a/docs/api-input.md +++ b/docs/api-input.md @@ -4,6 +4,7 @@ - [clone](#clone) - [metadata](#metadata) +- [stats](#stats) - [limitInputPixels](#limitinputpixels) - [sequentialRead](#sequentialread) @@ -67,6 +68,42 @@ image Returns **([Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)> | Sharp)** +## stats + +Access to pixel-derived image statistics for every channel in the image +A Promises/A+ promise is returned when `callback` is not provided. + +- `channels`: Array of channel statistics for each channel in the image. Each channel statistic contains + - `min` (minimum value in the channel) + - `max` (maximum value in the channel) + - `sum` (sum of all values in a channel) + - `squaresSum` (sum of squared values in a channel) + - `mean` (mean of the values in a channel) + - `stdev` (standard deviation for the values in a channel) + - `minX` (x-coordinate of one of the pixel where the minimum lies) + - `minY` (y-coordinate of one of the pixel where the minimum lies) + - `maxX` (x-coordinate of one of the pixel where the maximum lies) + - `maxY` (y-coordinate of one of the pixel where the maximum lies) +- `isOpaque`: Value to identify if the image is opaque or transparent, based on the presence and use of alpha channel + +**Parameters** + +- `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** called with the arguments `(err, metadata)` + +**Examples** + +```javascript +const image = sharp(inputJpg); +image + .stats() + .then(function(stats) { + // stats contains the channel-wise statistics array and the isOpaque value + }) +``` + +Returns **([Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)> | Sharp)** + + ## limitInputPixels Do not process input images where the number of pixels (width _ height) exceeds this limit. diff --git a/lib/input.js b/lib/input.js index 3a0f534ce..37e52b965 100644 --- a/lib/input.js +++ b/lib/input.js @@ -230,6 +230,46 @@ function metadata (callback) { } } +function stats (callback) { + const that = this; + if (is.fn(callback)) { + if (this._isStreamInput()) { + this.on('finish', function () { + that._flattenBufferIn(); + sharp.stats(that.options, callback); + }); + } else { + sharp.stats(this.options, callback); + } + return this; + } else { + if (this._isStreamInput()) { + return new Promise(function (resolve, reject) { + that.on('finish', function () { + that._flattenBufferIn(); + sharp.stats(that.options, function (err, stats) { + if (err) { + reject(err); + } else { + resolve(stats); + } + }); + }); + }); + } else { + return new Promise(function (resolve, reject) { + sharp.stats(that.options, function (err, stats) { + if (err) { + reject(err); + } else { + resolve(stats); + } + }); + }); + } + } +} + /** * Do not process input images where the number of pixels (width * height) exceeds this limit. * Assumes image dimensions contained in the input metadata can be trusted. @@ -281,6 +321,7 @@ module.exports = function (Sharp) { // Public clone, metadata, + stats, limitInputPixels, sequentialRead ].forEach(function (f) { diff --git a/src/sharp.cc b/src/sharp.cc index c9b0dd180..95d0062e4 100644 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -20,6 +20,7 @@ #include "metadata.h" #include "pipeline.h" #include "utilities.h" +#include "stats.h" NAN_MODULE_INIT(init) { vips_init("sharp"); @@ -46,6 +47,8 @@ NAN_MODULE_INIT(init) { Nan::GetFunction(Nan::New(format)).ToLocalChecked()); Nan::Set(target, Nan::New("_maxColourDistance").ToLocalChecked(), Nan::GetFunction(Nan::New(_maxColourDistance)).ToLocalChecked()); + Nan::Set(target, Nan::New("stats").ToLocalChecked(), + Nan::GetFunction(Nan::New(stats)).ToLocalChecked()); } NODE_MODULE(sharp, init) diff --git a/src/stats.cc b/src/stats.cc new file mode 100644 index 000000000..052506ae2 --- /dev/null +++ b/src/stats.cc @@ -0,0 +1,182 @@ +// Copyright 2013, 2014, 2015, 2016, 2017 Lovell Fuller and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include +#include +#include + +#include "common.h" +#include "stats.h" + +class StatsWorker : public Nan::AsyncWorker { + public: + StatsWorker( + Nan::Callback *callback, StatsBaton *baton, Nan::Callback *debuglog, + std::vector> const buffersToPersist) : + Nan::AsyncWorker(callback), baton(baton), debuglog(debuglog), + buffersToPersist(buffersToPersist) { + // Protect Buffer objects from GC, keyed on index + std::accumulate(buffersToPersist.begin(), buffersToPersist.end(), 0, + [this](uint32_t index, v8::Local const buffer) -> uint32_t { + SaveToPersistent(index, buffer); + return index + 1; + }); + } + ~StatsWorker() {} + + const int STAT_MIN_INDEX = 0; + const int STAT_MAX_INDEX = 1; + const int STAT_SUM_INDEX = 2; + const int STAT_SQ_SUM_INDEX = 3; + const int STAT_MEAN_INDEX = 4; + const int STAT_STDEV_INDEX = 5; + const int STAT_MINX_INDEX = 6; + const int STAT_MINY_INDEX = 7; + const int STAT_MAXX_INDEX = 8; + const int STAT_MAXY_INDEX = 9; + + void Execute() { + // Decrement queued task counter + g_atomic_int_dec_and_test(&sharp::counterQueue); + using Nan::New; + using Nan::Set; + using sharp::MaximumImageAlpha; + + vips::VImage image; + vips::VImage stats; + sharp::ImageType imageType = sharp::ImageType::UNKNOWN; + + try { + std::tie(image, imageType) = OpenInput(baton->input, VIPS_ACCESS_SEQUENTIAL); + } catch (vips::VError const &err) { + (baton->err).append(err.what()); + } + if (imageType != sharp::ImageType::UNKNOWN) { + try { + stats = image.stats(); + int bands = image.bands(); + double const max = MaximumImageAlpha(image.interpretation()); + for (int b = 1; b <= bands; b++) { + ChannelStats cStats(stats.getpoint(STAT_MIN_INDEX, b).front(), stats.getpoint(STAT_MAX_INDEX, b).front(), + stats.getpoint(STAT_SUM_INDEX, b).front(), stats.getpoint(STAT_SQ_SUM_INDEX, b).front(), + stats.getpoint(STAT_MEAN_INDEX, b).front(), stats.getpoint(STAT_STDEV_INDEX, b).front(), + stats.getpoint(STAT_MINX_INDEX, b).front(), stats.getpoint(STAT_MINY_INDEX, b).front(), + stats.getpoint(STAT_MAXX_INDEX, b).front(), stats.getpoint(STAT_MAXY_INDEX, b).front()); + baton->channelStats.push_back(cStats); + } + + // alpha layer is there and the last band i.e. alpha has its max value greater than 0) + if (sharp::HasAlpha(image) && stats.getpoint(STAT_MIN_INDEX, bands).front() != max) { + baton->isOpaque = false; + } + } catch (vips::VError const &err) { + (baton->err).append(err.what()); + } + } + + // Clean up + vips_error_clear(); + vips_thread_shutdown(); + } + + void HandleOKCallback() { + using Nan::New; + using Nan::Set; + Nan::HandleScope(); + + v8::Local argv[2] = { Nan::Null(), Nan::Null() }; + if (!baton->err.empty()) { + argv[0] = Nan::Error(baton->err.data()); + } else { + // Stats Object + v8::Local info = New(); + v8::Local channels = New(); + + std::vector::iterator it; + int i = 0; + for (it=baton->channelStats.begin() ; it < baton->channelStats.end(); it++, i++) { + v8::Local channelStat = New(); + Set(channelStat, New("min").ToLocalChecked(), New(it->min)); + Set(channelStat, New("max").ToLocalChecked(), New(it->max)); + Set(channelStat, New("sum").ToLocalChecked(), New(it->sum)); + Set(channelStat, New("squaresSum").ToLocalChecked(), New(it->squaresSum)); + Set(channelStat, New("mean").ToLocalChecked(), New(it->mean)); + Set(channelStat, New("stdev").ToLocalChecked(), New(it->stdev)); + Set(channelStat, New("minX").ToLocalChecked(), New(it->minX)); + Set(channelStat, New("minY").ToLocalChecked(), New(it->minY)); + Set(channelStat, New("maxX").ToLocalChecked(), New(it->maxX)); + Set(channelStat, New("maxY").ToLocalChecked(), New(it->maxY)); + channels->Set(i, channelStat); + } + + Set(info, New("channels").ToLocalChecked(), channels); + Set(info, New("isOpaque").ToLocalChecked(), New(baton->isOpaque)); + argv[1] = info; + } + + // Dispose of Persistent wrapper around input Buffers so they can be garbage collected + std::accumulate(buffersToPersist.begin(), buffersToPersist.end(), 0, + [this](uint32_t index, v8::Local const buffer) -> uint32_t { + GetFromPersistent(index); + return index + 1; + }); + delete baton->input; + delete baton; + + // Handle warnings + std::string warning = sharp::VipsWarningPop(); + while (!warning.empty()) { + v8::Local message[1] = { New(warning).ToLocalChecked() }; + debuglog->Call(1, message); + warning = sharp::VipsWarningPop(); + } + + // Return to JavaScript + callback->Call(2, argv); + } + + private: + StatsBaton* baton; + Nan::Callback *debuglog; + std::vector> buffersToPersist; +}; + +/* + stats(options, callback) +*/ +NAN_METHOD(stats) { + // Input Buffers must not undergo GC compaction during processing + std::vector> buffersToPersist; + + // V8 objects are converted to non-V8 types held in the baton struct + StatsBaton *baton = new StatsBaton; + v8::Local options = info[0].As(); + + // Input + baton->input = sharp::CreateInputDescriptor(sharp::AttrAs(options, "input"), buffersToPersist); + + // Function to notify of libvips warnings + Nan::Callback *debuglog = new Nan::Callback(sharp::AttrAs(options, "debuglog")); + + // Join queue for worker thread + Nan::Callback *callback = new Nan::Callback(info[1].As()); + Nan::AsyncQueueWorker(new StatsWorker(callback, baton, debuglog, buffersToPersist)); + + // Increment queued task counter + g_atomic_int_inc(&sharp::counterQueue); +} diff --git a/src/stats.h b/src/stats.h new file mode 100644 index 000000000..d4ffaad81 --- /dev/null +++ b/src/stats.h @@ -0,0 +1,64 @@ +// Copyright 2013, 2014, 2015, 2016, 2017 Lovell Fuller and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SRC_STATS_H_ +#define SRC_STATS_H_ + +#include +#include + +#include "./common.h" + +struct ChannelStats { + // stats per channel + int min; + int max; + double sum; + double squaresSum; + double mean; + double stdev; + int minX; + int minY; + int maxX; + int maxY; + + ChannelStats(): + min(0), max(0), sum(0), squaresSum(0), mean(0), stdev(0) + , minX(0), minY(0), maxX(0), maxY(0) {} + + ChannelStats(int minVal, int maxVal, double sumVal, double squaresSumVal, + double meanVal, double stdevVal, int minXVal, int minYVal, int maxXVal, int maxYVal): + min(minVal), max(maxVal), sum(sumVal), squaresSum(squaresSumVal), + mean(meanVal), stdev(stdevVal), minX(minXVal), minY(minYVal), maxX(maxXVal), maxY(maxYVal) {} +}; + +struct StatsBaton { + // Input + sharp::InputDescriptor *input; + + // Output + std::vector channelStats; + bool isOpaque; + + std::string err; + + StatsBaton(): + input(nullptr), + isOpaque(true) + {} +}; + +NAN_METHOD(stats); + +#endif // SRC_STATS_H_ diff --git a/test/fixtures/full-transparent.png b/test/fixtures/full-transparent.png new file mode 100644 index 000000000..c87eaf4a1 Binary files /dev/null and b/test/fixtures/full-transparent.png differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 2876fd87b..cc1a892e5 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -69,6 +69,7 @@ module.exports = { inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png inputPngWithTransparency: getPath('blackbug.png'), // public domain + inputPngCompleteTransparency: getPath('full-transparent.png'), inputPngWithGreyAlpha: getPath('grey-8bit-alpha.png'), inputPngWithOneColor: getPath('2x2_fdcce6.png'), inputPngWithTransparency16bit: getPath('tbgn2c16.png'), // http://www.schaik.com/pngsuite/tbgn2c16.png diff --git a/test/unit/stats.js b/test/unit/stats.js new file mode 100644 index 000000000..a98e1d083 --- /dev/null +++ b/test/unit/stats.js @@ -0,0 +1,488 @@ +'use strict'; + +const fs = require('fs'); +const assert = require('assert'); +// const exifReader = require('exif-reader'); +// const icc = require('icc'); + +const sharp = require('../../'); +const fixtures = require('../fixtures'); + +// Test Helpers +var threshold = 0.001; +function isInAcceptableRange (actual, expected) { + return actual >= ((1 - threshold) * expected) && actual <= ((1 + threshold) * expected); +} +function isInRange (actual, min, max) { + return actual >= min && actual <= max; +} +function isInteger (val) { + return Number.isInteger(val); +} + +describe('Image Stats', function () { + it('JPEG', function (done) { + sharp(fixtures.inputJpg).stats(function (err, stats) { + if (err) throw err; + + assert.strictEqual(true, stats.isOpaque); + + // red channel + assert.strictEqual(0, stats.channels[0]['min']); + assert.strictEqual(255, stats.channels[0]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['sum'], 615101275)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['squaresSum'], 83061892917)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['mean'], 101.44954540768993)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['stdev'], 58.373870588815414)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 2725)); + + // green channel + assert.strictEqual(0, stats.channels[1]['min']); + assert.strictEqual(255, stats.channels[1]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['sum'], 462824115)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['squaresSum'], 47083677255)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['mean'], 76.33425255128337)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['stdev'], 44.03023262954866)); + assert.strictEqual(true, isInteger(stats.channels[1]['minX']) && isInRange(stats.channels[1]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[1]['minY']) && isInRange(stats.channels[1]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxX']) && isInRange(stats.channels[1]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxY']) && isInRange(stats.channels[1]['maxY'], 0, 2725)); + + // blue channel + assert.strictEqual(0, stats.channels[2]['min']); + assert.strictEqual(255, stats.channels[2]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['sum'], 372986756)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['squaresSum'], 32151543524)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['mean'], 61.51724663436759)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['stdev'], 38.96702865090125)); + assert.strictEqual(true, isInteger(stats.channels[2]['minX']) && isInRange(stats.channels[2]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[2]['minY']) && isInRange(stats.channels[2]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[2]['maxX']) && isInRange(stats.channels[2]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[2]['maxY']) && isInRange(stats.channels[2]['maxY'], 0, 2725)); + + done(); + }); + }); + + it('PNG without transparency', function (done) { + sharp(fixtures.inputPng).stats(function (err, stats) { + if (err) throw err; + + assert.strictEqual(true, stats.isOpaque); + + // red channel + assert.strictEqual(0, stats.channels[0]['min']); + assert.strictEqual(255, stats.channels[0]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['sum'], 1391368230)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['squaresSum'], 354798898650)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['mean'], 238.8259925648822)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['stdev'], 62.15121915523771)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2809)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 2074)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2809)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 2074)); + done(); + }); + }); + + it('PNG with transparency', function (done) { + sharp(fixtures.inputPngWithTransparency).stats(function (err, stats) { + if (err) throw err; + assert.strictEqual(false, stats.isOpaque); + + // red channel + assert.strictEqual(0, stats.channels[0]['min']); + assert.strictEqual(255, stats.channels[0]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['sum'], 795678795)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['squaresSum'], 202898092725)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['mean'], 252.9394769668579)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['stdev'], 22.829537532816)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2048)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 1536)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2048)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 1536)); + + // green channel + assert.strictEqual(0, stats.channels[1]['min']); + assert.strictEqual(255, stats.channels[1]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['sum'], 795678795)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['squaresSum'], 202898092725)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['mean'], 252.9394769668579)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['stdev'], 22.829537532816)); + assert.strictEqual(true, isInteger(stats.channels[1]['minX']) && isInRange(stats.channels[1]['minX'], 0, 2048)); + assert.strictEqual(true, isInteger(stats.channels[1]['minY']) && isInRange(stats.channels[1]['minY'], 0, 1536)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxX']) && isInRange(stats.channels[1]['maxX'], 0, 2048)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxY']) && isInRange(stats.channels[1]['maxY'], 0, 1536)); + + // blue channel + assert.strictEqual(0, stats.channels[2]['min']); + assert.strictEqual(255, stats.channels[2]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['sum'], 795678795)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['squaresSum'], 202898092725)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['mean'], 252.9394769668579)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['stdev'], 22.829537532816)); + assert.strictEqual(true, isInteger(stats.channels[2]['minX']) && isInRange(stats.channels[2]['minX'], 0, 2048)); + assert.strictEqual(true, isInteger(stats.channels[2]['minY']) && isInRange(stats.channels[2]['minY'], 0, 1536)); + assert.strictEqual(true, isInteger(stats.channels[2]['maxX']) && isInRange(stats.channels[2]['maxX'], 0, 2048)); + assert.strictEqual(true, isInteger(stats.channels[2]['maxY']) && isInRange(stats.channels[2]['maxY'], 0, 1536)); + + // alpha channel + assert.strictEqual(0, stats.channels[3]['min']); + assert.strictEqual(255, stats.channels[3]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[3]['sum'], 5549142)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[3]['squaresSum'], 1333571132)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[3]['mean'], 1.7640247344970703)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[3]['stdev'], 20.51387814157297)); + assert.strictEqual(true, isInteger(stats.channels[3]['minX']) && isInRange(stats.channels[3]['minX'], 0, 2048)); + assert.strictEqual(true, isInteger(stats.channels[3]['minY']) && isInRange(stats.channels[3]['minY'], 0, 1536)); + assert.strictEqual(true, isInteger(stats.channels[3]['maxX']) && isInRange(stats.channels[3]['maxX'], 0, 2048)); + assert.strictEqual(true, isInteger(stats.channels[3]['maxY']) && isInRange(stats.channels[3]['maxY'], 0, 1536)); + + done(); + }); + }); + + it('PNG fully transparent', function (done) { + sharp(fixtures.inputPngCompleteTransparency).stats(function (err, stats) { + if (err) throw err; + + assert.strictEqual(false, stats.isOpaque); + + // alpha channel + assert.strictEqual(0, stats.channels[3]['min']); + assert.strictEqual(0, stats.channels[3]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[3]['sum'], 0)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[3]['squaresSum'], 0)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[3]['mean'], 0)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[3]['stdev'], 0)); + assert.strictEqual(true, isInteger(stats.channels[3]['minX']) && isInRange(stats.channels[3]['minX'], 0, 300)); + assert.strictEqual(true, isInteger(stats.channels[3]['minY']) && isInRange(stats.channels[3]['minY'], 0, 300)); + assert.strictEqual(true, isInteger(stats.channels[3]['maxX']) && isInRange(stats.channels[3]['maxX'], 0, 300)); + assert.strictEqual(true, isInteger(stats.channels[3]['maxY']) && isInRange(stats.channels[3]['maxY'], 0, 300)); + + done(); + }); + }); + + it('Tiff', function (done) { + sharp(fixtures.inputTiff).stats(function (err, stats) { + if (err) throw err; + assert.strictEqual(true, stats.isOpaque); + + // red channel + assert.strictEqual(0, stats.channels[0]['min']); + assert.strictEqual(255, stats.channels[0]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['sum'], 1887266220)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['squaresSum'], 481252886100)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['mean'], 235.81772349417824)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['stdev'], 67.25712856093298)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2464)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 3248)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2464)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 3248)); + + done(); + }); + }); + + it('WebP', function (done) { + sharp(fixtures.inputWebP).stats(function (err, stats) { + if (err) throw err; + + assert.strictEqual(true, stats.isOpaque); + + // red channel + assert.strictEqual(0, stats.channels[0]['min']); + assert.strictEqual(255, stats.channels[0]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['sum'], 83291370)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['squaresSum'], 11379783198)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['mean'], 105.36169496842616)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['stdev'], 57.39412151419967)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 1024)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 772)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 1024)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 772)); + + // green channel + assert.strictEqual(0, stats.channels[1]['min']); + assert.strictEqual(255, stats.channels[1]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['sum'], 120877425)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['squaresSum'], 20774687595)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['mean'], 152.9072025279307)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['stdev'], 53.84143349689916)); + assert.strictEqual(true, isInteger(stats.channels[1]['minX']) && isInRange(stats.channels[1]['minX'], 0, 1024)); + assert.strictEqual(true, isInteger(stats.channels[1]['minY']) && isInRange(stats.channels[1]['minY'], 0, 772)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxX']) && isInRange(stats.channels[1]['maxX'], 0, 1024)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxY']) && isInRange(stats.channels[1]['maxY'], 0, 772)); + + // blue channel + assert.strictEqual(0, stats.channels[2]['min']); + assert.strictEqual(255, stats.channels[2]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['sum'], 138938859)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['squaresSum'], 28449125593)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['mean'], 175.75450711423252)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['stdev'], 71.39929031070358)); + assert.strictEqual(true, isInteger(stats.channels[2]['minX']) && isInRange(stats.channels[2]['minX'], 0, 1024)); + assert.strictEqual(true, isInteger(stats.channels[2]['minY']) && isInRange(stats.channels[2]['minY'], 0, 772)); + assert.strictEqual(true, isInteger(stats.channels[2]['maxX']) && isInRange(stats.channels[2]['maxX'], 0, 1024)); + assert.strictEqual(true, isInteger(stats.channels[2]['maxY']) && isInRange(stats.channels[2]['maxY'], 0, 772)); + + done(); + }); + }); + + it('GIF', function (done) { + sharp(fixtures.inputGif).stats(function (err, stats) { + if (err) throw err; + + assert.strictEqual(true, stats.isOpaque); + + // red channel + assert.strictEqual(35, stats.channels[0]['min']); + assert.strictEqual(254, stats.channels[0]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['sum'], 56088385)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['squaresSum'], 8002132113)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['mean'], 131.53936444652908)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['stdev'], 38.26389131415863)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 800)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 533)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 800)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 533)); + + // green channel + assert.strictEqual(43, stats.channels[1]['min']); + assert.strictEqual(255, stats.channels[1]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['sum'], 58612156)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['squaresSum'], 8548344254)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['mean'], 137.45815196998123)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['stdev'], 33.955424103758205)); + assert.strictEqual(true, isInteger(stats.channels[1]['minX']) && isInRange(stats.channels[1]['minX'], 0, 800)); + assert.strictEqual(true, isInteger(stats.channels[1]['minY']) && isInRange(stats.channels[1]['minY'], 0, 533)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxX']) && isInRange(stats.channels[1]['maxX'], 0, 800)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxY']) && isInRange(stats.channels[1]['maxY'], 0, 533)); + + // blue channel + assert.strictEqual(51, stats.channels[2]['min']); + assert.strictEqual(254, stats.channels[2]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['sum'], 49628525)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['squaresSum'], 6450556071)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['mean'], 116.38959896810506)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['stdev'], 39.7669551046809)); + assert.strictEqual(true, isInteger(stats.channels[2]['minX']) && isInRange(stats.channels[2]['minX'], 0, 800)); + assert.strictEqual(true, isInteger(stats.channels[2]['minY']) && isInRange(stats.channels[2]['minY'], 0, 533)); + assert.strictEqual(true, isInteger(stats.channels[2]['maxX']) && isInRange(stats.channels[2]['maxX'], 0, 800)); + assert.strictEqual(true, isInteger(stats.channels[2]['maxY']) && isInRange(stats.channels[2]['maxY'], 0, 533)); + + done(); + }); + }); + + it('Grayscale GIF with alpha', function (done) { + sharp(fixtures.inputGifGreyPlusAlpha).stats(function (err, stats) { + if (err) throw err; + assert.strictEqual(false, stats.isOpaque); + + // gray channel + assert.strictEqual(0, stats.channels[0]['min']); + assert.strictEqual(101, stats.channels[0]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['sum'], 101)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['squaresSum'], 10201)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['mean'], 50.5)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['stdev'], 71.4177848998413)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 1)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 1)); + + // alpha channel + assert.strictEqual(0, stats.channels[1]['min']); + assert.strictEqual(255, stats.channels[1]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['sum'], 255)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['squaresSum'], 65025)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['mean'], 127.5)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['stdev'], 180.31222920256963)); + assert.strictEqual(true, isInteger(stats.channels[1]['minX']) && isInRange(stats.channels[1]['minX'], 0, 2)); + assert.strictEqual(true, isInteger(stats.channels[1]['minY']) && isInRange(stats.channels[1]['minY'], 0, 1)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxX']) && isInRange(stats.channels[1]['maxX'], 0, 2)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxY']) && isInRange(stats.channels[1]['maxY'], 0, 1)); + + done(); + }); + }); + + it('Stream in, Callback out', function (done) { + const readable = fs.createReadStream(fixtures.inputJpg); + const pipeline = sharp().stats(function (err, stats) { + if (err) throw err; + assert.strictEqual(true, stats.isOpaque); + + // red channel + assert.strictEqual(0, stats.channels[0]['min']); + assert.strictEqual(255, stats.channels[0]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['sum'], 615101275)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['squaresSum'], 83061892917)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['mean'], 101.44954540768993)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['stdev'], 58.373870588815414)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 2725)); + + // green channel + assert.strictEqual(0, stats.channels[1]['min']); + assert.strictEqual(255, stats.channels[1]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['sum'], 462824115)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['squaresSum'], 47083677255)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['mean'], 76.33425255128337)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['stdev'], 44.03023262954866)); + assert.strictEqual(true, isInteger(stats.channels[1]['minX']) && isInRange(stats.channels[1]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[1]['minY']) && isInRange(stats.channels[1]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxX']) && isInRange(stats.channels[1]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[1]['maxY']) && isInRange(stats.channels[1]['maxY'], 0, 2725)); + + // blue channel + assert.strictEqual(0, stats.channels[2]['min']); + assert.strictEqual(255, stats.channels[2]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['sum'], 372986756)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['squaresSum'], 32151543524)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['mean'], 61.51724663436759)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['stdev'], 38.96702865090125)); + assert.strictEqual(true, isInteger(stats.channels[2]['minX']) && isInRange(stats.channels[2]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[2]['minY']) && isInRange(stats.channels[2]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[2]['maxX']) && isInRange(stats.channels[2]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[2]['maxY']) && isInRange(stats.channels[2]['maxY'], 0, 2725)); + + done(); + }); + readable.pipe(pipeline); + }); + + it('Stream in, Promise out', function () { + const pipeline = sharp(); + + fs.createReadStream(fixtures.inputJpg).pipe(pipeline); + + return pipeline.stats().then(function (stats) { + assert.strictEqual(true, stats.isOpaque); + + // red channel + assert.strictEqual(0, stats.channels[0]['min']); + assert.strictEqual(255, stats.channels[0]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['sum'], 615101275)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['squaresSum'], 83061892917)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['mean'], 101.44954540768993)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['stdev'], 58.373870588815414)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 2725)); + + // green channel + assert.strictEqual(0, stats.channels[1]['min']); + assert.strictEqual(255, stats.channels[1]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['sum'], 462824115)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['squaresSum'], 47083677255)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['mean'], 76.33425255128337)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['stdev'], 44.03023262954866)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 2725)); + + // blue channel + assert.strictEqual(0, stats.channels[2]['min']); + assert.strictEqual(255, stats.channels[2]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['sum'], 372986756)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['squaresSum'], 32151543524)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['mean'], 61.51724663436759)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['stdev'], 38.96702865090125)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 2725)); + }).catch(function (err) { + throw err; + }); + }); + + it('File in, Promise out', function () { + return sharp(fixtures.inputJpg).stats().then(function (stats) { + assert.strictEqual(true, stats.isOpaque); + + // red channel + assert.strictEqual(0, stats.channels[0]['min']); + assert.strictEqual(255, stats.channels[0]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['sum'], 615101275)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['squaresSum'], 83061892917)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['mean'], 101.44954540768993)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[0]['stdev'], 58.373870588815414)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 2725)); + + // green channel + assert.strictEqual(0, stats.channels[1]['min']); + assert.strictEqual(255, stats.channels[1]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['sum'], 462824115)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['squaresSum'], 47083677255)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['mean'], 76.33425255128337)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[1]['stdev'], 44.03023262954866)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 2725)); + + // blue channel + assert.strictEqual(0, stats.channels[2]['min']); + assert.strictEqual(255, stats.channels[2]['max']); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['sum'], 372986756)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['squaresSum'], 32151543524)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['mean'], 61.51724663436759)); + assert.strictEqual(true, isInAcceptableRange(stats.channels[2]['stdev'], 38.96702865090125)); + assert.strictEqual(true, isInteger(stats.channels[0]['minX']) && isInRange(stats.channels[0]['minX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['minY']) && isInRange(stats.channels[0]['minY'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxX']) && isInRange(stats.channels[0]['maxX'], 0, 2725)); + assert.strictEqual(true, isInteger(stats.channels[0]['maxY']) && isInRange(stats.channels[0]['maxY'], 0, 2725)); + }).catch(function (err) { + throw err; + }); + }); + + it('File input with corrupt header fails gracefully', function (done) { + sharp(fixtures.inputJpgWithCorruptHeader) + .stats(function (err) { + assert.strictEqual(true, !!err); + done(); + }); + }); + + it('File input with corrupt header fails gracefully, Promise out', function () { + return sharp(fixtures.inputJpgWithCorruptHeader) + .stats().then(function (stats) { + throw new Error('Corrupt Header file'); + }).catch(function (err) { + assert.ok(!!err); + }); + }); + + it('Buffer input with corrupt header fails gracefully', function (done) { + sharp(fs.readFileSync(fixtures.inputJpgWithCorruptHeader)) + .stats(function (err) { + assert.strictEqual(true, !!err); + done(); + }); + }); + + it('Non-existent file in, Promise out', function (done) { + sharp('fail').stats().then(function (stats) { + throw new Error('Non-existent file'); + }, function (err) { + assert.ok(!!err); + done(); + }); + }); +});