Reputation: 1091
I've developed a few GUI-oriented applications that implement their own text line-breaking algorithms. For example, consider that my applications consist of typical GUI "widgets" that can be laid out on a screen. Widgets such as checkboxes, textfields, simple labels, etc, are fairly easy to draw. However, a widget such as a "paragraph" (an arbitrary amount of multiline text, which should be fit into a specified box, with line-breaking occurring as necessary) is much more difficult owing to the, well, line-breaking part.
Every time I've implemented such an algorithm, I've used an approach that's worked but has been pretty inefficient. My general approach (to fit a string into a box with width w) has been to iteratively take a string s, use font metrics to measure its pixel length l, and whittle away at it until l <= w. Then the remainder is assigned to s, and I repeat the process until I'm left with a value of s that's less than or equal to w.
At the bottom of this is a Javascript example (which admittedly probably isn't the best environment in which to be doing this sort of thing). This code would be part of the aforementioned "paragraph" widget, and is written for the HTML5 Canvas API (ctx is the Canvas' graphics context). Clearly, the Big-O analysis of this approach is pretty poor. But... is there a better way to do this sort of thing? I'm assuming it depends somewhat on the environment in which we're working. But I also assume that given the number of text-editing tools that exist, an efficient solution exists out there.
// the paragraph widgets' main drawing function
this.drawText = function(ctx) {
...
var lines = this.text.split("\n"); // here we account for user-entered line breaks
var y = this.y;
for (var i=0; i<lines.length; i++) {
var currTxt = lines[i]; // a chunk of text in between user-entered line breaks
var miniLines = this.breakToLines(currTxt, this.textWidth(), ctx);
for (var j = 0; j < miniLines.length; j++) {
var miniTxt = miniLines[j];
var x = this.x + ( (this.round) ? this.cornerRadius : 0 );
x += this.textOffset();
y += this.fontSize;
ctx.save();
ctx.rect(this.x, this.y, this.width, this.height);
ctx.clip();
ctx.fillText(miniTxt, x, y);
ctx.restore();
}
};
};
// take a chunk of text and break it into lines that fit within width 'w'
this.breakToLines = function(txt, w, ctx) {
var arr = [];
while (true) {
var txt2 = this.popLine(txt, w, ctx);
if (txt2 == null)
break;
arr.push(txt2);
if (txt.length <= txt2.length)
break;
txt = txt.substring(txt2.length);
}
return arr;
};
this.popLine = function(txt, w, ctx) {
var m = ctx.measureText(txt); // 'm' represents the size of the text
if (m.length == 0)
return null; // 'm' is empty, so we're done
while (m.width > w) {
// remove a word from txt and re-measure it
txt = txt.substring(0, txt.lastIndexOf(' '));
m = ctx.measureText(txt);
}
return txt;
};
Upvotes: 0
Views: 1759
Reputation: 34839
I wonder if the text metrics give reliable results when measuring the size of a word followed by a space. For example, does width( "aaa " ) + width( "bbb" ) = width( "aaa bbb" )
? If so you can measure each word in the text, with and without a space after it, and figure the rest out from there. Plan B (assuming that text metrics for a word followed by a space doesn't give precise results) is to measure each word without the space, and use a fixed value to estimate the space between words.
The inefficiency in the current algorithm, as I see it, is that you're calling the measureText
method O(n^2) times, and you're measuring the width of long strings. By breaking the text into words and measuring each word, you would only call measureText
O(n) times, and you would be calling it on relatively short strings.
The proposed algorithm then is to start at the beginning of each line and add words until the wrap limit is reached. This additive approach to the problem reduces the number of strings that must be measured, as well as reducing the length of the strings that must measured.
Upvotes: 3