From d0f66c373435bc8700bcba99105bdbdc2cd10dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarda=20Kot=C4=9B=C5=A1ovec?= Date: Tue, 10 Oct 2017 20:03:36 +0200 Subject: [PATCH] Switch to libvips' resize, make fastShrinkOnLoad optional (#977) --- lib/constructor.js | 1 + lib/resize.js | 5 + src/pipeline.cc | 118 ++++-------------- src/pipeline.h | 1 + test/fixtures/centered_image.jpeg | Bin 0 -> 3348 bytes test/fixtures/expected/embed-4-into-4.png | Bin 670 -> 392 bytes .../expected/fast-shrink-on-load-false.png | Bin 0 -> 257 bytes .../expected/fast-shrink-on-load-true.png | Bin 0 -> 263 bytes test/fixtures/index.js | 1 + test/unit/extract.js | 1 + test/unit/gamma.js | 5 +- test/unit/resize.js | 30 +++++ 12 files changed, 67 insertions(+), 95 deletions(-) create mode 100644 test/fixtures/centered_image.jpeg create mode 100644 test/fixtures/expected/fast-shrink-on-load-false.png create mode 100644 test/fixtures/expected/fast-shrink-on-load-true.png diff --git a/lib/constructor.js b/lib/constructor.js index c49394aa..73f37d45 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -149,6 +149,7 @@ const Sharp = function (input, options) { kernel: 'lanczos3', interpolator: 'bicubic', centreSampling: false, + fastShrinkOnLoad: true, // operations background: [0, 0, 0, 255], flatten: false, diff --git a/lib/resize.js b/lib/resize.js index dc8a67ec..b51050d4 100644 --- a/lib/resize.js +++ b/lib/resize.js @@ -142,6 +142,11 @@ function resize (width, height, options) { if (is.defined(options.centreSampling)) { this._setBooleanOption('centreSampling', options.centreSampling); } + + // Shrink on load + if (is.defined(options.fastShrinkOnLoad)) { + this._setBooleanOption('fastShrinkOnLoad', options.fastShrinkOnLoad); + } } return this; } diff --git a/src/pipeline.cc b/src/pipeline.cc index 23863f54..16a8870d 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -222,20 +222,27 @@ class PipelineWorker : public Nan::AsyncWorker { // If integral x and y shrink are equal, try to use shrink-on-load for JPEG and WebP, // but not when applying gamma correction, pre-resize extract or trim int shrink_on_load = 1; + + int shrink_on_load_factor = 1; + // Leave at least a factor of two for the final resize step, when fastShrinkOnLoad: false + // for more consistent results and avoid occasional small image shifting + if (!baton->fastShrinkOnLoad) { + shrink_on_load_factor = 2; + } if ( - xshrink == yshrink && xshrink >= 2 && + xshrink == yshrink && xshrink >= 2 * shrink_on_load_factor && (inputImageType == ImageType::JPEG || inputImageType == ImageType::WEBP) && baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimTolerance == 0 ) { - if (xshrink >= 8) { + if (xshrink >= 8 * shrink_on_load_factor) { xfactor = xfactor / 8; yfactor = yfactor / 8; shrink_on_load = 8; - } else if (xshrink >= 4) { + } else if (xshrink >= 4 * shrink_on_load_factor) { xfactor = xfactor / 4; yfactor = yfactor / 4; shrink_on_load = 4; - } else if (xshrink >= 2) { + } else if (xshrink >= 2 * shrink_on_load_factor) { xfactor = xfactor / 2; yfactor = yfactor / 2; shrink_on_load = 2; @@ -282,23 +289,6 @@ class PipelineWorker : public Nan::AsyncWorker { } xfactor = static_cast(shrunkOnLoadWidth) / static_cast(targetResizeWidth); yfactor = static_cast(shrunkOnLoadHeight) / static_cast(targetResizeHeight); - xshrink = std::max(1, static_cast(floor(xfactor))); - yshrink = std::max(1, static_cast(floor(yfactor))); - xresidual = static_cast(xshrink) / xfactor; - yresidual = static_cast(yshrink) / yfactor; - if ( - !baton->rotateBeforePreExtract && - (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270) - ) { - std::swap(xresidual, yresidual); - } - } - // Help ensure a final kernel-based reduction to prevent shrink aliasing - if (xshrink > 1 && yshrink > 1 && (xresidual == 1.0 || yresidual == 1.0)) { - xshrink = xshrink / 2; - yshrink = yshrink / 2; - xresidual = static_cast(xshrink) / xfactor; - yresidual = static_cast(yshrink) / yfactor; } // Ensure we're using a device-independent colour space @@ -364,13 +354,12 @@ class PipelineWorker : public Nan::AsyncWorker { } } - bool const shouldShrink = xshrink > 1 || yshrink > 1; - bool const shouldReduce = xresidual != 1.0 || yresidual != 1.0; + bool const shouldResize = xfactor != 1.0 || yfactor != 1.0; bool const shouldBlur = baton->blurSigma != 0.0; bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0; bool const shouldSharpen = baton->sharpenSigma != 0.0; bool const shouldPremultiplyAlpha = HasAlpha(image) && - (shouldShrink || shouldReduce || shouldBlur || shouldConv || shouldSharpen || shouldOverlayWithAlpha); + (shouldResize || shouldBlur || shouldConv || shouldSharpen || shouldOverlayWithAlpha); // Premultiply image alpha channel before all transformations to avoid // dark fringing around bright pixels @@ -379,79 +368,21 @@ class PipelineWorker : public Nan::AsyncWorker { image = image.premultiply(); } - // Fast, integral box-shrink - if (shouldShrink) { - if (yshrink > 1) { - image = image.shrinkv(yshrink); - } - if (xshrink > 1) { - image = image.shrinkh(xshrink); - } - // Recalculate residual float based on dimensions of required vs shrunk images - int shrunkWidth = image.width(); - int shrunkHeight = image.height(); - if (!baton->rotateBeforePreExtract && - (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270)) { - // Swap input output width and height when rotating by 90 or 270 degrees - std::swap(shrunkWidth, shrunkHeight); - } - xresidual = static_cast(targetResizeWidth) / static_cast(shrunkWidth); - yresidual = static_cast(targetResizeHeight) / static_cast(shrunkHeight); + // Resize + if (shouldResize) { + VipsKernel kernel = static_cast( + vips_enum_from_nick(nullptr, VIPS_TYPE_KERNEL, baton->kernel.data())); if ( - !baton->rotateBeforePreExtract && - (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270) + kernel != VIPS_KERNEL_NEAREST && kernel != VIPS_KERNEL_CUBIC && kernel != VIPS_KERNEL_LANCZOS2 && + kernel != VIPS_KERNEL_LANCZOS3 ) { - std::swap(xresidual, yresidual); + throw vips::VError("Unknown kernel"); } - } - // Use affine increase or kernel reduce with the remaining float part - if (xresidual != 1.0 || yresidual != 1.0) { - // Insert tile cache to prevent over-computation of previous operations - if (baton->accessMethod == VIPS_ACCESS_SEQUENTIAL) { - image = sharp::TileCache(image, yresidual); - } - // Perform kernel-based reduction - if (yresidual < 1.0 || xresidual < 1.0) { - VipsKernel kernel = static_cast( - vips_enum_from_nick(nullptr, VIPS_TYPE_KERNEL, baton->kernel.data())); - if ( - kernel != VIPS_KERNEL_NEAREST && kernel != VIPS_KERNEL_CUBIC && kernel != VIPS_KERNEL_LANCZOS2 && - kernel != VIPS_KERNEL_LANCZOS3 - ) { - throw vips::VError("Unknown kernel"); - } - if (yresidual < 1.0) { - image = image.reducev(1.0 / yresidual, VImage::option() - ->set("kernel", kernel) - ->set("centre", baton->centreSampling)); - } - if (xresidual < 1.0) { - image = image.reduceh(1.0 / xresidual, VImage::option() - ->set("kernel", kernel) - ->set("centre", baton->centreSampling)); - } - } - // Perform enlargement - if (yresidual > 1.0 || xresidual > 1.0) { - if (trunc(xresidual) == xresidual && trunc(yresidual) == yresidual && baton->interpolator == "nearest") { - // Fast, integral nearest neighbour enlargement - image = image.zoom(static_cast(xresidual), static_cast(yresidual)); - } else { - // Floating point affine transformation - vips::VInterpolate interpolator = vips::VInterpolate::new_from_name(baton->interpolator.data()); - if (yresidual > 1.0 && xresidual > 1.0) { - image = image.affine({xresidual, 0.0, 0.0, yresidual}, VImage::option() - ->set("interpolate", interpolator)); - } else if (yresidual > 1.0) { - image = image.affine({1.0, 0.0, 0.0, yresidual}, VImage::option() - ->set("interpolate", interpolator)); - } else if (xresidual > 1.0) { - image = image.affine({xresidual, 0.0, 0.0, 1.0}, VImage::option() - ->set("interpolate", interpolator)); - } - } - } + image = image.resize(1.0 / xfactor, VImage::option() + ->set("vscale", 1.0 / yfactor) + ->set("kernel", kernel) + ->set("centre", baton->centreSampling)); } // Rotate @@ -1211,6 +1142,7 @@ NAN_METHOD(pipeline) { baton->kernel = AttrAsStr(options, "kernel"); baton->interpolator = AttrAsStr(options, "interpolator"); baton->centreSampling = AttrTo(options, "centreSampling"); + baton->fastShrinkOnLoad = AttrTo(options, "fastShrinkOnLoad"); // Join Channel Options if (HasAttr(options, "joinChannelIn")) { v8::Local joinChannelObject = Nan::Get(options, Nan::New("joinChannelIn").ToLocalChecked()) diff --git a/src/pipeline.h b/src/pipeline.h index 7d7455d3..6146f479 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -69,6 +69,7 @@ struct PipelineBaton { std::string kernel; std::string interpolator; bool centreSampling; + bool fastShrinkOnLoad; double background[4]; bool flatten; bool negate; diff --git a/test/fixtures/centered_image.jpeg b/test/fixtures/centered_image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..4c3afa6bf4c8654181944608f63495db3e7f72b9 GIT binary patch literal 3348 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/expected/embed-4-into-4.png b/test/fixtures/expected/embed-4-into-4.png index 79cf1802f4a0799d3f8628f6c53db5d451876e11..071909cba6c1b8827df268757fb10e780fa9a20f 100644 GIT binary patch delta 345 zcmV-f0jB<*1&9NXIDY{;NklrnB&}>C?c0>BK+y_}J_s+Q5~Pcw zdw)rPzRnA0VOdgi{Q&hm@W8Ue&O2x39dC5bx!)C0K@>zm)+LA#LI@#*5JHFr6Ts{$ z1lfV7eD?p0Z^7!E*RYXT&vx2LZij?04q#`yWNr8gZ{R)O zy}%lG=aGxKjC~t&2m5TjkS7~*+!lV!1=)@Q0@m73^`L zqfRpzqiqA$iBf8vb90Kk54KsK@EY#3_FdY`=Gh|Aow(w2X2zGg#1mM0_Sinj9|Awi rBHjrhgb+fAwL$;0D~N(9$ny9H_nu=RHgm)Z00000NkvXXu0mjfhE=7f delta 625 zcmV-%0*?KN1D*wtIDZ0ANklC-heER1j2pDHIC5nhH@=T%iq?CK%{MDO1uPNqlzLJaz3>nVYQ7=aR$A z`|y0pOW=J-2ms+fApPqG{uB-}X*dXDj4{R-V~nxKj)){8x_|d(Nkrmr^)nQD=!SZ| z9>w=rSnp@+L?kzx&6h-E2_eQZGb18l+qN2y$1hJ$PiFv#2jAIKsnq55^>y;Db<;FM zwr$S?xb;>Fg+lsQ4gO9KySuxqdwY8;hGD!BLMVwuqW9n+LI_XS^&|jfGMRVTY&HP^ zxm->;IyzD;%YTwDE-q#PhgF&Ea+LunJ6ZU=IR21b30DRvUsee?;Y_(bo?RH!4cDqVA9F94T z69oXvvSa|^e!suw`~DOW&9_>uIny*3iAb*3>+9umS=-#)T#dzI%^(Oa0YH}JucKrD z0C=AFd}Cwd698;&ZG8ZMY&QF`SS)J5{YRwV?@K`t+_~<$t~A_FL_*i~XO&7NVi<-D z0F6fD)oT9!{#v0>cztqm5-*iXZx0R*Rz?o9y}i8-0FL9#8HTYma-n-1A0JQEYPIEF zucwTo{w?GB+&Ydky|c43GmhGfF~%5U>=6oq|3#TB9AwgPkV*3m7+wzg%1V==00000 LNkvXXu0mjf6GSFG diff --git a/test/fixtures/expected/fast-shrink-on-load-false.png b/test/fixtures/expected/fast-shrink-on-load-false.png new file mode 100644 index 0000000000000000000000000000000000000000..7d506d557570fcb69d440e540332b13bf62ce6b4 GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRM!2~3itao|@5-9M9EM{O}egVRaTdRYzfr6Vo zT^vI=uFLkF277AWx^^%B?9s#pqOFHGS62M4v72(L|FObhgN(HSM`j3Z5m@;=P;+y4^t9HE z2TJV=WezcZc^4U9`Pn8qJJJ90j7#QTu7MZ6%Wko6o7*L_eNDwBpz|3#UHx3vIVCg! E0H2j66C_Lqb*i;uR7S9@*?H z`fQ_|a>ypO{*YM3s#Q}ym2cYje(?*P>{+t}-t3xUxa+<32K(4Yy*tBp$DRawg2B_( K&t;ucLK6UdGHpiy literal 0 HcmV?d00001 diff --git a/test/fixtures/index.js b/test/fixtures/index.js index ee9aaa02..e82a20d2 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -67,6 +67,7 @@ module.exports = { inputJpg320x240: getPath('320x240.jpg'), // http://www.andrewault.net/2010/01/26/create-a-test-pattern-video-with-perl/ inputJpgOverlayLayer2: getPath('alpha-layer-2-ink.jpg'), inputJpgTruncated: getPath('truncated.jpg'), // head -c 10000 2569067123_aca715a2ee_o.jpg > truncated.jpg + inputJpgCenteredImage: getPath('centered_image.jpeg'), inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png inputPngWithTransparency: getPath('blackbug.png'), // public domain diff --git a/test/unit/extract.js b/test/unit/extract.js index 22a70b74..8d980ee2 100644 --- a/test/unit/extract.js +++ b/test/unit/extract.js @@ -98,6 +98,7 @@ describe('Partial image extraction', function () { sharp(fixtures.inputPngWithGreyAlpha) .extract({ left: 20, top: 10, width: 380, height: 280 }) .rotate(90) + .jpeg() .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual(280, info.width); diff --git a/test/unit/gamma.js b/test/unit/gamma.js index f5c3e45c..b5c56e1b 100644 --- a/test/unit/gamma.js +++ b/test/unit/gamma.js @@ -48,11 +48,12 @@ describe('Gamma correction', function () { sharp(fixtures.inputPngOverlayLayer1) .resize(320) .gamma() + .jpeg() .toBuffer(function (err, data, info) { if (err) throw err; - assert.strictEqual('png', info.format); + assert.strictEqual('jpeg', info.format); assert.strictEqual(320, info.width); - fixtures.assertSimilar(fixtures.expected('gamma-alpha.jpg'), data, { threshold: 20 }, done); + fixtures.assertSimilar(fixtures.expected('gamma-alpha.jpg'), data, done); }); }); diff --git a/test/unit/resize.js b/test/unit/resize.js index 47958dad..4325090d 100644 --- a/test/unit/resize.js +++ b/test/unit/resize.js @@ -448,4 +448,34 @@ describe('Resize dimensions', function () { }); }); }); + + it('fastShrinkOnLoad: false ensures image is not shifted', function (done) { + return sharp(fixtures.inputJpgCenteredImage) + .resize(9, 8, { + fastShrinkOnLoad: false, + centreSampling: true + }) + .png() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(9, info.width); + assert.strictEqual(8, info.height); + // higher threshold makes it pass for both jpeg and jpeg-turbo libs + fixtures.assertSimilar(fixtures.expected('fast-shrink-on-load-false.png'), data, { threshold: 7 }, done); + }); + }); + + it('fastShrinkOnLoad: true (default) might result in shifted image', function (done) { + return sharp(fixtures.inputJpgCenteredImage) + .resize(9, 8, { + centreSampling: true + }) + .png() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(9, info.width); + assert.strictEqual(8, info.height); + fixtures.assertSimilar(fixtures.expected('fast-shrink-on-load-true.png'), data, done); + }); + }); });