Skip to content

Commit ea599ad

Browse files
committed
Allow sharpen options to be provided as an Object
Also exposes x1, y2, y3 parameters #2561 #2935
1 parent 1de49f3 commit ea599ad

10 files changed

Lines changed: 214 additions & 46 deletions

File tree

docs/api-operation.md

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,41 @@ When used without parameters, performs a fast, mild sharpen of the output image.
129129
When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space.
130130
Separate control over the level of sharpening in "flat" and "jagged" areas is available.
131131

132+
See [libvips sharpen][8] operation.
133+
132134
### Parameters
133135

134-
* `sigma` **[number][1]?** the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
135-
* `flat` **[number][1]** the level of sharpening to apply to "flat" areas. (optional, default `1.0`)
136-
* `jagged` **[number][1]** the level of sharpening to apply to "jagged" areas. (optional, default `2.0`)
136+
* `options` **[Object][2]?** if present, is an Object with optional attributes.
137137

138-
<!---->
138+
* `options.sigma` **[number][1]?** the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
139+
* `options.m1` **[number][1]** the level of sharpening to apply to "flat" areas. (optional, default `1.0`)
140+
* `options.m2` **[number][1]** the level of sharpening to apply to "jagged" areas. (optional, default `2.0`)
141+
* `options.x1` **[number][1]** threshold between "flat" and "jagged" (optional, default `2.0`)
142+
* `options.y2` **[number][1]** maximum amount of brightening. (optional, default `10.0`)
143+
* `options.y3` **[number][1]** maximum amount of darkening. (optional, default `20.0`)
144+
145+
### Examples
146+
147+
```javascript
148+
const data = await sharp(input).sharpen().toBuffer();
149+
```
150+
151+
```javascript
152+
const data = await sharp(input).sharpen({ sigma: 2 }).toBuffer();
153+
```
154+
155+
```javascript
156+
const data = await sharp(input)
157+
.sharpen({
158+
sigma: 2,
159+
m1: 0
160+
m2: 3,
161+
x1: 3,
162+
y2: 15,
163+
y3: 15,
164+
})
165+
.toBuffer();
166+
```
139167

140168
* Throws **[Error][5]** Invalid parameters
141169

@@ -190,7 +218,7 @@ Returns **Sharp**
190218

191219
Merge alpha transparency channel, if any, with a background, then remove the alpha channel.
192220

193-
See also [removeAlpha][8].
221+
See also [removeAlpha][9].
194222

195223
### Parameters
196224

@@ -264,7 +292,7 @@ Returns **Sharp**
264292
## clahe
265293

266294
Perform contrast limiting adaptive histogram equalization
267-
[CLAHE][9].
295+
[CLAHE][10].
268296

269297
This will, in general, enhance the clarity of the image by bringing out darker details.
270298

@@ -349,7 +377,7 @@ the selected bitwise boolean `operation` between the corresponding pixels of the
349377

350378
### Parameters
351379

352-
* `operand` **([Buffer][10] | [string][3])** Buffer containing image data or string containing the path to an image file.
380+
* `operand` **([Buffer][11] | [string][3])** Buffer containing image data or string containing the path to an image file.
353381
* `operator` **[string][3]** one of `and`, `or` or `eor` to perform that bitwise operation, like the C logic operators `&`, `|` and `^` respectively.
354382
* `options` **[Object][2]?**
355383

@@ -474,8 +502,10 @@ Returns **Sharp**
474502

475503
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
476504

477-
[8]: /api-channel#removealpha
505+
[8]: https://www.libvips.org/API/current/libvips-convolution.html#vips-sharpen
506+
507+
[9]: /api-channel#removealpha
478508

479-
[9]: https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE
509+
[10]: https://en.wikipedia.org/wiki/Adaptive_histogram_equalization#Contrast_Limited_AHE
480510

481-
[10]: https://nodejs.org/api/buffer.html
511+
[11]: https://nodejs.org/api/buffer.html

docs/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ Requires libvips v8.12.2
66

77
### v0.30.3 - TBD
88

9+
* Allow `sharpen` options to be provided more consistently as an Object.
10+
[#2561](https://github.com/lovell/sharp/issues/2561)
11+
12+
* Expose `x1`, `y2` and `y3` parameters of `sharpen` operation.
13+
[#2935](https://github.com/lovell/sharp/issues/2935)
14+
915
* Prevent double unpremultiply with some composite blend modes (regression in 0.30.2).
1016
[#3118](https://github.com/lovell/sharp/issues/3118)
1117

docs/search-index.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

lib/constructor.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,11 @@ const Sharp = function (input, options) {
186186
medianSize: 0,
187187
blurSigma: 0,
188188
sharpenSigma: 0,
189-
sharpenFlat: 1,
190-
sharpenJagged: 2,
189+
sharpenM1: 1,
190+
sharpenM2: 2,
191+
sharpenX1: 2,
192+
sharpenY2: 10,
193+
sharpenY3: 20,
191194
threshold: 0,
192195
thresholdGrayscale: true,
193196
trimThreshold: 0,

lib/operation.js

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -185,40 +185,105 @@ function affine (matrix, options) {
185185
* When a `sigma` is provided, performs a slower, more accurate sharpen of the L channel in the LAB colour space.
186186
* Separate control over the level of sharpening in "flat" and "jagged" areas is available.
187187
*
188-
* @param {number} [sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
189-
* @param {number} [flat=1.0] - the level of sharpening to apply to "flat" areas.
190-
* @param {number} [jagged=2.0] - the level of sharpening to apply to "jagged" areas.
188+
* See {@link https://www.libvips.org/API/current/libvips-convolution.html#vips-sharpen|libvips sharpen} operation.
189+
*
190+
* @example
191+
* const data = await sharp(input).sharpen().toBuffer();
192+
*
193+
* @example
194+
* const data = await sharp(input).sharpen({ sigma: 2 }).toBuffer();
195+
*
196+
* @example
197+
* const data = await sharp(input)
198+
* .sharpen({
199+
* sigma: 2,
200+
* m1: 0
201+
* m2: 3,
202+
* x1: 3,
203+
* y2: 15,
204+
* y3: 15,
205+
* })
206+
* .toBuffer();
207+
*
208+
* @param {Object} [options] - if present, is an Object with optional attributes.
209+
* @param {number} [options.sigma] - the sigma of the Gaussian mask, where `sigma = 1 + radius / 2`.
210+
* @param {number} [options.m1=1.0] - the level of sharpening to apply to "flat" areas.
211+
* @param {number} [options.m2=2.0] - the level of sharpening to apply to "jagged" areas.
212+
* @param {number} [options.x1=2.0] - threshold between "flat" and "jagged"
213+
* @param {number} [options.y2=10.0] - maximum amount of brightening.
214+
* @param {number} [options.y3=20.0] - maximum amount of darkening.
191215
* @returns {Sharp}
192216
* @throws {Error} Invalid parameters
193217
*/
194-
function sharpen (sigma, flat, jagged) {
195-
if (!is.defined(sigma)) {
218+
function sharpen (options) {
219+
if (!is.defined(options)) {
196220
// No arguments: default to mild sharpen
197221
this.options.sharpenSigma = -1;
198-
} else if (is.bool(sigma)) {
199-
// Boolean argument: apply mild sharpen?
200-
this.options.sharpenSigma = sigma ? -1 : 0;
201-
} else if (is.number(sigma) && is.inRange(sigma, 0.01, 10000)) {
202-
// Numeric argument: specific sigma
203-
this.options.sharpenSigma = sigma;
204-
// Control over flat areas
205-
if (is.defined(flat)) {
206-
if (is.number(flat) && is.inRange(flat, 0, 10000)) {
207-
this.options.sharpenFlat = flat;
222+
} else if (is.bool(options)) {
223+
// Deprecated boolean argument: apply mild sharpen?
224+
this.options.sharpenSigma = options ? -1 : 0;
225+
} else if (is.number(options) && is.inRange(options, 0.01, 10000)) {
226+
// Deprecated numeric argument: specific sigma
227+
this.options.sharpenSigma = options;
228+
// Deprecated control over flat areas
229+
if (is.defined(arguments[1])) {
230+
if (is.number(arguments[1]) && is.inRange(arguments[1], 0, 10000)) {
231+
this.options.sharpenM1 = arguments[1];
232+
} else {
233+
throw is.invalidParameterError('flat', 'number between 0 and 10000', arguments[1]);
234+
}
235+
}
236+
// Deprecated control over jagged areas
237+
if (is.defined(arguments[2])) {
238+
if (is.number(arguments[2]) && is.inRange(arguments[2], 0, 10000)) {
239+
this.options.sharpenM2 = arguments[2];
240+
} else {
241+
throw is.invalidParameterError('jagged', 'number between 0 and 10000', arguments[2]);
242+
}
243+
}
244+
} else if (is.plainObject(options)) {
245+
if (is.number(options.sigma) && is.inRange(options.sigma, 0.01, 10000)) {
246+
this.options.sharpenSigma = options.sigma;
247+
} else {
248+
throw is.invalidParameterError('options.sigma', 'number between 0.01 and 10000', options.sigma);
249+
}
250+
if (is.defined(options.m1)) {
251+
if (is.number(options.m1) && is.inRange(options.m1, 0, 10000)) {
252+
this.options.sharpenM1 = options.m1;
253+
} else {
254+
throw is.invalidParameterError('options.m1', 'number between 0 and 10000', options.m1);
255+
}
256+
}
257+
if (is.defined(options.m2)) {
258+
if (is.number(options.m2) && is.inRange(options.m2, 0, 10000)) {
259+
this.options.sharpenM2 = options.m2;
260+
} else {
261+
throw is.invalidParameterError('options.m2', 'number between 0 and 10000', options.m2);
262+
}
263+
}
264+
if (is.defined(options.x1)) {
265+
if (is.number(options.x1) && is.inRange(options.x1, 0, 10000)) {
266+
this.options.sharpenX1 = options.x1;
267+
} else {
268+
throw is.invalidParameterError('options.x1', 'number between 0 and 10000', options.x1);
269+
}
270+
}
271+
if (is.defined(options.y2)) {
272+
if (is.number(options.y2) && is.inRange(options.y2, 0, 10000)) {
273+
this.options.sharpenY2 = options.y2;
208274
} else {
209-
throw is.invalidParameterError('flat', 'number between 0 and 10000', flat);
275+
throw is.invalidParameterError('options.y2', 'number between 0 and 10000', options.y2);
210276
}
211277
}
212-
// Control over jagged areas
213-
if (is.defined(jagged)) {
214-
if (is.number(jagged) && is.inRange(jagged, 0, 10000)) {
215-
this.options.sharpenJagged = jagged;
278+
if (is.defined(options.y3)) {
279+
if (is.number(options.y3) && is.inRange(options.y3, 0, 10000)) {
280+
this.options.sharpenY3 = options.y3;
216281
} else {
217-
throw is.invalidParameterError('jagged', 'number between 0 and 10000', jagged);
282+
throw is.invalidParameterError('options.y3', 'number between 0 and 10000', options.y3);
218283
}
219284
}
220285
} else {
221-
throw is.invalidParameterError('sigma', 'number between 0.01 and 10000', sigma);
286+
throw is.invalidParameterError('sigma', 'number between 0.01 and 10000', options);
222287
}
223288
return this;
224289
}

src/operations.cc

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ namespace sharp {
209209
/*
210210
* Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
211211
*/
212-
VImage Sharpen(VImage image, double const sigma, double const flat, double const jagged) {
212+
VImage Sharpen(VImage image, double const sigma, double const m1, double const m2,
213+
double const x1, double const y2, double const y3) {
213214
if (sigma == -1.0) {
214215
// Fast, mild sharpen
215216
VImage sharpen = VImage::new_matrixv(3, 3,
@@ -224,8 +225,14 @@ namespace sharp {
224225
if (colourspaceBeforeSharpen == VIPS_INTERPRETATION_RGB) {
225226
colourspaceBeforeSharpen = VIPS_INTERPRETATION_sRGB;
226227
}
227-
return image.sharpen(
228-
VImage::option()->set("sigma", sigma)->set("m1", flat)->set("m2", jagged))
228+
return image
229+
.sharpen(VImage::option()
230+
->set("sigma", sigma)
231+
->set("m1", m1)
232+
->set("m2", m2)
233+
->set("x1", x1)
234+
->set("y2", y2)
235+
->set("y3", y3))
229236
.colourspace(colourspaceBeforeSharpen);
230237
}
231238
}

src/operations.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ namespace sharp {
6464
/*
6565
* Sharpen flat and jagged areas. Use sigma of -1.0 for fast sharpen.
6666
*/
67-
VImage Sharpen(VImage image, double const sigma, double const flat, double const jagged);
67+
VImage Sharpen(VImage image, double const sigma, double const m1, double const m2,
68+
double const x1, double const y2, double const y3);
6869

6970
/*
7071
Threshold an image

src/pipeline.cc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,8 @@ class PipelineWorker : public Napi::AsyncWorker {
577577

578578
// Sharpen
579579
if (shouldSharpen) {
580-
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenFlat, baton->sharpenJagged);
580+
image = sharp::Sharpen(image, baton->sharpenSigma, baton->sharpenM1, baton->sharpenM2,
581+
baton->sharpenX1, baton->sharpenY2, baton->sharpenY3);
581582
}
582583

583584
// Composite
@@ -1400,8 +1401,11 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
14001401
baton->lightness = sharp::AttrAsDouble(options, "lightness");
14011402
baton->medianSize = sharp::AttrAsUint32(options, "medianSize");
14021403
baton->sharpenSigma = sharp::AttrAsDouble(options, "sharpenSigma");
1403-
baton->sharpenFlat = sharp::AttrAsDouble(options, "sharpenFlat");
1404-
baton->sharpenJagged = sharp::AttrAsDouble(options, "sharpenJagged");
1404+
baton->sharpenM1 = sharp::AttrAsDouble(options, "sharpenM1");
1405+
baton->sharpenM2 = sharp::AttrAsDouble(options, "sharpenM2");
1406+
baton->sharpenX1 = sharp::AttrAsDouble(options, "sharpenX1");
1407+
baton->sharpenY2 = sharp::AttrAsDouble(options, "sharpenY2");
1408+
baton->sharpenY3 = sharp::AttrAsDouble(options, "sharpenY3");
14051409
baton->threshold = sharp::AttrAsInt32(options, "threshold");
14061410
baton->thresholdGrayscale = sharp::AttrAsBool(options, "thresholdGrayscale");
14071411
baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");

src/pipeline.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,11 @@ struct PipelineBaton {
9090
double lightness;
9191
int medianSize;
9292
double sharpenSigma;
93-
double sharpenFlat;
94-
double sharpenJagged;
93+
double sharpenM1;
94+
double sharpenM2;
95+
double sharpenX1;
96+
double sharpenY2;
97+
double sharpenY3;
9598
int threshold;
9699
bool thresholdGrayscale;
97100
double trimThreshold;
@@ -234,8 +237,11 @@ struct PipelineBaton {
234237
lightness(0),
235238
medianSize(0),
236239
sharpenSigma(0.0),
237-
sharpenFlat(1.0),
238-
sharpenJagged(2.0),
240+
sharpenM1(1.0),
241+
sharpenM2(2.0),
242+
sharpenX1(2.0),
243+
sharpenY2(10.0),
244+
sharpenY3(20.0),
239245
threshold(0),
240246
thresholdGrayscale(true),
241247
trimThreshold(0.0),

test/unit/sharpen.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ describe('Sharpen', function () {
4545
});
4646
});
4747

48+
it('sigma=3.5, m1=2, m2=4', (done) => {
49+
sharp(fixtures.inputJpg)
50+
.resize(320, 240)
51+
.sharpen({ sigma: 3.5, m1: 2, m2: 4 })
52+
.toBuffer()
53+
.then(data => fixtures.assertSimilar(fixtures.expected('sharpen-5-2-4.jpg'), data, done));
54+
});
55+
56+
it('sigma=3.5, m1=2, m2=4, x1=2, y2=5, y3=25', (done) => {
57+
sharp(fixtures.inputJpg)
58+
.resize(320, 240)
59+
.sharpen({ sigma: 3.5, m1: 2, m2: 4, x1: 2, y2: 5, y3: 25 })
60+
.toBuffer()
61+
.then(data => fixtures.assertSimilar(fixtures.expected('sharpen-5-2-4.jpg'), data, done));
62+
});
63+
4864
if (!process.env.SHARP_TEST_WITHOUT_CACHE) {
4965
it('specific radius/levels with alpha channel', function (done) {
5066
sharp(fixtures.inputPngWithTransparency)
@@ -92,6 +108,36 @@ describe('Sharpen', function () {
92108
});
93109
});
94110

111+
it('invalid options.sigma', () => assert.throws(
112+
() => sharp().sharpen({ sigma: -1 }),
113+
/Expected number between 0\.01 and 10000 for options\.sigma but received -1 of type number/
114+
));
115+
116+
it('invalid options.m1', () => assert.throws(
117+
() => sharp().sharpen({ sigma: 1, m1: -1 }),
118+
/Expected number between 0 and 10000 for options\.m1 but received -1 of type number/
119+
));
120+
121+
it('invalid options.m2', () => assert.throws(
122+
() => sharp().sharpen({ sigma: 1, m2: -1 }),
123+
/Expected number between 0 and 10000 for options\.m2 but received -1 of type number/
124+
));
125+
126+
it('invalid options.x1', () => assert.throws(
127+
() => sharp().sharpen({ sigma: 1, x1: -1 }),
128+
/Expected number between 0 and 10000 for options\.x1 but received -1 of type number/
129+
));
130+
131+
it('invalid options.y2', () => assert.throws(
132+
() => sharp().sharpen({ sigma: 1, y2: -1 }),
133+
/Expected number between 0 and 10000 for options\.y2 but received -1 of type number/
134+
));
135+
136+
it('invalid options.y3', () => assert.throws(
137+
() => sharp().sharpen({ sigma: 1, y3: -1 }),
138+
/Expected number between 0 and 10000 for options\.y3 but received -1 of type number/
139+
));
140+
95141
it('sharpened image is larger than non-sharpened', function (done) {
96142
sharp(fixtures.inputJpg)
97143
.resize(320, 240)

0 commit comments

Comments
 (0)