Add experimental overlayWith API

Composites an overlay image with alpha channel into the input image (which
must have alpha channel) using ‘over’ alpha compositing blend mode. This API
requires both images to have the same dimensions.

References:
- http://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
- https://github.com/jcupitt/ruby-vips/issues/28#issuecomment-9014826

See #97.
This commit is contained in:
Daniel Gasienica
2015-04-24 14:57:48 -07:00
committed by Lovell Fuller
parent c886eaa6b0
commit 64f7f1d662
17 changed files with 312 additions and 8 deletions

125
src/composite.c Normal file
View File

@@ -0,0 +1,125 @@
#include <stdio.h>
#include <vips/vips.h>
const int ALPHA_BAND_INDEX = 3;
const int NUM_COLOR_BANDS = 3;
/*
Composite images `src` and `dst`
*/
int Composite(VipsObject *context, VipsImage *src, VipsImage *dst, VipsImage **out) {
if (src->Bands != 4 || dst->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))
return -1;
vips_object_local(context, srcRGB);
vips_object_local(context, dstRGB);
// 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))
return -1;
vips_object_local(context, srcAlpha);
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))
return -1;
vips_object_local(context, srcAlphaNormalized);
vips_object_local(context, dstAlphaNormalized);
//
// Compute normalized output alpha channel:
//
// References:
// - http://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
// - https://github.com/jcupitt/ruby-vips/issues/28#issuecomment-9014826
//
// out_a = src_a + dst_a * (1 - src_a)
// ^^^^^^^^^^^
// t0
// ^^^^^^^^^^^^^^^^^^^
// 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))
return -1;
vips_object_local(context, t0);
vips_object_local(context, t1);
vips_object_local(context, outAlphaNormalized);
//
// Compute output RGB channels:
//
// Wikipedia:
// out_rgb = (src_rgb * src_a + dst_rgb * dst_a * (1 - src_a)) / out_a
//
// `vips_ifthenelse` with `blend=TRUE`: http://bit.ly/1KoSsga
// out = (cond / 255) * in1 + (1 - cond / 255) * in2
//
// 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))
return -1;
vips_object_local(context, dstRGBPremultiplied);
VipsImage *outRGBPremultiplied;
if (vips_ifthenelse(srcAlpha, srcRGB, dstRGBPremultiplied,
&outRGBPremultiplied, "blend", TRUE, 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))
return -1;
// Return a reference to the output image:
*out = joined;
return 0;
}

17
src/composite.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef SRC_COMPOSITE_H_
#define SRC_COMPOSITE_H_
#ifdef __cplusplus
extern "C" {
#endif
/*
Composite images `src` and `dst`.
*/
int Composite(VipsObject *context, VipsImage *src, VipsImage *dst, VipsImage **out);
#ifdef __cplusplus
}
#endif
#endif // SRC_COMPOSITE_H_

View File

@@ -8,6 +8,7 @@
#include "nan.h"
#include "common.h"
#include "composite.h"
#include "resize.h"
using v8::Handle;
@@ -81,6 +82,7 @@ struct ResizeBaton {
int sharpenRadius;
double sharpenFlat;
double sharpenJagged;
std::string overlayPath;
double gamma;
bool greyscale;
bool normalize;
@@ -790,6 +792,54 @@ class ResizeWorker : public NanAsyncWorker {
}
#endif
// Composite with overlay, if present
if (!baton->overlayPath.empty()) {
VipsImage *overlayImage = NULL;
ImageType overlayImageType = ImageType::UNKNOWN;
overlayImageType = DetermineImageType(baton->overlayPath.c_str());
if (overlayImageType != ImageType::UNKNOWN) {
overlayImage = InitImage(baton->overlayPath.c_str(), baton->accessMethod);
if (overlayImage == NULL) {
(baton->err).append("Overlay input file has corrupt header");
overlayImageType = ImageType::UNKNOWN;
}
} else {
(baton->err).append("Overlay input file is of an unsupported image format");
}
if (overlayImage == NULL || overlayImageType == ImageType::UNKNOWN) {
return Error();
}
if (!HasAlpha(overlayImage)) {
(baton->err).append("Overlay input must have an alpha channel");
return Error();
}
if (!HasAlpha(image)) {
(baton->err).append("Input image must have an alpha channel");
return Error();
}
if (overlayImage->Bands != 4) {
(baton->err).append("Overlay input image must have 4 channels");
return Error();
}
if (image->Bands != 4) {
(baton->err).append("Input image must have 4 channels");
return Error();
}
VipsImage *composited;
if (Composite(hook, overlayImage, image, &composited)) {
(baton->err).append("Failed to composite images");
return Error();
}
vips_object_local(hook, composited);
image = composited;
}
// Convert image to sRGB, if not already
if (image->Type != VIPS_INTERPRETATION_sRGB) {
// Switch intrepretation to sRGB
@@ -1175,6 +1225,8 @@ NAN_METHOD(resize) {
for (int i = 0; i < 4; i++) {
baton->background[i] = background->Get(i)->NumberValue();
}
// Overlay options
baton->overlayPath = *String::Utf8Value(options->Get(NanNew<String>("overlayPath"))->ToString());
// Resize options
baton->withoutEnlargement = options->Get(NanNew<String>("withoutEnlargement"))->BooleanValue();
baton->gravity = options->Get(NanNew<String>("gravity"))->Int32Value();