mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 18:40:16 +02:00
Expose vips_text to create an image containing rendered text (#3252)
This commit is contained in:
parent
76c4c51e2a
commit
ea7cf2a2ef
@ -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.
|
||||||
|
@ -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]**
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
89
lib/input.js
89
lib/input.js
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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)",
|
||||||
|
@ -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());
|
||||||
|
21
src/common.h
21
src/common.h
@ -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
|
||||||
|
@ -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
297
test/unit/text.js
Normal 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user