How to Find the Height of Text on an HTML Canvas

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 text
    • left distance from left of first pixel in canvas pixels
    • right distance from left to last detected pixel in canvas pixels
    • rightOffset distance in canvas pixel from measured text width and detected right edge
    • measuredWidth the measured width as returned by ctx.measureText
    • baseSize the font size in pixels
    • font 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



Leave a reply



Submit