sharp 0.26.2 → 0.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  The typical use case for this high speed Node.js module
6
6
  is to convert large images in common formats to
7
- smaller, web-friendly JPEG, PNG and WebP images of varying dimensions.
7
+ smaller, web-friendly JPEG, PNG, WebP and AVIF images of varying dimensions.
8
8
 
9
9
  Resizing an image is typically 4x-5x faster than using the
10
10
  quickest ImageMagick and GraphicsMagick settings
@@ -16,7 +16,7 @@ Lanczos resampling ensures quality is not sacrificed for speed.
16
16
  As well as image resizing, operations such as
17
17
  rotation, extraction, compositing and gamma correction are available.
18
18
 
19
- Most modern macOS, Windows and Linux systems running Node.js v10.16.0+
19
+ Most modern macOS, Windows and Linux systems running Node.js v10+
20
20
  do not require any additional install or runtime dependencies.
21
21
 
22
22
  ## Examples
@@ -102,7 +102,7 @@ covers reporting bugs, requesting features and submitting code changes.
102
102
 
103
103
  ### Licensing
104
104
 
105
- Copyright 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Lovell Fuller and contributors.
105
+ Copyright 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Lovell Fuller and contributors.
106
106
 
107
107
  Licensed under the Apache License, Version 2.0 (the "License");
108
108
  you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ const minimumGlibcVersionByArch = {
25
25
  const { minimumLibvipsVersion, minimumLibvipsVersionLabelled } = libvips;
26
26
  const distHost = process.env.npm_config_sharp_libvips_binary_host || 'https://github.com/lovell/sharp-libvips/releases/download';
27
27
  const distBaseUrl = process.env.npm_config_sharp_dist_base_url || process.env.SHARP_DIST_BASE_URL || `${distHost}/v${minimumLibvipsVersionLabelled}/`;
28
+ const supportsBrotli = ('BrotliDecompress' in zlib);
28
29
 
29
30
  const fail = function (err) {
30
31
  npmLog.error('sharp', err.message);
@@ -43,7 +44,7 @@ const extractTarball = function (tarPath) {
43
44
  libvips.mkdirSync(versionedVendorPath);
44
45
  stream.pipeline(
45
46
  fs.createReadStream(tarPath),
46
- new zlib.BrotliDecompress(),
47
+ supportsBrotli ? new zlib.BrotliDecompress() : new zlib.Gunzip(),
47
48
  tarFs.extract(versionedVendorPath),
48
49
  function (err) {
49
50
  if (err) {
@@ -58,6 +59,7 @@ const extractTarball = function (tarPath) {
58
59
 
59
60
  try {
60
61
  const useGlobalLibvips = libvips.useGlobalLibvips();
62
+
61
63
  if (useGlobalLibvips) {
62
64
  const globalLibvipsVersion = libvips.globalLibvipsVersion();
63
65
  npmLog.info('sharp', `Detected globally-installed libvips v${globalLibvipsVersion}`);
@@ -72,6 +74,9 @@ try {
72
74
  if (arch === 'ia32' && !platformAndArch.startsWith('win32')) {
73
75
  throw new Error(`Intel Architecture 32-bit systems require manual installation of libvips >= ${minimumLibvipsVersion}`);
74
76
  }
77
+ if (platformAndArch === 'darwin-arm64') {
78
+ throw new Error("Please run 'brew install vips' to install libvips on Apple M1 (ARM64) systems");
79
+ }
75
80
  if (platformAndArch === 'freebsd-x64' || platformAndArch === 'openbsd-x64' || platformAndArch === 'sunos-x64') {
76
81
  throw new Error(`BSD/SunOS systems require manual installation of libvips >= ${minimumLibvipsVersion}`);
77
82
  }
@@ -80,14 +85,21 @@ try {
80
85
  throw new Error(`Use with glibc ${detectLibc.version} requires manual installation of libvips >= ${minimumLibvipsVersion}`);
81
86
  }
82
87
  }
88
+ if (detectLibc.family === detectLibc.MUSL && detectLibc.version) {
89
+ if (!semver.satisfies(detectLibc.version, '>=1.1.24 <1.2.0')) {
90
+ throw new Error(`Use with musl ${detectLibc.version} requires manual installation of libvips >= ${minimumLibvipsVersion}`);
91
+ }
92
+ }
83
93
 
84
94
  const supportedNodeVersion = process.env.npm_package_engines_node || require('../package.json').engines.node;
85
95
  if (!semver.satisfies(process.versions.node, supportedNodeVersion)) {
86
96
  throw new Error(`Expected Node.js version ${supportedNodeVersion} but found ${process.versions.node}`);
87
97
  }
88
98
 
99
+ const extension = supportsBrotli ? 'br' : 'gz';
100
+
89
101
  // Download to per-process temporary file
90
- const tarFilename = ['libvips', minimumLibvipsVersion, platformAndArch].join('-') + '.tar.br';
102
+ const tarFilename = ['libvips', minimumLibvipsVersion, platformAndArch].join('-') + '.tar.' + extension;
91
103
  const tarPathCache = path.join(libvips.cachePath(), tarFilename);
92
104
  if (fs.existsSync(tarPathCache)) {
93
105
  npmLog.info('sharp', `Using cached ${tarPathCache}`);
package/lib/channel.js CHANGED
@@ -85,7 +85,7 @@ function extractChannel (channel) {
85
85
  * - sRGB: 0: Red, 1: Green, 2: Blue, 3: Alpha.
86
86
  * - CMYK: 0: Magenta, 1: Cyan, 2: Yellow, 3: Black, 4: Alpha.
87
87
  *
88
- * Buffers may be any of the image formats supported by sharp: JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data.
88
+ * Buffers may be any of the image formats supported by sharp.
89
89
  * For raw pixel input, the `options` object should contain a `raw` attribute, which follows the format of the attribute of the same name in the `sharp()` constructor.
90
90
  *
91
91
  * @param {Array<string|Buffer>|string|Buffer} images - one or more images (file paths, Buffers).
package/lib/colour.js CHANGED
@@ -57,6 +57,13 @@ function grayscale (grayscale) {
57
57
  /**
58
58
  * Set the output colourspace.
59
59
  * By default output image will be web-friendly sRGB, with additional channels interpreted as alpha channels.
60
+ *
61
+ * @example
62
+ * // Output 16 bits per pixel RGB
63
+ * await sharp(input)
64
+ * .toColourspace('rgb16')
65
+ * .toFile('16-bpp.png')
66
+ *
60
67
  * @param {string} [colourspace] - output colourspace e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://github.com/libvips/libvips/blob/master/libvips/iofuncs/enumtypes.c#L568)
61
68
  * @returns {Sharp}
62
69
  * @throws {Error} Invalid parameters
package/lib/composite.js CHANGED
@@ -105,8 +105,9 @@ function composite (images) {
105
105
  input: this._createInputDescriptor(image.input, inputOptions, { allowStream: false }),
106
106
  blend: 'over',
107
107
  tile: false,
108
- left: -1,
109
- top: -1,
108
+ left: 0,
109
+ top: 0,
110
+ hasOffset: false,
110
111
  gravity: 0,
111
112
  premultiplied: false
112
113
  };
@@ -125,21 +126,23 @@ function composite (images) {
125
126
  }
126
127
  }
127
128
  if (is.defined(image.left)) {
128
- if (is.integer(image.left) && image.left >= 0) {
129
+ if (is.integer(image.left)) {
129
130
  composite.left = image.left;
130
131
  } else {
131
- throw is.invalidParameterError('left', 'positive integer', image.left);
132
+ throw is.invalidParameterError('left', 'integer', image.left);
132
133
  }
133
134
  }
134
135
  if (is.defined(image.top)) {
135
- if (is.integer(image.top) && image.top >= 0) {
136
+ if (is.integer(image.top)) {
136
137
  composite.top = image.top;
137
138
  } else {
138
- throw is.invalidParameterError('top', 'positive integer', image.top);
139
+ throw is.invalidParameterError('top', 'integer', image.top);
139
140
  }
140
141
  }
141
- if (composite.left !== composite.top && Math.min(composite.left, composite.top) === -1) {
142
+ if (is.defined(image.top) !== is.defined(image.left)) {
142
143
  throw new Error('Expected both left and top to be set');
144
+ } else {
145
+ composite.hasOffset = is.integer(image.top) && is.integer(image.left);
143
146
  }
144
147
  if (is.defined(image.gravity)) {
145
148
  if (is.integer(image.gravity) && is.inRange(image.gravity, 0, 8)) {
@@ -40,7 +40,7 @@ const debuglog = util.debuglog('sharp');
40
40
  /**
41
41
  * Constructor factory to create an instance of `sharp`, to which further methods are chained.
42
42
  *
43
- * JPEG, PNG, WebP or TIFF format image data can be streamed out from this object.
43
+ * JPEG, PNG, WebP, AVIF or TIFF format image data can be streamed out from this object.
44
44
  * When using Stream based output, derived attributes are available from the `info` event.
45
45
  *
46
46
  * Non-critical problems encountered during processing are emitted as `warning` events.
@@ -90,10 +90,39 @@ const debuglog = util.debuglog('sharp');
90
90
  * // Convert an animated GIF to an animated WebP
91
91
  * await sharp('in.gif', { animated: true }).toFile('out.webp');
92
92
  *
93
- * @param {(Buffer|string)} [input] - if present, can be
94
- * a Buffer containing JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data, or
95
- * a String containing the filesystem path to an JPEG, PNG, WebP, GIF, SVG or TIFF image file.
96
- * JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
93
+ * @example
94
+ * // Read a raw array of pixels and save it to a png
95
+ * const input = Uint8Array.from([255, 255, 255, 0, 0, 0]); // or Uint8ClampedArray
96
+ * const image = sharp(input, {
97
+ * // because the input does not contain its dimensions or how many channels it has
98
+ * // we need to specify it in the constructor options
99
+ * raw: {
100
+ * width: 2,
101
+ * height: 1,
102
+ * channels: 3
103
+ * }
104
+ * });
105
+ * await image.toFile('my-two-pixels.png');
106
+ *
107
+ * @example
108
+ * // Generate RGB Gaussian noise
109
+ * await sharp({
110
+ * create: {
111
+ * width: 300,
112
+ * height: 200,
113
+ * channels: 3,
114
+ * noise: {
115
+ * type: 'gaussian',
116
+ * mean: 128,
117
+ * sigma: 30
118
+ * }
119
+ * }
120
+ * }).toFile('noise.png');
121
+ *
122
+ * @param {(Buffer|Uint8Array|Uint8ClampedArray|string)} [input] - if present, can be
123
+ * a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data, or
124
+ * a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file.
125
+ * JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
97
126
  * @param {Object} [options] - if present, is an Object with optional attributes.
98
127
  * @param {boolean} [options.failOnError=true] - by default halt processing and raise an error when loading invalid images.
99
128
  * Set this flag to `false` if you'd rather apply a "best effort" to decode images, even if the data is corrupt or invalid.
@@ -103,19 +132,23 @@ const debuglog = util.debuglog('sharp');
103
132
  * @param {boolean} [options.sequentialRead=false] - Set this to `true` to use sequential rather than random access where possible.
104
133
  * This can reduce memory usage and might improve performance on some systems.
105
134
  * @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000.
106
- * @param {number} [options.pages=1] - number of pages to extract for multi-page input (GIF, TIFF, PDF), use -1 for all pages.
107
- * @param {number} [options.page=0] - page number to start extracting from for multi-page input (GIF, TIFF, PDF), zero based.
135
+ * @param {number} [options.pages=1] - number of pages to extract for multi-page input (GIF, WebP, AVIF, TIFF, PDF), use -1 for all pages.
136
+ * @param {number} [options.page=0] - page number to start extracting from for multi-page input (GIF, WebP, AVIF, TIFF, PDF), zero based.
108
137
  * @param {number} [options.level=0] - level to extract from a multi-level input (OpenSlide), zero based.
109
138
  * @param {boolean} [options.animated=false] - Set to `true` to read all frames/pages of an animated image (equivalent of setting `pages` to `-1`).
110
139
  * @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering.
111
- * @param {number} [options.raw.width]
112
- * @param {number} [options.raw.height]
113
- * @param {number} [options.raw.channels] - 1-4
140
+ * @param {number} [options.raw.width] - integral number of pixels wide.
141
+ * @param {number} [options.raw.height] - integral number of pixels high.
142
+ * @param {number} [options.raw.channels] - integral number of channels, between 1 and 4.
114
143
  * @param {Object} [options.create] - describes a new image to be created.
115
- * @param {number} [options.create.width]
116
- * @param {number} [options.create.height]
117
- * @param {number} [options.create.channels] - 3-4
144
+ * @param {number} [options.create.width] - integral number of pixels wide.
145
+ * @param {number} [options.create.height] - integral number of pixels high.
146
+ * @param {number} [options.create.channels] - integral number of channels, either 3 (RGB) or 4 (RGBA).
118
147
  * @param {string|Object} [options.create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
148
+ * @param {Object} [options.create.noise] - describes a noise to be created.
149
+ * @param {string} [options.create.noise.type] - type of generated noise, currently only `gaussian` is supported.
150
+ * @param {number} [options.create.noise.mean] - mean of pixels in generated noise.
151
+ * @param {number} [options.create.noise.sigma] - standard deviation of pixels in generated noise.
119
152
  * @returns {Sharp}
120
153
  * @throws {Error} Invalid parameters
121
154
  */
@@ -155,6 +188,13 @@ const Sharp = function (input, options) {
155
188
  extendRight: 0,
156
189
  extendBackground: [0, 0, 0, 255],
157
190
  withoutEnlargement: false,
191
+ affineMatrix: [],
192
+ affineBackground: [0, 0, 0, 255],
193
+ affineIdx: 0,
194
+ affineIdy: 0,
195
+ affineOdx: 0,
196
+ affineOdy: 0,
197
+ affineInterpolator: this.constructor.interpolators.bilinear,
158
198
  kernel: 'lanczos3',
159
199
  fastShrinkOnLoad: true,
160
200
  // operations
@@ -226,9 +266,11 @@ const Sharp = function (input, options) {
226
266
  tiffTileWidth: 256,
227
267
  tiffXres: 1.0,
228
268
  tiffYres: 1.0,
229
- heifQuality: 80,
269
+ heifQuality: 50,
230
270
  heifLossless: false,
231
- heifCompression: 'hevc',
271
+ heifCompression: 'av1',
272
+ heifSpeed: 5,
273
+ heifChromaSubsampling: '4:2:0',
232
274
  tileSize: 256,
233
275
  tileOverlap: 0,
234
276
  tileContainer: 'fs',
package/lib/input.js CHANGED
@@ -31,6 +31,9 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
31
31
  } else if (is.buffer(input)) {
32
32
  // Buffer
33
33
  inputDescriptor.buffer = input;
34
+ } else if (is.uint8Array(input)) {
35
+ // Uint8Array or Uint8ClampedArray
36
+ inputDescriptor.buffer = Buffer.from(input.buffer);
34
37
  } else if (is.plainObject(input) && !is.defined(inputOptions)) {
35
38
  // Plain Object descriptor, e.g. create
36
39
  inputOptions = input;
@@ -134,22 +137,50 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
134
137
  is.object(inputOptions.create) &&
135
138
  is.integer(inputOptions.create.width) && inputOptions.create.width > 0 &&
136
139
  is.integer(inputOptions.create.height) && inputOptions.create.height > 0 &&
137
- is.integer(inputOptions.create.channels) && is.inRange(inputOptions.create.channels, 3, 4) &&
138
- is.defined(inputOptions.create.background)
140
+ is.integer(inputOptions.create.channels)
139
141
  ) {
140
142
  inputDescriptor.createWidth = inputOptions.create.width;
141
143
  inputDescriptor.createHeight = inputOptions.create.height;
142
144
  inputDescriptor.createChannels = inputOptions.create.channels;
143
- const background = color(inputOptions.create.background);
144
- inputDescriptor.createBackground = [
145
- background.red(),
146
- background.green(),
147
- background.blue(),
148
- Math.round(background.alpha() * 255)
149
- ];
145
+ // Noise
146
+ if (is.defined(inputOptions.create.noise)) {
147
+ if (!is.object(inputOptions.create.noise)) {
148
+ throw new Error('Expected noise to be an object');
149
+ }
150
+ if (!is.inArray(inputOptions.create.noise.type, ['gaussian'])) {
151
+ throw new Error('Only gaussian noise is supported at the moment');
152
+ }
153
+ if (!is.inRange(inputOptions.create.channels, 1, 4)) {
154
+ throw is.invalidParameterError('create.channels', 'number between 1 and 4', inputOptions.create.channels);
155
+ }
156
+ inputDescriptor.createNoiseType = inputOptions.create.noise.type;
157
+ if (is.number(inputOptions.create.noise.mean) && is.inRange(inputOptions.create.noise.mean, 0, 10000)) {
158
+ inputDescriptor.createNoiseMean = inputOptions.create.noise.mean;
159
+ } else {
160
+ throw is.invalidParameterError('create.noise.mean', 'number between 0 and 10000', inputOptions.create.noise.mean);
161
+ }
162
+ if (is.number(inputOptions.create.noise.sigma) && is.inRange(inputOptions.create.noise.sigma, 0, 10000)) {
163
+ inputDescriptor.createNoiseSigma = inputOptions.create.noise.sigma;
164
+ } else {
165
+ throw is.invalidParameterError('create.noise.sigma', 'number between 0 and 10000', inputOptions.create.noise.sigma);
166
+ }
167
+ } else if (is.defined(inputOptions.create.background)) {
168
+ if (!is.inRange(inputOptions.create.channels, 3, 4)) {
169
+ throw is.invalidParameterError('create.channels', 'number between 3 and 4', inputOptions.create.channels);
170
+ }
171
+ const background = color(inputOptions.create.background);
172
+ inputDescriptor.createBackground = [
173
+ background.red(),
174
+ background.green(),
175
+ background.blue(),
176
+ Math.round(background.alpha() * 255)
177
+ ];
178
+ } else {
179
+ throw new Error('Expected valid noise or background to create a new input image');
180
+ }
150
181
  delete inputDescriptor.buffer;
151
182
  } else {
152
- throw new Error('Expected width, height, channels and background to create a new input image');
183
+ throw new Error('Expected valid width, height and channels to create a new input image');
153
184
  }
154
185
  }
155
186
  } else if (is.defined(inputOptions)) {
package/lib/is.js CHANGED
@@ -48,6 +48,15 @@ const buffer = function (val) {
48
48
  return val instanceof Buffer;
49
49
  };
50
50
 
51
+ /**
52
+ * Is this value a Uint8Array or Uint8ClampedArray object?
53
+ * @private
54
+ */
55
+ const uint8Array = function (val) {
56
+ // allow both since Uint8ClampedArray simply clamps the values between 0-255
57
+ return val instanceof Uint8Array || val instanceof Uint8ClampedArray;
58
+ };
59
+
51
60
  /**
52
61
  * Is this value a non-empty string?
53
62
  * @private
@@ -110,6 +119,7 @@ module.exports = {
110
119
  fn: fn,
111
120
  bool: bool,
112
121
  buffer: buffer,
122
+ uint8Array: uint8Array,
113
123
  string: string,
114
124
  number: number,
115
125
  integer: integer,
package/lib/libvips.js CHANGED
@@ -37,10 +37,20 @@ const cachePath = function () {
37
37
  return libvipsCachePath;
38
38
  };
39
39
 
40
+ const isRosetta = function () {
41
+ /* istanbul ignore next */
42
+ if (process.platform === 'darwin' && process.arch === 'x64') {
43
+ const translated = spawnSync('sysctl sysctl.proc_translated', spawnSyncOptions).stdout;
44
+ return (translated || '').trim() === 'sysctl.proc_translated: 1';
45
+ }
46
+ return false;
47
+ };
48
+
40
49
  const globalLibvipsVersion = function () {
41
50
  if (process.platform !== 'win32') {
42
- const globalLibvipsVersion = spawnSync(`PKG_CONFIG_PATH="${pkgConfigPath()}" pkg-config --modversion vips-cpp`, spawnSyncOptions).stdout || '';
43
- return globalLibvipsVersion.trim();
51
+ const globalLibvipsVersion = spawnSync(`PKG_CONFIG_PATH="${pkgConfigPath()}" pkg-config --modversion vips-cpp`, spawnSyncOptions).stdout;
52
+ /* istanbul ignore next */
53
+ return (globalLibvipsVersion || '').trim();
44
54
  } else {
45
55
  return '';
46
56
  }
@@ -81,7 +91,10 @@ const useGlobalLibvips = function () {
81
91
  if (Boolean(env.SHARP_IGNORE_GLOBAL_LIBVIPS) === true) {
82
92
  return false;
83
93
  }
84
-
94
+ /* istanbul ignore next */
95
+ if (isRosetta()) {
96
+ return false;
97
+ }
85
98
  const globalVipsVersion = globalLibvipsVersion();
86
99
  return !!globalVipsVersion && /* istanbul ignore next */
87
100
  semver.gte(globalVipsVersion, minimumLibvipsVersion);
package/lib/operation.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const { flatten: flattenArray } = require('array-flatten');
3
4
  const color = require('color');
4
5
  const is = require('./is');
5
6
 
@@ -82,6 +83,103 @@ function flop (flop) {
82
83
  return this;
83
84
  }
84
85
 
86
+ /**
87
+ * Perform an affine transform on an image. This operation will always occur after resizing, extraction and rotation, if any.
88
+ *
89
+ * You must provide an array of length 4 or a 2x2 affine transformation matrix.
90
+ * By default, new pixels are filled with a black background. You can provide a background color with the `background` option.
91
+ * A particular interpolator may also be specified. Set the `interpolator` option to an attribute of the `sharp.interpolator` Object e.g. `sharp.interpolator.nohalo`.
92
+ *
93
+ * In the case of a 2x2 matrix, the transform is:
94
+ * - X = `matrix[0, 0]` \* (x + `idx`) + `matrix[0, 1]` \* (y + `idy`) + `odx`
95
+ * - Y = `matrix[1, 0]` \* (x + `idx`) + `matrix[1, 1]` \* (y + `idy`) + `ody`
96
+ *
97
+ * where:
98
+ * - x and y are the coordinates in input image.
99
+ * - X and Y are the coordinates in output image.
100
+ * - (0,0) is the upper left corner.
101
+ *
102
+ * @since 0.27.0
103
+ *
104
+ * @example
105
+ * const pipeline = sharp()
106
+ * .affine([[1, 0.3], [0.1, 0.7]], {
107
+ * background: 'white',
108
+ * interpolate: sharp.interpolators.nohalo
109
+ * })
110
+ * .toBuffer((err, outputBuffer, info) => {
111
+ * // outputBuffer contains the transformed image
112
+ * // info.width and info.height contain the new dimensions
113
+ * });
114
+ *
115
+ * inputStream
116
+ * .pipe(pipeline);
117
+ *
118
+ * @param {Array<Array<number>>|Array<number>} matrix - affine transformation matrix
119
+ * @param {Object} [options] - if present, is an Object with optional attributes.
120
+ * @param {String|Object} [options.background="#000000"] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
121
+ * @param {Number} [options.idx=0] - input horizontal offset
122
+ * @param {Number} [options.idy=0] - input vertical offset
123
+ * @param {Number} [options.odx=0] - output horizontal offset
124
+ * @param {Number} [options.ody=0] - output vertical offset
125
+ * @param {String} [options.interpolator=sharp.interpolators.bicubic] - interpolator
126
+ * @returns {Sharp}
127
+ * @throws {Error} Invalid parameters
128
+ */
129
+ function affine (matrix, options) {
130
+ const flatMatrix = flattenArray(matrix);
131
+ if (flatMatrix.length === 4 && flatMatrix.every(is.number)) {
132
+ this.options.affineMatrix = flatMatrix;
133
+ } else {
134
+ throw is.invalidParameterError('matrix', '1x4 or 2x2 array', matrix);
135
+ }
136
+
137
+ if (is.defined(options)) {
138
+ if (is.object(options)) {
139
+ this._setBackgroundColourOption('affineBackground', options.background);
140
+ if (is.defined(options.idx)) {
141
+ if (is.number(options.idx)) {
142
+ this.options.affineIdx = options.idx;
143
+ } else {
144
+ throw is.invalidParameterError('options.idx', 'number', options.idx);
145
+ }
146
+ }
147
+ if (is.defined(options.idy)) {
148
+ if (is.number(options.idy)) {
149
+ this.options.affineIdy = options.idy;
150
+ } else {
151
+ throw is.invalidParameterError('options.idy', 'number', options.idy);
152
+ }
153
+ }
154
+ if (is.defined(options.odx)) {
155
+ if (is.number(options.odx)) {
156
+ this.options.affineOdx = options.odx;
157
+ } else {
158
+ throw is.invalidParameterError('options.odx', 'number', options.odx);
159
+ }
160
+ }
161
+ if (is.defined(options.ody)) {
162
+ if (is.number(options.ody)) {
163
+ this.options.affineOdy = options.ody;
164
+ } else {
165
+ throw is.invalidParameterError('options.ody', 'number', options.ody);
166
+ }
167
+ }
168
+ if (is.defined(options.interpolator)) {
169
+ if (is.inArray(options.interpolator, Object.values(this.constructor.interpolators))) {
170
+ this.options.affineInterpolator = options.interpolator;
171
+ } else {
172
+ throw is.invalidParameterError('options.interpolator', 'valid interpolator name', options.interpolator);
173
+ }
174
+ }
175
+ } else {
176
+ throw is.invalidParameterError('options', 'object', options);
177
+ }
178
+ }
179
+
180
+ return this;
181
+ }
182
+
85
183
  /**
86
184
  * Sharpen the image.
87
185
  * When used without parameters, performs a fast, mild sharpen of the output image.
@@ -482,6 +580,7 @@ module.exports = function (Sharp) {
482
580
  rotate,
483
581
  flip,
484
582
  flop,
583
+ affine,
485
584
  sharpen,
486
585
  median,
487
586
  blur,