Skip to content

Commit

Permalink
Merge 0c55d0e into 9a0d9ee
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmarsden committed Aug 7, 2022
2 parents 9a0d9ee + 0c55d0e commit 9f70b6c
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 21 deletions.
4 changes: 2 additions & 2 deletions docs/api-operation.md
Expand Up @@ -456,8 +456,8 @@ Apply the linear formula a \* input + b to the image (levels adjustment)

### Parameters

* `a` **[number][1]** multiplier (optional, default `1.0`)
* `b` **[number][1]** offset (optional, default `0.0`)
* `a` **([number][1] | [Array][7]<[number][1]>)** multiplier (optional, default `1.0`)
* `b` **([number][1] | [Array][7]<[number][1]>)** offset (optional, default `0.0`)

<!---->

Expand Down
4 changes: 2 additions & 2 deletions lib/constructor.js
Expand Up @@ -319,8 +319,8 @@ const Sharp = function (input, options) {
tileId: 'https://example.com/iiif',
tileBasename: '',
timeoutSeconds: 0,
linearA: 1,
linearB: 0,
linearA: [],
linearB: [],
// Function to notify of libvips warnings
debuglog: warning => {
this.emit('warning', warning);
Expand Down
18 changes: 17 additions & 1 deletion lib/is.js
Expand Up @@ -126,6 +126,21 @@ const invalidParameterError = function (name, expected, actual) {
);
};

/**
* Create an Error with a message relating to an array length mismatch.
*
* @param {string} fn - function name.
* @param {string} first - first parameter.
* @param {string} second - second parameter.
* @returns {Error} Containing the formatted message.
* @private
*/
const arrayLengthMismatch = function (fn, first, second) {
return new Error(
`Array length mismatch for function ${fn}: ${first} and ${second} have different lengths`
);
};

module.exports = {
defined: defined,
object: object,
Expand All @@ -139,5 +154,6 @@ module.exports = {
integer: integer,
inRange: inRange,
inArray: inArray,
invalidParameterError: invalidParameterError
invalidParameterError: invalidParameterError,
arrayLengthMismatch: arrayLengthMismatch
};
24 changes: 18 additions & 6 deletions lib/operation.js
Expand Up @@ -629,25 +629,37 @@ function boolean (operand, operator, options) {

/**
* Apply the linear formula a * input + b to the image (levels adjustment)
* @param {number} [a=1.0] multiplier
* @param {number} [b=0.0] offset
* @param {number | Array<number>} [a=[]] multiplier
* @param {number | Array<number>} [b=[]] offset
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
function linear (a, b) {
if (!is.defined(a) && is.number(b)) {
a = 1.0;
} else if (is.number(a) && !is.defined(b)) {
b = 0.0;
}
if (!is.defined(a)) {
this.options.linearA = 1.0;
this.options.linearA = [];
} else if (is.number(a)) {
this.options.linearA = [a];
} else if (Array.isArray(a) && a.length) {
this.options.linearA = a;
} else {
throw is.invalidParameterError('a', 'numeric', a);
throw is.invalidParameterError('a', 'numeric | Array', a);
}
if (!is.defined(b)) {
this.options.linearB = 0.0;
this.options.linearB = [];
} else if (is.number(b)) {
this.options.linearB = [b];
} else if (Array.isArray(b) && b.length) {
this.options.linearB = b;
} else {
throw is.invalidParameterError('b', 'numeric', b);
throw is.invalidParameterError('b', 'numeric | Array', b);
}
if (this.options.linearA.length !== this.options.linearB.length) {
throw is.arrayLengthMismatch('linear', 'a', 'b');
}
return this;
}
Expand Down
19 changes: 17 additions & 2 deletions src/operations.cc
Expand Up @@ -306,8 +306,23 @@ namespace sharp {
/*
* Calculate (a * in + b)
*/
VImage Linear(VImage image, double const a, double const b) {
if (HasAlpha(image)) {
VImage Linear(VImage image, std::vector<double> const a, std::vector<double> const b) {
// From libvips:
// If the arrays of constants have just one element, that constant is used for all image bands.
// If the arrays have more than one element and they have the same number of elements as there
// are bands in the image, then one array element is used for each band.
// If the arrays have more than one element and the image only has a single band,
// the result is a many-band image where each band corresponds to one array element.

if (a.size() > image.bands()) {
throw VError("Band expansion using linear() is not currently supported.");
}

// To allow for alpha channel manipulation with linear, the alpha channel removal decision is a bit trickier now.
// Hopefully it does The Right Thing in most scenarios.
if (HasAlpha(image) &&
a.size() != image.bands() &&
(a.size() == 1 || a.size() == image.bands() - 1 || image.bands() - 1 == 1)) {
// Separate alpha channel
VImage alpha = image[image.bands() - 1];
return RemoveAlpha(image).linear(a, b).bandjoin(alpha);
Expand Down
2 changes: 1 addition & 1 deletion src/operations.h
Expand Up @@ -90,7 +90,7 @@ namespace sharp {
/*
* Linear adjustment (a * in + b)
*/
VImage Linear(VImage image, double const a, double const b);
VImage Linear(VImage image, std::vector<double> const a, std::vector<double> const b);

/*
* Recomb with a Matrix of the given bands/channel size.
Expand Down
6 changes: 3 additions & 3 deletions src/pipeline.cc
Expand Up @@ -688,7 +688,7 @@ class PipelineWorker : public Napi::AsyncWorker {
}

// Linear adjustment (a * in + b)
if (baton->linearA != 1.0 || baton->linearB != 0.0) {
if (!baton->linearA.empty()) {
image = sharp::Linear(image, baton->linearA, baton->linearB);
}

Expand Down Expand Up @@ -1454,8 +1454,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->trimThreshold = sharp::AttrAsDouble(options, "trimThreshold");
baton->gamma = sharp::AttrAsDouble(options, "gamma");
baton->gammaOut = sharp::AttrAsDouble(options, "gammaOut");
baton->linearA = sharp::AttrAsDouble(options, "linearA");
baton->linearB = sharp::AttrAsDouble(options, "linearB");
baton->linearA = sharp::AttrAsVectorOfDouble(options, "linearA");
baton->linearB = sharp::AttrAsVectorOfDouble(options, "linearB");
baton->greyscale = sharp::AttrAsBool(options, "greyscale");
baton->normalise = sharp::AttrAsBool(options, "normalise");
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
Expand Down
8 changes: 4 additions & 4 deletions src/pipeline.h
Expand Up @@ -100,8 +100,8 @@ struct PipelineBaton {
double trimThreshold;
int trimOffsetLeft;
int trimOffsetTop;
double linearA;
double linearB;
std::vector<double> linearA;
std::vector<double> linearB;
double gamma;
double gammaOut;
bool greyscale;
Expand Down Expand Up @@ -251,8 +251,8 @@ struct PipelineBaton {
trimThreshold(0.0),
trimOffsetLeft(0),
trimOffsetTop(0),
linearA(1.0),
linearB(0.0),
linearA{},
linearB{},
gamma(0.0),
greyscale(false),
normalise(false),
Expand Down
Binary file added test/fixtures/expected/linear-per-channel.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions test/unit/linear.js
Expand Up @@ -65,6 +65,14 @@ describe('Linear adjustment', function () {
});
});

it('per channel level adjustment', function (done) {
sharp(fixtures.inputWebP)
.linear([0.25, 0.5, 0.75], [150, 100, 50]).toBuffer(function (err, data, info) {
if (err) throw err;
fixtures.assertSimilar(fixtures.expected('linear-per-channel.jpg'), data, done);
});
});

it('Invalid linear arguments', function () {
assert.throws(function () {
sharp(fixtures.inputPngOverlayLayer1)
Expand All @@ -75,5 +83,20 @@ describe('Linear adjustment', function () {
sharp(fixtures.inputPngOverlayLayer1)
.linear(undefined, { bar: 'baz' });
});

assert.throws(function () {
sharp(fixtures.inputPngOverlayLayer1)
.linear([], [1]);
});

assert.throws(function () {
sharp(fixtures.inputPngOverlayLayer1)
.linear([1, 2], [1]);
});

assert.throws(function () {
sharp(fixtures.inputPngOverlayLayer1)
.linear([1]);
});
});
});

0 comments on commit 9f70b6c

Please sign in to comment.