@revizly/sharp 0.33.2-revizly3

Sign up to get free protection for your applications and to get access to all the features.
package/lib/resize.js ADDED
@@ -0,0 +1,582 @@
1
+ // Copyright 2013 Lovell Fuller and others.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ 'use strict';
5
+
6
+ const is = require('./is');
7
+
8
+ /**
9
+ * Weighting to apply when using contain/cover fit.
10
+ * @member
11
+ * @private
12
+ */
13
+ const gravity = {
14
+ center: 0,
15
+ centre: 0,
16
+ north: 1,
17
+ east: 2,
18
+ south: 3,
19
+ west: 4,
20
+ northeast: 5,
21
+ southeast: 6,
22
+ southwest: 7,
23
+ northwest: 8
24
+ };
25
+
26
+ /**
27
+ * Position to apply when using contain/cover fit.
28
+ * @member
29
+ * @private
30
+ */
31
+ const position = {
32
+ top: 1,
33
+ right: 2,
34
+ bottom: 3,
35
+ left: 4,
36
+ 'right top': 5,
37
+ 'right bottom': 6,
38
+ 'left bottom': 7,
39
+ 'left top': 8
40
+ };
41
+
42
+ /**
43
+ * How to extend the image.
44
+ * @member
45
+ * @private
46
+ */
47
+ const extendWith = {
48
+ background: 'background',
49
+ copy: 'copy',
50
+ repeat: 'repeat',
51
+ mirror: 'mirror'
52
+ };
53
+
54
+ /**
55
+ * Strategies for automagic cover behaviour.
56
+ * @member
57
+ * @private
58
+ */
59
+ const strategy = {
60
+ entropy: 16,
61
+ attention: 17
62
+ };
63
+
64
+ /**
65
+ * Reduction kernels.
66
+ * @member
67
+ * @private
68
+ */
69
+ const kernel = {
70
+ nearest: 'nearest',
71
+ cubic: 'cubic',
72
+ mitchell: 'mitchell',
73
+ lanczos2: 'lanczos2',
74
+ lanczos3: 'lanczos3'
75
+ };
76
+
77
+ /**
78
+ * Methods by which an image can be resized to fit the provided dimensions.
79
+ * @member
80
+ * @private
81
+ */
82
+ const fit = {
83
+ contain: 'contain',
84
+ cover: 'cover',
85
+ fill: 'fill',
86
+ inside: 'inside',
87
+ outside: 'outside'
88
+ };
89
+
90
+ /**
91
+ * Map external fit property to internal canvas property.
92
+ * @member
93
+ * @private
94
+ */
95
+ const mapFitToCanvas = {
96
+ contain: 'embed',
97
+ cover: 'crop',
98
+ fill: 'ignore_aspect',
99
+ inside: 'max',
100
+ outside: 'min'
101
+ };
102
+
103
+ /**
104
+ * @private
105
+ */
106
+ function isRotationExpected (options) {
107
+ return (options.angle % 360) !== 0 || options.useExifOrientation === true || options.rotationAngle !== 0;
108
+ }
109
+
110
+ /**
111
+ * @private
112
+ */
113
+ function isResizeExpected (options) {
114
+ return options.width !== -1 || options.height !== -1;
115
+ }
116
+
117
+ /**
118
+ * Resize image to `width`, `height` or `width x height`.
119
+ *
120
+ * When both a `width` and `height` are provided, the possible methods by which the image should **fit** these are:
121
+ * - `cover`: (default) Preserving aspect ratio, attempt to ensure the image covers both provided dimensions by cropping/clipping to fit.
122
+ * - `contain`: Preserving aspect ratio, contain within both provided dimensions using "letterboxing" where necessary.
123
+ * - `fill`: Ignore the aspect ratio of the input and stretch to both provided dimensions.
124
+ * - `inside`: Preserving aspect ratio, resize the image to be as large as possible while ensuring its dimensions are less than or equal to both those specified.
125
+ * - `outside`: Preserving aspect ratio, resize the image to be as small as possible while ensuring its dimensions are greater than or equal to both those specified.
126
+ *
127
+ * Some of these values are based on the [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) CSS property.
128
+ *
129
+ * <img alt="Examples of various values for the fit property when resizing" width="100%" style="aspect-ratio: 998/243" src="https://cdn.jsdelivr.net/gh/lovell/sharp@main/docs/image/api-resize-fit.svg">
130
+ *
131
+ * When using a **fit** of `cover` or `contain`, the default **position** is `centre`. Other options are:
132
+ * - `sharp.position`: `top`, `right top`, `right`, `right bottom`, `bottom`, `left bottom`, `left`, `left top`.
133
+ * - `sharp.gravity`: `north`, `northeast`, `east`, `southeast`, `south`, `southwest`, `west`, `northwest`, `center` or `centre`.
134
+ * - `sharp.strategy`: `cover` only, dynamically crop using either the `entropy` or `attention` strategy.
135
+ *
136
+ * Some of these values are based on the [object-position](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) CSS property.
137
+ *
138
+ * The experimental strategy-based approach resizes so one dimension is at its target length
139
+ * then repeatedly ranks edge regions, discarding the edge with the lowest score based on the selected strategy.
140
+ * - `entropy`: focus on the region with the highest [Shannon entropy](https://en.wikipedia.org/wiki/Entropy_%28information_theory%29).
141
+ * - `attention`: focus on the region with the highest luminance frequency, colour saturation and presence of skin tones.
142
+ *
143
+ * Possible interpolation kernels are:
144
+ * - `nearest`: Use [nearest neighbour interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation).
145
+ * - `cubic`: Use a [Catmull-Rom spline](https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline).
146
+ * - `mitchell`: Use a [Mitchell-Netravali spline](https://www.cs.utexas.edu/~fussell/courses/cs384g-fall2013/lectures/mitchell/Mitchell.pdf).
147
+ * - `lanczos2`: Use a [Lanczos kernel](https://en.wikipedia.org/wiki/Lanczos_resampling#Lanczos_kernel) with `a=2`.
148
+ * - `lanczos3`: Use a Lanczos kernel with `a=3` (the default).
149
+ *
150
+ * Only one resize can occur per pipeline.
151
+ * Previous calls to `resize` in the same pipeline will be ignored.
152
+ *
153
+ * @example
154
+ * sharp(input)
155
+ * .resize({ width: 100 })
156
+ * .toBuffer()
157
+ * .then(data => {
158
+ * // 100 pixels wide, auto-scaled height
159
+ * });
160
+ *
161
+ * @example
162
+ * sharp(input)
163
+ * .resize({ height: 100 })
164
+ * .toBuffer()
165
+ * .then(data => {
166
+ * // 100 pixels high, auto-scaled width
167
+ * });
168
+ *
169
+ * @example
170
+ * sharp(input)
171
+ * .resize(200, 300, {
172
+ * kernel: sharp.kernel.nearest,
173
+ * fit: 'contain',
174
+ * position: 'right top',
175
+ * background: { r: 255, g: 255, b: 255, alpha: 0.5 }
176
+ * })
177
+ * .toFile('output.png')
178
+ * .then(() => {
179
+ * // output.png is a 200 pixels wide and 300 pixels high image
180
+ * // containing a nearest-neighbour scaled version
181
+ * // contained within the north-east corner of a semi-transparent white canvas
182
+ * });
183
+ *
184
+ * @example
185
+ * const transformer = sharp()
186
+ * .resize({
187
+ * width: 200,
188
+ * height: 200,
189
+ * fit: sharp.fit.cover,
190
+ * position: sharp.strategy.entropy
191
+ * });
192
+ * // Read image data from readableStream
193
+ * // Write 200px square auto-cropped image data to writableStream
194
+ * readableStream
195
+ * .pipe(transformer)
196
+ * .pipe(writableStream);
197
+ *
198
+ * @example
199
+ * sharp(input)
200
+ * .resize(200, 200, {
201
+ * fit: sharp.fit.inside,
202
+ * withoutEnlargement: true
203
+ * })
204
+ * .toFormat('jpeg')
205
+ * .toBuffer()
206
+ * .then(function(outputBuffer) {
207
+ * // outputBuffer contains JPEG image data
208
+ * // no wider and no higher than 200 pixels
209
+ * // and no larger than the input image
210
+ * });
211
+ *
212
+ * @example
213
+ * sharp(input)
214
+ * .resize(200, 200, {
215
+ * fit: sharp.fit.outside,
216
+ * withoutReduction: true
217
+ * })
218
+ * .toFormat('jpeg')
219
+ * .toBuffer()
220
+ * .then(function(outputBuffer) {
221
+ * // outputBuffer contains JPEG image data
222
+ * // of at least 200 pixels wide and 200 pixels high while maintaining aspect ratio
223
+ * // and no smaller than the input image
224
+ * });
225
+ *
226
+ * @example
227
+ * const scaleByHalf = await sharp(input)
228
+ * .metadata()
229
+ * .then(({ width }) => sharp(input)
230
+ * .resize(Math.round(width * 0.5))
231
+ * .toBuffer()
232
+ * );
233
+ *
234
+ * @param {number} [width] - How many pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height.
235
+ * @param {number} [height] - How many pixels high the resultant image should be. Use `null` or `undefined` to auto-scale the height to match the width.
236
+ * @param {Object} [options]
237
+ * @param {number} [options.width] - An alternative means of specifying `width`. If both are present this takes priority.
238
+ * @param {number} [options.height] - An alternative means of specifying `height`. If both are present this takes priority.
239
+ * @param {String} [options.fit='cover'] - How the image should be resized/cropped to fit the target dimension(s), one of `cover`, `contain`, `fill`, `inside` or `outside`.
240
+ * @param {String} [options.position='centre'] - A position, gravity or strategy to use when `fit` is `cover` or `contain`.
241
+ * @param {String|Object} [options.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour when `fit` is `contain`, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency.
242
+ * @param {String} [options.kernel='lanczos3'] - The kernel to use for image reduction. Use the `fastShrinkOnLoad` option to control kernel vs shrink-on-load.
243
+ * @param {Boolean} [options.withoutEnlargement=false] - Do not scale up if the width *or* height are already less than the target dimensions, equivalent to GraphicsMagick's `>` geometry option. This may result in output dimensions smaller than the target dimensions.
244
+ * @param {Boolean} [options.withoutReduction=false] - Do not scale down if the width *or* height are already greater than the target dimensions, equivalent to GraphicsMagick's `<` geometry option. This may still result in a crop to reach the target dimensions.
245
+ * @param {Boolean} [options.fastShrinkOnLoad=true] - Take greater advantage of the JPEG and WebP shrink-on-load feature, which can lead to a slight moiré pattern or round-down of an auto-scaled dimension.
246
+ * @returns {Sharp}
247
+ * @throws {Error} Invalid parameters
248
+ */
249
+ function resize (widthOrOptions, height, options) {
250
+ if (isResizeExpected(this.options)) {
251
+ this.options.debuglog('ignoring previous resize options');
252
+ }
253
+ if (this.options.widthPost !== -1) {
254
+ this.options.debuglog('operation order will be: extract, resize, extract');
255
+ }
256
+ if (is.defined(widthOrOptions)) {
257
+ if (is.object(widthOrOptions) && !is.defined(options)) {
258
+ options = widthOrOptions;
259
+ } else if (is.integer(widthOrOptions) && widthOrOptions > 0) {
260
+ this.options.width = widthOrOptions;
261
+ } else {
262
+ throw is.invalidParameterError('width', 'positive integer', widthOrOptions);
263
+ }
264
+ } else {
265
+ this.options.width = -1;
266
+ }
267
+ if (is.defined(height)) {
268
+ if (is.integer(height) && height > 0) {
269
+ this.options.height = height;
270
+ } else {
271
+ throw is.invalidParameterError('height', 'positive integer', height);
272
+ }
273
+ } else {
274
+ this.options.height = -1;
275
+ }
276
+ if (is.object(options)) {
277
+ // Width
278
+ if (is.defined(options.width)) {
279
+ if (is.integer(options.width) && options.width > 0) {
280
+ this.options.width = options.width;
281
+ } else {
282
+ throw is.invalidParameterError('width', 'positive integer', options.width);
283
+ }
284
+ }
285
+ // Height
286
+ if (is.defined(options.height)) {
287
+ if (is.integer(options.height) && options.height > 0) {
288
+ this.options.height = options.height;
289
+ } else {
290
+ throw is.invalidParameterError('height', 'positive integer', options.height);
291
+ }
292
+ }
293
+ // Fit
294
+ if (is.defined(options.fit)) {
295
+ const canvas = mapFitToCanvas[options.fit];
296
+ if (is.string(canvas)) {
297
+ this.options.canvas = canvas;
298
+ } else {
299
+ throw is.invalidParameterError('fit', 'valid fit', options.fit);
300
+ }
301
+ }
302
+ // Position
303
+ if (is.defined(options.position)) {
304
+ const pos = is.integer(options.position)
305
+ ? options.position
306
+ : strategy[options.position] || position[options.position] || gravity[options.position];
307
+ if (is.integer(pos) && (is.inRange(pos, 0, 8) || is.inRange(pos, 16, 17))) {
308
+ this.options.position = pos;
309
+ } else {
310
+ throw is.invalidParameterError('position', 'valid position/gravity/strategy', options.position);
311
+ }
312
+ }
313
+ // Background
314
+ this._setBackgroundColourOption('resizeBackground', options.background);
315
+ // Kernel
316
+ if (is.defined(options.kernel)) {
317
+ if (is.string(kernel[options.kernel])) {
318
+ this.options.kernel = kernel[options.kernel];
319
+ } else {
320
+ throw is.invalidParameterError('kernel', 'valid kernel name', options.kernel);
321
+ }
322
+ }
323
+ // Without enlargement
324
+ if (is.defined(options.withoutEnlargement)) {
325
+ this._setBooleanOption('withoutEnlargement', options.withoutEnlargement);
326
+ }
327
+ // Without reduction
328
+ if (is.defined(options.withoutReduction)) {
329
+ this._setBooleanOption('withoutReduction', options.withoutReduction);
330
+ }
331
+ // Shrink on load
332
+ if (is.defined(options.fastShrinkOnLoad)) {
333
+ this._setBooleanOption('fastShrinkOnLoad', options.fastShrinkOnLoad);
334
+ }
335
+ }
336
+ if (isRotationExpected(this.options) && isResizeExpected(this.options)) {
337
+ this.options.rotateBeforePreExtract = true;
338
+ }
339
+ return this;
340
+ }
341
+
342
+ /**
343
+ * Extend / pad / extrude one or more edges of the image with either
344
+ * the provided background colour or pixels derived from the image.
345
+ * This operation will always occur after resizing and extraction, if any.
346
+ *
347
+ * @example
348
+ * // Resize to 140 pixels wide, then add 10 transparent pixels
349
+ * // to the top, left and right edges and 20 to the bottom edge
350
+ * sharp(input)
351
+ * .resize(140)
352
+ * .extend({
353
+ * top: 10,
354
+ * bottom: 20,
355
+ * left: 10,
356
+ * right: 10,
357
+ * background: { r: 0, g: 0, b: 0, alpha: 0 }
358
+ * })
359
+ * ...
360
+ *
361
+ * @example
362
+ * // Add a row of 10 red pixels to the bottom
363
+ * sharp(input)
364
+ * .extend({
365
+ * bottom: 10,
366
+ * background: 'red'
367
+ * })
368
+ * ...
369
+ *
370
+ * @example
371
+ * // Extrude image by 8 pixels to the right, mirroring existing right hand edge
372
+ * sharp(input)
373
+ * .extend({
374
+ * right: 8,
375
+ * background: 'mirror'
376
+ * })
377
+ * ...
378
+ *
379
+ * @param {(number|Object)} extend - single pixel count to add to all edges or an Object with per-edge counts
380
+ * @param {number} [extend.top=0]
381
+ * @param {number} [extend.left=0]
382
+ * @param {number} [extend.bottom=0]
383
+ * @param {number} [extend.right=0]
384
+ * @param {String} [extend.extendWith='background'] - populate new pixels using this method, one of: background, copy, repeat, mirror.
385
+ * @param {String|Object} [extend.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency.
386
+ * @returns {Sharp}
387
+ * @throws {Error} Invalid parameters
388
+ */
389
+ function extend (extend) {
390
+ if (is.integer(extend) && extend > 0) {
391
+ this.options.extendTop = extend;
392
+ this.options.extendBottom = extend;
393
+ this.options.extendLeft = extend;
394
+ this.options.extendRight = extend;
395
+ } else if (is.object(extend)) {
396
+ if (is.defined(extend.top)) {
397
+ if (is.integer(extend.top) && extend.top >= 0) {
398
+ this.options.extendTop = extend.top;
399
+ } else {
400
+ throw is.invalidParameterError('top', 'positive integer', extend.top);
401
+ }
402
+ }
403
+ if (is.defined(extend.bottom)) {
404
+ if (is.integer(extend.bottom) && extend.bottom >= 0) {
405
+ this.options.extendBottom = extend.bottom;
406
+ } else {
407
+ throw is.invalidParameterError('bottom', 'positive integer', extend.bottom);
408
+ }
409
+ }
410
+ if (is.defined(extend.left)) {
411
+ if (is.integer(extend.left) && extend.left >= 0) {
412
+ this.options.extendLeft = extend.left;
413
+ } else {
414
+ throw is.invalidParameterError('left', 'positive integer', extend.left);
415
+ }
416
+ }
417
+ if (is.defined(extend.right)) {
418
+ if (is.integer(extend.right) && extend.right >= 0) {
419
+ this.options.extendRight = extend.right;
420
+ } else {
421
+ throw is.invalidParameterError('right', 'positive integer', extend.right);
422
+ }
423
+ }
424
+ this._setBackgroundColourOption('extendBackground', extend.background);
425
+ if (is.defined(extend.extendWith)) {
426
+ if (is.string(extendWith[extend.extendWith])) {
427
+ this.options.extendWith = extendWith[extend.extendWith];
428
+ } else {
429
+ throw is.invalidParameterError('extendWith', 'one of: background, copy, repeat, mirror', extend.extendWith);
430
+ }
431
+ }
432
+ } else {
433
+ throw is.invalidParameterError('extend', 'integer or object', extend);
434
+ }
435
+ return this;
436
+ }
437
+
438
+ /**
439
+ * Extract/crop a region of the image.
440
+ *
441
+ * - Use `extract` before `resize` for pre-resize extraction.
442
+ * - Use `extract` after `resize` for post-resize extraction.
443
+ * - Use `extract` twice and `resize` once for extract-then-resize-then-extract in a fixed operation order.
444
+ *
445
+ * @example
446
+ * sharp(input)
447
+ * .extract({ left: left, top: top, width: width, height: height })
448
+ * .toFile(output, function(err) {
449
+ * // Extract a region of the input image, saving in the same format.
450
+ * });
451
+ * @example
452
+ * sharp(input)
453
+ * .extract({ left: leftOffsetPre, top: topOffsetPre, width: widthPre, height: heightPre })
454
+ * .resize(width, height)
455
+ * .extract({ left: leftOffsetPost, top: topOffsetPost, width: widthPost, height: heightPost })
456
+ * .toFile(output, function(err) {
457
+ * // Extract a region, resize, then extract from the resized image
458
+ * });
459
+ *
460
+ * @param {Object} options - describes the region to extract using integral pixel values
461
+ * @param {number} options.left - zero-indexed offset from left edge
462
+ * @param {number} options.top - zero-indexed offset from top edge
463
+ * @param {number} options.width - width of region to extract
464
+ * @param {number} options.height - height of region to extract
465
+ * @returns {Sharp}
466
+ * @throws {Error} Invalid parameters
467
+ */
468
+ function extract (options) {
469
+ const suffix = isResizeExpected(this.options) || this.options.widthPre !== -1 ? 'Post' : 'Pre';
470
+ if (this.options[`width${suffix}`] !== -1) {
471
+ this.options.debuglog('ignoring previous extract options');
472
+ }
473
+ ['left', 'top', 'width', 'height'].forEach(function (name) {
474
+ const value = options[name];
475
+ if (is.integer(value) && value >= 0) {
476
+ this.options[name + (name === 'left' || name === 'top' ? 'Offset' : '') + suffix] = value;
477
+ } else {
478
+ throw is.invalidParameterError(name, 'integer', value);
479
+ }
480
+ }, this);
481
+ // Ensure existing rotation occurs before pre-resize extraction
482
+ if (isRotationExpected(this.options) && !isResizeExpected(this.options)) {
483
+ if (this.options.widthPre === -1 || this.options.widthPost === -1) {
484
+ this.options.rotateBeforePreExtract = true;
485
+ }
486
+ }
487
+ return this;
488
+ }
489
+
490
+ /**
491
+ * Trim pixels from all edges that contain values similar to the given background colour, which defaults to that of the top-left pixel.
492
+ *
493
+ * Images with an alpha channel will use the combined bounding box of alpha and non-alpha channels.
494
+ *
495
+ * If the result of this operation would trim an image to nothing then no change is made.
496
+ *
497
+ * The `info` response Object will contain `trimOffsetLeft` and `trimOffsetTop` properties.
498
+ *
499
+ * @example
500
+ * // Trim pixels with a colour similar to that of the top-left pixel.
501
+ * await sharp(input)
502
+ * .trim()
503
+ * .toFile(output);
504
+ *
505
+ * @example
506
+ * // Trim pixels with the exact same colour as that of the top-left pixel.
507
+ * await sharp(input)
508
+ * .trim({
509
+ * threshold: 0
510
+ * })
511
+ * .toFile(output);
512
+ *
513
+ * @example
514
+ * // Assume input is line art and trim only pixels with a similar colour to red.
515
+ * const output = await sharp(input)
516
+ * .trim({
517
+ * background: "#FF0000",
518
+ * lineArt: true
519
+ * })
520
+ * .toBuffer();
521
+ *
522
+ * @example
523
+ * // Trim all "yellow-ish" pixels, being more lenient with the higher threshold.
524
+ * const output = await sharp(input)
525
+ * .trim({
526
+ * background: "yellow",
527
+ * threshold: 42,
528
+ * })
529
+ * .toBuffer();
530
+ *
531
+ * @param {Object} [options]
532
+ * @param {string|Object} [options.background='top-left pixel'] - Background colour, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to that of the top-left pixel.
533
+ * @param {number} [options.threshold=10] - Allowed difference from the above colour, a positive number.
534
+ * @param {boolean} [options.lineArt=false] - Does the input more closely resemble line art (e.g. vector) rather than being photographic?
535
+ * @returns {Sharp}
536
+ * @throws {Error} Invalid parameters
537
+ */
538
+ function trim (options) {
539
+ this.options.trimThreshold = 10;
540
+ if (is.defined(options)) {
541
+ if (is.object(options)) {
542
+ if (is.defined(options.background)) {
543
+ this._setBackgroundColourOption('trimBackground', options.background);
544
+ }
545
+ if (is.defined(options.threshold)) {
546
+ if (is.number(options.threshold) && options.threshold >= 0) {
547
+ this.options.trimThreshold = options.threshold;
548
+ } else {
549
+ throw is.invalidParameterError('threshold', 'positive number', options.threshold);
550
+ }
551
+ }
552
+ if (is.defined(options.lineArt)) {
553
+ this._setBooleanOption('trimLineArt', options.lineArt);
554
+ }
555
+ } else {
556
+ throw is.invalidParameterError('trim', 'object', options);
557
+ }
558
+ }
559
+ if (isRotationExpected(this.options)) {
560
+ this.options.rotateBeforePreExtract = true;
561
+ }
562
+ return this;
563
+ }
564
+
565
+ /**
566
+ * Decorate the Sharp prototype with resize-related functions.
567
+ * @private
568
+ */
569
+ module.exports = function (Sharp) {
570
+ Object.assign(Sharp.prototype, {
571
+ resize,
572
+ extend,
573
+ extract,
574
+ trim
575
+ });
576
+ // Class attributes
577
+ Sharp.gravity = gravity;
578
+ Sharp.strategy = strategy;
579
+ Sharp.kernel = kernel;
580
+ Sharp.fit = fit;
581
+ Sharp.position = position;
582
+ };
package/lib/sharp.js ADDED
@@ -0,0 +1,115 @@
1
+ // Copyright 2013 Lovell Fuller and others.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ 'use strict';
5
+
6
+ // Inspects the runtime environment and exports the relevant sharp.node binary
7
+
8
+ const { familySync, versionSync } = require('detect-libc');
9
+
10
+ const { runtimePlatformArch, isUnsupportedNodeRuntime, prebuiltPlatforms, minimumLibvipsVersion } = require('./libvips');
11
+ const runtimePlatform = runtimePlatformArch();
12
+
13
+ const paths = [
14
+ `../src/build/Release/sharp-${runtimePlatform}.node`,
15
+ '../src/build/Release/sharp-wasm32.node',
16
+ `@revizly/sharp-${runtimePlatform}/sharp.node`,
17
+ '@revizly/sharp-wasm32/sharp.node'
18
+ ];
19
+
20
+ let sharp;
21
+ const errors = [];
22
+ for (const path of paths) {
23
+ try {
24
+ sharp = require(path);
25
+ break;
26
+ } catch (err) {
27
+ /* istanbul ignore next */
28
+ errors.push(err);
29
+ }
30
+ }
31
+
32
+ /* istanbul ignore next */
33
+ if (sharp) {
34
+ module.exports = sharp;
35
+ } else {
36
+ const [isLinux, isMacOs, isWindows] = ['linux', 'darwin', 'win32'].map(os => runtimePlatform.startsWith(os));
37
+
38
+ const help = [`Could not load the "sharp" module using the ${runtimePlatform} runtime`];
39
+ errors.forEach(err => {
40
+ if (err.code !== 'MODULE_NOT_FOUND') {
41
+ help.push(`${err.code}: ${err.message}`);
42
+ }
43
+ });
44
+ const messages = errors.map(err => err.message).join(' ');
45
+ help.push('Possible solutions:');
46
+ // Common error messages
47
+ if (isUnsupportedNodeRuntime()) {
48
+ const { found, expected } = isUnsupportedNodeRuntime();
49
+ help.push(
50
+ '- Please upgrade Node.js:',
51
+ ` Found ${found}`,
52
+ ` Requires ${expected}`
53
+ );
54
+ } else if (prebuiltPlatforms.includes(runtimePlatform)) {
55
+ const [os, cpu] = runtimePlatform.split('-');
56
+ const libc = os.endsWith('musl') ? ' --libc=musl' : '';
57
+ help.push(
58
+ '- Ensure optional dependencies can be installed:',
59
+ ' npm install --include=optional sharp',
60
+ ' yarn add sharp --ignore-engines',
61
+ '- Ensure your package manager supports multi-platform installation:',
62
+ ' See https://sharp.pixelplumbing.com/install#cross-platform',
63
+ '- Add platform-specific dependencies:',
64
+ ` npm install --os=${os.replace('musl', '')}${libc} --cpu=${cpu} sharp`
65
+ );
66
+ } else {
67
+ help.push(
68
+ `- Manually install libvips >= ${minimumLibvipsVersion}`,
69
+ '- Add experimental WebAssembly-based dependencies:',
70
+ ' npm install --cpu=wasm32 sharp',
71
+ ' npm install @revizly/sharp-wasm32'
72
+ );
73
+ }
74
+ if (isLinux && /(symbol not found|CXXABI_)/i.test(messages)) {
75
+ try {
76
+ const { engines } = require(`@revizly/sharp-libvips-${runtimePlatform}/package`);
77
+ const libcFound = `${familySync()} ${versionSync()}`;
78
+ const libcRequires = `${engines.musl ? 'musl' : 'glibc'} ${engines.musl || engines.glibc}`;
79
+ help.push(
80
+ '- Update your OS:',
81
+ ` Found ${libcFound}`,
82
+ ` Requires ${libcRequires}`
83
+ );
84
+ } catch (errEngines) {}
85
+ }
86
+ if (isLinux && /\/snap\/core[0-9]{2}/.test(messages)) {
87
+ help.push(
88
+ '- Remove the Node.js Snap, which does not support native modules',
89
+ ' snap remove node'
90
+ );
91
+ }
92
+ if (isMacOs && /Incompatible library version/.test(messages)) {
93
+ help.push(
94
+ '- Update Homebrew:',
95
+ ' brew update && brew upgrade vips'
96
+ );
97
+ }
98
+ if (errors.some(err => err.code === 'ERR_DLOPEN_DISABLED')) {
99
+ help.push('- Run Node.js without using the --no-addons flag');
100
+ }
101
+ // Link to installation docs
102
+ if (isWindows && /The specified procedure could not be found/.test(messages)) {
103
+ help.push(
104
+ '- Using the canvas package on Windows?',
105
+ ' See https://sharp.pixelplumbing.com/install#canvas-and-windows',
106
+ '- Check for outdated versions of sharp in the dependency tree:',
107
+ ' npm ls sharp'
108
+ );
109
+ }
110
+ help.push(
111
+ '- Consult the installation documentation:',
112
+ ' See https://sharp.pixelplumbing.com/install'
113
+ );
114
+ throw new Error(help.join('\n'));
115
+ }