Skip to content

Commit 5ab9168

Browse files
committed
Add support for input array to join or animate #1580
1 parent 67ff930 commit 5ab9168

File tree

12 files changed

+377
-20
lines changed

12 files changed

+377
-20
lines changed

docs/src/content/docs/api-constructor.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ where the overall height is the `pageHeight` multiplied by the number of `pages`
3333

3434
| Param | Type | Default | Description |
3535
| --- | --- | --- | --- |
36-
| [input] | <code>Buffer</code> \| <code>ArrayBuffer</code> \| <code>Uint8Array</code> \| <code>Uint8ClampedArray</code> \| <code>Int8Array</code> \| <code>Uint16Array</code> \| <code>Int16Array</code> \| <code>Uint32Array</code> \| <code>Int32Array</code> \| <code>Float32Array</code> \| <code>Float64Array</code> \| <code>string</code> | | if present, can be a Buffer / ArrayBuffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image data, or a TypedArray containing raw pixel image data, or a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file. JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present. |
36+
| [input] | <code>Buffer</code> \| <code>ArrayBuffer</code> \| <code>Uint8Array</code> \| <code>Uint8ClampedArray</code> \| <code>Int8Array</code> \| <code>Uint16Array</code> \| <code>Int16Array</code> \| <code>Uint32Array</code> \| <code>Int32Array</code> \| <code>Float32Array</code> \| <code>Float64Array</code> \| <code>string</code> \| <code>Array</code> | | if present, can be a Buffer / ArrayBuffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image data, or a TypedArray containing raw pixel image data, or a String containing the filesystem path to an JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image file. An array of inputs can be provided, and these will be joined together. JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present. |
3737
| [options] | <code>Object</code> | | if present, is an Object with optional attributes. |
3838
| [options.failOn] | <code>string</code> | <code>&quot;&#x27;warning&#x27;&quot;</code> | 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. |
3939
| [options.limitInputPixels] | <code>number</code> \| <code>boolean</code> | <code>268402689</code> | Do not process input images where the number of pixels (width x height) exceeds this limit. Assumes image dimensions contained in the input metadata can be trusted. An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF). |
@@ -74,6 +74,13 @@ where the overall height is the `pageHeight` multiplied by the number of `pages`
7474
| [options.text.rgba] | <code>boolean</code> | <code>false</code> | 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>`. |
7575
| [options.text.spacing] | <code>number</code> | <code>0</code> | text line height in points. Will use the font line height if none is specified. |
7676
| [options.text.wrap] | <code>string</code> | <code>&quot;&#x27;word&#x27;&quot;</code> | word wrapping style when width is provided, one of: 'word', 'char', 'word-char' (prefer word, fallback to char) or 'none'. |
77+
| [options.join] | <code>Object</code> | | describes how an array of input images should be joined. |
78+
| [options.join.across] | <code>number</code> | <code>1</code> | number of images to join horizontally. |
79+
| [options.join.animated] | <code>boolean</code> | <code>false</code> | set this to `true` to join the images as an animated image. |
80+
| [options.join.shim] | <code>number</code> | <code>0</code> | number of pixels to insert between joined images. |
81+
| [options.join.background] | <code>string</code> \| <code>Object</code> | | parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. |
82+
| [options.join.halign] | <code>string</code> | <code>&quot;&#x27;left&#x27;&quot;</code> | horizontal alignment style for images joined horizontally (`'left'`, `'centre'`, `'center'`, `'right'`). |
83+
| [options.join.valign] | <code>string</code> | <code>&quot;&#x27;top&#x27;&quot;</code> | vertical alignment style for images joined vertically (`'top'`, `'centre'`, `'center'`, `'bottom'`). |
7784

7885
**Example**
7986
```js
@@ -173,6 +180,22 @@ await sharp({
173180
}
174181
}).toFile('text_rgba.png');
175182
```
183+
**Example**
184+
```js
185+
// Join four input images as a 2x2 grid with a 4 pixel gutter
186+
const data = await sharp(
187+
[image1, image2, image3, image4],
188+
{ join: { across: 2, shim: 4 } }
189+
).toBuffer();
190+
```
191+
**Example**
192+
```js
193+
// Generate a two-frame animated image from emoji
194+
const images = ['😀', '😛'].map(text => ({
195+
text: { text, width: 64, height: 64, channels: 4, rgba: true }
196+
}));
197+
await sharp(images, { join: { animated: true } }).toFile('out.gif');
198+
```
176199

177200

178201
## clone

docs/src/content/docs/changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ Requires libvips v8.16.0
88

99
### v0.34.0 - TBD
1010

11+
* Breaking: Support array of input images to be joined or animated.
12+
[#1580](https://github.com/lovell/sharp/issues/1580)
13+
1114
* Breaking: Support `info.size` on wide-character systems via upgrade to C++17.
1215
[#3943](https://github.com/lovell/sharp/issues/3943)
1316

lib/constructor.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,25 @@ const debuglog = util.debuglog('sharp');
121121
* }
122122
* }).toFile('text_rgba.png');
123123
*
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
125139
* a Buffer / ArrayBuffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image data, or
126140
* a TypedArray containing raw pixel image data, or
127141
* 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.
128143
* JPEG, PNG, WebP, AVIF, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present.
129144
* @param {Object} [options] - if present, is an Object with optional attributes.
130145
* @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.
@@ -169,6 +184,14 @@ const debuglog = util.debuglog('sharp');
169184
* @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>`.
170185
* @param {number} [options.text.spacing=0] - text line height in points. Will use the font line height if none is specified.
171186
* @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+
*
172195
* @returns {Sharp}
173196
* @throws {Error} Invalid parameters
174197
*/

lib/index.d.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,7 @@ import { Duplex } from 'stream';
4040
*/
4141
declare function sharp(options?: sharp.SharpOptions): sharp.Sharp;
4242
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>,
5644
options?: sharp.SharpOptions,
5745
): sharp.Sharp;
5846

@@ -945,6 +933,19 @@ declare namespace sharp {
945933
//#endregion
946934
}
947935

936+
type SharpInput = Buffer
937+
| ArrayBuffer
938+
| Uint8Array
939+
| Uint8ClampedArray
940+
| Int8Array
941+
| Uint16Array
942+
| Int16Array
943+
| Uint32Array
944+
| Int32Array
945+
| Float32Array
946+
| Float64Array
947+
| string;
948+
948949
interface SharpOptions {
949950
/**
950951
* Auto-orient based on the EXIF `Orientation` tag, if present.
@@ -998,6 +999,8 @@ declare namespace sharp {
998999
create?: Create | undefined;
9991000
/** Describes a new text image to be created. */
10001001
text?: CreateText | undefined;
1002+
/** Describes how array of input images should be joined. */
1003+
join?: Join | undefined;
10011004
}
10021005

10031006
interface CacheOptions {
@@ -1078,6 +1081,21 @@ declare namespace sharp {
10781081
wrap?: TextWrap;
10791082
}
10801083

1084+
interface Join {
1085+
/** Number of images per row. */
1086+
across?: number | undefined;
1087+
/** Treat input as frames of an animated image. */
1088+
animated?: boolean | undefined;
1089+
/** Space between images, in pixels. */
1090+
shim?: number | undefined;
1091+
/** Background colour. */
1092+
background?: Colour | Color | undefined;
1093+
/** Horizontal alignment. */
1094+
halign?: HorizontalAlignment | undefined;
1095+
/** Vertical alignment. */
1096+
valign?: VerticalAlignment | undefined;
1097+
}
1098+
10811099
interface ExifDir {
10821100
[k: string]: string;
10831101
}
@@ -1716,6 +1734,10 @@ declare namespace sharp {
17161734

17171735
type TextWrap = 'word' | 'char' | 'word-char' | 'none';
17181736

1737+
type HorizontalAlignment = 'left' | 'centre' | 'center' | 'right';
1738+
1739+
type VerticalAlignment = 'top' | 'centre' | 'center' | 'bottom';
1740+
17191741
type TileContainer = 'fs' | 'zip';
17201742

17211743
type TileLayout = 'dz' | 'iiif' | 'iiif3' | 'zoomify' | 'google';

lib/input.js

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ const sharp = require('./sharp');
1414
*/
1515
const align = {
1616
left: 'low',
17+
top: 'low',
18+
low: 'low',
1719
center: 'centre',
1820
centre: 'centre',
19-
right: 'high'
21+
right: 'high',
22+
bottom: 'high',
23+
high: 'high'
2024
};
2125

2226
/**
@@ -72,6 +76,18 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
7276
} else if (!is.defined(input) && !is.defined(inputOptions) && is.object(containerOptions) && containerOptions.allowStream) {
7377
// Stream without options
7478
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+
}
7591
} else {
7692
throw new Error(`Unsupported input '${input}' of type ${typeof input}${
7793
is.defined(inputOptions) ? ` when also providing options of type ${typeof inputOptions}` : ''
@@ -369,6 +385,57 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
369385
throw new Error('Expected a valid string to create an image with text.');
370386
}
371387
}
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+
}
372439
} else if (is.defined(inputOptions)) {
373440
throw new Error('Invalid input options ' + inputOptions);
374441
}

src/common.cc

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,30 @@ namespace sharp {
160160
descriptor->textWrap = AttrAsEnum<VipsTextWrap>(input, "textWrap", VIPS_TYPE_TEXT_WRAP);
161161
}
162162
}
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+
}
163182
// Limit input images to a given number of pixels, where pixels = width * height
164183
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+
}
167187
// Remove safety features and allow unlimited input
168188
descriptor->unlimited = AttrAsBool(input, "unlimited");
169189
// Use the EXIF orientation to auto orient the image

src/common.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ namespace sharp {
7171
int textSpacing;
7272
VipsTextWrap textWrap;
7373
int textAutofitDpi;
74+
bool joinAnimated;
75+
int joinAcross;
76+
int joinShim;
77+
std::vector<double> joinBackground;
78+
VipsAlign joinHalign;
79+
VipsAlign joinValign;
7480
std::vector<double> pdfBackground;
7581

7682
InputDescriptor():
@@ -79,7 +85,7 @@ namespace sharp {
7985
failOn(VIPS_FAIL_ON_WARNING),
8086
limitInputPixels(0x3FFF * 0x3FFF),
8187
unlimited(false),
82-
access(VIPS_ACCESS_RANDOM),
88+
access(VIPS_ACCESS_SEQUENTIAL),
8389
bufferLength(0),
8490
isBuffer(false),
8591
density(72.0),
@@ -108,6 +114,12 @@ namespace sharp {
108114
textSpacing(0),
109115
textWrap(VIPS_TEXT_WRAP_WORD),
110116
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),
111123
pdfBackground{ 255.0, 255.0, 255.0, 255.0 } {}
112124
};
113125

0 commit comments

Comments
 (0)