Skip to content

Commit

Permalink
Expose vips_text to create an image containing rendered text #512
Browse files Browse the repository at this point in the history
  • Loading branch information
brahima committed Jun 13, 2022
1 parent b10d8f8 commit 287208e
Show file tree
Hide file tree
Showing 25 changed files with 802 additions and 2 deletions.
12 changes: 12 additions & 0 deletions docs/api-composite.md
Expand Up @@ -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 `<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[].gravity` **[String][6]** gravity at which to place the overlay. (optional, default `'centre'`)
* `images[].top` **[Number][7]?** the pixel offset from the top edge.
Expand Down
12 changes: 12 additions & 0 deletions docs/api-constructor.md
Expand Up @@ -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 `<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

Expand Down
11 changes: 11 additions & 0 deletions lib/composite.js
Expand Up @@ -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 `<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[].gravity='centre'] - gravity at which to place the overlay.
* @param {Number} [images[].top] - the pixel offset from the top edge.
Expand Down
11 changes: 11 additions & 0 deletions lib/constructor.js
Expand Up @@ -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 `<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}
* @throws {Error} Invalid parameters
*/
Expand Down
86 changes: 86 additions & 0 deletions lib/input.js
Expand Up @@ -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
Expand Down Expand Up @@ -245,6 +257,78 @@ 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.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.textFontfile)) {
if (is.string(inputOptions.text.textFontfile)) {
inputDescriptor.textFontfile = inputOptions.text.fontfile;
} else {
throw is.invalidParameterError('text.textFontfile', 'string', inputOptions.text.textFontfile);
}
}
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);
}
Expand Down Expand Up @@ -504,4 +588,6 @@ module.exports = function (Sharp) {
metadata,
stats
});
// Class attributes
Sharp.align = align;
};
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -83,7 +83,8 @@
"Chris Banks <christopher.bradley.banks@gmail.com>",
"Ompal Singh <ompal.hitm09@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": {
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)",
Expand Down
55 changes: 55 additions & 0 deletions src/common.cc
Expand Up @@ -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<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
descriptor->limitInputPixels = static_cast<uint64_t>(AttrAsInt64(input, "limitInputPixels"));
// Allow switch from random to sequential access
Expand Down Expand Up @@ -394,6 +430,25 @@ 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("width", descriptor->textWidth)
->set("height", descriptor->textHeight)
->set("align", descriptor->textAlign)
->set("justify", descriptor->textJustify)
->set("dpi", descriptor->textDpi)
->set("rgba", descriptor->textRgba)
->set("spacing", descriptor->textSpacing);
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::new_memory().text(const_cast<char *>(descriptor->textValue.data()), textOptions);
image.get_image()->Type = image.guess_interpretation();
imageType = ImageType::RAW;
} else {
// From filesystem
imageType = DetermineImageType(descriptor->file.data());
Expand Down
19 changes: 18 additions & 1 deletion src/common.h
Expand Up @@ -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),
Expand All @@ -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
Expand Down
Binary file added test/fixtures/expected/text-color-pango.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/text-composite.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/text-font.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/text-justify-center.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/text-justify-centre.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/text-justify-left.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/text-justify-right.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/text-justify.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/fixtures/expected/text-spacing.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 287208e

Please sign in to comment.