SVG get text element width

JavascriptDomTextSvg

Javascript Problem Overview


I'm working on some ECMAScript/JavaScript for an SVG file and need to get the width and height of a text element so I can resize a rectangle that surrounds it. In HTML I would be able to use the offsetWidth and offsetHeight attributes on the element but it appears that those properties are unavailable.

Here's a fragment that I need to work with. I need to change the width of the rectangle whenever I change the text but I don't know how to get the actual width (in pixels) of the text element.

<rect x="100" y="100" width="100" height="100" />
<text>Some Text</text>

Any ideas?

Javascript Solutions


Solution 1 - Javascript

var bbox = textElement.getBBox();
var width = bbox.width;
var height = bbox.height;

and then set the rect's attributes accordingly.

Link: getBBox() in the SVG v1.1 standard.

Solution 2 - Javascript

document.getElementById('yourTextId').getComputedTextLength();

worked for me in

Solution 3 - Javascript

Regarding the length of text the link seems to indicate BBox and getComputedTextLength() may return slightly different values, but ones that are fairly close to each other.

http://bl.ocks.org/MSCAU/58bba77cdcae42fc2f44

Solution 4 - Javascript

How about something like this for compatibility:

function svgElemWidth(elem) {
    var methods = [ // name of function and how to process its result
        { fn: 'getBBox', w: function(x) { return x.width; }, },
        { fn: 'getBoundingClientRect', w: function(x) { return x.width; }, },
        { fn: 'getComputedTextLength', w: function(x) { return x; }, }, // text elements only
    ];
    var widths = [];
    var width, i, method;
    for (i = 0; i < methods.length; i++) {
        method = methods[i];
        if (typeof elem[method.fn] === 'function') {
            width = method.w(elem[method.fn]());
            if (width !== 0) {
                widths.push(width);
            }
        }
    }
    var result;
    if (widths.length) {
        result = 0;
        for (i = 0; i < widths.length; i++) {
            result += widths[i];
        }
        result /= widths.length;
    }
    return result;
}

This returns the average of any valid results of the three methods. You could improve it to cast out outliers or to favor getComputedTextLength if the element is a text element.

Warning: As the comment says, getBoundingClientRect is tricky. Either remove it from the methods or use this only on elements where getBoundingClientRect will return good results, so no rotation and probably no scaling(?)

Solution 5 - Javascript

Not sure why, but none of the above methods work for me. I had some success with the canvas method, but I had to apply all kinds of scale factors. Even with the scale factors I still had inconsistent results between Safari, Chrome, and Firefox.

So, I tried the following:

            var div = document.createElement('div');
            div.style.position = 'absolute';
            div.style.visibility = 'hidden';
            div.style.height = 'auto';
            div.style.width = 'auto';
            div.style.whiteSpace = 'nowrap';
            div.style.fontFamily = 'YOUR_FONT_GOES_HERE';
            div.style.fontSize = '100';
            div.style.border = "1px solid blue"; // for convenience when visible

            div.innerHTML = "YOUR STRING";
            document.body.appendChild(div);
            
            var offsetWidth = div.offsetWidth;
            var clientWidth = div.clientWidth;
            
            document.body.removeChild(div);
            
            return clientWidth;

Worked awesome and super precise, but only in Firefox. Scale factors to the rescue for Chrome and Safari, but no joy. Turns out that Safari and Chrome errors are not linear with either string length or font size.

So, approach number two. I don't much care for the brute force approach, but after struggling with this on and off for years I decided to give it a try. I decided to generate constant values for each individual printable character. Normally this would be kind of tedious, but luckily Firefox happens to be super accurate. Here is my two part brute force solution:

<body>
        <script>
            
            var div = document.createElement('div');
            div.style.position = 'absolute';
            div.style.height = 'auto';
            div.style.width = 'auto';
            div.style.whiteSpace = 'nowrap';
            div.style.fontFamily = 'YOUR_FONT';
            div.style.fontSize = '100';          // large enough for good resolution
            div.style.border = "1px solid blue"; // for visible convenience
            
            var character = "";
            var string = "array = [";
            for(var i=0; i<127; i++) {
                character = String.fromCharCode(i);
                div.innerHTML = character;
                document.body.appendChild(div);
                
                var offsetWidth = div.offsetWidth;
                var clientWidth = div.clientWidth;
                console.log("ASCII: " + i + ", " + character + ", client width: " + div.clientWidth);
                
                string = string + div.clientWidth;
                if(i<126) {
                    string = string + ", ";
                }

                document.body.removeChild(div);
                
            }
        
            var space_string = "! !";
            div.innerHTML = space_string;
            document.body.appendChild(div);
            var space_string_width = div.clientWidth;
            document.body.removeChild(div);
            var no_space_string = "!!";
            div.innerHTML = no_space_string;
            document.body.appendChild(div);
            var no_space_string_width = div.clientWidth;
            console.log("space width: " + (space_string_width - no_space_string_width));
            document.body.removeChild(div);


            string = string + "]";
            div.innerHTML = string;
            document.body.appendChild(div);
            </script>
    </body>

Note: The above snippet has to executed in Firefox to generate an accurate array of values. Also, you do have to replace array item 32 with the space width value in the console log.

I simply copy the Firefox on screen text, and paste it into my javascript code. Now that I have the array of printable character lengths, I can implement a get width function. Here is the code:

const LCARS_CHAR_SIZE_ARRAY = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 26, 46, 63, 42, 105, 45, 20, 25, 25, 47, 39, 21, 34, 26, 36, 36, 28, 36, 36, 36, 36, 36, 36, 36, 36, 27, 27, 36, 35, 36, 35, 65, 42, 43, 42, 44, 35, 34, 43, 46, 25, 39, 40, 31, 59, 47, 43, 41, 43, 44, 39, 28, 44, 43, 65, 37, 39, 34, 37, 42, 37, 50, 37, 32, 43, 43, 39, 43, 40, 30, 42, 45, 23, 25, 39, 23, 67, 45, 41, 43, 42, 30, 40, 28, 45, 33, 52, 33, 36, 31, 39, 26, 39, 55];


    static getTextWidth3(text, fontSize) {
        let width = 0;
        let scaleFactor = fontSize/100;
        
        for(let i=0; i<text.length; i++) {
            width = width + LCARS_CHAR_SIZE_ARRAY[text.charCodeAt(i)];
        }
        
        return width * scaleFactor;
    }

Well, that is it. Brute force, but it is super accurate in all three browsers, and my frustration level has gone to zero. Not sure how long it will last as the browsers evolve, but it should be long enough for me to develop a robust font metrics technique for my SVG text.

Solution 6 - Javascript

SVG spec has a specific method to return this info: getComputedTextLength()

var width = textElement.getComputedTextLength(); // returns a pixel number

Solution 7 - Javascript

If you are using NodeJS and want to keep your application light (e.g. not use a headless browser). A solution that can be used, is to pre-calculate the font size.

  1. Using: https://github.com/nicktaras/getFontCharWidth you can determine the width of each character within a font.

  2. Then within a node js app for example, you can iterate through each character to calculate the actual width.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionStephen SorensenView Question on Stackoverflow
Solution 1 - JavascriptNickFitzView Answer on Stackoverflow
Solution 2 - JavascriptZombiView Answer on Stackoverflow
Solution 3 - Javascriptandrew pateView Answer on Stackoverflow
Solution 4 - JavascriptHostedMetrics.comView Answer on Stackoverflow
Solution 5 - Javascriptagent-pView Answer on Stackoverflow
Solution 6 - JavascriptcuixipingView Answer on Stackoverflow
Solution 7 - JavascriptNick TarasView Answer on Stackoverflow