diff --git a/docs/api-composite.md b/docs/api-composite.md
index 12cc100ba..ebc38f3a6 100644
--- a/docs/api-composite.md
+++ b/docs/api-composite.md
@@ -29,6 +29,18 @@ and [https://www.cairographics.org/operators/][2]
* `images[].input.create.height` **[Number][7]?**
* `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.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 `LeMonde`.
+ * `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 `Red!`. (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[].gravity` **[String][6]** gravity at which to place the overlay. (optional, default `'centre'`)
* `images[].top` **[Number][7]?** the pixel offset from the top edge.
diff --git a/docs/api-constructor.md b/docs/api-constructor.md
index b690c5390..52b7df97d 100644
--- a/docs/api-constructor.md
+++ b/docs/api-constructor.md
@@ -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.mean` **[number][14]?** mean 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 `LeMonde`.
+ * `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 `Red!`. (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
diff --git a/lib/composite.js b/lib/composite.js
index b6b576bc6..8373767e1 100644
--- a/lib/composite.js
+++ b/lib/composite.js
@@ -93,6 +93,17 @@ const blend = {
* @param {Number} [images[].input.create.height]
* @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 {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 `LeMonde`.
+ * @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 `Red!`.
+ * @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[].gravity='centre'] - gravity at which to place the overlay.
* @param {Number} [images[].top] - the pixel offset from the top edge.
diff --git a/lib/constructor.js b/lib/constructor.js
index eb2c3c4ae..249406889 100644
--- a/lib/constructor.js
+++ b/lib/constructor.js
@@ -126,6 +126,17 @@ const debuglog = util.debuglog('sharp');
* @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.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 `LeMonde`.
+ * @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 `Red!`.
+ * @param {number} [options.text.spacing=0] - text line height in points. Will use the font line height if none is specified.
* @returns {Sharp}
* @throws {Error} Invalid parameters
*/
diff --git a/lib/input.js b/lib/input.js
index c90f7a631..7520859a6 100644
--- a/lib/input.js
+++ b/lib/input.js
@@ -4,6 +4,18 @@ const color = require('color');
const is = require('./is');
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.
* @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');
}
}
+ // 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)) {
throw new Error('Invalid input options ' + inputOptions);
}
@@ -504,4 +591,6 @@ module.exports = function (Sharp) {
metadata,
stats
});
+ // Class attributes
+ Sharp.align = align;
};
diff --git a/package.json b/package.json
index 1b31fd7ba..a8bf87fe3 100644
--- a/package.json
+++ b/package.json
@@ -83,7 +83,8 @@
"Chris Banks ",
"Ompal Singh ",
"Brodan "
+ "Ankur Parihar ",
+ "Brahim Ait elhaj "
],
"scripts": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)",
diff --git a/src/common.cc b/src/common.cc
index 751e41d73..409bef368 100644
--- a/src/common.cc
+++ b/src/common.cc
@@ -133,6 +133,42 @@ namespace sharp {
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, "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(
+ 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
descriptor->limitInputPixels = static_cast(AttrAsInt64(input, "limitInputPixels"));
// Allow switch from random to sequential access
@@ -394,6 +430,31 @@ namespace sharp {
image.get_image()->Type = image.guess_interpretation();
image = image.cast(VIPS_FORMAT_UCHAR);
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);
+ 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(descriptor->textFont.data()));
+ }
+ if (descriptor->textFontfile.length() > 0) {
+ textOptions->set("fontfile", const_cast(descriptor->textFontfile.data()));
+ }
+ image = VImage::new_memory().text(const_cast(descriptor->textValue.data()), textOptions);
+ image.get_image()->Type = image.guess_interpretation();
+ imageType = ImageType::RAW;
} else {
// From filesystem
imageType = DetermineImageType(descriptor->file.data());
diff --git a/src/common.h b/src/common.h
index 2a43597d6..3e29c4404 100644
--- a/src/common.h
+++ b/src/common.h
@@ -71,6 +71,16 @@ namespace sharp {
std::string createNoiseType;
double createNoiseMean;
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;
InputDescriptor():
buffer(nullptr),
@@ -95,7 +105,14 @@ namespace sharp {
createHeight(0),
createBackground{ 0.0, 0.0, 0.0, 255.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) {}
};
// Convenience methods to access the attributes of a Napi::Object
diff --git a/test/unit/text.js b/test/unit/text.js
new file mode 100644
index 000000000..1f081c5cc
--- /dev/null
+++ b/test/unit/text.js
@@ -0,0 +1,274 @@
+'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);
+ });
+
+ 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);
+ 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: 'redblue',
+ 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: 'Watermark is cool',
+ width: 300,
+ height: 300,
+ justify: true,
+ align: 'right',
+ spacing: 50,
+ rgba: true
+ }
+ },
+ gravity: 'northeast'
+ }, {
+ input: {
+ text: {
+ text: 'cool',
+ 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('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 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
+ }
+ });
+ });
+ });
+});