mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 02:30:12 +02:00
Add support for input array to join or animate #1580
This commit is contained in:
parent
67ff930535
commit
5ab9168813
@ -33,7 +33,7 @@ where the overall height is the `pageHeight` multiplied by the number of `pages`
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [options] | <code>Object</code> | | if present, is an Object with optional attributes. |
|
||||
| [options.failOn] | <code>string</code> | <code>"'warning'"</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. |
|
||||
| [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`
|
||||
| [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>`. |
|
||||
| [options.text.spacing] | <code>number</code> | <code>0</code> | text line height in points. Will use the font line height if none is specified. |
|
||||
| [options.text.wrap] | <code>string</code> | <code>"'word'"</code> | word wrapping style when width is provided, one of: 'word', 'char', 'word-char' (prefer word, fallback to char) or 'none'. |
|
||||
| [options.join] | <code>Object</code> | | describes how an array of input images should be joined. |
|
||||
| [options.join.across] | <code>number</code> | <code>1</code> | number of images to join horizontally. |
|
||||
| [options.join.animated] | <code>boolean</code> | <code>false</code> | set this to `true` to join the images as an animated image. |
|
||||
| [options.join.shim] | <code>number</code> | <code>0</code> | number of pixels to insert between joined images. |
|
||||
| [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. |
|
||||
| [options.join.halign] | <code>string</code> | <code>"'left'"</code> | horizontal alignment style for images joined horizontally (`'left'`, `'centre'`, `'center'`, `'right'`). |
|
||||
| [options.join.valign] | <code>string</code> | <code>"'top'"</code> | vertical alignment style for images joined vertically (`'top'`, `'centre'`, `'center'`, `'bottom'`). |
|
||||
|
||||
**Example**
|
||||
```js
|
||||
@ -173,6 +180,22 @@ await sharp({
|
||||
}
|
||||
}).toFile('text_rgba.png');
|
||||
```
|
||||
**Example**
|
||||
```js
|
||||
// Join four input images as a 2x2 grid with a 4 pixel gutter
|
||||
const data = await sharp(
|
||||
[image1, image2, image3, image4],
|
||||
{ join: { across: 2, shim: 4 } }
|
||||
).toBuffer();
|
||||
```
|
||||
**Example**
|
||||
```js
|
||||
// Generate a two-frame animated image from emoji
|
||||
const images = ['😀', '😛'].map(text => ({
|
||||
text: { text, width: 64, height: 64, channels: 4, rgba: true }
|
||||
}));
|
||||
await sharp(images, { join: { animated: true } }).toFile('out.gif');
|
||||
```
|
||||
|
||||
|
||||
## clone
|
||||
|
@ -8,6 +8,9 @@ Requires libvips v8.16.0
|
||||
|
||||
### v0.34.0 - TBD
|
||||
|
||||
* Breaking: Support array of input images to be joined or animated.
|
||||
[#1580](https://github.com/lovell/sharp/issues/1580)
|
||||
|
||||
* Breaking: Support `info.size` on wide-character systems via upgrade to C++17.
|
||||
[#3943](https://github.com/lovell/sharp/issues/3943)
|
||||
|
||||
|
@ -121,10 +121,25 @@ const debuglog = util.debuglog('sharp');
|
||||
* }
|
||||
* }).toFile('text_rgba.png');
|
||||
*
|
||||
* @param {(Buffer|ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|string)} [input] - if present, can be
|
||||
* @example
|
||||
* // Join four input images as a 2x2 grid with a 4 pixel gutter
|
||||
* const data = await sharp(
|
||||
* [image1, image2, image3, image4],
|
||||
* { join: { across: 2, shim: 4 } }
|
||||
* ).toBuffer();
|
||||
*
|
||||
* @example
|
||||
* // Generate a two-frame animated image from emoji
|
||||
* const images = ['😀', '😛'].map(text => ({
|
||||
* text: { text, width: 64, height: 64, channels: 4, rgba: true }
|
||||
* }));
|
||||
* await sharp(images, { join: { animated: true } }).toFile('out.gif');
|
||||
*
|
||||
* @param {(Buffer|ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|string|Array)} [input] - 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.
|
||||
* @param {Object} [options] - if present, is an Object with optional attributes.
|
||||
* @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');
|
||||
* @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>`.
|
||||
* @param {number} [options.text.spacing=0] - text line height in points. Will use the font line height if none is specified.
|
||||
* @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'.
|
||||
* @param {Object} [options.join] - describes how an array of input images should be joined.
|
||||
* @param {number} [options.join.across=1] - number of images to join horizontally.
|
||||
* @param {boolean} [options.join.animated=false] - set this to `true` to join the images as an animated image.
|
||||
* @param {number} [options.join.shim=0] - number of pixels to insert between joined images.
|
||||
* @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.
|
||||
* @param {string} [options.join.halign='left'] - horizontal alignment style for images joined horizontally (`'left'`, `'centre'`, `'center'`, `'right'`).
|
||||
* @param {string} [options.join.valign='top'] - vertical alignment style for images joined vertically (`'top'`, `'centre'`, `'center'`, `'bottom'`).
|
||||
*
|
||||
* @returns {Sharp}
|
||||
* @throws {Error} Invalid parameters
|
||||
*/
|
||||
|
48
lib/index.d.ts
vendored
48
lib/index.d.ts
vendored
@ -40,19 +40,7 @@ import { Duplex } from 'stream';
|
||||
*/
|
||||
declare function sharp(options?: sharp.SharpOptions): sharp.Sharp;
|
||||
declare function sharp(
|
||||
input?:
|
||||
| Buffer
|
||||
| ArrayBuffer
|
||||
| Uint8Array
|
||||
| Uint8ClampedArray
|
||||
| Int8Array
|
||||
| Uint16Array
|
||||
| Int16Array
|
||||
| Uint32Array
|
||||
| Int32Array
|
||||
| Float32Array
|
||||
| Float64Array
|
||||
| string,
|
||||
input?: sharp.SharpInput | Array<sharp.SharpInput>,
|
||||
options?: sharp.SharpOptions,
|
||||
): sharp.Sharp;
|
||||
|
||||
@ -945,6 +933,19 @@ declare namespace sharp {
|
||||
//#endregion
|
||||
}
|
||||
|
||||
type SharpInput = Buffer
|
||||
| ArrayBuffer
|
||||
| Uint8Array
|
||||
| Uint8ClampedArray
|
||||
| Int8Array
|
||||
| Uint16Array
|
||||
| Int16Array
|
||||
| Uint32Array
|
||||
| Int32Array
|
||||
| Float32Array
|
||||
| Float64Array
|
||||
| string;
|
||||
|
||||
interface SharpOptions {
|
||||
/**
|
||||
* Auto-orient based on the EXIF `Orientation` tag, if present.
|
||||
@ -998,6 +999,8 @@ declare namespace sharp {
|
||||
create?: Create | undefined;
|
||||
/** Describes a new text image to be created. */
|
||||
text?: CreateText | undefined;
|
||||
/** Describes how array of input images should be joined. */
|
||||
join?: Join | undefined;
|
||||
}
|
||||
|
||||
interface CacheOptions {
|
||||
@ -1078,6 +1081,21 @@ declare namespace sharp {
|
||||
wrap?: TextWrap;
|
||||
}
|
||||
|
||||
interface Join {
|
||||
/** Number of images per row. */
|
||||
across?: number | undefined;
|
||||
/** Treat input as frames of an animated image. */
|
||||
animated?: boolean | undefined;
|
||||
/** Space between images, in pixels. */
|
||||
shim?: number | undefined;
|
||||
/** Background colour. */
|
||||
background?: Colour | Color | undefined;
|
||||
/** Horizontal alignment. */
|
||||
halign?: HorizontalAlignment | undefined;
|
||||
/** Vertical alignment. */
|
||||
valign?: VerticalAlignment | undefined;
|
||||
}
|
||||
|
||||
interface ExifDir {
|
||||
[k: string]: string;
|
||||
}
|
||||
@ -1716,6 +1734,10 @@ declare namespace sharp {
|
||||
|
||||
type TextWrap = 'word' | 'char' | 'word-char' | 'none';
|
||||
|
||||
type HorizontalAlignment = 'left' | 'centre' | 'center' | 'right';
|
||||
|
||||
type VerticalAlignment = 'top' | 'centre' | 'center' | 'bottom';
|
||||
|
||||
type TileContainer = 'fs' | 'zip';
|
||||
|
||||
type TileLayout = 'dz' | 'iiif' | 'iiif3' | 'zoomify' | 'google';
|
||||
|
69
lib/input.js
69
lib/input.js
@ -14,9 +14,13 @@ const sharp = require('./sharp');
|
||||
*/
|
||||
const align = {
|
||||
left: 'low',
|
||||
top: 'low',
|
||||
low: 'low',
|
||||
center: 'centre',
|
||||
centre: 'centre',
|
||||
right: 'high'
|
||||
right: 'high',
|
||||
bottom: 'high',
|
||||
high: 'high'
|
||||
};
|
||||
|
||||
/**
|
||||
@ -72,6 +76,18 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
|
||||
} else if (!is.defined(input) && !is.defined(inputOptions) && is.object(containerOptions) && containerOptions.allowStream) {
|
||||
// Stream without options
|
||||
inputDescriptor.buffer = [];
|
||||
} else if (Array.isArray(input)) {
|
||||
if (input.length > 1) {
|
||||
// Join images together
|
||||
if (!this.options.joining) {
|
||||
this.options.joining = true;
|
||||
this.options.join = input.map(i => this._createInputDescriptor(i));
|
||||
} else {
|
||||
throw new Error('Recursive join is unsupported');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Expected at least two images to join');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unsupported input '${input}' of type ${typeof input}${
|
||||
is.defined(inputOptions) ? ` when also providing options of type ${typeof inputOptions}` : ''
|
||||
@ -369,6 +385,57 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
|
||||
throw new Error('Expected a valid string to create an image with text.');
|
||||
}
|
||||
}
|
||||
// Join images together
|
||||
if (is.defined(inputOptions.join)) {
|
||||
if (is.defined(this.options.join)) {
|
||||
if (is.defined(inputOptions.join.animated)) {
|
||||
if (is.bool(inputOptions.join.animated)) {
|
||||
inputDescriptor.joinAnimated = inputOptions.join.animated;
|
||||
} else {
|
||||
throw is.invalidParameterError('join.animated', 'boolean', inputOptions.join.animated);
|
||||
}
|
||||
}
|
||||
if (is.defined(inputOptions.join.across)) {
|
||||
if (is.integer(inputOptions.join.across) && is.inRange(inputOptions.join.across, 1, 1000000)) {
|
||||
inputDescriptor.joinAcross = inputOptions.join.across;
|
||||
} else {
|
||||
throw is.invalidParameterError('join.across', 'integer between 1 and 100000', inputOptions.join.across);
|
||||
}
|
||||
}
|
||||
if (is.defined(inputOptions.join.shim)) {
|
||||
if (is.integer(inputOptions.join.shim) && is.inRange(inputOptions.join.shim, 0, 1000000)) {
|
||||
inputDescriptor.joinShim = inputOptions.join.shim;
|
||||
} else {
|
||||
throw is.invalidParameterError('join.shim', 'integer between 0 and 100000', inputOptions.join.shim);
|
||||
}
|
||||
}
|
||||
if (is.defined(inputOptions.join.background)) {
|
||||
const background = color(inputOptions.join.background);
|
||||
inputDescriptor.joinBackground = [
|
||||
background.red(),
|
||||
background.green(),
|
||||
background.blue(),
|
||||
Math.round(background.alpha() * 255)
|
||||
];
|
||||
}
|
||||
if (is.defined(inputOptions.join.halign)) {
|
||||
if (is.string(inputOptions.join.halign) && is.string(this.constructor.align[inputOptions.join.halign])) {
|
||||
inputDescriptor.joinHalign = this.constructor.align[inputOptions.join.halign];
|
||||
} else {
|
||||
throw is.invalidParameterError('join.halign', 'valid alignment', inputOptions.join.halign);
|
||||
}
|
||||
}
|
||||
if (is.defined(inputOptions.join.valign)) {
|
||||
if (is.string(inputOptions.join.valign) && is.string(this.constructor.align[inputOptions.join.valign])) {
|
||||
inputDescriptor.joinValign = this.constructor.align[inputOptions.join.valign];
|
||||
} else {
|
||||
throw is.invalidParameterError('join.valign', 'valid alignment', inputOptions.join.valign);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('Expected input to be an array of images to join');
|
||||
}
|
||||
}
|
||||
} else if (is.defined(inputOptions)) {
|
||||
throw new Error('Invalid input options ' + inputOptions);
|
||||
}
|
||||
|
@ -160,10 +160,30 @@ namespace sharp {
|
||||
descriptor->textWrap = AttrAsEnum<VipsTextWrap>(input, "textWrap", VIPS_TYPE_TEXT_WRAP);
|
||||
}
|
||||
}
|
||||
// Join images together
|
||||
if (HasAttr(input, "joinAnimated")) {
|
||||
descriptor->joinAnimated = AttrAsBool(input, "joinAnimated");
|
||||
}
|
||||
if (HasAttr(input, "joinAcross")) {
|
||||
descriptor->joinAcross = AttrAsUint32(input, "joinAcross");
|
||||
}
|
||||
if (HasAttr(input, "joinShim")) {
|
||||
descriptor->joinShim = AttrAsUint32(input, "joinShim");
|
||||
}
|
||||
if (HasAttr(input, "joinBackground")) {
|
||||
descriptor->joinBackground = AttrAsVectorOfDouble(input, "joinBackground");
|
||||
}
|
||||
if (HasAttr(input, "joinHalign")) {
|
||||
descriptor->joinHalign = AttrAsEnum<VipsAlign>(input, "joinHalign", VIPS_TYPE_ALIGN);
|
||||
}
|
||||
if (HasAttr(input, "joinValign")) {
|
||||
descriptor->joinValign = AttrAsEnum<VipsAlign>(input, "joinValign", VIPS_TYPE_ALIGN);
|
||||
}
|
||||
// Limit input images to a given number of pixels, where pixels = width * height
|
||||
descriptor->limitInputPixels = static_cast<uint64_t>(AttrAsInt64(input, "limitInputPixels"));
|
||||
// Allow switch from random to sequential access
|
||||
descriptor->access = AttrAsBool(input, "sequentialRead") ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
|
||||
if (HasAttr(input, "access")) {
|
||||
descriptor->access = AttrAsBool(input, "sequentialRead") ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
|
||||
}
|
||||
// Remove safety features and allow unlimited input
|
||||
descriptor->unlimited = AttrAsBool(input, "unlimited");
|
||||
// Use the EXIF orientation to auto orient the image
|
||||
|
14
src/common.h
14
src/common.h
@ -71,6 +71,12 @@ namespace sharp {
|
||||
int textSpacing;
|
||||
VipsTextWrap textWrap;
|
||||
int textAutofitDpi;
|
||||
bool joinAnimated;
|
||||
int joinAcross;
|
||||
int joinShim;
|
||||
std::vector<double> joinBackground;
|
||||
VipsAlign joinHalign;
|
||||
VipsAlign joinValign;
|
||||
std::vector<double> pdfBackground;
|
||||
|
||||
InputDescriptor():
|
||||
@ -79,7 +85,7 @@ namespace sharp {
|
||||
failOn(VIPS_FAIL_ON_WARNING),
|
||||
limitInputPixels(0x3FFF * 0x3FFF),
|
||||
unlimited(false),
|
||||
access(VIPS_ACCESS_RANDOM),
|
||||
access(VIPS_ACCESS_SEQUENTIAL),
|
||||
bufferLength(0),
|
||||
isBuffer(false),
|
||||
density(72.0),
|
||||
@ -108,6 +114,12 @@ namespace sharp {
|
||||
textSpacing(0),
|
||||
textWrap(VIPS_TEXT_WRAP_WORD),
|
||||
textAutofitDpi(0),
|
||||
joinAnimated(false),
|
||||
joinAcross(1),
|
||||
joinShim(0),
|
||||
joinBackground{ 0.0, 0.0, 0.0, 255.0 },
|
||||
joinHalign(VIPS_ALIGN_LOW),
|
||||
joinValign(VIPS_ALIGN_LOW),
|
||||
pdfBackground{ 255.0, 255.0, 255.0, 255.0 } {}
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,39 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
// Open input
|
||||
vips::VImage image;
|
||||
sharp::ImageType inputImageType;
|
||||
std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
|
||||
if (baton->join.empty()) {
|
||||
std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
|
||||
} else {
|
||||
std::vector<VImage> images;
|
||||
bool hasAlpha = false;
|
||||
for (auto &join : baton->join) {
|
||||
std::tie(image, inputImageType) = sharp::OpenInput(join);
|
||||
image = sharp::EnsureColourspace(image, baton->colourspacePipeline);
|
||||
images.push_back(image);
|
||||
hasAlpha |= sharp::HasAlpha(image);
|
||||
}
|
||||
if (hasAlpha) {
|
||||
for (auto &image : images) {
|
||||
if (!sharp::HasAlpha(image)) {
|
||||
image = sharp::EnsureAlpha(image, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
baton->input->joinBackground.pop_back();
|
||||
}
|
||||
inputImageType = sharp::ImageType::PNG;
|
||||
image = VImage::arrayjoin(images, VImage::option()
|
||||
->set("across", baton->input->joinAcross)
|
||||
->set("shim", baton->input->joinShim)
|
||||
->set("background", baton->input->joinBackground)
|
||||
->set("halign", baton->input->joinHalign)
|
||||
->set("valign", baton->input->joinValign));
|
||||
if (baton->input->joinAnimated) {
|
||||
image = image.copy();
|
||||
image.set(VIPS_META_N_PAGES, static_cast<int>(images.size()));
|
||||
image.set(VIPS_META_PAGE_HEIGHT, static_cast<int>(image.height() / images.size()));
|
||||
}
|
||||
}
|
||||
VipsAccess access = baton->input->access;
|
||||
image = sharp::EnsureColourspace(image, baton->colourspacePipeline);
|
||||
|
||||
@ -1069,6 +1101,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
// Unsupported output format
|
||||
(baton->err).append("Unsupported output format ");
|
||||
if (baton->formatOut == "input") {
|
||||
(baton->err).append("when trying to match input format of ");
|
||||
(baton->err).append(ImageTypeId(inputImageType));
|
||||
} else {
|
||||
(baton->err).append(baton->formatOut);
|
||||
@ -1495,6 +1528,14 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
|
||||
|
||||
// Input
|
||||
baton->input = sharp::CreateInputDescriptor(options.Get("input").As<Napi::Object>());
|
||||
// Join images together
|
||||
if (sharp::HasAttr(options, "join")) {
|
||||
Napi::Array join = options.Get("join").As<Napi::Array>();
|
||||
for (unsigned int i = 0; i < join.Length(); i++) {
|
||||
baton->join.push_back(
|
||||
sharp::CreateInputDescriptor(join.Get(i).As<Napi::Object>()));
|
||||
}
|
||||
}
|
||||
// Extract image options
|
||||
baton->topOffsetPre = sharp::AttrAsInt32(options, "topOffsetPre");
|
||||
baton->leftOffsetPre = sharp::AttrAsInt32(options, "leftOffsetPre");
|
||||
|
@ -39,6 +39,7 @@ struct Composite {
|
||||
|
||||
struct PipelineBaton {
|
||||
sharp::InputDescriptor *input;
|
||||
std::vector<sharp::InputDescriptor *> join;
|
||||
std::string formatOut;
|
||||
std::string fileOut;
|
||||
void *bufferOut;
|
||||
|
BIN
test/fixtures/expected/join2x2.png
vendored
Normal file
BIN
test/fixtures/expected/join2x2.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
@ -724,3 +724,19 @@ sharp({ pdfBackground: color });
|
||||
sharp({ autoOrient: true });
|
||||
sharp({ autoOrient: false });
|
||||
sharp().autoOrient();
|
||||
|
||||
sharp([input, input]);
|
||||
sharp([input, input], {
|
||||
join: {
|
||||
animated: true
|
||||
}
|
||||
});
|
||||
sharp([input, input], {
|
||||
join: {
|
||||
across: 2,
|
||||
shim: 5,
|
||||
background: colour,
|
||||
halign: 'centre',
|
||||
valign: 'bottom'
|
||||
}
|
||||
});
|
||||
|
129
test/unit/join.js
Normal file
129
test/unit/join.js
Normal file
@ -0,0 +1,129 @@
|
||||
// Copyright 2013 Lovell Fuller and others.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
const sharp = require('../../');
|
||||
const fixtures = require('../fixtures');
|
||||
|
||||
describe('Join input images together', function () {
|
||||
it('Join two images horizontally', async () => {
|
||||
const data = await sharp([
|
||||
fixtures.inputPngPalette,
|
||||
{ create: { width: 68, height: 68, channels: 3, background: 'green' } }
|
||||
], { join: { across: 2 } }).toBuffer();
|
||||
|
||||
const metadata = await sharp(data).metadata();
|
||||
assert.strictEqual(metadata.format, 'png');
|
||||
assert.strictEqual(metadata.width, 136);
|
||||
assert.strictEqual(metadata.height, 68);
|
||||
assert.strictEqual(metadata.space, 'srgb');
|
||||
assert.strictEqual(metadata.channels, 3);
|
||||
assert.strictEqual(metadata.hasAlpha, false);
|
||||
});
|
||||
|
||||
it('Join two images vertically with shim and alpha channel', async () => {
|
||||
const data = await sharp([
|
||||
fixtures.inputPngPalette,
|
||||
{ create: { width: 68, height: 68, channels: 4, background: 'green' } }
|
||||
], { join: { across: 1, shim: 8 } }).toBuffer();
|
||||
|
||||
const metadata = await sharp(data).metadata();
|
||||
assert.strictEqual(metadata.format, 'png');
|
||||
assert.strictEqual(metadata.width, 68);
|
||||
assert.strictEqual(metadata.height, 144);
|
||||
assert.strictEqual(metadata.space, 'srgb');
|
||||
assert.strictEqual(metadata.channels, 4);
|
||||
assert.strictEqual(metadata.hasAlpha, true);
|
||||
});
|
||||
|
||||
it('Join four images in 2x2 grid, with centre alignment', async () => {
|
||||
const output = fixtures.path('output.join2x2.png');
|
||||
const info = await sharp([
|
||||
fixtures.inputPngPalette,
|
||||
{ create: { width: 128, height: 128, channels: 3, background: 'green' } },
|
||||
{ create: { width: 128, height: 128, channels: 3, background: 'red' } },
|
||||
fixtures.inputPngPalette
|
||||
], { join: { across: 2, halign: 'centre', valign: 'centre', background: 'blue' } })
|
||||
.toFile(output);
|
||||
|
||||
fixtures.assertMaxColourDistance(output, fixtures.expected('join2x2.png'));
|
||||
|
||||
assert.strictEqual(info.format, 'png');
|
||||
assert.strictEqual(info.width, 256);
|
||||
assert.strictEqual(info.height, 256);
|
||||
assert.strictEqual(info.channels, 3);
|
||||
});
|
||||
|
||||
it('Join two images as animation', async () => {
|
||||
const data = await sharp([
|
||||
fixtures.inputPngPalette,
|
||||
{ create: { width: 68, height: 68, channels: 3, background: 'green' } }
|
||||
], { join: { animated: true } }).gif().toBuffer();
|
||||
|
||||
const metadata = await sharp(data).metadata();
|
||||
assert.strictEqual(metadata.format, 'gif');
|
||||
assert.strictEqual(metadata.width, 68);
|
||||
assert.strictEqual(metadata.height, 68);
|
||||
assert.strictEqual(metadata.pages, 2);
|
||||
});
|
||||
|
||||
it('Empty array of inputs throws', () => {
|
||||
assert.throws(
|
||||
() => sharp([]),
|
||||
/Expected at least two images to join/
|
||||
);
|
||||
});
|
||||
it('Attempt to recursively join throws', () => {
|
||||
assert.throws(
|
||||
() => sharp([fixtures.inputJpg, [fixtures.inputJpg, fixtures.inputJpg]]),
|
||||
/Recursive join is unsupported/
|
||||
);
|
||||
});
|
||||
it('Attempt to set join props on non-array input throws', () => {
|
||||
assert.throws(
|
||||
() => sharp(fixtures.inputJpg, { join: { across: 2 } }),
|
||||
/Expected input to be an array of images to join/
|
||||
);
|
||||
});
|
||||
it('Invalid animated throws', () => {
|
||||
assert.throws(
|
||||
() => sharp([fixtures.inputJpg, fixtures.inputJpg], { join: { animated: 'fail' } }),
|
||||
/Expected boolean for join.animated but received fail of type string/
|
||||
);
|
||||
});
|
||||
it('Invalid across throws', () => {
|
||||
assert.throws(
|
||||
() => sharp([fixtures.inputJpg, fixtures.inputJpg], { join: { across: 'fail' } }),
|
||||
/Expected integer between 1 and 100000 for join.across but received fail of type string/
|
||||
);
|
||||
assert.throws(
|
||||
() => sharp([fixtures.inputJpg, fixtures.inputJpg], { join: { across: 0 } }),
|
||||
/Expected integer between 1 and 100000 for join.across but received 0 of type number/
|
||||
);
|
||||
});
|
||||
it('Invalid shim throws', () => {
|
||||
assert.throws(
|
||||
() => sharp([fixtures.inputJpg, fixtures.inputJpg], { join: { shim: 'fail' } }),
|
||||
/Expected integer between 0 and 100000 for join.shim but received fail of type string/
|
||||
);
|
||||
assert.throws(
|
||||
() => sharp([fixtures.inputJpg, fixtures.inputJpg], { join: { shim: -1 } }),
|
||||
/Expected integer between 0 and 100000 for join.shim but received -1 of type number/
|
||||
);
|
||||
});
|
||||
it('Invalid halign', () => {
|
||||
assert.throws(
|
||||
() => sharp([fixtures.inputJpg, fixtures.inputJpg], { join: { halign: 'fail' } }),
|
||||
/Expected valid alignment for join.halign but received fail of type string/
|
||||
);
|
||||
});
|
||||
it('Invalid valign', () => {
|
||||
assert.throws(
|
||||
() => sharp([fixtures.inputJpg, fixtures.inputJpg], { join: { valign: 'fail' } }),
|
||||
/Expected valid alignment for join.valign but received fail of type string/
|
||||
);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user