├── .babelrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.dev.js ├── index.js └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.2 / 2016-03-01 2 | ================== 3 | 4 | * **textFillStyle** and **rectFillStyle** now behave properly 5 | * The default for **rectFillStyle** is now *transparent* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 4-digit year, Company or Person's Name 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-canvas-text 2 | 3 | Draws your string on a _canvas_, fit inside of a rectangle. I had to make this, because _measureText()_ from node-canvas is unpredictable, and ignores font selectors besides font-size and font-family. 4 | 5 | ## Requirements: 6 | * [node-canvas](https://github.com/Automattic/node-canvas) to draw onto 7 | * [opentype.js](https://github.com/nodebox/opentype.js/blob/master/README.md) to load OpenType fonts 8 | 9 | ## Installation 10 | 11 | ```npm install node-canvas-text canvas opentype.js --save``` 12 | 13 | ## Parameters 14 | 15 | This module exports a single function with signature: 16 | 17 | 1. context from node-canvas 18 | 2. a string to draw 19 | 3. font object 20 | 4. bounding rectangle ```{ x, y, width, height }``` 21 | 5. options ```{ minSize, maxSize, granularity, hAlign, vAlign, fitMethod, drawRect }``` 22 | 23 | ### Options 24 | 25 | * **minSize**: minimum font size ```float``` 26 | * **maxSize**: maximum font size ```float``` 27 | * **granularity**: a step, in which to scale font size ```float``` 28 | * **hAlign**: horizontal text alignment ```'left' | 'center' | 'right'``` 29 | * **vAlign**: vertical text alignment ```'top' | 'center' | 'bottom'``` 30 | * **fitMethod**: ```'baseline' | 'box'``` 31 | * **drawRect**: draw the bounding rectangle ```'true' | 'false'``` 32 | * **textFillStyle**: fill style for text ```string``` 33 | * **rectFillStyle**: fill style for rectangle ```string``` 34 | * **rectFillOnlyText**: fill only the exact resulting text rectangle, not the bounding one ```'true' | 'false'``` 35 | * **textPadding**: text padding ```float``` 36 | * **fillPadding**: fill padding ```float``` 37 | 38 | #### Defaults 39 | 40 | ```javascript 41 | { 42 | minSize: 10, 43 | maxSize: 200, 44 | granularity: 1, 45 | hAlign: 'left', 46 | vAlign: 'bottom', 47 | fitMethod: 'box', 48 | textFillStyle: '#000', 49 | rectFillStyle: '#fff', 50 | rectFillOnlyText: false, 51 | textPadding: 0, 52 | fillPadding: 0, 53 | drawRect: false 54 | } 55 | ``` 56 | 57 | ### Fit method: box vs baseline 58 | 59 | ![fitMethod: box](http://i.imgur.com/wuLdnPs.jpg) 60 | ![fitMethod: baseline](http://i.imgur.com/oxJQvYZ.jpg) 61 | 62 | ## Example 63 | ```javascript 64 | import drawText from 'node-canvas-text' 65 | import opentype from 'opentype.js' 66 | import Canvas from 'canvas' 67 | 68 | let canvas = new Canvas(imgWidth, imgHeight); 69 | let ctx = canvas.getContext('2d'); 70 | 71 | // Load OpenType fonts from files 72 | let titleFont = opentype.loadSync(__dirname + '/fonts/PTN57F.ttf'); 73 | let priceFont = opentype.loadSync(__dirname + '/fonts/PTC75F.ttf'); 74 | let barcodeFont = opentype.loadSync(__dirname + '/fonts/code128.ttf'); 75 | 76 | // Strings to draw 77 | let titleString = "A string, but not too long", 78 | priceString = "200", 79 | barcodeString = "54490000052117"; 80 | 81 | // Calculate bounding rectangles 82 | let headerRect = { 83 | x: 0, 84 | y: 0, 85 | width: canvas.width, 86 | height: canvas.height / 3.5 }; 87 | 88 | let priceRect = { 89 | x: canvas.width / 2, 90 | y: headerRect.height, 91 | width: canvas.width / 2, 92 | height: canvas.height - headerRect.height }; 93 | 94 | let barcodeRect = { 95 | x: 0, 96 | y: headerRect.height + priceRect.height / 2, 97 | width: canvas.width - priceRect.width, 98 | height: priceRect.height / 2 99 | }; 100 | 101 | // Draw 102 | let drawRect = true; 103 | 104 | drawText(ctx, titleString, titleFont, headerRect, 105 | { 106 | minSize: 5, 107 | maxSize: 100, 108 | vAlign: 'bottom', 109 | hAlign: 'left', 110 | fitMethod: 'box', 111 | drawRect: drawRect} ); 112 | 113 | drawText(ctx, priceString, priceFont, priceRect, 114 | { 115 | minSize: 5, 116 | maxSize: 200, 117 | hAlign: 'right', 118 | vAlign: 'bottom', 119 | fitMethod: 'box', 120 | drawRect: drawRect } ); 121 | 122 | drawText(ctx, barcodeString, barcodeFont, barcodeRect, 123 | { 124 | minSize: 5, 125 | maxSize: 200, 126 | hAlign: 'center', 127 | vAlign: 'center', 128 | fitMethod: 'box', 129 | drawRect: drawRect }); 130 | ``` 131 | -------------------------------------------------------------------------------- /index.dev.js: -------------------------------------------------------------------------------- 1 | var measureText = (text, font, fontSize, method = 'box') => { 2 | let ascent = 0, 3 | descent = 0, 4 | width = 0, 5 | scale = 1 / font.unitsPerEm * fontSize, 6 | glyphs = font.stringToGlyphs(text); 7 | 8 | for (var i = 0; i < glyphs.length; i++) { 9 | let glyph = glyphs[i]; 10 | if (glyph.advanceWidth) { 11 | width += glyph.advanceWidth * scale; 12 | } 13 | if (i < glyphs.length - 1) { 14 | let kerningValue = font.getKerningValue(glyph, glyphs[i + 1]); 15 | width += kerningValue * scale; 16 | } 17 | 18 | let { yMin, yMax } = glyph.getMetrics(); 19 | 20 | ascent = Math.max(ascent, yMax); 21 | descent = Math.min(descent, yMin); 22 | } 23 | 24 | return { 25 | width: width, 26 | height: method == 'box' ? Math.abs(ascent) * scale + Math.abs(descent) * scale : Math.abs(ascent) * scale, 27 | actualBoundingBoxAscent: ascent * scale, 28 | actualBoundingBoxDescent: descent * scale, 29 | fontBoundingBoxAscent: font.ascender * scale, 30 | fontBoundingBoxDescent: font.descender * scale 31 | }; 32 | }; 33 | 34 | var padRectangle = (rectangle, padding) => { 35 | return { 36 | x: rectangle.x - padding, 37 | y: rectangle.y - padding, 38 | width: rectangle.width + (padding * 2), 39 | height: rectangle.height + ( padding * 2 ) 40 | } 41 | }; 42 | 43 | export default (ctx, text, fontObject, _rectangle = {}, _options = {}) => { 44 | 45 | let paddedRect = { 46 | ...{ 47 | x: 0, 48 | y: 0, 49 | width: 100, 50 | height: 100 51 | }, ..._rectangle }; 52 | 53 | let options = { 54 | ...{ 55 | minSize: 10, 56 | maxSize: 200, 57 | granularity: 1, 58 | hAlign: 'left', 59 | vAlign: 'bottom', 60 | fitMethod: 'box', 61 | textFillStyle: '#000', 62 | rectFillStyle: 'transparent', 63 | rectFillOnlyText: false, 64 | textPadding: 0, 65 | fillPadding: 0, 66 | drawRect: false 67 | }, ..._options }; 68 | 69 | if (typeof text != 'string') throw 'Missing string parameter'; 70 | if (typeof fontObject != 'object') throw 'Missing fontObject parameter'; 71 | if (typeof ctx != 'object') throw 'Missing ctx parameter'; 72 | if (options.minSize > options.maxSize) throw 'Min font size can not be larger than max font size'; 73 | 74 | let originalRect= paddedRect; 75 | paddedRect = padRectangle(paddedRect, options.textPadding); 76 | 77 | ctx.save(); 78 | 79 | let fontSize = options.maxSize; 80 | let textMetrics = measureText(text, fontObject, fontSize, options.fitMethod); 81 | let textWidth = textMetrics.width; 82 | let textHeight = textMetrics.height; 83 | 84 | while((textWidth > paddedRect.width || textHeight > paddedRect.height) && fontSize >= options.minSize) { 85 | fontSize = fontSize - options.granularity; 86 | textMetrics = measureText(text, fontObject, fontSize, options.fitMethod); 87 | textWidth = textMetrics.width; 88 | textHeight = textMetrics.height; 89 | } 90 | 91 | // Calculate text coordinates based on options 92 | let xPos = paddedRect.x; 93 | let yPos = options.fitMethod == 'box' 94 | ? paddedRect.y + paddedRect.height - Math.abs(textMetrics.actualBoundingBoxDescent) 95 | : paddedRect.y + paddedRect.height; 96 | 97 | switch(options.hAlign) { 98 | case 'right': 99 | xPos = xPos + paddedRect.width - textWidth; 100 | break; 101 | case 'center': case 'middle': 102 | xPos = xPos + (paddedRect.width / 2) - (textWidth / 2); 103 | break; 104 | case 'left': 105 | break; 106 | default: 107 | throw "Invalid options.hAlign parameter: " + options.hAlign; 108 | break; 109 | } 110 | 111 | switch(options.vAlign) { 112 | case 'top': 113 | yPos = yPos - paddedRect.height + textHeight; 114 | break; 115 | case 'center': case 'middle': 116 | yPos = yPos + textHeight / 2 - paddedRect.height / 2; 117 | break; 118 | case 'bottom': case 'baseline': 119 | break; 120 | default: 121 | throw "Invalid options.vAlign parameter: " + options.vAlign; 122 | break; 123 | 124 | } 125 | 126 | ctx.fillStyle = 'transparent'; 127 | 128 | // Draw fill rectangle if needed 129 | if(options.rectFillStyle != 'transparent') { 130 | let fillRect = options.rectFillOnlyText ? { 131 | x: xPos, 132 | y: yPos - textHeight, 133 | width: textWidth, 134 | height: textHeight 135 | } : originalRect; 136 | 137 | fillRect = padRectangle(fillRect, options.fillPadding); 138 | 139 | ctx.fillStyle = options.rectFillStyle; 140 | ctx.fillRect(fillRect.x, fillRect.y, fillRect.width, fillRect.height); 141 | ctx.fillStyle = 'transparent'; 142 | } 143 | 144 | // Draw text 145 | let fontPath = fontObject.getPath(text, xPos, yPos, fontSize); 146 | fontPath.fill = options.textFillStyle; 147 | fontPath.draw(ctx); 148 | 149 | // Draw bounding rectangle 150 | if(options.drawRect) { 151 | // TODO: Figure out how to not stroke the text itself, just the rectangle 152 | ctx.save(); 153 | ctx.strokeStyle = 'red'; 154 | ctx.rect(paddedRect.x, paddedRect.y, paddedRect.width, paddedRect.height); 155 | ctx.stroke(); 156 | ctx.strokeStyle = 'transparent'; 157 | ctx.restore(); 158 | } 159 | 160 | ctx.restore(); 161 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 8 | 9 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 10 | 11 | var measureText = function measureText(text, font, fontSize) { 12 | var method = arguments.length <= 3 || arguments[3] === undefined ? 'box' : arguments[3]; 13 | 14 | var ascent = 0, 15 | descent = 0, 16 | width = 0, 17 | scale = 1 / font.unitsPerEm * fontSize, 18 | glyphs = font.stringToGlyphs(text); 19 | 20 | for (var i = 0; i < glyphs.length; i++) { 21 | var glyph = glyphs[i]; 22 | if (glyph.advanceWidth) { 23 | width += glyph.advanceWidth * scale; 24 | } 25 | if (i < glyphs.length - 1) { 26 | var kerningValue = font.getKerningValue(glyph, glyphs[i + 1]); 27 | width += kerningValue * scale; 28 | } 29 | 30 | var _glyph$getMetrics = glyph.getMetrics(); 31 | 32 | var yMin = _glyph$getMetrics.yMin; 33 | var yMax = _glyph$getMetrics.yMax; 34 | 35 | 36 | ascent = Math.max(ascent, yMax); 37 | descent = Math.min(descent, yMin); 38 | } 39 | 40 | return { 41 | width: width, 42 | height: method == 'box' ? Math.abs(ascent) * scale + Math.abs(descent) * scale : Math.abs(ascent) * scale, 43 | actualBoundingBoxAscent: ascent * scale, 44 | actualBoundingBoxDescent: descent * scale, 45 | fontBoundingBoxAscent: font.ascender * scale, 46 | fontBoundingBoxDescent: font.descender * scale 47 | }; 48 | }; 49 | 50 | var padRectangle = function padRectangle(rectangle, padding) { 51 | return { 52 | x: rectangle.x - padding, 53 | y: rectangle.y - padding, 54 | width: rectangle.width + padding * 2, 55 | height: rectangle.height + padding * 2 56 | }; 57 | }; 58 | 59 | exports.default = function (ctx, text, fontObject) { 60 | var _rectangle = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3]; 61 | 62 | var _options = arguments.length <= 4 || arguments[4] === undefined ? {} : arguments[4]; 63 | 64 | var paddedRect = _extends({ 65 | x: 0, 66 | y: 0, 67 | width: 100, 68 | height: 100 69 | }, _rectangle); 70 | 71 | var options = _extends({ 72 | minSize: 10, 73 | maxSize: 200, 74 | granularity: 1, 75 | hAlign: 'left', 76 | vAlign: 'bottom', 77 | fitMethod: 'box', 78 | textFillStyle: '#000', 79 | rectFillStyle: 'transparent', 80 | rectFillOnlyText: false, 81 | textPadding: 0, 82 | fillPadding: 0, 83 | drawRect: false 84 | }, _options); 85 | 86 | if (typeof text != 'string') throw 'Missing string parameter'; 87 | if ((typeof fontObject === 'undefined' ? 'undefined' : _typeof(fontObject)) != 'object') throw 'Missing fontObject parameter'; 88 | if ((typeof ctx === 'undefined' ? 'undefined' : _typeof(ctx)) != 'object') throw 'Missing ctx parameter'; 89 | if (options.minSize > options.maxSize) throw 'Min font size can not be larger than max font size'; 90 | 91 | var originalRect = paddedRect; 92 | paddedRect = padRectangle(paddedRect, options.textPadding); 93 | 94 | ctx.save(); 95 | 96 | var fontSize = options.maxSize; 97 | var textMetrics = measureText(text, fontObject, fontSize, options.fitMethod); 98 | var textWidth = textMetrics.width; 99 | var textHeight = textMetrics.height; 100 | 101 | while ((textWidth > paddedRect.width || textHeight > paddedRect.height) && fontSize >= options.minSize) { 102 | fontSize = fontSize - options.granularity; 103 | textMetrics = measureText(text, fontObject, fontSize, options.fitMethod); 104 | textWidth = textMetrics.width; 105 | textHeight = textMetrics.height; 106 | } 107 | 108 | // Calculate text coordinates based on options 109 | var xPos = paddedRect.x; 110 | var yPos = options.fitMethod == 'box' ? paddedRect.y + paddedRect.height - Math.abs(textMetrics.actualBoundingBoxDescent) : paddedRect.y + paddedRect.height; 111 | 112 | switch (options.hAlign) { 113 | case 'right': 114 | xPos = xPos + paddedRect.width - textWidth; 115 | break; 116 | case 'center':case 'middle': 117 | xPos = xPos + paddedRect.width / 2 - textWidth / 2; 118 | break; 119 | case 'left': 120 | break; 121 | default: 122 | throw "Invalid options.hAlign parameter: " + options.hAlign; 123 | break; 124 | } 125 | 126 | switch (options.vAlign) { 127 | case 'top': 128 | yPos = yPos - paddedRect.height + textHeight; 129 | break; 130 | case 'center':case 'middle': 131 | yPos = yPos + textHeight / 2 - paddedRect.height / 2; 132 | break; 133 | case 'bottom':case 'baseline': 134 | break; 135 | default: 136 | throw "Invalid options.vAlign parameter: " + options.vAlign; 137 | break; 138 | 139 | } 140 | 141 | ctx.fillStyle = 'transparent'; 142 | 143 | // Draw fill rectangle if needed 144 | if (options.rectFillStyle != 'transparent') { 145 | var fillRect = options.rectFillOnlyText ? { 146 | x: xPos, 147 | y: yPos - textHeight, 148 | width: textWidth, 149 | height: textHeight 150 | } : originalRect; 151 | 152 | fillRect = padRectangle(fillRect, options.fillPadding); 153 | 154 | ctx.fillStyle = options.rectFillStyle; 155 | ctx.fillRect(fillRect.x, fillRect.y, fillRect.width, fillRect.height); 156 | ctx.fillStyle = 'transparent'; 157 | } 158 | 159 | // Draw text 160 | var fontPath = fontObject.getPath(text, xPos, yPos, fontSize); 161 | fontPath.fill = options.textFillStyle; 162 | fontPath.draw(ctx); 163 | 164 | // Draw bounding rectangle 165 | if (options.drawRect) { 166 | // TODO: Figure out how to not stroke the text itself, just the rectangle 167 | ctx.save(); 168 | ctx.strokeStyle = 'red'; 169 | ctx.rect(paddedRect.x, paddedRect.y, paddedRect.width, paddedRect.height); 170 | ctx.stroke(); 171 | ctx.strokeStyle = 'transparent'; 172 | ctx.restore(); 173 | } 174 | 175 | ctx.restore(); 176 | }; 177 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-canvas-text", 3 | "version": "1.0.2", 4 | "description": "Using a provided Opentype.js font object, draws a string, sized to fit inside a given rectangle on an HTML5 canvas", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/kaivi/node-canvas-text.git" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "prepublish": "babel index.dev.js --out-file index.js", 13 | "watch": "babel index.dev.js --out-file index.js --watch" 14 | }, 15 | "keywords": [ 16 | "canvas", 17 | "text", 18 | "fit", 19 | "squeeze", 20 | "rectangle", 21 | "canvas2d" 22 | ], 23 | "author": "Kai Vik", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "babel-cli": "^6.5.1", 27 | "babel-preset-es2015": "^6.5.0", 28 | "babel-preset-stage-0": "^6.5.0" 29 | } 30 | } 31 | --------------------------------------------------------------------------------