Expose vips_text to create an image containing rendered text (#3252)

This commit is contained in:
brahima 2022-07-25 12:32:10 +02:00 committed by GitHub
parent 76c4c51e2a
commit ea7cf2a2ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 567 additions and 2 deletions

View File

@ -29,6 +29,18 @@ and [https://www.cairographics.org/operators/][2]
* `images[].input.create.height` **[Number][7]?** * `images[].input.create.height` **[Number][7]?**
* `images[].input.create.channels` **[Number][7]?** 3-4 * `images[].input.create.channels` **[Number][7]?** 3-4
* `images[].input.create.background` **([String][6] | [Object][4])?** parsed by the [color][8] module to extract values for red, green, blue and alpha. * `images[].input.create.background` **([String][6] | [Object][4])?** parsed by the [color][8] module to extract values for red, green, blue and alpha.
* `images[].input.text` **[Object][4]?** describes a new text image to be created.
* `images[].input.text.text` **[string][6]?** text to render as a UTF-8 string. It can contain Pango markup, for example `<i>Le</i>Monde`.
* `images[].input.text.font` **[string][6]?** font name to render with.
* `images[].input.text.fontfile` **[string][6]?** absolute filesystem path to a font file that can be used by `font`.
* `images[].input.text.width` **[number][7]** integral number of pixels to word-wrap at. Lines of text wider than this will be broken at word boundaries. (optional, default `0`)
* `images[].input.text.height` **[number][7]** integral number of pixels high. When defined, `dpi` will be ignored and the text will automatically fit the pixel resolution defined by `width` and `height`. Will be ignored if `width` is not specified or set to 0. (optional, default `0`)
* `images[].input.text.align` **[string][6]** text alignment (`'left'`, `'centre'`, `'center'`, `'right'`). (optional, default `'left'`)
* `images[].input.text.justify` **[boolean][9]** set this to true to apply justification to the text. (optional, default `false`)
* `images[].input.text.dpi` **[number][7]** the resolution (size) at which to render the text. Does not take effect if `height` is specified. (optional, default `72`)
* `images[].input.text.rgba` **[boolean][9]** 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>`. (optional, default `false`)
* `images[].input.text.spacing` **[number][7]** text line height in points. Will use the font line height if none is specified. (optional, default `0`)
* `images[].blend` **[String][6]** how to blend this image with the image below. (optional, default `'over'`) * `images[].blend` **[String][6]** how to blend this image with the image below. (optional, default `'over'`)
* `images[].gravity` **[String][6]** gravity at which to place the overlay. (optional, default `'centre'`) * `images[].gravity` **[String][6]** gravity at which to place the overlay. (optional, default `'centre'`)
* `images[].top` **[Number][7]?** the pixel offset from the top edge. * `images[].top` **[Number][7]?** the pixel offset from the top edge.

View File

@ -51,6 +51,18 @@ Implements the [stream.Duplex][1] class.
* `options.create.noise.type` **[string][12]?** type of generated noise, currently only `gaussian` is supported. * `options.create.noise.type` **[string][12]?** type of generated noise, currently only `gaussian` is supported.
* `options.create.noise.mean` **[number][14]?** mean of pixels in generated noise. * `options.create.noise.mean` **[number][14]?** mean of pixels in generated noise.
* `options.create.noise.sigma` **[number][14]?** standard deviation of pixels in generated noise. * `options.create.noise.sigma` **[number][14]?** standard deviation of pixels in generated noise.
* `options.text` **[Object][13]?** describes a new text image to be created.
* `options.text.text` **[string][12]?** text to render as a UTF-8 string. It can contain Pango markup, for example `<i>Le</i>Monde`.
* `options.text.font` **[string][12]?** font name to render with.
* `options.text.fontfile` **[string][12]?** absolute filesystem path to a font file that can be used by `font`.
* `options.text.width` **[number][14]** integral number of pixels to word-wrap at. Lines of text wider than this will be broken at word boundaries. (optional, default `0`)
* `options.text.height` **[number][14]** integral number of pixels high. When defined, `dpi` will be ignored and the text will automatically fit the pixel resolution defined by `width` and `height`. Will be ignored if `width` is not specified or set to 0. (optional, default `0`)
* `options.text.align` **[string][12]** text alignment (`'left'`, `'centre'`, `'center'`, `'right'`). (optional, default `'left'`)
* `options.text.justify` **[boolean][15]** set this to true to apply justification to the text. (optional, default `false`)
* `options.text.dpi` **[number][14]** the resolution (size) at which to render the text. Does not take effect if `height` is specified. (optional, default `72`)
* `options.text.rgba` **[boolean][15]** 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>`. (optional, default `false`)
* `options.text.spacing` **[number][14]** text line height in points. Will use the font line height if none is specified. (optional, default `0`)
### Examples ### Examples
@ -127,6 +139,29 @@ await sharp({
}).toFile('noise.png'); }).toFile('noise.png');
``` ```
```javascript
// Generate an image from text
await sharp({
text: {
text: 'Hello, world!',
width: 400, // max width
height: 300 // max height
}
}).toFile('text_bw.png');
```
```javascript
// Generate an rgba image from text using pango markup and font
await sharp({
text: {
text: '<span foreground="red">Red!</span><span background="cyan">blue</span>',
font: 'sans',
rgba: true,
dpi: 300
}
}).toFile('text_rgba.png');
```
* Throws **[Error][17]** Invalid parameters * Throws **[Error][17]** Invalid parameters
Returns **[Sharp][18]** Returns **[Sharp][18]**

View File

@ -22,6 +22,7 @@ A `Promise` is returned when `callback` is not provided.
`info` contains the output image `format`, `size` (bytes), `width`, `height`, `info` contains the output image `format`, `size` (bytes), `width`, `height`,
`channels` and `premultiplied` (indicating if premultiplication was used). `channels` and `premultiplied` (indicating if premultiplication was used).
When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`. When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`.
May also contain `textAutofitDpi` (dpi the font was rendered at) if image was created from text.
### Examples ### Examples
@ -61,6 +62,7 @@ See [withMetadata][1] for control over this.
`channels` and `premultiplied` (indicating if premultiplication was used). `channels` and `premultiplied` (indicating if premultiplication was used).
When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`. When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`.
May also contain `textAutofitDpi` (dpi the font was rendered at) if image was created from text.
A `Promise` is returned when `callback` is not provided. A `Promise` is returned when `callback` is not provided.

View File

@ -93,6 +93,17 @@ const blend = {
* @param {Number} [images[].input.create.height] * @param {Number} [images[].input.create.height]
* @param {Number} [images[].input.create.channels] - 3-4 * @param {Number} [images[].input.create.channels] - 3-4
* @param {String|Object} [images[].input.create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. * @param {String|Object} [images[].input.create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
* @param {Object} [images[].input.text] - describes a new text image to be created.
* @param {string} [images[].input.text.text] - text to render as a UTF-8 string. It can contain Pango markup, for example `<i>Le</i>Monde`.
* @param {string} [images[].input.text.font] - font name to render with.
* @param {string} [images[].input.text.fontfile] - absolute filesystem path to a font file that can be used by `font`.
* @param {number} [images[].input.text.width=0] - integral number of pixels to word-wrap at. Lines of text wider than this will be broken at word boundaries.
* @param {number} [images[].input.text.height=0] - integral number of pixels high. When defined, `dpi` will be ignored and the text will automatically fit the pixel resolution defined by `width` and `height`. Will be ignored if `width` is not specified or set to 0.
* @param {string} [images[].input.text.align='left'] - text alignment (`'left'`, `'centre'`, `'center'`, `'right'`).
* @param {boolean} [images[].input.text.justify=false] - set this to true to apply justification to the text.
* @param {number} [images[].input.text.dpi=72] - the resolution (size) at which to render the text. Does not take effect if `height` is specified.
* @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>`.
* @param {number} [images[].input.text.spacing=0] - text line height in points. Will use the font line height if none is specified.
* @param {String} [images[].blend='over'] - how to blend this image with the image below. * @param {String} [images[].blend='over'] - how to blend this image with the image below.
* @param {String} [images[].gravity='centre'] - gravity at which to place the overlay. * @param {String} [images[].gravity='centre'] - gravity at which to place the overlay.
* @param {Number} [images[].top] - the pixel offset from the top edge. * @param {Number} [images[].top] - the pixel offset from the top edge.

View File

@ -92,6 +92,27 @@ const debuglog = util.debuglog('sharp');
* } * }
* }).toFile('noise.png'); * }).toFile('noise.png');
* *
* @example
* // Generate an image from text
* await sharp({
* text: {
* text: 'Hello, world!',
* width: 400, // max width
* height: 300 // max height
* }
* }).toFile('text_bw.png');
*
* @example
* // Generate an rgba image from text using pango markup and font
* await sharp({
* text: {
* text: '<span foreground="red">Red!</span><span background="cyan">blue</span>',
* font: 'sans',
* rgba: true,
* dpi: 300
* }
* }).toFile('text_rgba.png');
*
* @param {(Buffer|Uint8Array|Uint8ClampedArray|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|string)} [input] - if present, can be * @param {(Buffer|Uint8Array|Uint8ClampedArray|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|string)} [input] - if present, can be
* a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image data, or * a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF image data, or
* a TypedArray containing raw pixel image data, or * a TypedArray containing raw pixel image data, or
@ -126,6 +147,17 @@ const debuglog = util.debuglog('sharp');
* @param {string} [options.create.noise.type] - type of generated noise, currently only `gaussian` is supported. * @param {string} [options.create.noise.type] - type of generated noise, currently only `gaussian` is supported.
* @param {number} [options.create.noise.mean] - mean of pixels in generated noise. * @param {number} [options.create.noise.mean] - mean of pixels in generated noise.
* @param {number} [options.create.noise.sigma] - standard deviation of pixels in generated noise. * @param {number} [options.create.noise.sigma] - standard deviation of pixels in generated noise.
* @param {Object} [options.text] - describes a new text image to be created.
* @param {string} [options.text.text] - text to render as a UTF-8 string. It can contain Pango markup, for example `<i>Le</i>Monde`.
* @param {string} [options.text.font] - font name to render with.
* @param {string} [options.text.fontfile] - absolute filesystem path to a font file that can be used by `font`.
* @param {number} [options.text.width=0] - integral number of pixels to word-wrap at. Lines of text wider than this will be broken at word boundaries.
* @param {number} [options.text.height=0] - integral number of pixels high. When defined, `dpi` will be ignored and the text will automatically fit the pixel resolution defined by `width` and `height`. Will be ignored if `width` is not specified or set to 0.
* @param {string} [options.text.align='left'] - text alignment (`'left'`, `'centre'`, `'center'`, `'right'`).
* @param {boolean} [options.text.justify=false] - set this to true to apply justification to the text.
* @param {number} [options.text.dpi=72] - the resolution (size) at which to render the text. Does not take effect if `height` is specified.
* @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.
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */

View File

@ -4,6 +4,18 @@ const color = require('color');
const is = require('./is'); const is = require('./is');
const sharp = require('./sharp'); const sharp = require('./sharp');
/**
* Justication alignment
* @member
* @private
*/
const align = {
left: 'low',
center: 'centre',
centre: 'centre',
right: 'high'
};
/** /**
* Extract input options, if any, from an object. * Extract input options, if any, from an object.
* @private * @private
@ -245,6 +257,81 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
throw new Error('Expected valid width, height and channels to create a new input image'); throw new Error('Expected valid width, height and channels to create a new input image');
} }
} }
// Create a new image with text
if (is.defined(inputOptions.text)) {
if (is.object(inputOptions.text) && is.string(inputOptions.text.text)) {
inputDescriptor.textValue = inputOptions.text.text;
if (is.defined(inputOptions.text.height) && is.defined(inputOptions.text.dpi)) {
throw new Error('Expected only one of dpi or height');
}
if (is.defined(inputOptions.text.font)) {
if (is.string(inputOptions.text.font)) {
inputDescriptor.textFont = inputOptions.text.font;
} else {
throw is.invalidParameterError('text.font', 'string', inputOptions.text.font);
}
}
if (is.defined(inputOptions.text.fontfile)) {
if (is.string(inputOptions.text.fontfile)) {
inputDescriptor.textFontfile = inputOptions.text.fontfile;
} else {
throw is.invalidParameterError('text.fontfile', 'string', inputOptions.text.fontfile);
}
}
if (is.defined(inputOptions.text.width)) {
if (is.number(inputOptions.text.width)) {
inputDescriptor.textWidth = inputOptions.text.width;
} else {
throw is.invalidParameterError('text.textWidth', 'number', inputOptions.text.width);
}
}
if (is.defined(inputOptions.text.height)) {
if (is.number(inputOptions.text.height)) {
inputDescriptor.textHeight = inputOptions.text.height;
} else {
throw is.invalidParameterError('text.height', 'number', inputOptions.text.height);
}
}
if (is.defined(inputOptions.text.align)) {
if (is.string(inputOptions.text.align) && is.string(this.constructor.align[inputOptions.text.align])) {
inputDescriptor.textAlign = this.constructor.align[inputOptions.text.align];
} else {
throw is.invalidParameterError('text.align', 'valid alignment', inputOptions.text.align);
}
}
if (is.defined(inputOptions.text.justify)) {
if (is.bool(inputOptions.text.justify)) {
inputDescriptor.textJustify = inputOptions.text.justify;
} else {
throw is.invalidParameterError('text.justify', 'boolean', inputOptions.text.justify);
}
}
if (is.defined(inputOptions.text.dpi)) {
if (is.number(inputOptions.text.dpi) && is.inRange(inputOptions.text.dpi, 1, 100000)) {
inputDescriptor.textDpi = inputOptions.text.dpi;
} else {
throw is.invalidParameterError('text.dpi', 'number between 1 and 100000', inputOptions.text.dpi);
}
}
if (is.defined(inputOptions.text.rgba)) {
if (is.bool(inputOptions.text.rgba)) {
inputDescriptor.textRgba = inputOptions.text.rgba;
} else {
throw is.invalidParameterError('text.rgba', 'bool', inputOptions.text.rgba);
}
}
if (is.defined(inputOptions.text.spacing)) {
if (is.number(inputOptions.text.spacing)) {
inputDescriptor.textSpacing = inputOptions.text.spacing;
} else {
throw is.invalidParameterError('text.spacing', 'number', inputOptions.text.spacing);
}
}
delete inputDescriptor.buffer;
} else {
throw new Error('Expected a valid string to create an image with text.');
}
}
} else if (is.defined(inputOptions)) { } else if (is.defined(inputOptions)) {
throw new Error('Invalid input options ' + inputOptions); throw new Error('Invalid input options ' + inputOptions);
} }
@ -504,4 +591,6 @@ module.exports = function (Sharp) {
metadata, metadata,
stats stats
}); });
// Class attributes
Sharp.align = align;
}; };

View File

@ -58,6 +58,7 @@ const bitdepthFromColourCount = (colours) => 1 << 31 - Math.clz32(Math.ceil(Math
* `info` contains the output image `format`, `size` (bytes), `width`, `height`, * `info` contains the output image `format`, `size` (bytes), `width`, `height`,
* `channels` and `premultiplied` (indicating if premultiplication was used). * `channels` and `premultiplied` (indicating if premultiplication was used).
* When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`. * When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`.
* May also contain `textAutofitDpi` (dpi the font was rendered at) if image was created from text.
* @returns {Promise<Object>} - when no callback is provided * @returns {Promise<Object>} - when no callback is provided
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
@ -98,6 +99,7 @@ function toFile (fileOut, callback) {
* - `info` contains the output image `format`, `size` (bytes), `width`, `height`, * - `info` contains the output image `format`, `size` (bytes), `width`, `height`,
* `channels` and `premultiplied` (indicating if premultiplication was used). * `channels` and `premultiplied` (indicating if premultiplication was used).
* When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`. * When using a crop strategy also contains `cropOffsetLeft` and `cropOffsetTop`.
* May also contain `textAutofitDpi` (dpi the font was rendered at) if image was created from text.
* *
* A `Promise` is returned when `callback` is not provided. * A `Promise` is returned when `callback` is not provided.
* *

View File

@ -83,7 +83,8 @@
"Chris Banks <christopher.bradley.banks@gmail.com>", "Chris Banks <christopher.bradley.banks@gmail.com>",
"Ompal Singh <ompal.hitm09@gmail.com>", "Ompal Singh <ompal.hitm09@gmail.com>",
"Brodan <christopher.hranj@gmail.com", "Brodan <christopher.hranj@gmail.com",
"Ankur Parihar <ankur.github@gmail.com>" "Ankur Parihar <ankur.github@gmail.com>",
"Brahim Ait elhaj <brahima@gmail.com>"
], ],
"scripts": { "scripts": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)", "install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)",

View File

@ -133,6 +133,39 @@ namespace sharp {
descriptor->createBackground = AttrAsVectorOfDouble(input, "createBackground"); descriptor->createBackground = AttrAsVectorOfDouble(input, "createBackground");
} }
} }
// Create new image with text
if (HasAttr(input, "textValue")) {
descriptor->textValue = AttrAsStr(input, "textValue");
if (HasAttr(input, "textFont")) {
descriptor->textFont = AttrAsStr(input, "textFont");
}
if (HasAttr(input, "textFontfile")) {
descriptor->textFontfile = AttrAsStr(input, "textFontfile");
}
if (HasAttr(input, "textWidth")) {
descriptor->textWidth = AttrAsUint32(input, "textWidth");
}
if (HasAttr(input, "textHeight")) {
descriptor->textHeight = AttrAsUint32(input, "textHeight");
}
if (HasAttr(input, "textAlign")) {
descriptor->textAlign = static_cast<VipsAlign>(
vips_enum_from_nick(nullptr, VIPS_TYPE_ALIGN,
AttrAsStr(input, "textAlign").data()));
}
if (HasAttr(input, "textJustify")) {
descriptor->textJustify = AttrAsBool(input, "textJustify");
}
if (HasAttr(input, "textDpi")) {
descriptor->textDpi = AttrAsUint32(input, "textDpi");
}
if (HasAttr(input, "textRgba")) {
descriptor->textRgba = AttrAsBool(input, "textRgba");
}
if (HasAttr(input, "textSpacing")) {
descriptor->textSpacing = AttrAsUint32(input, "textSpacing");
}
}
// Limit input images to a given number of pixels, where pixels = width * height // Limit input images to a given number of pixels, where pixels = width * height
descriptor->limitInputPixels = static_cast<uint64_t>(AttrAsInt64(input, "limitInputPixels")); descriptor->limitInputPixels = static_cast<uint64_t>(AttrAsInt64(input, "limitInputPixels"));
// Allow switch from random to sequential access // Allow switch from random to sequential access
@ -395,6 +428,34 @@ namespace sharp {
} }
image = image.cast(VIPS_FORMAT_UCHAR); image = image.cast(VIPS_FORMAT_UCHAR);
imageType = ImageType::RAW; imageType = ImageType::RAW;
} else if (descriptor->textValue.length() > 0) {
// Create a new image with text
vips::VOption *textOptions = VImage::option()
->set("align", descriptor->textAlign)
->set("justify", descriptor->textJustify)
->set("rgba", descriptor->textRgba)
->set("spacing", descriptor->textSpacing)
->set("autofit_dpi", &descriptor->textAutofitDpi);
if (descriptor->textWidth > 0) {
textOptions->set("width", descriptor->textWidth);
}
// Ignore dpi if height is set
if (descriptor->textWidth > 0 && descriptor->textHeight > 0) {
textOptions->set("height", descriptor->textHeight);
} else if (descriptor->textDpi > 0) {
textOptions->set("dpi", descriptor->textDpi);
}
if (descriptor->textFont.length() > 0) {
textOptions->set("font", const_cast<char*>(descriptor->textFont.data()));
}
if (descriptor->textFontfile.length() > 0) {
textOptions->set("fontfile", const_cast<char*>(descriptor->textFontfile.data()));
}
image = VImage::text(const_cast<char *>(descriptor->textValue.data()), textOptions);
if (!descriptor->textRgba) {
image = image.copy(VImage::option()->set("interpretation", VIPS_INTERPRETATION_B_W));
}
imageType = ImageType::RAW;
} else { } else {
// From filesystem // From filesystem
imageType = DetermineImageType(descriptor->file.data()); imageType = DetermineImageType(descriptor->file.data());

View File

@ -71,6 +71,17 @@ namespace sharp {
std::string createNoiseType; std::string createNoiseType;
double createNoiseMean; double createNoiseMean;
double createNoiseSigma; double createNoiseSigma;
std::string textValue;
std::string textFont;
std::string textFontfile;
int textWidth;
int textHeight;
VipsAlign textAlign;
bool textJustify;
int textDpi;
bool textRgba;
int textSpacing;
int textAutofitDpi;
InputDescriptor(): InputDescriptor():
buffer(nullptr), buffer(nullptr),
@ -95,7 +106,15 @@ namespace sharp {
createHeight(0), createHeight(0),
createBackground{ 0.0, 0.0, 0.0, 255.0 }, createBackground{ 0.0, 0.0, 0.0, 255.0 },
createNoiseMean(0.0), createNoiseMean(0.0),
createNoiseSigma(0.0) {} createNoiseSigma(0.0),
textWidth(0),
textHeight(0),
textAlign(VIPS_ALIGN_LOW),
textJustify(FALSE),
textDpi(72),
textRgba(FALSE),
textSpacing(0),
textAutofitDpi(0) {}
}; };
// Convenience methods to access the attributes of a Napi::Object // Convenience methods to access the attributes of a Napi::Object

View File

@ -1185,6 +1185,10 @@ class PipelineWorker : public Napi::AsyncWorker {
info.Set("trimOffsetTop", static_cast<int32_t>(baton->trimOffsetTop)); info.Set("trimOffsetTop", static_cast<int32_t>(baton->trimOffsetTop));
} }
if (baton->input->textAutofitDpi) {
info.Set("textAutofitDpi", static_cast<uint32_t>(baton->input->textAutofitDpi));
}
if (baton->bufferOutLength > 0) { if (baton->bufferOutLength > 0) {
// Add buffer size to info // Add buffer size to info
info.Set("size", static_cast<uint32_t>(baton->bufferOutLength)); info.Set("size", static_cast<uint32_t>(baton->bufferOutLength));

297
test/unit/text.js Normal file
View File

@ -0,0 +1,297 @@
'use strict';
const assert = require('assert');
const sharp = require('../../');
const fixtures = require('../fixtures');
describe('Text to image', () => {
it('text with default values', async () => {
const output = fixtures.path('output.text-default.png');
const text = sharp({
text: {
text: 'Hello, world !'
}
});
const info = await text.png().toFile(output);
assert.strictEqual('png', info.format);
assert.strictEqual(3, info.channels);
assert.strictEqual(false, info.premultiplied);
assert.ok(info.width > 10);
assert.ok(info.height > 8);
const metadata = await sharp(output).metadata();
assert.strictEqual('uchar', metadata.depth);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(72, metadata.density);
const stats = await sharp(output).stats();
assert.strictEqual(0, stats.channels[0].min);
assert.strictEqual(255, stats.channels[0].max);
assert.strictEqual(0, stats.channels[1].min);
assert.strictEqual(255, stats.channels[1].max);
assert.strictEqual(0, stats.channels[2].min);
assert.strictEqual(255, stats.channels[2].max);
assert.ok(info.textAutofitDpi > 0);
});
it('text with width and height', function (done) {
const output = fixtures.path('output.text-width-height.png');
const maxWidth = 500;
const maxHeight = 500;
const text = sharp({
text: {
text: 'Hello, world!',
width: maxWidth,
height: maxHeight
}
});
text.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(3, info.channels);
assert.ok(info.width > 10 && info.width <= maxWidth);
assert.ok(info.height > 10 && info.height <= maxHeight);
assert.ok(Math.abs(info.width - maxWidth) < 50);
assert.ok(info.textAutofitDpi > 0);
done();
});
});
it('text with dpi', function (done) {
const output = fixtures.path('output.text-dpi.png');
const dpi = 300;
const text = sharp({
text: {
text: 'Hello, world!',
dpi: dpi
}
});
text.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
sharp(output).metadata(function (err, metadata) {
if (err) throw err;
assert.strictEqual(dpi, metadata.density);
done();
});
});
});
it('text with color and pango markup', function (done) {
const output = fixtures.path('output.text-color-pango.png');
const dpi = 300;
const text = sharp({
text: {
text: '<span foreground="red" font="100">red</span><span font="50" background="cyan">blue</span>',
rgba: true,
dpi: dpi
}
});
text.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(4, info.channels);
sharp(output).metadata(function (err, metadata) {
if (err) throw err;
assert.strictEqual(dpi, metadata.density);
assert.strictEqual('uchar', metadata.depth);
assert.strictEqual(true, metadata.hasAlpha);
done();
});
});
});
it('text with font', function (done) {
const output = fixtures.path('output.text-with-font.png');
const text = sharp({
text: {
text: 'Hello, world!',
font: 'sans 100'
}
});
text.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(3, info.channels);
assert.ok(info.width > 30);
assert.ok(info.height > 10);
done();
});
});
it('text with justify and composite', done => {
const output = fixtures.path('output.text-composite.png');
const width = 500;
const dpi = 300;
const text = sharp(fixtures.inputJpg)
.resize(width)
.composite([{
input: {
text: {
text: '<span foreground="#ffff00">Watermark</span> <span foreground="white"><i>is cool</i></span>',
width: 300,
height: 300,
justify: true,
align: 'right',
spacing: 50,
rgba: true
}
},
gravity: 'northeast'
}, {
input: {
text: {
text: '<span background="cyan">cool</span>',
font: 'sans 30',
dpi: dpi,
rgba: true
}
},
left: 30,
top: 250
}]);
text.toFile(output, function (err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(4, info.channels);
assert.strictEqual(width, info.width);
assert.strictEqual(true, info.premultiplied);
sharp(output).metadata(function (err, metadata) {
if (err) throw err;
assert.strictEqual('srgb', metadata.space);
assert.strictEqual('uchar', metadata.depth);
assert.strictEqual(true, metadata.hasAlpha);
done();
});
});
});
it('bad text input', function () {
assert.throws(function () {
sharp({
text: {
}
});
});
});
it('fontfile input', function () {
// Added for code coverage
sharp({
text: {
text: 'text',
fontfile: 'UnknownFont.ttf'
}
});
});
it('bad font input', function () {
assert.throws(function () {
sharp({
text: {
text: 'text',
font: 12
}
});
});
});
it('bad fontfile input', function () {
assert.throws(function () {
sharp({
text: {
text: 'text',
fontfile: true
}
});
});
});
it('bad width input', function () {
assert.throws(function () {
sharp({
text: {
text: 'text',
width: 'bad'
}
});
});
});
it('bad height input', function () {
assert.throws(function () {
sharp({
text: {
text: 'text',
height: 'bad'
}
});
});
});
it('bad align input', function () {
assert.throws(function () {
sharp({
text: {
text: 'text',
align: 'unknown'
}
});
});
});
it('bad justify input', function () {
assert.throws(function () {
sharp({
text: {
text: 'text',
justify: 'unknown'
}
});
});
});
it('bad dpi input', function () {
assert.throws(function () {
sharp({
text: {
text: 'text',
dpi: -10
}
});
});
});
it('bad rgba input', function () {
assert.throws(function () {
sharp({
text: {
text: 'text',
rgba: -10
}
});
});
});
it('bad spacing input', function () {
assert.throws(function () {
sharp({
text: {
text: 'text',
spacing: 'number expected'
}
});
});
});
it('only height or dpi not both', function () {
assert.throws(function () {
sharp({
text: {
text: 'text',
height: 400,
dpi: 100
}
});
});
});
});