Premultiply alpha channel to avoid dark artifacts during tranformation
Add `Sharp.compare(file1, file2, callback)` function for comparing images using mean squared error (MSE). This is useful for unit tests. See: - https://github.com/jcupitt/libvips/issues/291 - http://entropymine.com/imageworsener/resizealpha/
@ -74,11 +74,10 @@ npm test
|
||||
|
||||
### Memory leak tests
|
||||
|
||||
Requires _valgrind_.
|
||||
Requires [Valgrind](http://valgrind.org/).
|
||||
|
||||
```sh
|
||||
cd sharp/test/leak
|
||||
./leak.sh
|
||||
npm run test-leak
|
||||
```
|
||||
|
||||
## Finally
|
||||
|
22
README.md
@ -169,6 +169,7 @@ sharp('input.png')
|
||||
.resize(300)
|
||||
.flatten()
|
||||
.background('#ff6600')
|
||||
.overlayWith('overlay.png')
|
||||
.sharpen()
|
||||
.withMetadata()
|
||||
.quality(90)
|
||||
@ -176,8 +177,8 @@ sharp('input.png')
|
||||
.toBuffer()
|
||||
.then(function(outputBuffer) {
|
||||
// outputBuffer contains upside down, 300px wide, alpha channel flattened
|
||||
// onto orange background, sharpened, with metadata, 90% quality WebP image
|
||||
// data
|
||||
// onto orange background, composited with `overlay.png`, sharpened,
|
||||
// with metadata, 90% quality WebP image data
|
||||
});
|
||||
```
|
||||
|
||||
@ -416,6 +417,10 @@ If the background contains an alpha value then WebP and PNG format output images
|
||||
|
||||
Merge alpha transparency channel, if any, with `background`.
|
||||
|
||||
#### Experimental: overlayWith(filename)
|
||||
|
||||
**Experimental:** Composite image with transparent overlay. Both input and overlay image must be RGBA and their dimensions must match.
|
||||
|
||||
#### rotate([angle])
|
||||
|
||||
Rotate the output image by either an explicit angle or auto-orient based on the EXIF `Orientation` tag.
|
||||
@ -661,6 +666,17 @@ Provides access to internal task counters.
|
||||
var counters = sharp.counters(); // { queue: 2, process: 4 }
|
||||
```
|
||||
|
||||
#### Experimental: sharp.compare(filename1, filename2, callback)
|
||||
|
||||
`filename1` and `filename2` are strings containing the filenames of the images to compare.
|
||||
|
||||
`callback` is called with two arguments `(err, info)` where:
|
||||
|
||||
* `err` contains an error message, if any.
|
||||
* `info` contains the info about the difference between the two images such as `isEqual` (Boolean), `meanSquaredError` (Number; present iff `status='success'`, otherwise `undefined`), and `status` (String; one of `success`, `mismatchedDimensions`, `mismatchedBands`, `mismatchedType`).
|
||||
|
||||
This function can be useful for comparing test output images against reference images.
|
||||
|
||||
## Contributing
|
||||
|
||||
A [guide for contributors](https://github.com/lovell/sharp/blob/master/CONTRIBUTING.md) covers reporting bugs, requesting features and submitting code changes.
|
||||
@ -771,7 +787,7 @@ This module would never have been possible without the help and code contributio
|
||||
* [Brandon Aaron](https://github.com/brandonaaron)
|
||||
* [Andreas Lind](https://github.com/papandreou)
|
||||
* [Maurus Cuelenaere](https://github.com/mcuelenaere)
|
||||
* [Linus Unnebäck](https://github.com/LinusU)
|
||||
* [Linus Unnebäck](https://github.com/LinusU)
|
||||
* [Victor Mateevitsi](https://github.com/mvictoras)
|
||||
* [Alaric Holloway](https://github.com/skedastik)
|
||||
* [Bernhard K. Weisshuhn](https://github.com/bkw)
|
||||
|
@ -3,11 +3,13 @@
|
||||
'target_name': 'sharp',
|
||||
'sources': [
|
||||
'src/common.cc',
|
||||
'src/compare-internal.c',
|
||||
'src/compare.cc',
|
||||
'src/composite.c',
|
||||
'src/utilities.cc',
|
||||
'src/metadata.cc',
|
||||
'src/resize.cc',
|
||||
'src/sharp.cc'
|
||||
'src/sharp.cc',
|
||||
'src/utilities.cc'
|
||||
],
|
||||
'conditions': [
|
||||
['OS=="win"', {
|
||||
|
24
index.js
@ -758,6 +758,30 @@ Sharp.prototype.metadata = function(callback) {
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Reads two input images and returns stats about their differences.
|
||||
Supports only callbacks.
|
||||
*/
|
||||
Sharp.compare = function(input1, input2, callback) {
|
||||
if (typeof input1 !== 'string' || input1 === '') {
|
||||
throw new TypeError('`input1` must be a non-empty string');
|
||||
}
|
||||
|
||||
if (typeof input2 !== 'string' || input2 === '') {
|
||||
throw new TypeError('`input2` must be a non-empty string');
|
||||
}
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('`callback` must be a function');
|
||||
}
|
||||
|
||||
var options = {
|
||||
fileIn1: input1,
|
||||
fileIn2: input2
|
||||
};
|
||||
sharp.compare(options, callback);
|
||||
};
|
||||
|
||||
/*
|
||||
Get and set cache memory and item limits
|
||||
*/
|
||||
|
@ -20,7 +20,10 @@
|
||||
],
|
||||
"description": "High performance Node.js module to resize JPEG, PNG, WebP and TIFF images using the libvips library",
|
||||
"scripts": {
|
||||
"clean": "rm -rf test/fixtures/output.*",
|
||||
"test": "VIPS_WARNING=0 node ./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- --slow=5000 --timeout=20000 ./test/unit/*.js",
|
||||
"test-clean": "npm run clean && npm install && npm test",
|
||||
"test-leak": "cd test/leak; ./leak.sh; cd - > /dev/null",
|
||||
"test-win32-node": "node ./node_modules/mocha/bin/mocha --slow=5000 --timeout=20000 ./test/unit/*.js",
|
||||
"test-win32-iojs": "iojs ./node_modules/mocha/bin/mocha --slow=5000 --timeout=20000 ./test/unit/*.js"
|
||||
},
|
||||
|
47
src/compare-internal.c
Normal file
@ -0,0 +1,47 @@
|
||||
#include <stdio.h>
|
||||
#include <vips/vips.h>
|
||||
|
||||
#include "composite.h"
|
||||
|
||||
const int STATS_SUM2_COLUMN = 3;
|
||||
|
||||
/*
|
||||
Compare images `actual` and `expected` and return mean squared error (MSE).
|
||||
*/
|
||||
int Compare(VipsObject *context, VipsImage *actual, VipsImage *expected, double *out) {
|
||||
if (actual->Bands != expected->Bands)
|
||||
return -1;
|
||||
|
||||
if (actual->Type != expected->Type)
|
||||
return -1;
|
||||
|
||||
VipsImage *actualPremultiplied;
|
||||
if (Premultiply(context, actual, &actualPremultiplied))
|
||||
return -1;
|
||||
vips_object_local(context, actualPremultiplied);
|
||||
|
||||
VipsImage *expectedPremultiplied;
|
||||
if (Premultiply(context, expected, &expectedPremultiplied))
|
||||
return -1;
|
||||
vips_object_local(context, expectedPremultiplied);
|
||||
|
||||
VipsImage *difference;
|
||||
if (vips_subtract(expectedPremultiplied, actualPremultiplied, &difference, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, difference);
|
||||
|
||||
VipsImage *stats;
|
||||
if (vips_stats(difference, &stats, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, stats);
|
||||
|
||||
double *statsData = (double*) stats->data;
|
||||
int numValues = actual->Xsize * actual->Ysize * actual->Bands;
|
||||
double sumOfSquares = statsData[STATS_SUM2_COLUMN];
|
||||
double meanSquaredError = sumOfSquares / numValues;
|
||||
|
||||
// Return a reference to the mean squared error:
|
||||
*out = meanSquaredError;
|
||||
|
||||
return 0;
|
||||
}
|
15
src/compare-internal.h
Normal file
@ -0,0 +1,15 @@
|
||||
#ifndef SRC_COMPARE_INTERNAL_H_
|
||||
#define SRC_COMPARE_INTERNAL_H_
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int Compare(VipsObject *context, VipsImage *actual, VipsImage *expected, double *out);;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // SRC_COMPARE_INTERNAL_H_
|
186
src/compare.cc
Normal file
@ -0,0 +1,186 @@
|
||||
#include <node.h>
|
||||
#include <vips/vips.h>
|
||||
|
||||
#include "nan.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "compare-internal.h"
|
||||
|
||||
using v8::Boolean;
|
||||
using v8::Exception;
|
||||
using v8::Function;
|
||||
using v8::Handle;
|
||||
using v8::Local;
|
||||
using v8::Number;
|
||||
using v8::Object;
|
||||
using v8::String;
|
||||
using v8::Value;
|
||||
|
||||
using sharp::ImageType;
|
||||
using sharp::DetermineImageType;
|
||||
using sharp::InitImage;
|
||||
using sharp::counterQueue;
|
||||
|
||||
struct CompareBaton {
|
||||
// Input
|
||||
std::string fileIn1;
|
||||
std::string fileIn2;
|
||||
|
||||
// Output
|
||||
bool isEqual;
|
||||
float meanSquaredError;
|
||||
std::string err;
|
||||
std::string status;
|
||||
|
||||
CompareBaton():
|
||||
isEqual(false),
|
||||
meanSquaredError(0.0) {}
|
||||
};
|
||||
|
||||
class CompareWorker : public NanAsyncWorker {
|
||||
|
||||
public:
|
||||
CompareWorker(NanCallback *callback, CompareBaton *baton) : NanAsyncWorker(callback), baton(baton) {}
|
||||
~CompareWorker() {}
|
||||
|
||||
void Execute() {
|
||||
// Decrement queued task counter
|
||||
g_atomic_int_dec_and_test(&counterQueue);
|
||||
|
||||
ImageType imageType1 = ImageType::UNKNOWN;
|
||||
ImageType imageType2 = ImageType::UNKNOWN;
|
||||
VipsImage *image1 = NULL;
|
||||
VipsImage *image2 = NULL;
|
||||
|
||||
// Create "hook" VipsObject to hang image references from
|
||||
hook = reinterpret_cast<VipsObject*>(vips_image_new());
|
||||
|
||||
// From files
|
||||
imageType1 = DetermineImageType(baton->fileIn1.c_str());
|
||||
imageType2 = DetermineImageType(baton->fileIn2.c_str());
|
||||
|
||||
if (imageType1 != ImageType::UNKNOWN) {
|
||||
image1 = InitImage(baton->fileIn1.c_str(), VIPS_ACCESS_RANDOM);
|
||||
if (image1 == NULL) {
|
||||
(baton->err).append("Input file 1 has corrupt header");
|
||||
imageType1 = ImageType::UNKNOWN;
|
||||
} else {
|
||||
vips_object_local(hook, image1);
|
||||
}
|
||||
} else {
|
||||
(baton->err).append("Input file 1 is of an unsupported image format");
|
||||
}
|
||||
|
||||
if (imageType2 != ImageType::UNKNOWN) {
|
||||
image2 = InitImage(baton->fileIn2.c_str(), VIPS_ACCESS_RANDOM);
|
||||
if (image2 == NULL) {
|
||||
(baton->err).append("Input file 2 has corrupt header");
|
||||
imageType2 = ImageType::UNKNOWN;
|
||||
} else {
|
||||
vips_object_local(hook, image2);
|
||||
}
|
||||
} else {
|
||||
(baton->err).append("Input file 2 is of an unsupported image format");
|
||||
}
|
||||
|
||||
if (image1 != NULL && imageType1 != ImageType::UNKNOWN && image2 != NULL && imageType2 != ImageType::UNKNOWN) {
|
||||
double meanSquaredError;
|
||||
|
||||
baton->meanSquaredError = -1.0;
|
||||
baton->isEqual = FALSE;
|
||||
|
||||
if (image1->Type != image2->Type) {
|
||||
baton->status = "mismatchedTypes";
|
||||
} else if (image1->Bands != image2->Bands) {
|
||||
baton->status = "mismatchedBands";
|
||||
} else if (image1->Xsize != image2->Xsize || image1->Ysize != image2->Ysize) {
|
||||
baton->status = "mismatchedDimensions";
|
||||
} else {
|
||||
if (Compare(hook, image1, image2, &meanSquaredError)) {
|
||||
(baton->err).append("Failed to compare two images");
|
||||
return Error();
|
||||
} else {
|
||||
baton->status = "success";
|
||||
baton->meanSquaredError = meanSquaredError;
|
||||
baton->isEqual = meanSquaredError == 0.0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Error();
|
||||
}
|
||||
|
||||
CleanUp();
|
||||
}
|
||||
|
||||
void CleanUp() {
|
||||
// Clean up any dangling image references
|
||||
g_object_unref(hook);
|
||||
|
||||
// Clean up
|
||||
vips_error_clear();
|
||||
vips_thread_shutdown();
|
||||
}
|
||||
|
||||
void HandleOKCallback () {
|
||||
NanScope();
|
||||
|
||||
Handle<Value> argv[2] = { NanNull(), NanNull() };
|
||||
if (!baton->err.empty()) {
|
||||
// Error
|
||||
argv[0] = Exception::Error(NanNew<String>(baton->err.data(), baton->err.size()));
|
||||
} else {
|
||||
// Compare Object
|
||||
Local<Object> info = NanNew<Object>();
|
||||
info->Set(NanNew<String>("isEqual"), NanNew<Boolean>(baton->isEqual));
|
||||
info->Set(NanNew<String>("status"), NanNew<String>(baton->status));
|
||||
if (baton->meanSquaredError >= 0.0) {
|
||||
info->Set(NanNew<String>("meanSquaredError"), NanNew<Number>(baton->meanSquaredError));
|
||||
}
|
||||
argv[1] = info;
|
||||
}
|
||||
delete baton;
|
||||
|
||||
// Return to JavaScript
|
||||
callback->Call(2, argv);
|
||||
}
|
||||
|
||||
/*
|
||||
Copy then clear the error message.
|
||||
Unref all transitional images on the hook.
|
||||
Clear all thread-local data.
|
||||
*/
|
||||
void Error() {
|
||||
// Get libvips' error message
|
||||
(baton->err).append(vips_error_buffer());
|
||||
|
||||
CleanUp();
|
||||
}
|
||||
|
||||
private:
|
||||
CompareBaton* baton;
|
||||
VipsObject *hook;
|
||||
};
|
||||
|
||||
/*
|
||||
compare(options, callback)
|
||||
*/
|
||||
NAN_METHOD(compare) {
|
||||
NanScope();
|
||||
|
||||
// V8 objects are converted to non-V8 types held in the baton struct
|
||||
CompareBaton *baton = new CompareBaton;
|
||||
Local<Object> options = args[0]->ToObject();
|
||||
|
||||
// Input filename
|
||||
baton->fileIn1 = *String::Utf8Value(options->Get(NanNew<String>("fileIn1"))->ToString());
|
||||
baton->fileIn2 = *String::Utf8Value(options->Get(NanNew<String>("fileIn2"))->ToString());
|
||||
|
||||
// Join queue for worker thread
|
||||
NanCallback *callback = new NanCallback(args[1].As<v8::Function>());
|
||||
NanAsyncQueueWorker(new CompareWorker(callback, baton));
|
||||
|
||||
// Increment queued task counter
|
||||
g_atomic_int_inc(&counterQueue);
|
||||
|
||||
NanReturnUndefined();
|
||||
}
|
8
src/compare.h
Normal file
@ -0,0 +1,8 @@
|
||||
#ifndef SRC_COMPARE_H_
|
||||
#define SRC_COMPARE_H_
|
||||
|
||||
#include "nan.h"
|
||||
|
||||
NAN_METHOD(compare);
|
||||
|
||||
#endif // SRC_COMPARE_H_
|
210
src/composite.c
@ -2,45 +2,58 @@
|
||||
#include <vips/vips.h>
|
||||
|
||||
|
||||
// Constants
|
||||
const int ALPHA_BAND_INDEX = 3;
|
||||
const int NUM_COLOR_BANDS = 3;
|
||||
|
||||
// TODO: Copied from `common.cc`. Deduplicate once this becomes a C++ module.
|
||||
int HasAlpha(VipsImage *image) {
|
||||
return (
|
||||
(image->Bands == 2 && image->Type == VIPS_INTERPRETATION_B_W) ||
|
||||
(image->Bands == 4 && image->Type != VIPS_INTERPRETATION_CMYK) ||
|
||||
(image->Bands == 5 && image->Type == VIPS_INTERPRETATION_CMYK)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
Composite images `src` and `dst`
|
||||
Composite images `src` and `dst` with premultiplied alpha channel and output
|
||||
image with premultiplied alpha.
|
||||
*/
|
||||
int Composite(VipsObject *context, VipsImage *src, VipsImage *dst, VipsImage **out) {
|
||||
if (src->Bands != 4 || dst->Bands != 4)
|
||||
int Composite(VipsObject *context, VipsImage *srcPremultiplied, VipsImage *dstPremultiplied, VipsImage **outPremultiplied) {
|
||||
if (srcPremultiplied->Bands != 4 || dstPremultiplied->Bands != 4)
|
||||
return -1;
|
||||
|
||||
// Extract RGB bands:
|
||||
VipsImage *srcRGB;
|
||||
VipsImage *dstRGB;
|
||||
if (vips_extract_band(src, &srcRGB, 0, "n", NUM_COLOR_BANDS, NULL) ||
|
||||
vips_extract_band(dst, &dstRGB, 0, "n", NUM_COLOR_BANDS, NULL))
|
||||
VipsImage *srcRGBPremultiplied;
|
||||
if (vips_extract_band(srcPremultiplied, &srcRGBPremultiplied, 0, "n", NUM_COLOR_BANDS, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, srcRGBPremultiplied);
|
||||
|
||||
vips_object_local(context, srcRGB);
|
||||
vips_object_local(context, dstRGB);
|
||||
VipsImage *dstRGBPremultiplied;
|
||||
if (vips_extract_band(dstPremultiplied, &dstRGBPremultiplied, 0, "n", NUM_COLOR_BANDS, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, dstRGBPremultiplied);
|
||||
|
||||
// Extract alpha bands:
|
||||
VipsImage *srcAlpha;
|
||||
VipsImage *dstAlpha;
|
||||
if (vips_extract_band(src, &srcAlpha, ALPHA_BAND_INDEX, NULL) ||
|
||||
vips_extract_band(dst, &dstAlpha, ALPHA_BAND_INDEX, NULL))
|
||||
if (vips_extract_band(srcPremultiplied, &srcAlpha, ALPHA_BAND_INDEX, "n", 1, NULL))
|
||||
return -1;
|
||||
|
||||
vips_object_local(context, srcAlpha);
|
||||
|
||||
VipsImage *dstAlpha;
|
||||
if (vips_extract_band(dstPremultiplied, &dstAlpha, ALPHA_BAND_INDEX, "n", 1, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, dstAlpha);
|
||||
|
||||
// Compute normalized input alpha channels:
|
||||
VipsImage *srcAlphaNormalized;
|
||||
VipsImage *dstAlphaNormalized;
|
||||
if (vips_linear1(srcAlpha, &srcAlphaNormalized, 1.0 / 255.0, 0.0, NULL) ||
|
||||
vips_linear1(dstAlpha, &dstAlphaNormalized, 1.0 / 255.0, 0.0, NULL))
|
||||
if (vips_linear1(srcAlpha, &srcAlphaNormalized, 1.0 / 255.0, 0.0, NULL))
|
||||
return -1;
|
||||
|
||||
vips_object_local(context, srcAlphaNormalized);
|
||||
|
||||
VipsImage *dstAlphaNormalized;
|
||||
if (vips_linear1(dstAlpha, &dstAlphaNormalized, 1.0 / 255.0, 0.0, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, dstAlphaNormalized);
|
||||
|
||||
//
|
||||
@ -56,15 +69,18 @@ int Composite(VipsObject *context, VipsImage *src, VipsImage *dst, VipsImage **o
|
||||
// ^^^^^^^^^^^^^^^^^^^
|
||||
// t1
|
||||
VipsImage *t0;
|
||||
VipsImage *t1;
|
||||
VipsImage *outAlphaNormalized;
|
||||
if (vips_linear1(srcAlphaNormalized, &t0, -1.0, 1.0, NULL) ||
|
||||
vips_multiply(dstAlphaNormalized, t0, &t1, NULL) ||
|
||||
vips_add(srcAlphaNormalized, t1, &outAlphaNormalized, NULL))
|
||||
if (vips_linear1(srcAlphaNormalized, &t0, -1.0, 1.0, NULL))
|
||||
return -1;
|
||||
|
||||
vips_object_local(context, t0);
|
||||
|
||||
VipsImage *t1;
|
||||
if (vips_multiply(dstAlphaNormalized, t0, &t1, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, t1);
|
||||
|
||||
VipsImage *outAlphaNormalized;
|
||||
if (vips_add(srcAlphaNormalized, t1, &outAlphaNormalized, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, outAlphaNormalized);
|
||||
|
||||
//
|
||||
@ -72,54 +88,142 @@ int Composite(VipsObject *context, VipsImage *src, VipsImage *dst, VipsImage **o
|
||||
//
|
||||
// Wikipedia:
|
||||
// out_rgb = (src_rgb * src_a + dst_rgb * dst_a * (1 - src_a)) / out_a
|
||||
// ^^^^^^^^^^^
|
||||
// t0
|
||||
//
|
||||
// `vips_ifthenelse` with `blend=TRUE`: http://bit.ly/1KoSsga
|
||||
// out = (cond / 255) * in1 + (1 - cond / 255) * in2
|
||||
// Omit division by `out_a` since `Compose` is supposed to output a
|
||||
// premultiplied RGBA image as reversal of premultiplication is handled
|
||||
// externally.
|
||||
//
|
||||
// Substitutions:
|
||||
//
|
||||
// cond --> src_a
|
||||
// in1 --> src_rgb
|
||||
// in2 --> dst_rgb * dst_a (premultiplied destination RGB)
|
||||
//
|
||||
// Finally, manually divide by `out_a` to unpremultiply the RGB channels.
|
||||
// Failing to do so results in darker than expected output with low
|
||||
// opacity images.
|
||||
//
|
||||
VipsImage *dstRGBPremultiplied;
|
||||
if (vips_multiply(dstRGB, dstAlphaNormalized, &dstRGBPremultiplied, NULL))
|
||||
VipsImage *t2;
|
||||
if (vips_multiply(dstRGBPremultiplied, t0, &t2, NULL))
|
||||
return -1;
|
||||
|
||||
vips_object_local(context, dstRGBPremultiplied);
|
||||
vips_object_local(context, t2);
|
||||
|
||||
VipsImage *outRGBPremultiplied;
|
||||
if (vips_ifthenelse(srcAlpha, srcRGB, dstRGBPremultiplied,
|
||||
&outRGBPremultiplied, "blend", TRUE, NULL))
|
||||
if (vips_add(srcRGBPremultiplied, t2, &outRGBPremultiplied, NULL))
|
||||
return -1;
|
||||
|
||||
vips_object_local(context, outRGBPremultiplied);
|
||||
|
||||
// Unpremultiply RGB channels:
|
||||
VipsImage *outRGB;
|
||||
if (vips_divide(outRGBPremultiplied, outAlphaNormalized, &outRGB, NULL))
|
||||
return -1;
|
||||
|
||||
vips_object_local(context, outRGB);
|
||||
|
||||
// Denormalize output alpha channel:
|
||||
VipsImage *outAlpha;
|
||||
if (vips_linear1(outAlphaNormalized, &outAlpha, 255.0, 0.0, NULL))
|
||||
return -1;
|
||||
|
||||
vips_object_local(context, outAlpha);
|
||||
|
||||
// Combine RGB and alpha channel into output image:
|
||||
VipsImage *joined;
|
||||
if (vips_bandjoin2(outRGB, outAlpha, &joined, NULL))
|
||||
if (vips_bandjoin2(outRGBPremultiplied, outAlpha, &joined, NULL))
|
||||
return -1;
|
||||
|
||||
// Return a reference to the output image:
|
||||
*out = joined;
|
||||
// Return a reference to the composited output image:
|
||||
*outPremultiplied = joined;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Premultiply alpha channel of `image`.
|
||||
*/
|
||||
int Premultiply(VipsObject *context, VipsImage *image, VipsImage **out) {
|
||||
VipsImage *imagePremultiplied;
|
||||
|
||||
// Trivial case: Copy images without alpha channel:
|
||||
if (!HasAlpha(image)) {
|
||||
return vips_image_write(image, *out);
|
||||
}
|
||||
|
||||
#if (VIPS_MAJOR_VERSION >= 9 || (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 1))
|
||||
|
||||
if (vips_premultiply(image, &imagePremultiplied, NULL))
|
||||
return -1;
|
||||
|
||||
#else
|
||||
|
||||
if (image->Bands != 4)
|
||||
return -1;
|
||||
|
||||
VipsImage *imageRGB;
|
||||
if (vips_extract_band(image, &imageRGB, 0, "n", NUM_COLOR_BANDS, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, imageRGB);
|
||||
|
||||
VipsImage *imageAlpha;
|
||||
if (vips_extract_band(image, &imageAlpha, ALPHA_BAND_INDEX, "n", 1, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, imageAlpha);
|
||||
|
||||
VipsImage *imageAlphaNormalized;
|
||||
if (vips_linear1(imageAlpha, &imageAlphaNormalized, 1.0 / 255.0, 0.0, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, imageAlphaNormalized);
|
||||
|
||||
VipsImage *imageRGBPremultiplied;
|
||||
if (vips_multiply(imageRGB, imageAlphaNormalized, &imageRGBPremultiplied, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, imageRGBPremultiplied);
|
||||
|
||||
if (vips_bandjoin2(imageRGBPremultiplied, imageAlpha, &imagePremultiplied, NULL))
|
||||
return -1;
|
||||
|
||||
#endif
|
||||
|
||||
// Return a reference to the premultiplied output image:
|
||||
*out = imagePremultiplied;
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Unpremultiply alpha channel of `image`.
|
||||
*/
|
||||
int Unpremultiply(VipsObject *context, VipsImage *image, VipsImage **out) {
|
||||
VipsImage *imageUnpremultiplied;
|
||||
|
||||
// Trivial case: Copy images without alpha channel:
|
||||
if (!HasAlpha(image)) {
|
||||
return vips_image_write(image, *out);
|
||||
}
|
||||
|
||||
#if (VIPS_MAJOR_VERSION >= 9 || (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 1))
|
||||
|
||||
if (vips_unpremultiply(image, &imageUnpremultiplied, NULL))
|
||||
return -1;
|
||||
|
||||
#else
|
||||
|
||||
if (image->Bands != 4)
|
||||
return -1;
|
||||
|
||||
VipsImage *imageRGBPremultipliedTransformed;
|
||||
if (vips_extract_band(image, &imageRGBPremultipliedTransformed, 0, "n", NUM_COLOR_BANDS, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, imageRGBPremultipliedTransformed);
|
||||
|
||||
VipsImage *imageAlphaTransformed;
|
||||
if (vips_extract_band(image, &imageAlphaTransformed, ALPHA_BAND_INDEX, "n", 1, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, imageAlphaTransformed);
|
||||
|
||||
VipsImage *imageAlphaNormalizedTransformed;
|
||||
if (vips_linear1(imageAlphaTransformed, &imageAlphaNormalizedTransformed, 1.0 / 255.0, 0.0, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, imageAlphaNormalizedTransformed);
|
||||
|
||||
VipsImage *imageRGBUnpremultipliedTransformed;
|
||||
if (vips_divide(imageRGBPremultipliedTransformed, imageAlphaNormalizedTransformed, &imageRGBUnpremultipliedTransformed, NULL))
|
||||
return -1;
|
||||
vips_object_local(context, imageRGBUnpremultipliedTransformed);
|
||||
|
||||
if (vips_bandjoin2(imageRGBUnpremultipliedTransformed, imageAlphaTransformed, &imageUnpremultiplied, NULL))
|
||||
return -1;
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
// Return a reference to the unpremultiplied output image:
|
||||
*out = imageUnpremultiplied;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -5,10 +5,10 @@
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/*
|
||||
Composite images `src` and `dst`.
|
||||
*/
|
||||
|
||||
int Composite(VipsObject *context, VipsImage *src, VipsImage *dst, VipsImage **out);
|
||||
int Premultiply(VipsObject *context, VipsImage *image, VipsImage **out);
|
||||
int Unpremultiply(VipsObject *context, VipsImage *image, VipsImage **out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
@ -480,8 +480,29 @@ class ResizeWorker : public NanAsyncWorker {
|
||||
}
|
||||
}
|
||||
|
||||
// Premultiply image alpha channel before all transformations to avoid
|
||||
// dark fringing around bright pixels
|
||||
// See: http://entropymine.com/imageworsener/resizealpha/
|
||||
bool shouldAffineTransform = xresidual != 0.0 || yresidual != 0.0;
|
||||
bool shouldBlur = baton->blurSigma != 0.0;
|
||||
bool shouldSharpen = baton->sharpenRadius != 0;
|
||||
bool shouldTransform = shouldAffineTransform || shouldBlur || shouldSharpen;
|
||||
bool hasOverlay = !baton->overlayPath.empty();
|
||||
bool shouldPremultiplyAlpha = HasAlpha(image) && image->Bands == 4 && (shouldTransform || hasOverlay);
|
||||
|
||||
if (shouldPremultiplyAlpha) {
|
||||
VipsImage *imagePremultiplied;
|
||||
if (Premultiply(hook, image, &imagePremultiplied)) {
|
||||
(baton->err).append("Failed to premultiply alpha channel.");
|
||||
return Error();
|
||||
}
|
||||
|
||||
vips_object_local(hook, imagePremultiplied);
|
||||
image = imagePremultiplied;
|
||||
}
|
||||
|
||||
// Use vips_affine with the remaining float part
|
||||
if (xresidual != 0.0 || yresidual != 0.0) {
|
||||
if (shouldAffineTransform) {
|
||||
// Use average of x and y residuals to compute sigma for Gaussian blur
|
||||
double residual = (xresidual + yresidual) / 2.0;
|
||||
// Apply Gaussian blur before large affine reductions
|
||||
@ -646,7 +667,7 @@ class ResizeWorker : public NanAsyncWorker {
|
||||
}
|
||||
|
||||
// Blur
|
||||
if (baton->blurSigma != 0.0) {
|
||||
if (shouldBlur) {
|
||||
VipsImage *blurred;
|
||||
if (baton->blurSigma < 0.0) {
|
||||
// Fast, mild blur - averages neighbouring pixels
|
||||
@ -677,7 +698,7 @@ class ResizeWorker : public NanAsyncWorker {
|
||||
}
|
||||
|
||||
// Sharpen
|
||||
if (baton->sharpenRadius != 0) {
|
||||
if (shouldSharpen) {
|
||||
VipsImage *sharpened;
|
||||
if (baton->sharpenRadius == -1) {
|
||||
// Fast, mild sharpen
|
||||
@ -793,7 +814,7 @@ class ResizeWorker : public NanAsyncWorker {
|
||||
#endif
|
||||
|
||||
// Composite with overlay, if present
|
||||
if (!baton->overlayPath.empty()) {
|
||||
if (hasOverlay) {
|
||||
VipsImage *overlayImage = NULL;
|
||||
ImageType overlayImageType = ImageType::UNKNOWN;
|
||||
overlayImageType = DetermineImageType(baton->overlayPath.c_str());
|
||||
@ -802,6 +823,8 @@ class ResizeWorker : public NanAsyncWorker {
|
||||
if (overlayImage == NULL) {
|
||||
(baton->err).append("Overlay input file has corrupt header");
|
||||
overlayImageType = ImageType::UNKNOWN;
|
||||
} else {
|
||||
vips_object_local(hook, overlayImage);
|
||||
}
|
||||
} else {
|
||||
(baton->err).append("Overlay input file is of an unsupported image format");
|
||||
@ -831,8 +854,15 @@ class ResizeWorker : public NanAsyncWorker {
|
||||
return Error();
|
||||
}
|
||||
|
||||
VipsImage *overlayImagePremultiplied;
|
||||
if (Premultiply(hook, overlayImage, &overlayImagePremultiplied)) {
|
||||
(baton->err).append("Failed to premultiply alpha channel of overlay image.");
|
||||
return Error();
|
||||
}
|
||||
vips_object_local(hook, overlayImagePremultiplied);
|
||||
|
||||
VipsImage *composited;
|
||||
if (Composite(hook, overlayImage, image, &composited)) {
|
||||
if (Composite(hook, overlayImagePremultiplied, image, &composited)) {
|
||||
(baton->err).append("Failed to composite images");
|
||||
return Error();
|
||||
}
|
||||
@ -840,6 +870,18 @@ class ResizeWorker : public NanAsyncWorker {
|
||||
image = composited;
|
||||
}
|
||||
|
||||
// Reverse premultiplication after all transformations:
|
||||
if (shouldPremultiplyAlpha) {
|
||||
VipsImage *imageUnpremultiplied;
|
||||
if (Unpremultiply(hook, image, &imageUnpremultiplied)) {
|
||||
(baton->err).append("Failed to unpremultiply alpha channel.");
|
||||
return Error();
|
||||
}
|
||||
|
||||
vips_object_local(hook, imageUnpremultiplied);
|
||||
image = imageUnpremultiplied;
|
||||
}
|
||||
|
||||
// Convert image to sRGB, if not already
|
||||
if (image->Type != VIPS_INTERPRETATION_sRGB) {
|
||||
// Switch intrepretation to sRGB
|
||||
|
@ -4,6 +4,7 @@
|
||||
#include "nan.h"
|
||||
|
||||
#include "common.h"
|
||||
#include "compare.h"
|
||||
#include "metadata.h"
|
||||
#include "resize.h"
|
||||
#include "utilities.h"
|
||||
@ -19,6 +20,7 @@ extern "C" void init(v8::Handle<v8::Object> target) {
|
||||
// Methods available to JavaScript
|
||||
NODE_SET_METHOD(target, "metadata", metadata);
|
||||
NODE_SET_METHOD(target, "resize", resize);
|
||||
NODE_SET_METHOD(target, "compare", compare);
|
||||
NODE_SET_METHOD(target, "cache", cache);
|
||||
NODE_SET_METHOD(target, "concurrency", concurrency);
|
||||
NODE_SET_METHOD(target, "counters", counters);
|
||||
|
BIN
test/fixtures/alpha-premultiply-1024x768-paper.png
vendored
Normal file
After Width: | Height: | Size: 255 KiB |
BIN
test/fixtures/alpha-premultiply-2048x1536-paper.png
vendored
Normal file
After Width: | Height: | Size: 640 KiB |
BIN
test/fixtures/expected/alpha-layer-01-imagemagick.png
vendored
Normal file
After Width: | Height: | Size: 234 KiB |
BIN
test/fixtures/expected/alpha-layer-01-low-alpha-imagemagick.png
vendored
Normal file
After Width: | Height: | Size: 186 KiB |
BIN
test/fixtures/expected/alpha-layer-01-low-alpha.png
vendored
Normal file
After Width: | Height: | Size: 188 KiB |
BIN
test/fixtures/expected/alpha-layer-01.png
vendored
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 234 KiB |
BIN
test/fixtures/expected/alpha-layer-012-imagemagick.png
vendored
Normal file
After Width: | Height: | Size: 259 KiB |
BIN
test/fixtures/expected/alpha-layer-012-low-alpha-imagemagick.png
vendored
Normal file
After Width: | Height: | Size: 208 KiB |
BIN
test/fixtures/expected/alpha-layer-012-low-alpha.png
vendored
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 209 KiB |
BIN
test/fixtures/expected/alpha-layer-012.png
vendored
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 260 KiB |
BIN
test/fixtures/expected/alpha-layer-12-imagemagick.png
vendored
Normal file
After Width: | Height: | Size: 221 KiB |
BIN
test/fixtures/expected/alpha-layer-12-low-alpha-imagemagick.png
vendored
Normal file
After Width: | Height: | Size: 179 KiB |
BIN
test/fixtures/expected/alpha-layer-12-low-alpha.png
vendored
Normal file
After Width: | Height: | Size: 179 KiB |
BIN
test/fixtures/expected/alpha-layer-12.png
vendored
Normal file
After Width: | Height: | Size: 222 KiB |
BIN
test/fixtures/expected/alpha-premultiply-enlargement-2048x1536-paper.png
vendored
Normal file
After Width: | Height: | Size: 922 KiB |
BIN
test/fixtures/expected/alpha-premultiply-reduction-1024x768-paper.png
vendored
Normal file
After Width: | Height: | Size: 208 KiB |
35
test/fixtures/index.js
vendored
@ -2,9 +2,13 @@
|
||||
|
||||
var path = require('path');
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
|
||||
|
||||
// Constants
|
||||
var MAX_ALLOWED_MEAN_SQUARED_ERROR = 0.0005;
|
||||
|
||||
// Helpers
|
||||
var getPath = function(filename) {
|
||||
return path.join(__dirname, filename);
|
||||
};
|
||||
@ -57,6 +61,8 @@ module.exports = {
|
||||
inputPngOverlayLayer2: getPath('alpha-layer-2-ink.png'),
|
||||
inputPngOverlayLayer1LowAlpha: getPath('alpha-layer-1-fill-low-alpha.png'),
|
||||
inputPngOverlayLayer2LowAlpha: getPath('alpha-layer-2-ink-low-alpha.png'),
|
||||
inputPngAlphaPremultiplicationSmall: getPath('alpha-premultiply-1024x768-paper.png'),
|
||||
inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'),
|
||||
|
||||
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
|
||||
inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
|
||||
@ -116,12 +122,37 @@ module.exports = {
|
||||
}
|
||||
|
||||
if (distance > options.threshold) {
|
||||
return callback(new Error('Maximum similarity distance: ' + options.threshold + '. Actual: ' + distance));
|
||||
return callback(new Error('Expected maximum similarity distance: ' + options.threshold + '. Actual: ' + distance + '.'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
assertEqual: function(actualImagePath, expectedImagePath, callback) {
|
||||
if (typeof actualImagePath !== 'string') {
|
||||
throw new TypeError('`actualImagePath` must be a string; got ' + actualImagePath);
|
||||
}
|
||||
|
||||
if (typeof expectedImagePath !== 'string') {
|
||||
throw new TypeError('`expectedImagePath` must be a string; got ' + expectedImagePath);
|
||||
}
|
||||
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('`callback` must be a function');
|
||||
}
|
||||
|
||||
sharp.compare(actualImagePath, expectedImagePath, function (error, info) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var meanSquaredError = info.meanSquaredError;
|
||||
if (typeof meanSquaredError !== 'undefined' && meanSquaredError > MAX_ALLOWED_MEAN_SQUARED_ERROR) {
|
||||
return callback(new Error('Expected images be equal. Mean squared error: ' + meanSquaredError + '.'));
|
||||
}
|
||||
|
||||
return callback(null, info);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var sharp = require('../../index');
|
||||
var fixtures = require('../fixtures');
|
||||
var sharp = require('../../index');
|
||||
|
||||
sharp.cache(0);
|
||||
|
||||
@ -76,4 +75,30 @@ describe('Alpha transparency', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('Enlargement with non-nearest neighbor interpolation shouldn’t cause dark edges', function(done) {
|
||||
var BASE_NAME = 'alpha-premultiply-enlargement-2048x1536-paper.png';
|
||||
var actual = fixtures.path('output.' + BASE_NAME);
|
||||
var expected = fixtures.expected(BASE_NAME);
|
||||
sharp(fixtures.inputPngAlphaPremultiplicationSmall)
|
||||
.resize(2048, 1536)
|
||||
.interpolateWith('bicubic')
|
||||
.toFile(actual, function(err) {
|
||||
if (err) throw err;
|
||||
fixtures.assertEqual(actual, expected, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Reduction with non-nearest neighbor interpolation shouldn’t cause dark edges', function(done) {
|
||||
var BASE_NAME = 'alpha-premultiply-reduction-1024x768-paper.png';
|
||||
var actual = fixtures.path('output.' + BASE_NAME);
|
||||
var expected = fixtures.expected(BASE_NAME);
|
||||
sharp(fixtures.inputPngAlphaPremultiplicationLarge)
|
||||
.resize(1024, 768)
|
||||
.interpolateWith('bicubic')
|
||||
.toFile(actual, function(err) {
|
||||
if (err) throw err;
|
||||
fixtures.assertEqual(actual, expected, done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
77
test/unit/compare.js
Normal file
@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
var fixtures = require('../fixtures');
|
||||
var fs = require('fs');
|
||||
var sharp = require('../../index');
|
||||
|
||||
sharp.cache(0);
|
||||
|
||||
|
||||
// Constants
|
||||
var MAX_ALLOWED_MEAN_SQUARED_ERROR = 0.0005;
|
||||
|
||||
// Tests
|
||||
describe('sharp.compare', function() {
|
||||
it('should report equality when comparing an image to itself', function(done) {
|
||||
var image = fixtures.inputPngOverlayLayer0;
|
||||
|
||||
sharp.compare(image, image, function (error, info) {
|
||||
if (error) return done(error);
|
||||
|
||||
assert.strictEqual(info.isEqual, true, 'image is equal to itself');
|
||||
assert.strictEqual(info.status, 'success', 'status is correct');
|
||||
assert(0 <= info.meanSquaredError &&
|
||||
info.meanSquaredError <= MAX_ALLOWED_MEAN_SQUARED_ERROR,
|
||||
'MSE is within tolerance');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should report that two images have a mismatched number of bands (channels)', function(done) {
|
||||
var actual = fixtures.inputPngOverlayLayer1;
|
||||
var expected = fixtures.inputJpg;
|
||||
|
||||
sharp.compare(actual, expected, function (error, info) {
|
||||
if (error) return done(error);
|
||||
|
||||
assert.strictEqual(info.isEqual, false);
|
||||
assert.strictEqual(info.status, 'mismatchedBands');
|
||||
assert(typeof info.meanSquaredError === 'undefined', 'MSE is undefined');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should report that two images have a mismatched dimensions', function(done) {
|
||||
var actual = fixtures.inputJpg;
|
||||
var expected = fixtures.inputJpgWithExif;
|
||||
|
||||
sharp.compare(actual, expected, function (error, info) {
|
||||
if (error) return done(error);
|
||||
|
||||
assert.strictEqual(info.isEqual, false);
|
||||
assert.strictEqual(info.status, 'mismatchedDimensions');
|
||||
assert(typeof info.meanSquaredError === 'undefined', 'MSE is undefined');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should report the correct mean squared error for two different images', function(done) {
|
||||
var actual = fixtures.inputPngOverlayLayer0;
|
||||
var expected = fixtures.inputPngOverlayLayer1;
|
||||
|
||||
sharp.compare(actual, expected, function (error, info) {
|
||||
if (error) return done(error);
|
||||
|
||||
var meanSquaredError = info.meanSquaredError;
|
||||
assert.strictEqual(info.isEqual, false);
|
||||
assert.strictEqual(info.status, 'success');
|
||||
// ImageMagick reports: 42242.5
|
||||
// `compare -metric mse 'actual' 'expected' comparison.png`
|
||||
assert(41900 <= meanSquaredError && meanSquaredError <= 41950,
|
||||
'Expected: 41900 <= meanSquaredError <= 41950. Actual: ' + meanSquaredError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -6,19 +6,78 @@ var sharp = require('../../index');
|
||||
|
||||
sharp.cache(0);
|
||||
|
||||
// Main
|
||||
|
||||
// Constants
|
||||
var MAX_ALLOWED_IMAGE_MAGICK_MEAN_SQUARED_ERROR = 0.3;
|
||||
|
||||
// Helpers
|
||||
var getPaths = function(baseName, extension) {
|
||||
if (typeof extension === 'undefined') {
|
||||
extension = 'png';
|
||||
}
|
||||
|
||||
var actual = fixtures.path('output.' + baseName + '.' + extension);
|
||||
var expected = fixtures.expected(baseName + '.' + extension);
|
||||
var expectedMagick = fixtures.expected(baseName + '-imagemagick.' + extension);
|
||||
|
||||
return {
|
||||
actual: actual,
|
||||
expected: expected,
|
||||
expectedMagick: expectedMagick
|
||||
};
|
||||
};
|
||||
|
||||
var assertEqual = function (paths, callback) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('`callback` must be a function');
|
||||
}
|
||||
|
||||
fixtures.assertEqual(paths.actual, paths.expected, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
sharp.compare(paths.actual, paths.expectedMagick, function (error, info) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (info.meanSquaredError > MAX_ALLOWED_IMAGE_MAGICK_MEAN_SQUARED_ERROR) {
|
||||
return callback(new Error('Expected MSE against ImageMagick to be <= ' +
|
||||
MAX_ALLOWED_IMAGE_MAGICK_MEAN_SQUARED_ERROR + '. Actual: ' +
|
||||
info.meanSquaredError));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Test
|
||||
describe('Overlays', function() {
|
||||
it('Overlay transparent PNG on solid background', function(done) {
|
||||
var paths = getPaths('alpha-layer-01');
|
||||
|
||||
sharp(fixtures.inputPngOverlayLayer0)
|
||||
.overlayWith(fixtures.inputPngOverlayLayer1)
|
||||
.toBuffer(function (error, data, info) {
|
||||
.toFile(paths.actual, function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
fixtures.assertSimilar(fixtures.expected('alpha-layer-01.png'), data, {threshold: 0}, done);
|
||||
assertEqual(paths, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Overlay low-alpha transparent PNG on solid background', function(done) {
|
||||
var paths = getPaths('alpha-layer-01-low-alpha');
|
||||
|
||||
sharp(fixtures.inputPngOverlayLayer0)
|
||||
.overlayWith(fixtures.inputPngOverlayLayer1LowAlpha)
|
||||
.toFile(paths.actual, function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
assertEqual(paths, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Composite three transparent PNGs into one', function(done) {
|
||||
var paths = getPaths('alpha-layer-012');
|
||||
|
||||
sharp(fixtures.inputPngOverlayLayer0)
|
||||
.overlayWith(fixtures.inputPngOverlayLayer1)
|
||||
.toBuffer(function (error, data, info) {
|
||||
@ -26,26 +85,56 @@ describe('Overlays', function() {
|
||||
|
||||
sharp(data)
|
||||
.overlayWith(fixtures.inputPngOverlayLayer2)
|
||||
.toBuffer(function (error, data, info) {
|
||||
.toFile(paths.actual, function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
fixtures.assertSimilar(fixtures.expected('alpha-layer-012.png'), data, {threshold: 0}, done);
|
||||
assertEqual(paths, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This tests that alpha channel unpremultiplication is correct:
|
||||
it('Composite three low-alpha transparent PNGs into one', function(done) {
|
||||
it('Composite two transparent PNGs into one', function(done) {
|
||||
var paths = getPaths('alpha-layer-12');
|
||||
|
||||
sharp(fixtures.inputPngOverlayLayer1)
|
||||
.overlayWith(fixtures.inputPngOverlayLayer2)
|
||||
.toFile(paths.actual, function (error, data, info) {
|
||||
if (error) return done(error);
|
||||
|
||||
assertEqual(paths, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Composite two low-alpha transparent PNGs into one', function(done) {
|
||||
var paths = getPaths('alpha-layer-12-low-alpha');
|
||||
|
||||
sharp(fixtures.inputPngOverlayLayer1LowAlpha)
|
||||
.overlayWith(fixtures.inputPngOverlayLayer2LowAlpha)
|
||||
.toFile(paths.actual, function (error, data, info) {
|
||||
if (error) return done(error);
|
||||
|
||||
assertEqual(paths, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Composite three low-alpha transparent PNGs into one', function(done) {
|
||||
var paths = getPaths('alpha-layer-012-low-alpha');
|
||||
|
||||
sharp(fixtures.inputPngOverlayLayer0)
|
||||
.overlayWith(fixtures.inputPngOverlayLayer1LowAlpha)
|
||||
.toBuffer(function (error, data, info) {
|
||||
if (error) return done(error);
|
||||
|
||||
fixtures.assertSimilar(fixtures.expected('alpha-layer-012-low-alpha.png'), data, {threshold: 0}, done);
|
||||
sharp(data)
|
||||
.overlayWith(fixtures.inputPngOverlayLayer2LowAlpha)
|
||||
.toFile(paths.actual, function (error, data, info) {
|
||||
if (error) return done(error);
|
||||
|
||||
assertEqual(paths, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This tests that alpha channel unpremultiplication is correct:
|
||||
it('Composite transparent PNG onto JPEG', function(done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.overlayWith(fixtures.inputPngOverlayLayer1)
|
||||
|