Merge pull request #192 from skedastik/judgement

Add support for ignoreAspectRatio option when resizing
This commit is contained in:
Lovell Fuller 2015-04-16 15:53:43 +01:00
commit 3dfc7bea3a
4 changed files with 208 additions and 54 deletions

View File

@ -185,6 +185,11 @@ Sharp.prototype.min = function() {
return this; return this;
}; };
Sharp.prototype.ignoreAspectRatio = function() {
this.options.canvas = 'ignore_aspect';
return this;
};
Sharp.prototype.flatten = function(flatten) { Sharp.prototype.flatten = function(flatten) {
this.options.flatten = (typeof flatten === 'boolean') ? flatten : true; this.options.flatten = (typeof flatten === 'boolean') ? flatten : true;
return this; return this;

View File

@ -14,7 +14,8 @@
"Andreas Lind <andreas@one.com>", "Andreas Lind <andreas@one.com>",
"Maurus Cuelenaere <mcuelenaere@gmail.com>", "Maurus Cuelenaere <mcuelenaere@gmail.com>",
"Linus Unnebäck <linus@folkdatorn.se>", "Linus Unnebäck <linus@folkdatorn.se>",
"Victor Mateevitsi <mvictoras@gmail.com>" "Victor Mateevitsi <mvictoras@gmail.com>",
"Alaric Holloway <alaric.holloway@gmail.com>"
], ],
"description": "High performance Node.js module to resize JPEG, PNG, WebP and TIFF images using the libvips library", "description": "High performance Node.js module to resize JPEG, PNG, WebP and TIFF images using the libvips library",
"scripts": { "scripts": {

View File

@ -40,7 +40,8 @@ enum class Canvas {
CROP, CROP,
EMBED, EMBED,
MAX, MAX,
MIN MIN,
IGNORE_ASPECT
}; };
enum class Angle { enum class Angle {
@ -254,43 +255,63 @@ class ResizeWorker : public NanAsyncWorker {
int interpolatorWindowSize = InterpolatorWindowSize(baton->interpolator.c_str()); int interpolatorWindowSize = InterpolatorWindowSize(baton->interpolator.c_str());
// Scaling calculations // Scaling calculations
double factor = 1.0; double xfactor = 1.0;
double yfactor = 1.0;
if (baton->width > 0 && baton->height > 0) { if (baton->width > 0 && baton->height > 0) {
// Fixed width and height // Fixed width and height
double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width); xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height); yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
switch (baton->canvas) { switch (baton->canvas) {
case Canvas::CROP: case Canvas::CROP:
factor = std::min(xfactor, yfactor); xfactor = std::min(xfactor, yfactor);
yfactor = xfactor;
break; break;
case Canvas::EMBED: case Canvas::EMBED:
factor = std::max(xfactor, yfactor); xfactor = std::max(xfactor, yfactor);
yfactor = xfactor;
break; break;
case Canvas::MAX: case Canvas::MAX:
factor = std::max(xfactor, yfactor);
if (xfactor > yfactor) { if (xfactor > yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor); baton->height = round(static_cast<double>(inputHeight) / xfactor);
yfactor = xfactor;
} else { } else {
baton->width = round(static_cast<double>(inputWidth) / yfactor); baton->width = round(static_cast<double>(inputWidth) / yfactor);
xfactor = yfactor;
} }
break; break;
case Canvas::MIN: case Canvas::MIN:
factor = std::min(xfactor, yfactor);
if (xfactor < yfactor) { if (xfactor < yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor); baton->height = round(static_cast<double>(inputHeight) / xfactor);
yfactor = xfactor;
} else { } else {
baton->width = round(static_cast<double>(inputWidth) / yfactor); baton->width = round(static_cast<double>(inputWidth) / yfactor);
xfactor = yfactor;
} }
break; break;
case Canvas::IGNORE_ASPECT:
// xfactor, yfactor OK!
break;
} }
} else if (baton->width > 0) { } else if (baton->width > 0) {
// Fixed width, auto height // Fixed width
factor = static_cast<double>(inputWidth) / static_cast<double>(baton->width); xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
baton->height = floor(static_cast<double>(inputHeight) / factor); if (baton->canvas == Canvas::IGNORE_ASPECT) {
baton->height = inputHeight;
} else {
// Auto height
yfactor = xfactor;
baton->height = floor(static_cast<double>(inputHeight) / yfactor);
}
} else if (baton->height > 0) { } else if (baton->height > 0) {
// Fixed height, auto width // Fixed height
factor = static_cast<double>(inputHeight) / static_cast<double>(baton->height); yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
baton->width = floor(static_cast<double>(inputWidth) / factor); if (baton->canvas == Canvas::IGNORE_ASPECT) {
baton->width = inputWidth;
} else {
// Auto width
xfactor = yfactor;
baton->width = floor(static_cast<double>(inputWidth) / xfactor);
}
} else { } else {
// Identity transform // Identity transform
baton->width = inputWidth; baton->width = inputWidth;
@ -298,54 +319,52 @@ class ResizeWorker : public NanAsyncWorker {
} }
// Calculate integral box shrink // Calculate integral box shrink
int shrink = 1; int xshrink = CalculateShrink(xfactor, interpolatorWindowSize);
if (factor >= 2 && interpolatorWindowSize > 3) { int yshrink = CalculateShrink(yfactor, interpolatorWindowSize);
// Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic
shrink = floor(factor * 3.0 / interpolatorWindowSize);
} else {
shrink = floor(factor);
}
if (shrink < 1) {
shrink = 1;
}
// Calculate residual float affine transformation // Calculate residual float affine transformation
double residual = static_cast<double>(shrink) / factor; double xresidual = CalculateResidual(xshrink, xfactor);
double yresidual = CalculateResidual(yshrink, yfactor);
// Do not enlarge the output if the input width *or* height are already less than the required dimensions // Do not enlarge the output if the input width *or* height are already less than the required dimensions
if (baton->withoutEnlargement) { if (baton->withoutEnlargement) {
if (inputWidth < baton->width || inputHeight < baton->height) { if (inputWidth < baton->width || inputHeight < baton->height) {
factor = 1; xfactor = 1;
shrink = 1; yfactor = 1;
residual = 0; xshrink = 1;
yshrink = 1;
xresidual = 0;
yresidual = 0;
baton->width = inputWidth; baton->width = inputWidth;
baton->height = inputHeight; baton->height = inputHeight;
} }
} }
// Try to use libjpeg shrink-on-load, but not when applying gamma correction or pre-resize extract // If integral x and y shrink are equal, try to use libjpeg shrink-on-load, but not when applying gamma correction or pre-resize extract
int shrink_on_load = 1; int shrink_on_load = 1;
if (inputImageType == ImageType::JPEG && shrink >= 2 && baton->gamma == 0 && baton->topOffsetPre == -1) { if (xshrink == yshrink && inputImageType == ImageType::JPEG && xshrink >= 2 && baton->gamma == 0 && baton->topOffsetPre == -1) {
if (shrink >= 8) { if (xshrink >= 8) {
factor = factor / 8; xfactor = xfactor / 8;
yfactor = yfactor / 8;
shrink_on_load = 8; shrink_on_load = 8;
} else if (shrink >= 4) { } else if (xshrink >= 4) {
factor = factor / 4; xfactor = xfactor / 4;
yfactor = yfactor / 4;
shrink_on_load = 4; shrink_on_load = 4;
} else if (shrink >= 2) { } else if (xshrink >= 2) {
factor = factor / 2; xfactor = xfactor / 2;
yfactor = yfactor / 2;
shrink_on_load = 2; shrink_on_load = 2;
} }
} }
if (shrink_on_load > 1) { if (shrink_on_load > 1) {
// Recalculate integral shrink and double residual // Recalculate integral shrink and double residual
factor = std::max(factor, 1.0); xfactor = std::max(xfactor, 1.0);
if (factor >= 2 && interpolatorWindowSize > 3) { yfactor = std::max(yfactor, 1.0);
shrink = floor(factor * 3.0 / interpolatorWindowSize); xshrink = CalculateShrink(xfactor, interpolatorWindowSize);
} else { yshrink = CalculateShrink(yfactor, interpolatorWindowSize);
shrink = floor(factor); xresidual = CalculateResidual(xshrink, xfactor);
} yresidual = CalculateResidual(yshrink, yfactor);
residual = static_cast<double>(shrink) / factor;
// Reload input using shrink-on-load // Reload input using shrink-on-load
VipsImage *shrunkOnLoad; VipsImage *shrunkOnLoad;
if (baton->bufferInLength > 1) { if (baton->bufferInLength > 1) {
@ -420,10 +439,10 @@ class ResizeWorker : public NanAsyncWorker {
image = greyscale; image = greyscale;
} }
if (shrink > 1) { if (xshrink > 1 || yshrink > 1) {
VipsImage *shrunk; VipsImage *shrunk;
// Use vips_shrink with the integral reduction // Use vips_shrink with the integral reduction
if (vips_shrink(image, &shrunk, shrink, shrink, NULL)) { if (vips_shrink(image, &shrunk, xshrink, yshrink, NULL)) {
return Error(); return Error();
} }
vips_object_local(hook, shrunk); vips_object_local(hook, shrunk);
@ -437,17 +456,21 @@ class ResizeWorker : public NanAsyncWorker {
shrunkWidth = shrunkHeight; shrunkWidth = shrunkHeight;
shrunkHeight = swap; shrunkHeight = swap;
} }
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth); xresidual = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth);
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight); yresidual = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight);
if (baton->canvas == Canvas::EMBED) { if (baton->canvas == Canvas::EMBED) {
residual = std::min(residualx, residualy); xresidual = std::min(xresidual, yresidual);
} else { yresidual = xresidual;
residual = std::max(residualx, residualy); } else if (baton->canvas != Canvas::IGNORE_ASPECT) {
xresidual = std::max(xresidual, yresidual);
yresidual = xresidual;
} }
} }
// Use vips_affine with the remaining float part // Use vips_affine with the remaining float part
if (residual != 0.0) { if (xresidual != 0.0 || yresidual != 0.0) {
// 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 // Apply Gaussian blur before large affine reductions
if (residual < 1.0) { if (residual < 1.0) {
// Calculate standard deviation // Calculate standard deviation
@ -482,7 +505,7 @@ class ResizeWorker : public NanAsyncWorker {
vips_object_local(hook, interpolator); vips_object_local(hook, interpolator);
// Perform affine transformation // Perform affine transformation
VipsImage *affined; VipsImage *affined;
if (vips_affine(image, &affined, residual, 0.0, 0.0, residual, "interpolate", interpolator, NULL)) { if (vips_affine(image, &affined, xresidual, 0.0, 0.0, yresidual, "interpolate", interpolator, NULL)) {
return Error(); return Error();
} }
vips_object_local(hook, affined); vips_object_local(hook, affined);
@ -578,7 +601,7 @@ class ResizeWorker : public NanAsyncWorker {
vips_area_unref(reinterpret_cast<VipsArea*>(background)); vips_area_unref(reinterpret_cast<VipsArea*>(background));
vips_object_local(hook, embedded); vips_object_local(hook, embedded);
image = embedded; image = embedded;
} else { } else if (baton->canvas != Canvas::IGNORE_ASPECT) {
// Crop/max/min // Crop/max/min
int left; int left;
int top; int top;
@ -951,6 +974,30 @@ class ResizeWorker : public NanAsyncWorker {
return std::make_tuple(left, top); return std::make_tuple(left, top);
} }
/*
Calculate integral shrink given factor and interpolator window size
*/
int CalculateShrink(double factor, int interpolatorWindowSize) {
int shrink = 1;
if (factor >= 2 && interpolatorWindowSize > 3) {
// Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic
shrink = floor(factor * 3.0 / interpolatorWindowSize);
} else {
shrink = floor(factor);
}
if (shrink < 1) {
shrink = 1;
}
return shrink;
}
/*
Calculate residual given shrink and factor
*/
double CalculateResidual(int shrink, double factor) {
return static_cast<double>(shrink) / factor;
}
/* /*
Copy then clear the error message. Copy then clear the error message.
Unref all transitional images on the hook. Unref all transitional images on the hook.
@ -1015,6 +1062,8 @@ NAN_METHOD(resize) {
baton->canvas = Canvas::MAX; baton->canvas = Canvas::MAX;
} else if (canvas->Equals(NanNew<String>("min"))) { } else if (canvas->Equals(NanNew<String>("min"))) {
baton->canvas = Canvas::MIN; baton->canvas = Canvas::MIN;
} else if (canvas->Equals(NanNew<String>("ignore_aspect"))) {
baton->canvas = Canvas::IGNORE_ASPECT;
} }
// Background colour // Background colour
Local<Array> background = Local<Array>::Cast(options->Get(NanNew<String>("background"))); Local<Array> background = Local<Array>::Cast(options->Get(NanNew<String>("background")));

View File

@ -259,5 +259,104 @@ describe('Resize dimensions', function() {
done(); done();
}); });
}); });
it('Downscale width and height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(320, 320).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Downscale width, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(320).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Downscale height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(null, 320).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Upscale width and height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(3000, 3000).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(3000, info.height);
done();
});
});
it('Upscale width, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(3000).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
it('Upscale height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(null, 3000).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(3000, info.height);
done();
});
});
it('Downscale width, upscale height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(320, 3000).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(3000, info.height);
done();
});
});
it('Upscale width, downscale height, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).resize(3000, 320).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Identity transform, ignoring aspect ratio', function(done) {
sharp(fixtures.inputJpg).ignoreAspectRatio().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
});
}); });