@revizly/sharp 0.34.0-revizly3 → 0.34.0-revizly6

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/lib/channel.js CHANGED
@@ -16,7 +16,7 @@ const bool = {
16
16
  };
17
17
 
18
18
  /**
19
- * Remove alpha channel, if any. This is a no-op if the image does not have an alpha channel.
19
+ * Remove alpha channels, if any. This is a no-op if the image does not have an alpha channel.
20
20
  *
21
21
  * See also {@link /api-operation#flatten|flatten}.
22
22
  *
package/lib/composite.js CHANGED
@@ -46,8 +46,8 @@ const blend = {
46
46
  * The images to composite must be the same size or smaller than the processed image.
47
47
  * If both `top` and `left` options are provided, they take precedence over `gravity`.
48
48
  *
49
- * Any resize, rotate or extract operations in the same processing pipeline
50
- * will always be applied to the input image before composition.
49
+ * Other operations in the same processing pipeline (e.g. resize, rotate, flip,
50
+ * flop, extract) will always be applied to the input image before composition.
51
51
  *
52
52
  * The `blend` option can be one of `clear`, `source`, `over`, `in`, `out`, `atop`,
53
53
  * `dest`, `dest-over`, `dest-in`, `dest-out`, `dest-atop`,
@@ -110,6 +110,7 @@ const blend = {
110
110
  * @param {number} [images[].input.text.dpi=72] - the resolution (size) at which to render the text. Does not take effect if `height` is specified.
111
111
  * @param {boolean} [images[].input.text.rgba=false] - set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for Pango markup features like `<span foreground="red">Red!</span>`.
112
112
  * @param {number} [images[].input.text.spacing=0] - text line height in points. Will use the font line height if none is specified.
113
+ * @param {Boolean} [images[].autoOrient=false] - set to true to use EXIF orientation data, if present, to orient the image.
113
114
  * @param {String} [images[].blend='over'] - how to blend this image with the image below.
114
115
  * @param {String} [images[].gravity='centre'] - gravity at which to place the overlay.
115
116
  * @param {Number} [images[].top] - the pixel offset from the top edge.
@@ -121,10 +121,25 @@ const debuglog = util.debuglog('sharp');
121
121
  * }
122
122
  * }).toFile('text_rgba.png');
123
123
  *
124
- * @param {(Buffer|ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|string)} [input] - if present, can be
124
+ * @example
125
+ * // Join four input images as a 2x2 grid with a 4 pixel gutter
126
+ * const data = await sharp(
127
+ * [image1, image2, image3, image4],
128
+ * { join: { across: 2, shim: 4 } }
129
+ * ).toBuffer();
130
+ *
131
+ * @example
132
+ * // Generate a two-frame animated image from emoji
133
+ * const images = ['😀', '😛'].map(text => ({
134
+ * text: { text, width: 64, height: 64, channels: 4, rgba: true }
135
+ * }));
136
+ * await sharp(images, { join: { animated: true } }).toFile('out.gif');
137
+ *
138
+ * @param {(Buffer|ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|string|Array)} [input] - if present, can be
125
139
  * a Buffer / ArrayBuffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image data, or
126
140
  * a TypedArray containing raw pixel image data, or
127
141
  * a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file.
142
+ * An array of inputs can be provided, and these will be joined together.
128
143
  * JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
129
144
  * @param {Object} [options] - if present, is an Object with optional attributes.
130
145
  * @param {string} [options.failOn='warning'] - When to abort processing of invalid pixel data, one of (in order of sensitivity, least to most): 'none', 'truncated', 'error', 'warning'. Higher levels imply lower levels. Invalid metadata will always abort.
@@ -132,6 +147,7 @@ const debuglog = util.debuglog('sharp');
132
147
  * (width x height) exceeds this limit. Assumes image dimensions contained in the input metadata can be trusted.
133
148
  * An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF).
134
149
  * @param {boolean} [options.unlimited=false] - Set this to `true` to remove safety features that help prevent memory exhaustion (JPEG, PNG, SVG, HEIF).
150
+ * @param {boolean} [options.autoOrient=false] - Set this to `true` to rotate/flip the image to match EXIF `Orientation`, if any.
135
151
  * @param {boolean} [options.sequentialRead=true] - Set this to `false` to use random access rather than sequential read. Some operations will do this automatically.
136
152
  * @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000.
137
153
  * @param {number} [options.ignoreIcc=false] - should the embedded ICC profile, if any, be ignored.
@@ -168,6 +184,14 @@ const debuglog = util.debuglog('sharp');
168
184
  * @param {boolean} [options.text.rgba=false] - set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for pango markup features like `<span foreground="red">Red!</span>`.
169
185
  * @param {number} [options.text.spacing=0] - text line height in points. Will use the font line height if none is specified.
170
186
  * @param {string} [options.text.wrap='word'] - word wrapping style when width is provided, one of: 'word', 'char', 'word-char' (prefer word, fallback to char) or 'none'.
187
+ * @param {Object} [options.join] - describes how an array of input images should be joined.
188
+ * @param {number} [options.join.across=1] - number of images to join horizontally.
189
+ * @param {boolean} [options.join.animated=false] - set this to `true` to join the images as an animated image.
190
+ * @param {number} [options.join.shim=0] - number of pixels to insert between joined images.
191
+ * @param {string|Object} [options.join.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
192
+ * @param {string} [options.join.halign='left'] - horizontal alignment style for images joined horizontally (`'left'`, `'centre'`, `'center'`, `'right'`).
193
+ * @param {string} [options.join.valign='top'] - vertical alignment style for images joined vertically (`'top'`, `'centre'`, `'center'`, `'bottom'`).
194
+ *
171
195
  * @returns {Sharp}
172
196
  * @throws {Error} Invalid parameters
173
197
  */
@@ -194,7 +218,6 @@ const Sharp = function (input, options) {
194
218
  canvas: 'crop',
195
219
  position: 0,
196
220
  resizeBackground: [0, 0, 0, 255],
197
- useExifOrientation: false,
198
221
  angle: 0,
199
222
  rotationAngle: 0,
200
223
  rotationBackground: [0, 0, 0, 255],
package/lib/index.d.ts CHANGED
@@ -40,19 +40,7 @@ import { Duplex } from 'stream';
40
40
  */
41
41
  declare function sharp(options?: sharp.SharpOptions): sharp.Sharp;
42
42
  declare function sharp(
43
- input?:
44
- | Buffer
45
- | ArrayBuffer
46
- | Uint8Array
47
- | Uint8ClampedArray
48
- | Int8Array
49
- | Uint16Array
50
- | Int16Array
51
- | Uint32Array
52
- | Int32Array
53
- | Float32Array
54
- | Float64Array
55
- | string,
43
+ input?: sharp.SharpInput | Array<sharp.SharpInput>,
56
44
  options?: sharp.SharpOptions,
57
45
  ): sharp.Sharp;
58
46
 
@@ -62,33 +50,35 @@ declare namespace sharp {
62
50
 
63
51
  /** An Object containing the version numbers of sharp, libvips and its dependencies. */
64
52
  const versions: {
65
- vips: string;
53
+ aom?: string | undefined;
54
+ archive?: string | undefined;
66
55
  cairo?: string | undefined;
67
- croco?: string | undefined;
56
+ cgif?: string | undefined;
68
57
  exif?: string | undefined;
69
58
  expat?: string | undefined;
70
59
  ffi?: string | undefined;
71
60
  fontconfig?: string | undefined;
72
61
  freetype?: string | undefined;
73
- gdkpixbuf?: string | undefined;
74
- gif?: string | undefined;
62
+ fribidi?: string | undefined;
75
63
  glib?: string | undefined;
76
- gsf?: string | undefined;
77
64
  harfbuzz?: string | undefined;
78
- jpeg?: string | undefined;
65
+ heif?: string | undefined;
66
+ highway?: string | undefined;
67
+ imagequant?: string | undefined;
79
68
  lcms?: string | undefined;
80
- orc?: string | undefined;
69
+ mozjpeg?: string | undefined;
81
70
  pango?: string | undefined;
82
71
  pixman?: string | undefined;
83
72
  png?: string | undefined;
84
- sharp?: string | undefined;
85
- svg?: string | undefined;
73
+ "proxy-libintl"?: string | undefined;
74
+ rsvg?: string | undefined;
75
+ sharp: string;
76
+ spng?: string | undefined;
86
77
  tiff?: string | undefined;
78
+ vips: string;
87
79
  webp?: string | undefined;
88
- avif?: string | undefined;
89
- heif?: string | undefined;
90
80
  xml?: string | undefined;
91
- zlib?: string | undefined;
81
+ "zlib-ng"?: string | undefined;
92
82
  };
93
83
 
94
84
  /** An Object containing the available interpolators and their proper values */
@@ -364,24 +354,72 @@ declare namespace sharp {
364
354
  //#region Operation functions
365
355
 
366
356
  /**
367
- * Rotate the output image by either an explicit angle or auto-orient based on the EXIF Orientation tag.
357
+ * Rotate the output image by either an explicit angle
358
+ * or auto-orient based on the EXIF `Orientation` tag.
368
359
  *
369
- * If an angle is provided, it is converted to a valid positive degree rotation. For example, -450 will produce a 270deg rotation.
360
+ * If an angle is provided, it is converted to a valid positive degree rotation.
361
+ * For example, `-450` will produce a 270 degree rotation.
370
362
  *
371
- * When rotating by an angle other than a multiple of 90, the background colour can be provided with the background option.
363
+ * When rotating by an angle other than a multiple of 90,
364
+ * the background colour can be provided with the `background` option.
372
365
  *
373
- * If no angle is provided, it is determined from the EXIF data. Mirroring is supported and may infer the use of a flip operation.
366
+ * If no angle is provided, it is determined from the EXIF data.
367
+ * Mirroring is supported and may infer the use of a flip operation.
374
368
  *
375
- * The use of rotate implies the removal of the EXIF Orientation tag, if any.
369
+ * The use of `rotate` without an angle will remove the EXIF `Orientation` tag, if any.
376
370
  *
377
- * Method order is important when both rotating and extracting regions, for example rotate(x).extract(y) will produce a different result to extract(y).rotate(x).
378
- * @param angle angle of rotation. (optional, default auto)
379
- * @param options if present, is an Object with optional attributes.
371
+ * Only one rotation can occur per pipeline (aside from an initial call without
372
+ * arguments to orient via EXIF data). Previous calls to `rotate` in the same
373
+ * pipeline will be ignored.
374
+ *
375
+ * Multi-page images can only be rotated by 180 degrees.
376
+ *
377
+ * Method order is important when rotating, resizing and/or extracting regions,
378
+ * for example `.rotate(x).extract(y)` will produce a different result to `.extract(y).rotate(x)`.
379
+ *
380
+ * @example
381
+ * const pipeline = sharp()
382
+ * .rotate()
383
+ * .resize(null, 200)
384
+ * .toBuffer(function (err, outputBuffer, info) {
385
+ * // outputBuffer contains 200px high JPEG image data,
386
+ * // auto-rotated using EXIF Orientation tag
387
+ * // info.width and info.height contain the dimensions of the resized image
388
+ * });
389
+ * readableStream.pipe(pipeline);
390
+ *
391
+ * @example
392
+ * const rotateThenResize = await sharp(input)
393
+ * .rotate(90)
394
+ * .resize({ width: 16, height: 8, fit: 'fill' })
395
+ * .toBuffer();
396
+ * const resizeThenRotate = await sharp(input)
397
+ * .resize({ width: 16, height: 8, fit: 'fill' })
398
+ * .rotate(90)
399
+ * .toBuffer();
400
+ *
401
+ * @param {number} [angle=auto] angle of rotation.
402
+ * @param {Object} [options] - if present, is an Object with optional attributes.
403
+ * @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.
404
+ * @returns {Sharp}
380
405
  * @throws {Error} Invalid parameters
381
- * @returns A sharp instance that can be used to chain operations
382
406
  */
383
407
  rotate(angle?: number, options?: RotateOptions): Sharp;
384
408
 
409
+ /**
410
+ * Alias for calling `rotate()` with no arguments, which orients the image based
411
+ * on EXIF orientsion.
412
+ *
413
+ * This operation is aliased to emphasize its purpose, helping to remove any
414
+ * confusion between rotation and orientation.
415
+ *
416
+ * @example
417
+ * const output = await sharp(input).autoOrient().toBuffer();
418
+ *
419
+ * @returns {Sharp}
420
+ */
421
+ autoOrient(): Sharp
422
+
385
423
  /**
386
424
  * Flip the image about the vertical Y axis. This always occurs after rotation, if any.
387
425
  * The use of flip implies the removal of the EXIF Orientation tag, if any.
@@ -799,8 +837,6 @@ declare namespace sharp {
799
837
  * Use tile-based deep zoom (image pyramid) output.
800
838
  * Set the format and options for tile images via the toFormat, jpeg, png or webp functions.
801
839
  * Use a .zip or .szi file extension with toFile to write to a compressed archive file format.
802
- *
803
- * Warning: multiple sharp instances concurrently producing tile output can expose a possible race condition in some versions of libgsf.
804
840
  * @param tile tile options
805
841
  * @throws {Error} Invalid options
806
842
  * @returns A sharp instance that can be used to chain operations
@@ -899,7 +935,27 @@ declare namespace sharp {
899
935
  //#endregion
900
936
  }
901
937
 
938
+ type SharpInput = Buffer
939
+ | ArrayBuffer
940
+ | Uint8Array
941
+ | Uint8ClampedArray
942
+ | Int8Array
943
+ | Uint16Array
944
+ | Int16Array
945
+ | Uint32Array
946
+ | Int32Array
947
+ | Float32Array
948
+ | Float64Array
949
+ | string;
950
+
902
951
  interface SharpOptions {
952
+ /**
953
+ * Auto-orient based on the EXIF `Orientation` tag, if present.
954
+ * Mirroring is supported and may infer the use of a flip operation.
955
+ *
956
+ * Using this option will remove the EXIF `Orientation` tag, if any.
957
+ */
958
+ autoOrient?: boolean;
903
959
  /**
904
960
  * When to abort processing of invalid pixel data, one of (in order of sensitivity):
905
961
  * 'none' (least), 'truncated', 'error' or 'warning' (most), highers level imply lower levels, invalid metadata will always abort. (optional, default 'warning')
@@ -945,6 +1001,8 @@ declare namespace sharp {
945
1001
  create?: Create | undefined;
946
1002
  /** Describes a new text image to be created. */
947
1003
  text?: CreateText | undefined;
1004
+ /** Describes how array of input images should be joined. */
1005
+ join?: Join | undefined;
948
1006
  }
949
1007
 
950
1008
  interface CacheOptions {
@@ -1025,6 +1083,21 @@ declare namespace sharp {
1025
1083
  wrap?: TextWrap;
1026
1084
  }
1027
1085
 
1086
+ interface Join {
1087
+ /** Number of images per row. */
1088
+ across?: number | undefined;
1089
+ /** Treat input as frames of an animated image. */
1090
+ animated?: boolean | undefined;
1091
+ /** Space between images, in pixels. */
1092
+ shim?: number | undefined;
1093
+ /** Background colour. */
1094
+ background?: Colour | Color | undefined;
1095
+ /** Horizontal alignment. */
1096
+ halign?: HorizontalAlignment | undefined;
1097
+ /** Vertical alignment. */
1098
+ valign?: VerticalAlignment | undefined;
1099
+ }
1100
+
1028
1101
  interface ExifDir {
1029
1102
  [k: string]: string;
1030
1103
  }
@@ -1064,6 +1137,13 @@ declare namespace sharp {
1064
1137
  width?: number | undefined;
1065
1138
  /** Number of pixels high (EXIF orientation is not taken into consideration) */
1066
1139
  height?: number | undefined;
1140
+ /** Any changed metadata after the image orientation is applied. */
1141
+ autoOrient: {
1142
+ /** Number of pixels wide (EXIF orientation is taken into consideration) */
1143
+ width: number;
1144
+ /** Number of pixels high (EXIF orientation is taken into consideration) */
1145
+ height: number;
1146
+ };
1067
1147
  /** Name of colour space interpretation */
1068
1148
  space?: keyof ColourspaceEnum | undefined;
1069
1149
  /** Number of bands e.g. 3 for sRGB, 4 for CMYK */
@@ -1514,6 +1594,8 @@ declare namespace sharp {
1514
1594
  failOn?: FailOnOptions | undefined;
1515
1595
  /** see sharp() constructor, (optional, default 268402689) */
1516
1596
  limitInputPixels?: number | boolean | undefined;
1597
+ /** see sharp() constructor, (optional, default false) */
1598
+ autoOrient?: boolean | undefined;
1517
1599
  }
1518
1600
 
1519
1601
  interface TileOptions {
@@ -1654,6 +1736,10 @@ declare namespace sharp {
1654
1736
 
1655
1737
  type TextWrap = 'word' | 'char' | 'word-char' | 'none';
1656
1738
 
1739
+ type HorizontalAlignment = 'left' | 'centre' | 'center' | 'right';
1740
+
1741
+ type VerticalAlignment = 'top' | 'centre' | 'center' | 'bottom';
1742
+
1657
1743
  type TileContainer = 'fs' | 'zip';
1658
1744
 
1659
1745
  type TileLayout = 'dz' | 'iiif' | 'iiif3' | 'zoomify' | 'google';
package/lib/input.js CHANGED
@@ -14,9 +14,13 @@ const sharp = require('./sharp');
14
14
  */
15
15
  const align = {
16
16
  left: 'low',
17
+ top: 'low',
18
+ low: 'low',
17
19
  center: 'centre',
18
20
  centre: 'centre',
19
- right: 'high'
21
+ right: 'high',
22
+ bottom: 'high',
23
+ high: 'high'
20
24
  };
21
25
 
22
26
  /**
@@ -24,9 +28,9 @@ const align = {
24
28
  * @private
25
29
  */
26
30
  function _inputOptionsFromObject (obj) {
27
- const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground } = obj;
28
- return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground].some(is.defined)
29
- ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground }
31
+ const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient } = obj;
32
+ return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient].some(is.defined)
33
+ ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient }
30
34
  : undefined;
31
35
  }
32
36
 
@@ -36,6 +40,7 @@ function _inputOptionsFromObject (obj) {
36
40
  */
37
41
  function _createInputDescriptor (input, inputOptions, containerOptions) {
38
42
  const inputDescriptor = {
43
+ autoOrient: false,
39
44
  failOn: 'warning',
40
45
  limitInputPixels: Math.pow(0x3FFF, 2),
41
46
  ignoreIcc: false,
@@ -71,6 +76,18 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
71
76
  } else if (!is.defined(input) && !is.defined(inputOptions) && is.object(containerOptions) && containerOptions.allowStream) {
72
77
  // Stream without options
73
78
  inputDescriptor.buffer = [];
79
+ } else if (Array.isArray(input)) {
80
+ if (input.length > 1) {
81
+ // Join images together
82
+ if (!this.options.joining) {
83
+ this.options.joining = true;
84
+ this.options.join = input.map(i => this._createInputDescriptor(i));
85
+ } else {
86
+ throw new Error('Recursive join is unsupported');
87
+ }
88
+ } else {
89
+ throw new Error('Expected at least two images to join');
90
+ }
74
91
  } else {
75
92
  throw new Error(`Unsupported input '${input}' of type ${typeof input}${
76
93
  is.defined(inputOptions) ? ` when also providing options of type ${typeof inputOptions}` : ''
@@ -93,6 +110,14 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
93
110
  throw is.invalidParameterError('failOn', 'one of: none, truncated, error, warning', inputOptions.failOn);
94
111
  }
95
112
  }
113
+ // autoOrient
114
+ if (is.defined(inputOptions.autoOrient)) {
115
+ if (is.bool(inputOptions.autoOrient)) {
116
+ inputDescriptor.autoOrient = inputOptions.autoOrient;
117
+ } else {
118
+ throw is.invalidParameterError('autoOrient', 'boolean', inputOptions.autoOrient);
119
+ }
120
+ }
96
121
  // Density
97
122
  if (is.defined(inputOptions.density)) {
98
123
  if (is.inRange(inputOptions.density, 1, 100000)) {
@@ -360,6 +385,57 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
360
385
  throw new Error('Expected a valid string to create an image with text.');
361
386
  }
362
387
  }
388
+ // Join images together
389
+ if (is.defined(inputOptions.join)) {
390
+ if (is.defined(this.options.join)) {
391
+ if (is.defined(inputOptions.join.animated)) {
392
+ if (is.bool(inputOptions.join.animated)) {
393
+ inputDescriptor.joinAnimated = inputOptions.join.animated;
394
+ } else {
395
+ throw is.invalidParameterError('join.animated', 'boolean', inputOptions.join.animated);
396
+ }
397
+ }
398
+ if (is.defined(inputOptions.join.across)) {
399
+ if (is.integer(inputOptions.join.across) && is.inRange(inputOptions.join.across, 1, 1000000)) {
400
+ inputDescriptor.joinAcross = inputOptions.join.across;
401
+ } else {
402
+ throw is.invalidParameterError('join.across', 'integer between 1 and 100000', inputOptions.join.across);
403
+ }
404
+ }
405
+ if (is.defined(inputOptions.join.shim)) {
406
+ if (is.integer(inputOptions.join.shim) && is.inRange(inputOptions.join.shim, 0, 1000000)) {
407
+ inputDescriptor.joinShim = inputOptions.join.shim;
408
+ } else {
409
+ throw is.invalidParameterError('join.shim', 'integer between 0 and 100000', inputOptions.join.shim);
410
+ }
411
+ }
412
+ if (is.defined(inputOptions.join.background)) {
413
+ const background = color(inputOptions.join.background);
414
+ inputDescriptor.joinBackground = [
415
+ background.red(),
416
+ background.green(),
417
+ background.blue(),
418
+ Math.round(background.alpha() * 255)
419
+ ];
420
+ }
421
+ if (is.defined(inputOptions.join.halign)) {
422
+ if (is.string(inputOptions.join.halign) && is.string(this.constructor.align[inputOptions.join.halign])) {
423
+ inputDescriptor.joinHalign = this.constructor.align[inputOptions.join.halign];
424
+ } else {
425
+ throw is.invalidParameterError('join.halign', 'valid alignment', inputOptions.join.halign);
426
+ }
427
+ }
428
+ if (is.defined(inputOptions.join.valign)) {
429
+ if (is.string(inputOptions.join.valign) && is.string(this.constructor.align[inputOptions.join.valign])) {
430
+ inputDescriptor.joinValign = this.constructor.align[inputOptions.join.valign];
431
+ } else {
432
+ throw is.invalidParameterError('join.valign', 'valid alignment', inputOptions.join.valign);
433
+ }
434
+ }
435
+ } else {
436
+ throw new Error('Expected input to be an array of images to join');
437
+ }
438
+ }
363
439
  } else if (is.defined(inputOptions)) {
364
440
  throw new Error('Invalid input options ' + inputOptions);
365
441
  }
@@ -475,15 +551,9 @@ function _isStreamInput () {
475
551
  * });
476
552
  *
477
553
  * @example
478
- * // Based on EXIF rotation metadata, get the right-side-up width and height:
479
- *
480
- * const size = getNormalSize(await sharp(input).metadata());
481
- *
482
- * function getNormalSize({ width, height, orientation }) {
483
- * return (orientation || 0) >= 5
484
- * ? { width: height, height: width }
485
- * : { width, height };
486
- * }
554
+ * // Get dimensions taking EXIF Orientation into account.
555
+ * const { autoOrient } = await sharp(input).metadata();
556
+ * const { width, height } = autoOrient;
487
557
  *
488
558
  * @param {Function} [callback] - called with the arguments `(err, metadata)`
489
559
  * @returns {Promise<Object>|Sharp}
package/lib/operation.js CHANGED
@@ -18,22 +18,19 @@ const vipsPrecision = {
18
18
  };
19
19
 
20
20
  /**
21
- * Rotate the output image by either an explicit angle
22
- * or auto-orient based on the EXIF `Orientation` tag.
21
+ * Rotate the output image.
23
22
  *
24
- * If an angle is provided, it is converted to a valid positive degree rotation.
23
+ * The provided angle is converted to a valid positive degree rotation.
25
24
  * For example, `-450` will produce a 270 degree rotation.
26
25
  *
27
26
  * When rotating by an angle other than a multiple of 90,
28
27
  * the background colour can be provided with the `background` option.
29
28
  *
30
- * If no angle is provided, it is determined from the EXIF data.
31
- * Mirroring is supported and may infer the use of a flip operation.
32
- *
33
- * The use of `rotate` without an angle will remove the EXIF `Orientation` tag, if any.
29
+ * For backwards compatibility, if no angle is provided, `.autoOrient()` will be called.
34
30
  *
35
- * Only one rotation can occur per pipeline.
36
- * Previous calls to `rotate` in the same pipeline will be ignored.
31
+ * Only one rotation can occur per pipeline (aside from an initial call without
32
+ * arguments to orient via EXIF data). Previous calls to `rotate` in the same
33
+ * pipeline will be ignored.
37
34
  *
38
35
  * Multi-page images can only be rotated by 180 degrees.
39
36
  *
@@ -41,17 +38,6 @@ const vipsPrecision = {
41
38
  * for example `.rotate(x).extract(y)` will produce a different result to `.extract(y).rotate(x)`.
42
39
  *
43
40
  * @example
44
- * const pipeline = sharp()
45
- * .rotate()
46
- * .resize(null, 200)
47
- * .toBuffer(function (err, outputBuffer, info) {
48
- * // outputBuffer contains 200px high JPEG image data,
49
- * // auto-rotated using EXIF Orientation tag
50
- * // info.width and info.height contain the dimensions of the resized image
51
- * });
52
- * readableStream.pipe(pipeline);
53
- *
54
- * @example
55
41
  * const rotateThenResize = await sharp(input)
56
42
  * .rotate(90)
57
43
  * .resize({ width: 16, height: 8, fit: 'fill' })
@@ -68,12 +54,15 @@ const vipsPrecision = {
68
54
  * @throws {Error} Invalid parameters
69
55
  */
70
56
  function rotate (angle, options) {
71
- if (this.options.useExifOrientation || this.options.angle || this.options.rotationAngle) {
57
+ if (!is.defined(angle)) {
58
+ return this.autoOrient();
59
+ }
60
+ if (this.options.angle || this.options.rotationAngle) {
72
61
  this.options.debuglog('ignoring previous rotate options');
62
+ this.options.angle = 0;
63
+ this.options.rotationAngle = 0;
73
64
  }
74
- if (!is.defined(angle)) {
75
- this.options.useExifOrientation = true;
76
- } else if (is.integer(angle) && !(angle % 90)) {
65
+ if (is.integer(angle) && !(angle % 90)) {
77
66
  this.options.angle = angle;
78
67
  } else if (is.number(angle)) {
79
68
  this.options.rotationAngle = angle;
@@ -92,6 +81,34 @@ function rotate (angle, options) {
92
81
  return this;
93
82
  }
94
83
 
84
+ /**
85
+ * Auto-orient based on the EXIF `Orientation` tag, then remove the tag.
86
+ * Mirroring is supported and may infer the use of a flip operation.
87
+ *
88
+ * Previous or subsequent use of `rotate(angle)` and either `flip()` or `flop()`
89
+ * will logically occur after auto-orientation, regardless of call order.
90
+ *
91
+ * @example
92
+ * const output = await sharp(input).autoOrient().toBuffer();
93
+ *
94
+ * @example
95
+ * const pipeline = sharp()
96
+ * .autoOrient()
97
+ * .resize(null, 200)
98
+ * .toBuffer(function (err, outputBuffer, info) {
99
+ * // outputBuffer contains 200px high JPEG image data,
100
+ * // auto-oriented using EXIF Orientation tag
101
+ * // info.width and info.height contain the dimensions of the resized image
102
+ * });
103
+ * readableStream.pipe(pipeline);
104
+ *
105
+ * @returns {Sharp}
106
+ */
107
+ function autoOrient () {
108
+ this.options.input.autoOrient = true;
109
+ return this;
110
+ }
111
+
95
112
  /**
96
113
  * Mirror the image vertically (up-down) about the x-axis.
97
114
  * This always occurs before rotation, if any.
@@ -935,6 +952,7 @@ function modulate (options) {
935
952
  */
936
953
  module.exports = function (Sharp) {
937
954
  Object.assign(Sharp.prototype, {
955
+ autoOrient,
938
956
  rotate,
939
957
  flip,
940
958
  flop,
package/lib/output.js CHANGED
@@ -1232,10 +1232,6 @@ function raw (options) {
1232
1232
  *
1233
1233
  * The container will be set to `zip` when the output is a Buffer or Stream, otherwise it will default to `fs`.
1234
1234
  *
1235
- * Requires libvips compiled with support for libgsf.
1236
- * The prebuilt binaries do not include this - see
1237
- * {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}.
1238
- *
1239
1235
  * @example
1240
1236
  * sharp('input.tiff')
1241
1237
  * .png()
package/lib/resize.js CHANGED
@@ -107,7 +107,7 @@ const mapFitToCanvas = {
107
107
  * @private
108
108
  */
109
109
  function isRotationExpected (options) {
110
- return (options.angle % 360) !== 0 || options.useExifOrientation === true || options.rotationAngle !== 0;
110
+ return (options.angle % 360) !== 0 || options.input.autoOrient === true || options.rotationAngle !== 0;
111
111
  }
112
112
 
113
113
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revizly/sharp",
3
3
  "description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, GIF, AVIF and TIFF images",
4
- "version": "0.34.0-revizly3",
4
+ "version": "0.34.0-revizly6",
5
5
  "author": "Lovell Fuller <npm@lovell.info>",
6
6
  "homepage": "https://sharp.pixelplumbing.com",
7
7
  "contributors": [
@@ -102,9 +102,9 @@
102
102
  "test-types": "tsd",
103
103
  "package-from-local-build": "node npm/from-local-build",
104
104
  "package-from-github-release": "node npm/from-github-release",
105
- "docs-build": "node docs/build && node docs/search-index/build",
106
- "docs-serve": "cd docs && npx serve",
107
- "docs-publish": ""
105
+ "docs-build": "node docs/build.mjs",
106
+ "docs-serve": "cd docs && npm start",
107
+ "docs-publish": "cd docs && npm run build && npx firebase-tools deploy --project pixelplumbing --only hosting:pixelplumbing-sharp"
108
108
  },
109
109
  "type": "commonjs",
110
110
  "main": "lib/index.js",
@@ -142,28 +142,28 @@
142
142
  "semver": "^7.6.3"
143
143
  },
144
144
  "optionalDependencies": {
145
- "@revizly/sharp-libvips-linux-arm64": "1.0.9",
146
- "@revizly/sharp-libvips-linux-x64": "1.0.9",
145
+ "@revizly/sharp-libvips-linux-arm64": "1.0.10",
146
+ "@revizly/sharp-libvips-linux-x64": "1.0.10",
147
147
  "@revizly/sharp-linux-arm64": "0.34.0-revizly2",
148
148
  "@revizly/sharp-linux-x64": "0.34.0-revizly2"
149
149
  },
150
150
  "devDependencies": {
151
151
  "@emnapi/runtime": "^1.3.1",
152
- "@revizly/sharp-libvips-dev": "1.0.9",
152
+ "@revizly/sharp-libvips-dev": "1.0.10",
153
153
  "@types/node": "*",
154
154
  "cc": "^3.0.1",
155
155
  "emnapi": "^1.3.1",
156
- "exif-reader": "^2.0.1",
156
+ "exif-reader": "^2.0.2",
157
157
  "extract-zip": "^2.0.1",
158
158
  "icc": "^3.0.0",
159
- "jsdoc-to-markdown": "^9.0.5",
159
+ "jsdoc-to-markdown": "^9.1.1",
160
160
  "license-checker": "^25.0.1",
161
- "mocha": "^10.8.2",
162
- "node-addon-api": "^8.2.1",
161
+ "mocha": "^11.0.1",
162
+ "node-addon-api": "^8.3.0",
163
163
  "nyc": "^17.1.0",
164
164
  "prebuild": "^13.0.1",
165
165
  "semistandard": "^17.0.0",
166
- "tar-fs": "^3.0.6",
166
+ "tar-fs": "3.0.6",
167
167
  "tsd": "^0.31.2"
168
168
  },
169
169
  "license": "Apache-2.0",
package/src/common.cc CHANGED
@@ -160,12 +160,34 @@ namespace sharp {
160
160
  descriptor->textWrap = AttrAsEnum<VipsTextWrap>(input, "textWrap", VIPS_TYPE_TEXT_WRAP);
161
161
  }
162
162
  }
163
+ // Join images together
164
+ if (HasAttr(input, "joinAnimated")) {
165
+ descriptor->joinAnimated = AttrAsBool(input, "joinAnimated");
166
+ }
167
+ if (HasAttr(input, "joinAcross")) {
168
+ descriptor->joinAcross = AttrAsUint32(input, "joinAcross");
169
+ }
170
+ if (HasAttr(input, "joinShim")) {
171
+ descriptor->joinShim = AttrAsUint32(input, "joinShim");
172
+ }
173
+ if (HasAttr(input, "joinBackground")) {
174
+ descriptor->joinBackground = AttrAsVectorOfDouble(input, "joinBackground");
175
+ }
176
+ if (HasAttr(input, "joinHalign")) {
177
+ descriptor->joinHalign = AttrAsEnum<VipsAlign>(input, "joinHalign", VIPS_TYPE_ALIGN);
178
+ }
179
+ if (HasAttr(input, "joinValign")) {
180
+ descriptor->joinValign = AttrAsEnum<VipsAlign>(input, "joinValign", VIPS_TYPE_ALIGN);
181
+ }
163
182
  // Limit input images to a given number of pixels, where pixels = width * height
164
183
  descriptor->limitInputPixels = static_cast<uint64_t>(AttrAsInt64(input, "limitInputPixels"));
165
- // Allow switch from random to sequential access
166
- descriptor->access = AttrAsBool(input, "sequentialRead") ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
184
+ if (HasAttr(input, "access")) {
185
+ descriptor->access = AttrAsBool(input, "sequentialRead") ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
186
+ }
167
187
  // Remove safety features and allow unlimited input
168
188
  descriptor->unlimited = AttrAsBool(input, "unlimited");
189
+ // Use the EXIF orientation to auto orient the image
190
+ descriptor->autoOrient = AttrAsBool(input, "autoOrient");
169
191
  return descriptor;
170
192
  }
171
193
 
@@ -248,6 +270,7 @@ namespace sharp {
248
270
  case ImageType::FITS: id = "fits"; break;
249
271
  case ImageType::EXR: id = "exr"; break;
250
272
  case ImageType::JXL: id = "jxl"; break;
273
+ case ImageType::RAD: id = "rad"; break;
251
274
  case ImageType::VIPS: id = "vips"; break;
252
275
  case ImageType::RAW: id = "raw"; break;
253
276
  case ImageType::UNKNOWN: id = "unknown"; break;
@@ -294,6 +317,8 @@ namespace sharp {
294
317
  { "VipsForeignLoadOpenexr", ImageType::EXR },
295
318
  { "VipsForeignLoadJxlFile", ImageType::JXL },
296
319
  { "VipsForeignLoadJxlBuffer", ImageType::JXL },
320
+ { "VipsForeignLoadRadFile", ImageType::RAD },
321
+ { "VipsForeignLoadRadBuffer", ImageType::RAD },
297
322
  { "VipsForeignLoadVips", ImageType::VIPS },
298
323
  { "VipsForeignLoadVipsFile", ImageType::VIPS },
299
324
  { "VipsForeignLoadRaw", ImageType::RAW }
@@ -570,14 +595,6 @@ namespace sharp {
570
595
  return image;
571
596
  }
572
597
 
573
- /*
574
- Does this image have an alpha channel?
575
- Uses colour space interpretation with number of channels to guess this.
576
- */
577
- bool HasAlpha(VImage image) {
578
- return image.has_alpha();
579
- }
580
-
581
598
  static void* RemoveExifCallback(VipsImage *image, char const *field, GValue *value, void *data) {
582
599
  std::vector<std::string> *fieldNames = static_cast<std::vector<std::string> *>(data);
583
600
  std::string fieldName(field);
@@ -986,13 +1003,13 @@ namespace sharp {
986
1003
  };
987
1004
  }
988
1005
  // Add alpha channel to alphaColour colour
989
- if (colour[3] < 255.0 || HasAlpha(image)) {
1006
+ if (colour[3] < 255.0 || image.has_alpha()) {
990
1007
  alphaColour.push_back(colour[3] * multiplier);
991
1008
  }
992
1009
  // Ensure alphaColour colour uses correct colourspace
993
1010
  alphaColour = sharp::GetRgbaAsColourspace(alphaColour, image.interpretation(), premultiply);
994
1011
  // Add non-transparent alpha channel, if required
995
- if (colour[3] < 255.0 && !HasAlpha(image)) {
1012
+ if (colour[3] < 255.0 && !image.has_alpha()) {
996
1013
  image = image.bandjoin(
997
1014
  VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier).cast(image.format()));
998
1015
  }
@@ -1000,10 +1017,10 @@ namespace sharp {
1000
1017
  }
1001
1018
 
1002
1019
  /*
1003
- Removes alpha channel, if any.
1020
+ Removes alpha channels, if any.
1004
1021
  */
1005
1022
  VImage RemoveAlpha(VImage image) {
1006
- if (HasAlpha(image)) {
1023
+ while (image.bands() > 1 && image.has_alpha()) {
1007
1024
  image = image.extract_band(0, VImage::option()->set("n", image.bands() - 1));
1008
1025
  }
1009
1026
  return image;
@@ -1013,7 +1030,7 @@ namespace sharp {
1013
1030
  Ensures alpha channel, if missing.
1014
1031
  */
1015
1032
  VImage EnsureAlpha(VImage image, double const value) {
1016
- if (!HasAlpha(image)) {
1033
+ if (!image.has_alpha()) {
1017
1034
  std::vector<double> alpha;
1018
1035
  alpha.push_back(value * sharp::MaximumImageAlpha(image.interpretation()));
1019
1036
  image = image.bandjoin_const(alpha);
package/src/common.h CHANGED
@@ -33,6 +33,7 @@ namespace sharp {
33
33
  struct InputDescriptor { // NOLINT(runtime/indentation_namespace)
34
34
  std::string name;
35
35
  std::string file;
36
+ bool autoOrient;
36
37
  char *buffer;
37
38
  VipsFailOn failOn;
38
39
  uint64_t limitInputPixels;
@@ -70,14 +71,21 @@ namespace sharp {
70
71
  int textSpacing;
71
72
  VipsTextWrap textWrap;
72
73
  int textAutofitDpi;
74
+ bool joinAnimated;
75
+ int joinAcross;
76
+ int joinShim;
77
+ std::vector<double> joinBackground;
78
+ VipsAlign joinHalign;
79
+ VipsAlign joinValign;
73
80
  std::vector<double> pdfBackground;
74
81
 
75
82
  InputDescriptor():
83
+ autoOrient(false),
76
84
  buffer(nullptr),
77
85
  failOn(VIPS_FAIL_ON_WARNING),
78
86
  limitInputPixels(0x3FFF * 0x3FFF),
79
87
  unlimited(false),
80
- access(VIPS_ACCESS_RANDOM),
88
+ access(VIPS_ACCESS_SEQUENTIAL),
81
89
  bufferLength(0),
82
90
  isBuffer(false),
83
91
  density(72.0),
@@ -106,6 +114,12 @@ namespace sharp {
106
114
  textSpacing(0),
107
115
  textWrap(VIPS_TEXT_WRAP_WORD),
108
116
  textAutofitDpi(0),
117
+ joinAnimated(false),
118
+ joinAcross(1),
119
+ joinShim(0),
120
+ joinBackground{ 0.0, 0.0, 0.0, 255.0 },
121
+ joinHalign(VIPS_ALIGN_LOW),
122
+ joinValign(VIPS_ALIGN_LOW),
109
123
  pdfBackground{ 255.0, 255.0, 255.0, 255.0 } {}
110
124
  };
111
125
 
@@ -145,6 +159,7 @@ namespace sharp {
145
159
  FITS,
146
160
  EXR,
147
161
  JXL,
162
+ RAD,
148
163
  VIPS,
149
164
  RAW,
150
165
  UNKNOWN,
@@ -230,12 +245,6 @@ namespace sharp {
230
245
  */
231
246
  VImage SetProfile(VImage image, std::pair<char*, size_t> icc);
232
247
 
233
- /*
234
- Does this image have an alpha channel?
235
- Uses colour space interpretation with number of channels to guess this.
236
- */
237
- bool HasAlpha(VImage image);
238
-
239
248
  /*
240
249
  Remove all EXIF-related image fields.
241
250
  */
@@ -366,7 +375,7 @@ namespace sharp {
366
375
  std::tuple<VImage, std::vector<double>> ApplyAlpha(VImage image, std::vector<double> colour, bool premultiply);
367
376
 
368
377
  /*
369
- Removes alpha channel, if any.
378
+ Removes alpha channels, if any.
370
379
  */
371
380
  VImage RemoveAlpha(VImage image);
372
381
 
package/src/metadata.cc CHANGED
@@ -95,7 +95,7 @@ class MetadataWorker : public Napi::AsyncWorker {
95
95
  baton->background = image.get_array_double("background");
96
96
  }
97
97
  // Derived attributes
98
- baton->hasAlpha = sharp::HasAlpha(image);
98
+ baton->hasAlpha = image.has_alpha();
99
99
  baton->orientation = sharp::ExifOrientation(image);
100
100
  // EXIF
101
101
  if (image.get_typeof(VIPS_META_EXIF_NAME) == VIPS_TYPE_BLOB) {
@@ -242,6 +242,15 @@ class MetadataWorker : public Napi::AsyncWorker {
242
242
  if (baton->orientation > 0) {
243
243
  info.Set("orientation", baton->orientation);
244
244
  }
245
+ Napi::Object autoOrient = Napi::Object::New(env);
246
+ info.Set("autoOrient", autoOrient);
247
+ if (baton->orientation >= 5) {
248
+ autoOrient.Set("width", baton->height);
249
+ autoOrient.Set("height", baton->width);
250
+ } else {
251
+ autoOrient.Set("width", baton->width);
252
+ autoOrient.Set("height", baton->height);
253
+ }
245
254
  if (baton->exifLength > 0) {
246
255
  info.Set("exif", Napi::Buffer<char>::NewOrCopy(env, baton->exif, baton->exifLength, sharp::FreeCallback));
247
256
  }
package/src/operations.cc CHANGED
@@ -40,7 +40,7 @@ namespace sharp {
40
40
  typeBeforeTint = VIPS_INTERPRETATION_sRGB;
41
41
  }
42
42
  // Apply lookup table
43
- if (HasAlpha(image)) {
43
+ if (image.has_alpha()) {
44
44
  VImage alpha = image[image.bands() - 1];
45
45
  image = RemoveAlpha(image)
46
46
  .colourspace(VIPS_INTERPRETATION_B_W)
@@ -83,7 +83,7 @@ namespace sharp {
83
83
  // Scale luminance, join to chroma, convert back to original colourspace
84
84
  VImage normalized = luminance.linear(f, a).bandjoin(chroma).colourspace(typeBeforeNormalize);
85
85
  // Attach original alpha channel, if any
86
- if (HasAlpha(image)) {
86
+ if (image.has_alpha()) {
87
87
  // Extract original alpha channel
88
88
  VImage alpha = image[image.bands() - 1];
89
89
  // Join alpha channel to normalised image
@@ -106,7 +106,7 @@ namespace sharp {
106
106
  * Gamma encoding/decoding
107
107
  */
108
108
  VImage Gamma(VImage image, double const exponent) {
109
- if (HasAlpha(image)) {
109
+ if (image.has_alpha()) {
110
110
  // Separate alpha channel
111
111
  VImage alpha = image[image.bands() - 1];
112
112
  return RemoveAlpha(image).gamma(VImage::option()->set("exponent", exponent)).bandjoin(alpha);
@@ -132,7 +132,7 @@ namespace sharp {
132
132
  * Produce the "negative" of the image.
133
133
  */
134
134
  VImage Negate(VImage image, bool const negateAlpha) {
135
- if (HasAlpha(image) && !negateAlpha) {
135
+ if (image.has_alpha() && !negateAlpha) {
136
136
  // Separate alpha channel
137
137
  VImage alpha = image[image.bands() - 1];
138
138
  return RemoveAlpha(image).invert().bandjoin(alpha);
@@ -205,7 +205,7 @@ namespace sharp {
205
205
  VImage Modulate(VImage image, double const brightness, double const saturation,
206
206
  int const hue, double const lightness) {
207
207
  VipsInterpretation colourspaceBeforeModulate = image.interpretation();
208
- if (HasAlpha(image)) {
208
+ if (image.has_alpha()) {
209
209
  // Separate alpha channel
210
210
  VImage alpha = image[image.bands() - 1];
211
211
  return RemoveAlpha(image)
@@ -297,7 +297,7 @@ namespace sharp {
297
297
  threshold *= 256.0;
298
298
  }
299
299
  std::vector<double> backgroundAlpha({ background.back() });
300
- if (HasAlpha(image)) {
300
+ if (image.has_alpha()) {
301
301
  background.pop_back();
302
302
  } else {
303
303
  background.resize(image.bands());
@@ -307,7 +307,7 @@ namespace sharp {
307
307
  ->set("background", background)
308
308
  ->set("line_art", lineArt)
309
309
  ->set("threshold", threshold));
310
- if (HasAlpha(image)) {
310
+ if (image.has_alpha()) {
311
311
  // Search alpha channel (A)
312
312
  int leftA, topA, widthA, heightA;
313
313
  VImage alpha = image[image.bands() - 1];
@@ -344,7 +344,7 @@ namespace sharp {
344
344
  throw VError("Band expansion using linear is unsupported");
345
345
  }
346
346
  bool const uchar = !Is16Bit(image.interpretation());
347
- if (HasAlpha(image) && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) {
347
+ if (image.has_alpha() && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) {
348
348
  // Separate alpha channel
349
349
  VImage alpha = image[bands - 1];
350
350
  return RemoveAlpha(image).linear(a, b, VImage::option()->set("uchar", uchar)).bandjoin(alpha);
@@ -357,7 +357,7 @@ namespace sharp {
357
357
  * Unflatten
358
358
  */
359
359
  VImage Unflatten(VImage image) {
360
- if (HasAlpha(image)) {
360
+ if (image.has_alpha()) {
361
361
  VImage alpha = image[image.bands() - 1];
362
362
  VImage noAlpha = RemoveAlpha(image);
363
363
  return noAlpha.bandjoin(alpha & (noAlpha.colourspace(VIPS_INTERPRETATION_B_W) < 255));
package/src/pipeline.cc CHANGED
@@ -42,7 +42,39 @@ class PipelineWorker : public Napi::AsyncWorker {
42
42
  // Open input
43
43
  vips::VImage image;
44
44
  sharp::ImageType inputImageType;
45
- std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
45
+ if (baton->join.empty()) {
46
+ std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
47
+ } else {
48
+ std::vector<VImage> images;
49
+ bool hasAlpha = false;
50
+ for (auto &join : baton->join) {
51
+ std::tie(image, inputImageType) = sharp::OpenInput(join);
52
+ image = sharp::EnsureColourspace(image, baton->colourspacePipeline);
53
+ images.push_back(image);
54
+ hasAlpha |= image.has_alpha();
55
+ }
56
+ if (hasAlpha) {
57
+ for (auto &image : images) {
58
+ if (!image.has_alpha()) {
59
+ image = sharp::EnsureAlpha(image, 1);
60
+ }
61
+ }
62
+ } else {
63
+ baton->input->joinBackground.pop_back();
64
+ }
65
+ inputImageType = sharp::ImageType::PNG;
66
+ image = VImage::arrayjoin(images, VImage::option()
67
+ ->set("across", baton->input->joinAcross)
68
+ ->set("shim", baton->input->joinShim)
69
+ ->set("background", baton->input->joinBackground)
70
+ ->set("halign", baton->input->joinHalign)
71
+ ->set("valign", baton->input->joinValign));
72
+ if (baton->input->joinAnimated) {
73
+ image = image.copy();
74
+ image.set(VIPS_META_N_PAGES, static_cast<int>(images.size()));
75
+ image.set(VIPS_META_PAGE_HEIGHT, static_cast<int>(image.height() / images.size()));
76
+ }
77
+ }
46
78
  VipsAccess access = baton->input->access;
47
79
  image = sharp::EnsureColourspace(image, baton->colourspacePipeline);
48
80
 
@@ -63,14 +95,14 @@ class PipelineWorker : public Napi::AsyncWorker {
63
95
  bool autoFlip = false;
64
96
  bool autoFlop = false;
65
97
 
66
- if (baton->useExifOrientation) {
98
+ if (baton->input->autoOrient) {
67
99
  // Rotate and flip image according to Exif orientation
68
100
  std::tie(autoRotation, autoFlip, autoFlop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
69
101
  image = sharp::RemoveExifOrientation(image);
70
- } else {
71
- rotation = CalculateAngleRotation(baton->angle);
72
102
  }
73
103
 
104
+ rotation = CalculateAngleRotation(baton->angle);
105
+
74
106
  // Rotate pre-extract
75
107
  bool const shouldRotateBefore = baton->rotateBeforePreExtract &&
76
108
  (rotation != VIPS_ANGLE_D0 || autoRotation != VIPS_ANGLE_D0 ||
@@ -92,18 +124,14 @@ class PipelineWorker : public Napi::AsyncWorker {
92
124
  image = image.rot(autoRotation);
93
125
  autoRotation = VIPS_ANGLE_D0;
94
126
  }
95
- if (autoFlip) {
127
+ if (autoFlip != baton->flip) {
96
128
  image = image.flip(VIPS_DIRECTION_VERTICAL);
97
129
  autoFlip = false;
98
- } else if (baton->flip) {
99
- image = image.flip(VIPS_DIRECTION_VERTICAL);
100
130
  baton->flip = false;
101
131
  }
102
- if (autoFlop) {
132
+ if (autoFlop != baton->flop) {
103
133
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
104
134
  autoFlop = false;
105
- } else if (baton->flop) {
106
- image = image.flip(VIPS_DIRECTION_HORIZONTAL);
107
135
  baton->flop = false;
108
136
  }
109
137
  if (rotation != VIPS_ANGLE_D0) {
@@ -344,7 +372,7 @@ class PipelineWorker : public Napi::AsyncWorker {
344
372
  }
345
373
 
346
374
  // Flatten image to remove alpha channel
347
- if (baton->flatten && sharp::HasAlpha(image)) {
375
+ if (baton->flatten && image.has_alpha()) {
348
376
  image = sharp::Flatten(image, baton->flattenBackground);
349
377
  }
350
378
 
@@ -364,12 +392,12 @@ class PipelineWorker : public Napi::AsyncWorker {
364
392
  bool const shouldSharpen = baton->sharpenSigma != 0.0;
365
393
  bool const shouldComposite = !baton->composite.empty();
366
394
 
367
- if (shouldComposite && !sharp::HasAlpha(image)) {
395
+ if (shouldComposite && !image.has_alpha()) {
368
396
  image = sharp::EnsureAlpha(image, 1);
369
397
  }
370
398
 
371
399
  VipsBandFormat premultiplyFormat = image.format();
372
- bool const shouldPremultiplyAlpha = sharp::HasAlpha(image) &&
400
+ bool const shouldPremultiplyAlpha = image.has_alpha() &&
373
401
  (shouldResize || shouldBlur || shouldConv || shouldSharpen);
374
402
 
375
403
  if (shouldPremultiplyAlpha) {
@@ -396,11 +424,11 @@ class PipelineWorker : public Napi::AsyncWorker {
396
424
  image = image.rot(autoRotation);
397
425
  }
398
426
  // Mirror vertically (up-down) about the x-axis
399
- if (baton->flip || autoFlip) {
427
+ if (baton->flip != autoFlip) {
400
428
  image = image.flip(VIPS_DIRECTION_VERTICAL);
401
429
  }
402
430
  // Mirror horizontally (left-right) about the y-axis
403
- if (baton->flop || autoFlop) {
431
+ if (baton->flop != autoFlop) {
404
432
  image = image.flip(VIPS_DIRECTION_HORIZONTAL);
405
433
  }
406
434
  // Rotate post-extract 90-angle
@@ -631,6 +659,30 @@ class PipelineWorker : public Napi::AsyncWorker {
631
659
  composite->input->access = access;
632
660
  std::tie(compositeImage, compositeImageType) = sharp::OpenInput(composite->input);
633
661
  compositeImage = sharp::EnsureColourspace(compositeImage, baton->colourspacePipeline);
662
+
663
+ if (composite->input->autoOrient) {
664
+ // Respect EXIF Orientation
665
+ VipsAngle compositeAutoRotation = VIPS_ANGLE_D0;
666
+ bool compositeAutoFlip = false;
667
+ bool compositeAutoFlop = false;
668
+ std::tie(compositeAutoRotation, compositeAutoFlip, compositeAutoFlop) =
669
+ CalculateExifRotationAndFlip(sharp::ExifOrientation(compositeImage));
670
+
671
+ compositeImage = sharp::RemoveExifOrientation(compositeImage);
672
+ compositeImage = sharp::StaySequential(compositeImage,
673
+ compositeAutoRotation != VIPS_ANGLE_D0 || compositeAutoFlip);
674
+
675
+ if (compositeAutoRotation != VIPS_ANGLE_D0) {
676
+ compositeImage = compositeImage.rot(compositeAutoRotation);
677
+ }
678
+ if (compositeAutoFlip) {
679
+ compositeImage = compositeImage.flip(VIPS_DIRECTION_VERTICAL);
680
+ }
681
+ if (compositeAutoFlop) {
682
+ compositeImage = compositeImage.flip(VIPS_DIRECTION_HORIZONTAL);
683
+ }
684
+ }
685
+
634
686
  // Verify within current dimensions
635
687
  if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
636
688
  throw vips::VError("Image to composite must have same dimensions or smaller");
@@ -673,9 +725,7 @@ class PipelineWorker : public Napi::AsyncWorker {
673
725
  }
674
726
  // Ensure image to composite is sRGB with unpremultiplied alpha
675
727
  compositeImage = compositeImage.colourspace(VIPS_INTERPRETATION_sRGB);
676
- if (!sharp::HasAlpha(compositeImage)) {
677
- compositeImage = sharp::EnsureAlpha(compositeImage, 1);
678
- }
728
+ compositeImage = sharp::EnsureAlpha(compositeImage, 1);
679
729
  if (composite->premultiplied) compositeImage = compositeImage.unpremultiply();
680
730
  // Calculate position
681
731
  int left;
@@ -776,7 +826,7 @@ class PipelineWorker : public Napi::AsyncWorker {
776
826
  // Extract channel
777
827
  if (baton->extractChannel > -1) {
778
828
  if (baton->extractChannel >= image.bands()) {
779
- if (baton->extractChannel == 3 && sharp::HasAlpha(image)) {
829
+ if (baton->extractChannel == 3 && image.has_alpha()) {
780
830
  baton->extractChannel = image.bands() - 1;
781
831
  } else {
782
832
  (baton->err)
@@ -1000,7 +1050,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1000
1050
  } else if (baton->formatOut == "dz") {
1001
1051
  // Write DZ to buffer
1002
1052
  baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
1003
- if (!sharp::HasAlpha(image)) {
1053
+ if (!image.has_alpha()) {
1004
1054
  baton->tileBackground.pop_back();
1005
1055
  }
1006
1056
  image = sharp::StaySequential(image, baton->tileAngle != 0);
@@ -1049,6 +1099,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1049
1099
  // Unsupported output format
1050
1100
  (baton->err).append("Unsupported output format ");
1051
1101
  if (baton->formatOut == "input") {
1102
+ (baton->err).append("when trying to match input format of ");
1052
1103
  (baton->err).append(ImageTypeId(inputImageType));
1053
1104
  } else {
1054
1105
  (baton->err).append(baton->formatOut);
@@ -1203,7 +1254,7 @@ class PipelineWorker : public Napi::AsyncWorker {
1203
1254
  if (isDzZip) {
1204
1255
  baton->tileContainer = VIPS_FOREIGN_DZ_CONTAINER_ZIP;
1205
1256
  }
1206
- if (!sharp::HasAlpha(image)) {
1257
+ if (!image.has_alpha()) {
1207
1258
  baton->tileBackground.pop_back();
1208
1259
  }
1209
1260
  image = sharp::StaySequential(image, baton->tileAngle != 0);
@@ -1475,6 +1526,14 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1475
1526
 
1476
1527
  // Input
1477
1528
  baton->input = sharp::CreateInputDescriptor(options.Get("input").As<Napi::Object>());
1529
+ // Join images together
1530
+ if (sharp::HasAttr(options, "join")) {
1531
+ Napi::Array join = options.Get("join").As<Napi::Array>();
1532
+ for (unsigned int i = 0; i < join.Length(); i++) {
1533
+ baton->join.push_back(
1534
+ sharp::CreateInputDescriptor(join.Get(i).As<Napi::Object>()));
1535
+ }
1536
+ }
1478
1537
  // Extract image options
1479
1538
  baton->topOffsetPre = sharp::AttrAsInt32(options, "topOffsetPre");
1480
1539
  baton->leftOffsetPre = sharp::AttrAsInt32(options, "leftOffsetPre");
@@ -1567,7 +1626,6 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
1567
1626
  baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
1568
1627
  baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
1569
1628
  baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");
1570
- baton->useExifOrientation = sharp::AttrAsBool(options, "useExifOrientation");
1571
1629
  baton->angle = sharp::AttrAsInt32(options, "angle");
1572
1630
  baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle");
1573
1631
  baton->rotationBackground = sharp::AttrAsVectorOfDouble(options, "rotationBackground");
package/src/pipeline.h CHANGED
@@ -39,6 +39,7 @@ struct Composite {
39
39
 
40
40
  struct PipelineBaton {
41
41
  sharp::InputDescriptor *input;
42
+ std::vector<sharp::InputDescriptor *> join;
42
43
  std::string formatOut;
43
44
  std::string fileOut;
44
45
  void *bufferOut;
@@ -109,7 +110,6 @@ struct PipelineBaton {
109
110
  int claheWidth;
110
111
  int claheHeight;
111
112
  int claheMaxSlope;
112
- bool useExifOrientation;
113
113
  int angle;
114
114
  double rotationAngle;
115
115
  std::vector<double> rotationBackground;
@@ -282,7 +282,6 @@ struct PipelineBaton {
282
282
  claheWidth(0),
283
283
  claheHeight(0),
284
284
  claheMaxSlope(3),
285
- useExifOrientation(false),
286
285
  angle(0),
287
286
  rotationAngle(0.0),
288
287
  rotationBackground{ 0.0, 0.0, 0.0, 255.0 },
package/src/stats.cc CHANGED
@@ -58,7 +58,7 @@ class StatsWorker : public Napi::AsyncWorker {
58
58
  baton->channelStats.push_back(cStats);
59
59
  }
60
60
  // Image is not opaque when alpha layer is present and contains a non-mamixa value
61
- if (sharp::HasAlpha(image)) {
61
+ if (image.has_alpha()) {
62
62
  double const minAlpha = static_cast<double>(stats.getpoint(STAT_MIN_INDEX, bands).front());
63
63
  if (minAlpha != sharp::MaximumImageAlpha(image.interpretation())) {
64
64
  baton->isOpaque = false;
package/src/utilities.cc CHANGED
@@ -119,7 +119,7 @@ Napi::Value format(const Napi::CallbackInfo& info) {
119
119
  Napi::Object format = Napi::Object::New(env);
120
120
  for (std::string const f : {
121
121
  "jpeg", "png", "webp", "tiff", "magick", "openslide", "dz",
122
- "ppm", "fits", "gif", "svg", "heif", "pdf", "vips", "jp2k", "jxl"
122
+ "ppm", "fits", "gif", "svg", "heif", "pdf", "vips", "jp2k", "jxl", "rad"
123
123
  }) {
124
124
  // Input
125
125
  const VipsObjectClass *oc = vips_class_find("VipsOperation", (f + "load").c_str());
@@ -233,10 +233,10 @@ Napi::Value _maxColourDistance(const Napi::CallbackInfo& info) {
233
233
  double maxColourDistance;
234
234
  try {
235
235
  // Premultiply and remove alpha
236
- if (sharp::HasAlpha(image1)) {
236
+ if (image1.has_alpha()) {
237
237
  image1 = image1.premultiply().extract_band(1, VImage::option()->set("n", image1.bands() - 1));
238
238
  }
239
- if (sharp::HasAlpha(image2)) {
239
+ if (image2.has_alpha()) {
240
240
  image2 = image2.premultiply().extract_band(1, VImage::option()->set("n", image2.bands() - 1));
241
241
  }
242
242
  // Calculate colour distance