Skip to content

Commit

Permalink
Support resize without preserving aspect ratio #118
Browse files Browse the repository at this point in the history
  • Loading branch information
skedastik committed Apr 16, 2015
1 parent 3810f64 commit f72435c
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 54 deletions.
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ Sharp.prototype.min = function() {
return this;
};

Sharp.prototype.ignoreAspectRatio = function() {
this.options.canvas = 'ignore_aspect';
return this;
};

Sharp.prototype.flatten = function(flatten) {
this.options.flatten = (typeof flatten === 'boolean') ? flatten : true;
return this;
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"Andreas Lind <andreas@one.com>",
"Maurus Cuelenaere <mcuelenaere@gmail.com>",
"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",
"scripts": {
Expand Down
155 changes: 102 additions & 53 deletions src/resize.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ enum class Canvas {
CROP,
EMBED,
MAX,
MIN
MIN,
IGNORE_ASPECT
};

enum class Angle {
Expand Down Expand Up @@ -254,98 +255,116 @@ class ResizeWorker : public NanAsyncWorker {
int interpolatorWindowSize = InterpolatorWindowSize(baton->interpolator.c_str());

// Scaling calculations
double factor = 1.0;
double xfactor = 1.0;
double yfactor = 1.0;
if (baton->width > 0 && baton->height > 0) {
// Fixed width and height
double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
switch (baton->canvas) {
case Canvas::CROP:
factor = std::min(xfactor, yfactor);
xfactor = std::min(xfactor, yfactor);
yfactor = xfactor;
break;
case Canvas::EMBED:
factor = std::max(xfactor, yfactor);
xfactor = std::max(xfactor, yfactor);
yfactor = xfactor;
break;
case Canvas::MAX:
factor = std::max(xfactor, yfactor);
if (xfactor > yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor);
yfactor = xfactor;
} else {
baton->width = round(static_cast<double>(inputWidth) / yfactor);
xfactor = yfactor;
}
break;
case Canvas::MIN:
factor = std::min(xfactor, yfactor);
if (xfactor < yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor);
yfactor = xfactor;
} else {
baton->width = round(static_cast<double>(inputWidth) / yfactor);
xfactor = yfactor;
}
break;
case Canvas::IGNORE_ASPECT:
// xfactor, yfactor OK!
break;
}
} else if (baton->width > 0) {
// Fixed width, auto height
factor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
baton->height = floor(static_cast<double>(inputHeight) / factor);
// Fixed width
xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
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) {
// Fixed height, auto width
factor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
baton->width = floor(static_cast<double>(inputWidth) / factor);
// Fixed height
yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
if (baton->canvas == Canvas::IGNORE_ASPECT) {
baton->width = inputWidth;
} else {
// Auto width
xfactor = yfactor;
baton->width = floor(static_cast<double>(inputWidth) / xfactor);
}
} else {
// Identity transform
baton->width = inputWidth;
baton->height = inputHeight;
}

// Calculate integral box shrink
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;
}
int xshrink = CalculateShrink(xfactor, interpolatorWindowSize);
int yshrink = CalculateShrink(yfactor, interpolatorWindowSize);

// 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
if (baton->withoutEnlargement) {
if (inputWidth < baton->width || inputHeight < baton->height) {
factor = 1;
shrink = 1;
residual = 0;
xfactor = 1;
yfactor = 1;
xshrink = 1;
yshrink = 1;
xresidual = 0;
yresidual = 0;
baton->width = inputWidth;
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;
if (inputImageType == ImageType::JPEG && shrink >= 2 && baton->gamma == 0 && baton->topOffsetPre == -1) {
if (shrink >= 8) {
factor = factor / 8;
if (xshrink == yshrink && inputImageType == ImageType::JPEG && xshrink >= 2 && baton->gamma == 0 && baton->topOffsetPre == -1) {
if (xshrink >= 8) {
xfactor = xfactor / 8;
yfactor = yfactor / 8;
shrink_on_load = 8;
} else if (shrink >= 4) {
factor = factor / 4;
} else if (xshrink >= 4) {
xfactor = xfactor / 4;
yfactor = yfactor / 4;
shrink_on_load = 4;
} else if (shrink >= 2) {
factor = factor / 2;
} else if (xshrink >= 2) {
xfactor = xfactor / 2;
yfactor = yfactor / 2;
shrink_on_load = 2;
}
}
if (shrink_on_load > 1) {
// Recalculate integral shrink and double residual
factor = std::max(factor, 1.0);
if (factor >= 2 && interpolatorWindowSize > 3) {
shrink = floor(factor * 3.0 / interpolatorWindowSize);
} else {
shrink = floor(factor);
}
residual = static_cast<double>(shrink) / factor;
xfactor = std::max(xfactor, 1.0);
yfactor = std::max(yfactor, 1.0);
xshrink = CalculateShrink(xfactor, interpolatorWindowSize);
yshrink = CalculateShrink(yfactor, interpolatorWindowSize);
xresidual = CalculateResidual(xshrink, xfactor);
yresidual = CalculateResidual(yshrink, yfactor);
// Reload input using shrink-on-load
VipsImage *shrunkOnLoad;
if (baton->bufferInLength > 1) {
Expand Down Expand Up @@ -420,10 +439,10 @@ class ResizeWorker : public NanAsyncWorker {
image = greyscale;
}

if (shrink > 1) {
if (xshrink > 1 || yshrink > 1) {
VipsImage *shrunk;
// 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();
}
vips_object_local(hook, shrunk);
Expand All @@ -437,17 +456,21 @@ class ResizeWorker : public NanAsyncWorker {
shrunkWidth = shrunkHeight;
shrunkHeight = swap;
}
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth);
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight);
xresidual = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth);
yresidual = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight);
if (baton->canvas == Canvas::EMBED) {
residual = std::min(residualx, residualy);
} else {
residual = std::max(residualx, residualy);
xresidual = std::min(xresidual, yresidual);
yresidual = xresidual;
} else if (baton->canvas != Canvas::IGNORE_ASPECT) {
xresidual = std::max(xresidual, yresidual);
yresidual = xresidual;
}
}

// 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
if (residual < 1.0) {
// Calculate standard deviation
Expand Down Expand Up @@ -482,7 +505,7 @@ class ResizeWorker : public NanAsyncWorker {
vips_object_local(hook, interpolator);
// Perform affine transformation
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();
}
vips_object_local(hook, affined);
Expand Down Expand Up @@ -578,7 +601,7 @@ class ResizeWorker : public NanAsyncWorker {
vips_area_unref(reinterpret_cast<VipsArea*>(background));
vips_object_local(hook, embedded);
image = embedded;
} else {
} else if (baton->canvas != Canvas::IGNORE_ASPECT) {
// Crop/max/min
int left;
int top;
Expand Down Expand Up @@ -951,6 +974,30 @@ class ResizeWorker : public NanAsyncWorker {
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.
Unref all transitional images on the hook.
Expand Down Expand Up @@ -1015,6 +1062,8 @@ NAN_METHOD(resize) {
baton->canvas = Canvas::MAX;
} else if (canvas->Equals(NanNew<String>("min"))) {
baton->canvas = Canvas::MIN;
} else if (canvas->Equals(NanNew<String>("ignore_aspect"))) {
baton->canvas = Canvas::IGNORE_ASPECT;
}
// Background colour
Local<Array> background = Local<Array>::Cast(options->Get(NanNew<String>("background")));
Expand Down
99 changes: 99 additions & 0 deletions test/unit/resize.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,104 @@ describe('Resize dimensions', function() {
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();
});
});

});

0 comments on commit f72435c

Please sign in to comment.