Get exact size of text on a canvas in JavaScript
Ok, so I just got it working.
What am I doing?
Well, in my case I know, that the text will always start at a x-value of 0.
The length of the text is therefore the non-transparent pixel with the highest x-value in the array given by getImageData()
.
So I am looping through the getImageData()
-array. If I find a pixel that has a higher alpha-value than 0, I will save its x and y value into highestPixel
. The next time I find a pixel, I will check if its x-value is higher as the one that is currently in highestPixel
. If so, I will overwrite highestPixel
with the new values. At the end, I return highestPixel
and its x-value will be the exact length of the text.
Here is the code:
// a function to draw the text on the canvas
let text = "Hello World";
let canvas = document.getElementById('happy-canvas');
let width = 1000
let height = 100
canvas.width = width
canvas.height = height
let ctx = canvas.getContext('2d');
ctx.save();
ctx.font = "30px cursive";
ctx.clearRect(0, 0, width, height);
ctx.fillText(text, 0, 60);
// get the image data
let data = ctx.getImageData(0, 0, width, height).data,
first = false,
last = false,
r = height,
c = 0
// get the width of the text and convert it to an integer
let getPixelwithHighestX = () => {
let xOfPixel = 0
let yOfPixel = 0
let highestPixel = {
x: 0,
y: 0
}
for (let i = 3; i < data.length; i += 4) {
if (data[i] !== 0) {
yOfPixel = Math.floor(i / 4 / width)
xOfPixel = Math.floor(i / 4) - yOfPixel * width
if (xOfPixel > highestPixel.x) {
highestPixel.x = xOfPixel
highestPixel.y = yOfPixel
}
}
}
return highestPixel
}
let hightestPixel = getPixelwithHighestX()
//Find the last line with a non-transparent pixel
while (!last && r) {
r--
for (c = 0; c < width; c++) {
if (data[r * width * 4 + c * 4 + 3]) {
last = r
break
}
}
}
let canvasHeight = 0
// Find the first line with a non-transparent pixel
while (r) {
r--
for (c = 0; c < width; c++) {
if (data[r * width * 4 + c * 4 + 3]) {
first = r
break
}
}
canvasHeight = last - first
}
//draw a rectangle around the text
ctx.strokeRect(0, first, hightestPixel.x, canvasHeight)
<div> The text is now completely inside the box
<canvas id="happy-canvas" width="150" height="150"> I wonder what is here</canvas>
</div>
How to fit text to a precise width on html canvas?
Measuring text width
Measuring text is problematic on many levels.
The full and experimental textMetric
has been defined for many years yet is available only on 1 main stream browser (Safari), hidden behind flags (Chrome), covered up due to bugs (Firefox), status unknown (Edge, IE).
Using width
only
At best you can use the width
property of the object returned by ctx.measureText
to estimate the width. This width is greater or equal to the actual pixel width (left to right most). Note web fonts must be fully loaded or the width may be that of the placeholder font.
Brute force
The only method that seams to work reliably is unfortunately a brute force technique that renders the font to a temp / or work canvas and calculates the extent by querying the pixels.
This will work across all browsers that support the canvas.
It is not suitable for real-time animations and applications.
The following function
Will return an object with the following properties
width
width in canvas pixels of textleft
distance from left of first pixel in canvas pixelsright
distance from left to last detected pixel in canvas pixelsrightOffset
distance in canvas pixel from measured text width and detected right edgemeasuredWidth
the measured width as returned byctx.measureText
baseSize
the font size in pixelsfont
the font used to measure the text
It will return
undefined
if width is zero or the string contains no visible text.
You can then use the fixed size font and 2D transform to scale the text to fit the desired width. This will work for very small fonts resulting in higher quality font rendering at smaller sizes.
The accuracy is dependent on the size of the font being measure. The function uses a fixed font size of 120px
you can set the base size by passing the property
The function can use partial text (Short cut) to reduce RAM and processing overheads. The property rightOffset
is the distance in pixels from the right ctx.measureText
edge to the first pixel with content.
Thus you can measure the text "CB"
and use that measure to accurately align any text starting with "C"
and ending with "B"
Example if using short cut text
const txtSize = measureText({font: "arial", text: "BB"});
ctx.font = txtSize.font;
const width = ctx.measureText("BabcdefghB").width;
const actualWidth = width - txtSize.left - txtSize.rightOffset;
const scale = canvas.width / actualWidth;
ctx.setTransform(scale, 0, 0, scale, -txtSize.left * scale, 0);
ctx.fillText("BabcdefghB",0,0);
measureText
function
const measureText = (() => {
var data, w, size = 120; // for higher accuracy increase this size in pixels.
const isColumnEmpty = x => {
var idx = x, h = size * 2;
while (h--) {
if (data[idx]) { return false }
idx += can.width;
}
return true;
}
const can = document.createElement("canvas");
const ctx = can.getContext("2d");
return ({text, font, baseSize = size}) => {
size = baseSize;
can.height = size * 2;
font = size + "px "+ font;
if (text.trim() === "") { return }
ctx.font = font;
can.width = (w = ctx.measureText(text).width) + 8;
ctx.font = font;
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.fillText(text, 0, size);
data = new Uint32Array(ctx.getImageData(0, 0, can.width, can.height).data.buffer);
var left, right;
var lIdx = 0, rIdx = can.width - 1;
while(lIdx < rIdx) {
if (left === undefined && !isColumnEmpty(lIdx)) { left = lIdx }
if (right === undefined && !isColumnEmpty(rIdx)) { right = rIdx }
if (right !== undefined && left !== undefined) { break }
lIdx += 1;
rIdx -= 1;
}
data = undefined; // release RAM held
can.width = 1; // release RAM held
return right - left >= 1 ? {
left, right, rightOffset: w - right, width: right - left,
measuredWidth: w, font, baseSize} : undefined;
}
})();
Usage example
The example use the function above and short cuts the measurement by supplying only the first and last non white space character.
Enter text into the text input.
- If the text is too large to fit the canvas the console will display a warning.
- If the text scale is greater than 1 (meaning the displayed font is larger than the measured font) the console will show a warning as there may be some loss of alignment precision.
inText.addEventListener("input", updateCanvasText);const ctx = canvas.getContext("2d");canvas.height = canvas.width = 500;
function updateCanvasText() { const text = inText.value.trim(); const shortText = text[0] + text[text.length - 1]; const txtSize = measureText({font: "arial", text: text.length > 1 ? shortText: text}); if(txtSize) { ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height) ctx.font = txtSize.font; const width = ctx.measureText(text).width; const actualWidth = width - txtSize.left - txtSize.rightOffset; const scale = (canvas.width - 20) / actualWidth; console.clear(); if(txtSize.baseSize * scale > canvas.height) { console.log("Font scale too large to fit vertically"); } else if(scale > 1) { console.log("Scaled > 1, can result in loss of precision "); } ctx.textBaseline = "top"; ctx.fillStyle = "#000"; ctx.textAlign = "left"; ctx.setTransform(scale, 0, 0, scale, 10 - txtSize.left * scale, 0); ctx.fillText(text,0,0); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.fillStyle = "#CCC8"; ctx.fillRect(0, 0, 10, canvas.height); ctx.fillRect(canvas.width - 10, 0, 10, canvas.height); } else { console.clear(); console.log("Empty string ignored"); }}const measureText = (() => { var data, w, size = 120; const isColumnEmpty = x => { var idx = x, h = size * 2; while (h--) { if (data[idx]) { return false } idx += can.width; } return true; } const can = document.createElement("canvas"); const ctx = can.getContext("2d"); return ({text, font, baseSize = size}) => { size = baseSize; can.height = size * 2; font = size + "px "+ font; if (text.trim() === "") { return } ctx.font = font; can.width = (w = ctx.measureText(text).width) + 8; ctx.font = font; ctx.textBaseline = "middle"; ctx.textAlign = "left"; ctx.fillText(text, 0, size); data = new Uint32Array(ctx.getImageData(0, 0, can.width, can.height).data.buffer); var left, right; var lIdx = 0, rIdx = can.width - 1; while(lIdx < rIdx) { if (left === undefined && !isColumnEmpty(lIdx)) { left = lIdx } if (right === undefined && !isColumnEmpty(rIdx)) { right = rIdx } if (right !== undefined && left !== undefined) { break } lIdx += 1; rIdx -= 1; } data = undefined; // release RAM held can.width = 1; // release RAM held return right - left >= 1 ? {left, right, rightOffset: w - right, width: right - left, measuredWidth: w, font, baseSize} : undefined; } })();
body { font-family: arial;}canvas { border: 1px solid black; width: 500px; height: 500px; }
<label for="inText">Enter text </label><input type="text" id="inText" placeholder="Enter text..."/><canvas id="canvas"></canvas>
Javascript - get actual rendered font height
I am not aware of any method that would return the height
of a text such as measureText (which does currently return the width
).
However, in theory you can simply draw your text in the canvas then trim the surrounding transparent pixels then measure the canvas height..
Here is an example (the height will be logged in the console):
// Create a blank canvas (by not filling a background color).var canvas = document.getElementById('canvas');var ctx = canvas.getContext('2d');
// Fill it with some coloured text.. (black is default)ctx.font = "48px serif";ctx.textBaseline = "hanging";ctx.fillText("Hello world", 0, 0);
// Remove the surrounding transparent pixels// result is an actual canvas elementvar result = trim(canvas);
// you could query it's width, draw it, etc..document.body.appendChild(result);
// get the height of the trimmed areaconsole.log(result.height);
// Trim Canvas Pixels Method// https://gist.github.com/remy/784508function trim(c) {
var ctx = c.getContext('2d'),
// create a temporary canvas in which we will draw back the trimmed text copy = document.createElement('canvas').getContext('2d'),
// Use the Canvas Image Data API, in order to get all the // underlying pixels data of that canvas. This will basically // return an array (Uint8ClampedArray) containing the data in the // RGBA order. Every 4 items represent one pixel. pixels = ctx.getImageData(0, 0, c.width, c.height),
// total pixels l = pixels.data.length, // main loop counter and pixels coordinates i, x, y,
// an object that will store the area that isn't transparent bound = { top: null, left: null, right: null, bottom: null };
// for every pixel in there for (i = 0; i < l; i += 4) {
// if the alpha value isn't ZERO (transparent pixel) if (pixels.data[i+3] !== 0) {
// find it's coordinates x = (i / 4) % c.width; y = ~~((i / 4) / c.width); // store/update those coordinates // inside our bounding box Object
if (bound.top === null) { bound.top = y; } if (bound.left === null) { bound.left = x; } else if (x < bound.left) { bound.left = x; } if (bound.right === null) { bound.right = x; } else if (bound.right < x) { bound.right = x; } if (bound.bottom === null) { bound.bottom = y; } else if (bound.bottom < y) { bound.bottom = y; } } } // actual height and width of the text // (the zone that is actually filled with pixels) var trimHeight = bound.bottom - bound.top, trimWidth = bound.right - bound.left,
// get the zone (trimWidth x trimHeight) as an ImageData // (Uint8ClampedArray of pixels) from our canvas trimmed = ctx.getImageData(bound.left, bound.top, trimWidth, trimHeight); // Draw back the ImageData into the canvas copy.canvas.width = trimWidth; copy.canvas.height = trimHeight; copy.putImageData(trimmed, 0, 0);
// return the canvas element return copy.canvas;}
<canvas id="canvas"></canvas>
Determine width of string in HTML5 canvas
The fillText()
method has an optional fourth argument, which is the max width to render the string.
MDN's documentation says...
maxWidth
Optional; the maximum width to draw. If specified, and the string is
computed to be wider than this width, the font is adjusted to use a
more horizontally condensed font (if one is available or if a
reasonably readable one can be synthesized by scaling the current font
horizontally) or a smaller font.
However, at the time of writing, this argument isn't supported well cross browser, which leads to the second solution using measureText()
to determine the dimensions of a string without rendering it.
var width = ctx.measureText(text).width;
Here is how I may do it...
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d');
// Font sizes must be in descending order. You may need to use `sort()`
// if you can't guarantee this, e.g. user input.
var fontSizes = [72, 36, 28, 14, 12, 10, 5, 2],
text = 'Measure me!';
// Default styles.
ctx.textBaseline = 'top';
ctx.fillStyle = 'blue';
var textDimensions,
i = 0;
do {
ctx.font = fontSizes[i++] + 'px Arial';
textDimensions = ctx.measureText(text);
} while (textDimensions.width >= canvas.width);
ctx.fillText(text, (canvas.width - textDimensions.width) / 2, 10);
jsFiddle.
I have a list of font sizes in descending order and I iterate through the list, determining if the rendered text will fit within the canvas dimensions.
If it will, I render the text center aligned. If you must have padding on the left and right of the text (which will look nicer), add the padding value to the textDimensions.width
when calculating if the text will fit.
If you have a long list of font sizes to try, you'd be better off using a binary search algorithm. This will increase the complexity of your code, however.
For example, if you have 200 font sizes, the linear O(n) iteration through the array elements could be quite slow.
The binary chop should be O(log n).
Here is the guts of the function.
var textWidth = (function me(fontSizes, min, max) {
var index = Math.floor((min + max) / 2);
ctx.font = fontSizes[index] + 'px Arial';
var textWidth = ctx.measureText(text).width;
if (min > max) {
return textWidth;
}
if (textWidth > canvas.width) {
return me(fontSizes, min, index - 1);
} else {
return me(fontSizes, index + 1, max);
}
})(fontSizes, 0, fontSizes.length - 1);
jsFiddle.
Measuring text width/height without rendering
Please check this. is a solution using canvas
function get_tex_width(txt, font) {
this.element = document.createElement('canvas');
this.context = this.element.getContext("2d");
this.context.font = font;
return this.context.measureText(txt).width;
}
alert('Calculated width ' + get_tex_width("Hello World", "30px Arial"));
alert("Span text width "+$("span").width());
Demo using
EDIT
The solution using canvas is not the best, each browser deal different canvas size.
Here is a nice solution to get size of text using a temporary element.
Demo
EDIT
The canvas spec doesn't give us a method for measuring the height of a string, so for this we can use parseInt(context.font)
.
TO get width and height. This trick work only with px size.
function get_tex_size(txt, font) {
this.element = document.createElement('canvas');
this.context = this.element.getContext("2d");
this.context.font = font;
var tsize = {'width':this.context.measureText(txt).width, 'height':parseInt(this.context.font)};
return tsize;
}
var tsize = get_tex_size("Hello World", "30px Arial");
alert('Calculated width ' + tsize['width'] + '; Calculated height ' + tsize['height']);
Related Topics
How to Profile JavaScript Execution
How to Parse a Time into a Date Object from User Input in JavaScript
Set Additional Data to Highcharts Series
How to Capitalize First Letter of Each Word, Like a 2-Word City
Jquery Selector for Id Starts with Specific Text
Pass Parameters in Setinterval Function
Difference Between "Change" and "Input" Event for an 'Input' Element
How to Change the Content of a <Textarea> with JavaScript
Angularjs - How to Use $Routeparams in Generating the Templateurl
Cloud Firestore Case Insensitive Sorting Using Query
Get All Attributes of an Element Using Jquery
Pass Data or Modify Extension HTML in a New Tab/Window
How to Extract the Hostname Portion of a Url in JavaScript
Angularjs - Any Way for $Http.Post to Send Request Parameters Instead of JSON