124

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?

Eduard Luca
  • 6,514
  • 16
  • 85
  • 137
Stephen Sorensen
  • 11,455
  • 13
  • 33
  • 46

7 Answers7

182
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.

T. Cowart
  • 65
  • 5
NickFitz
  • 34,537
  • 8
  • 43
  • 40
  • 4
    Thanks. I also found another function that could have helped with width. textElement.getComputedTextLength(). I'll try out both tonight and see which works better for me. – Stephen Sorensen Oct 28 '09 at 12:58
  • 5
    I was playing with these last week; the getComputedTextLength() method returned the same result as getBBox().width for the things I tried it on, so I just went with the bounding box, as I needed both width and height. I'm not sure if there are any circumstances when the text length would be different from the width: I think perhaps that method is provided to help with text layout such as splitting text over multiple lines, whereas the bounding box is a generic method available on all (?) elements. – NickFitz Oct 28 '09 at 13:45
  • 2
    Problem is, if the text follows a path, then the width isn't the actual width of the text (for example if the text follows a circle path then the bbox is the one that encloses the circle, and getting the width of that isn't the width of the text prior to it going around the circle). Another thing is that bbox.width is greater by a factor of 2, so if element inspector shows a width of 200, then bbox.width is 400. I'm not sure why. – trusktr Nov 12 '16 at 05:10
  • 1
    `getBBox()` is not supported by IE11. (Yes still checking that.) – j4v1 Oct 07 '20 at 13:11
  • 4
    None of these methods appear to work unless the text is already appended to the DOM. – a2f0 Mar 28 '21 at 16:40
16
document.getElementById('yourTextId').getComputedTextLength();

worked for me in

Zombi
  • 1,050
  • 9
  • 8
  • 1
    Your solution returns the actual length of the text element which is the correct answer. Some answers use the getBBox() which can be correct if the text is not rotated. – Netsi1964 May 03 '20 at 10:03
8

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

andrew pate
  • 3,833
  • 36
  • 28
  • Thank you for that link!! I had to go through all the scenarios by myself, but this is a really good summary... My problem was using getBBox() on a Tspan element on firefox... It couldn't be a more specific and annoying issue... thanks again! – Andres Elizondo Oct 18 '17 at 10:11
2

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(?)

HostedMetrics.com
  • 3,525
  • 3
  • 26
  • 31
2

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.

agent-p
  • 164
  • 1
  • 5
  • Best solution so far! Thanks for sharing! – just_user Oct 23 '18 at 08:52
  • 2
    This doesn't handle kerning – pat Nov 26 '19 at 15:59
  • True, but doesn’t matter for the fonts I use. I am going to revisit this next year when I get some time. I have some ideas about making text metrics more accurate for my cases that don’t use a brute force approach. – agent-p Nov 27 '19 at 17:11
  • Great. Allows me to align SVG chart labels statically rather than having to align them on the fly in local script. That makes life a lot simpler when serving up charts in Ruby from RoR. Thanks! – Lex Lindsey Mar 31 '20 at 23:33
1

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

var width = textElement.getComputedTextLength(); // returns a pixel number
cuixiping
  • 24,167
  • 8
  • 82
  • 93
0

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.

Nick Taras
  • 696
  • 8
  • 15