From ea7cf2a2ef2001663b82b1ba0d02ed4e02e65e95 Mon Sep 17 00:00:00 2001 From: brahima Date: Mon, 25 Jul 2022 12:32:10 +0200 Subject: [PATCH] Expose vips_text to create an image containing rendered text (#3252) --- docs/api-composite.md | 12 ++ docs/api-constructor.md | 35 +++++ docs/api-output.md | 2 + lib/composite.js | 11 ++ lib/constructor.js | 32 +++++ lib/input.js | 89 ++++++++++++ lib/output.js | 2 + package.json | 3 +- src/common.cc | 61 +++++++++ src/common.h | 21 ++- src/pipeline.cc | 4 + test/unit/text.js | 297 ++++++++++++++++++++++++++++++++++++++++ 12 files changed, 567 insertions(+), 2 deletions(-) create mode 100644 test/unit/text.js diff --git a/docs/api-composite.md b/docs/api-composite.md index 12cc100b..ebc38f3a 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 b690c539..31bce0fd 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 @@ -127,6 +139,29 @@ await sharp({ }).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: 'Red!blue', + font: 'sans', + rgba: true, + dpi: 300 + } +}).toFile('text_rgba.png'); +``` + * Throws **[Error][17]** Invalid parameters Returns **[Sharp][18]** diff --git a/docs/api-output.md b/docs/api-output.md index f45c95d6..7d131dbd 100644 --- a/docs/api-output.md +++ b/docs/api-output.md @@ -22,6 +22,7 @@ A `Promise` is returned when `callback` is not provided. `info` contains the output image `format`, `size` (bytes), `width`, `height`, `channels` and `premultiplied` (indicating if premultiplication was used). 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 @@ -61,6 +62,7 @@ See [withMetadata][1] for control over this. `channels` and `premultiplied` (indicating if premultiplication was used). 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. diff --git a/lib/composite.js b/lib/composite.js index b6b576bc..8373767e 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 7ae48c3e..33e6ffe1 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -92,6 +92,27 @@ const debuglog = util.debuglog('sharp'); * } * }).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: 'Red!blue', + * 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 * a Buffer / Uint8Array / Uint8ClampedArray containing JPEG, PNG, WebP, AVIF, GIF, SVG or TIFF 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 {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 c90f7a63..7520859a 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/lib/output.js b/lib/output.js index 3ba4a99e..dcdfbb02 100644 --- a/lib/output.js +++ b/lib/output.js @@ -58,6 +58,7 @@ const bitdepthFromColourCount = (colours) => 1 << 31 - Math.clz32(Math.ceil(Math * `info` contains the output image `format`, `size` (bytes), `width`, `height`, * `channels` and `premultiplied` (indicating if premultiplication was used). * 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} - when no callback is provided * @throws {Error} Invalid parameters */ @@ -98,6 +99,7 @@ function toFile (fileOut, callback) { * - `info` contains the output image `format`, `size` (bytes), `width`, `height`, * `channels` and `premultiplied` (indicating if premultiplication was used). * 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. * diff --git a/package.json b/package.json index 1232b60e..ec75cc3e 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 f567080c..de21cbd0 100644 --- a/src/common.cc +++ b/src/common.cc @@ -133,6 +133,39 @@ 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, "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 @@ -395,6 +428,34 @@ namespace sharp { } 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) + ->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(descriptor->textFont.data())); + } + if (descriptor->textFontfile.length() > 0) { + textOptions->set("fontfile", const_cast(descriptor->textFontfile.data())); + } + image = VImage::text(const_cast(descriptor->textValue.data()), textOptions); + if (!descriptor->textRgba) { + image = image.copy(VImage::option()->set("interpretation", VIPS_INTERPRETATION_B_W)); + } + imageType = ImageType::RAW; } else { // From filesystem imageType = DetermineImageType(descriptor->file.data()); diff --git a/src/common.h b/src/common.h index e15715c3..4aaf357d 100644 --- a/src/common.h +++ b/src/common.h @@ -71,6 +71,17 @@ 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; + int textAutofitDpi; InputDescriptor(): buffer(nullptr), @@ -95,7 +106,15 @@ 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), + textAutofitDpi(0) {} }; // Convenience methods to access the attributes of a Napi::Object diff --git a/src/pipeline.cc b/src/pipeline.cc index 0996614c..396d35af 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -1185,6 +1185,10 @@ class PipelineWorker : public Napi::AsyncWorker { info.Set("trimOffsetTop", static_cast(baton->trimOffsetTop)); } + if (baton->input->textAutofitDpi) { + info.Set("textAutofitDpi", static_cast(baton->input->textAutofitDpi)); + } + if (baton->bufferOutLength > 0) { // Add buffer size to info info.Set("size", static_cast(baton->bufferOutLength)); diff --git a/test/unit/text.js b/test/unit/text.js new file mode 100644 index 00000000..d3803605 --- /dev/null +++ b/test/unit/text.js @@ -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: '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('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 + } + }); + }); + }); +});