mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 10:30:15 +02:00
Support resize without preserving aspect ratio #118
This commit is contained in:
parent
3810f642d3
commit
f72435c750
5
index.js
5
index.js
@ -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;
|
||||||
|
@ -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": {
|
||||||
|
155
src/resize.cc
155
src/resize.cc
@ -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")));
|
||||||
|
@ -260,4 +260,103 @@ describe('Resize dimensions', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user