Get height of text with wrapping

I love the wrapping function in text(), which does a great job. But I wish I could get the height of the block it creates so I can place further text below it.

I’ve got a pretty kludgy version going here, which creates a PGraphics with the wrapped text, then looks at the alpha values of its pixel array to find the bottom:

void setup() {
  size(600, 600);
  surface.setLocation(0, 0);
  background(255);
  
  String str = "Peter Piper picked a peck of pickled peppers; a peck of pickled peppers Peter Piper picked.";

  PGraphics pg = createGraphics(400, height);
  pg.beginDraw();
  pg.textSize(48);
  pg.textAlign(LEFT, TOP);
  pg.textLeading(48*1.3);
  pg.fill(0);
  pg.noStroke();
  pg.text(str, 0, 0, pg.width, pg.height);
  pg.endDraw();  

  image(pg, 100, 100);

  int h = getTextHeight(pg);
  println(h);
  stroke(0);
  line(0,h+100, width,h+100);
}


int getTextHeight(PGraphics pg) {
  pg.loadPixels();
  for (int y=pg.height-1; y>=0; y-=1) {
    for (int x=0; x<pg.width; x++) {
      color c = pg.pixels[y*pg.width+x];
      if (alpha(c) != 0) {
        return y;
      }
    }
  }
  return -1;
}

Not ideal by any means. For one thing, it returns the bottom-most part of the text and not the baseline, but it also seems like a totally inefficient way to accomplish this.

I did look at the text() function in the P5 core, but it’s built on so many other functions I think re-creating it is going to be really challenging.

Any ideas on how to do this better would be most appreciated!

Just an idea.
To discover how the text will be split; search in String for spaces, and measure with textWidth() if it fits in your box width. If not, go back to previous space and trim the String. Repeat until end of String. Use textLeading(); to ensure exact space between lines & fontSize in pixels.
Sum strings and you have your box height.

2 Likes

Thanks @noel, I had tried making my own wrap function with limited success. It’s way harder than it seems! Splitting at the nearest space after hitting the width is no problem, but often words hang over the edge more than is desirable. The P5 one also splits very long words in the middle.

I am seeing some major quality issues with my approach (text gets saved as raster and looks poopy) so this might be the route I need to go.

Yes, I verified it now, it’s really breaks long words.
But I think if you me measure each word in the String, you can write a routine that takes this into account.

Well, it’s not perfect but after a lot of trial-and-error I have something that works much better! Also will add hyphens if you ask for them (and you can specify the hyphen character used) as well as the ability to add indentation to lines after the first one (one of the reasons I started this whole crazy process).

(Mostly) supports LEFT, CENTER, and RIGHT alignment too.

Update: got this working a bit better with hyphenation and various alignments.

String s = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
//String s = "The quick brown fox jumps over the lazy dog";
//String s = "Thequickbrownfoxjumps overthelazydoglazydog";

int posX =      50;
int posY =      50;
int textWidth = 400;

PFont font;


void setup() {
  size(500,800);
  surface.setLocation(0,0);
  background(255);
  
  font = loadFont("CooperHewitt-Heavy-48.vlw");
  textAlign(LEFT, TOP);
  textFont(font);
  textSize(48);
  textLeading(48 * 1.3);
  
  // wrapText() command will draw text auto-wrapped to a specified
  // width (can also indent lines after the first and can add
  // hyphens to very long words, if specified)
  // returns the overall height of the text block created
  fill(0);
  noStroke();
  float h = wrapText(s, posX,posY, textWidth);
  
  // show overall height of rendered text
  noFill();
  stroke(255,150,0, 100);
  rect(posX,posY, textWidth,h);
}

// wraps text to a specified width (will flow to any height necessary to fit
// all the text), can optionally indent first/other lines and will hyphenate
// very large words (can also specify a particular hyphen character if desired)
// via: https://stackoverflow.com/a/45614206/1167783
float wrapText(String s, float x, float y, float w, int _indentFirst, int _indentOthers, boolean addHyphen, String hyphenChar) {  

  // optional: specify the threshold for long words to be hyphenated
  // try playing with this if you need to tune your hyphenation
  // here four characters past the boundary = add a hyphen please 
  float hyphenBufferWidth = textWidth(" ") * 4;

  // create indent strings from specified number of characters
  // via: https://stackoverflow.com/a/2807731/1167783
  String indentFirst =  new String(new char[_indentFirst]).replace('\0', ' ');
  String indentOthers = new String(new char[_indentOthers]).replace('\0', ' ');

  // if short enough, just send it back as is
  StringBuilder outputLines = new StringBuilder();
  if (textWidth(s) <= w) {
    outputLines.append(s);
  }

  // otherwise, split it!
  else {
    String[] words = s.split(" ");
    StringBuilder currentLine = new StringBuilder();
    currentLine.append(indentFirst);
    for (int i=0; i<words.length; i++) {
      String word = words[i];

      // check width, if not too long yet then add the current word
      // and keep going through the string
      float lineWidth = textWidth(currentLine.toString() + " " + word);
      if (lineWidth < w) {
        currentLine.append(word + " ");
      }

      // if too long, end current line and start a new one
      else {

        // if this line is waaayy too long (probably one long word
        // or a url, etc) then force a break
        if (textWidth(currentLine.toString()) > w + hyphenBufferWidth) {
          String[] parts = hyphenate(currentLine.toString(), w, addHyphen, hyphenChar);
          outputLines.append(parts[0] + "\n");                          // add current line to output
          currentLine = new StringBuilder();                            // start new line of text
          if (g.textAlign == LEFT) currentLine.append(indentOthers);    // add indent if specified
          currentLine.append(parts[1] + " " + word + " ");              // and add remaining words to new line
        }

        // otherwise, add this line to the output and start
        // a new one with the current word
        else {
          outputLines.append(currentLine.toString() + "\n");
          currentLine = new StringBuilder();
          if (g.textAlign == LEFT) currentLine.append(indentOthers);
          currentLine.append(word + " ");
        }
      }
    }

    // when out of words, add the current line
    // to the output, adding one last line-break if this
    // is a really long word like above
    if (currentLine.length() > 0) {
      if (textWidth(currentLine.toString()) > w + hyphenBufferWidth) {
        String[] parts = hyphenate(currentLine.toString(), w, addHyphen, hyphenChar);
        while (true) {
          outputLines.append(parts[0].trim() + "\n");
          if (textWidth(parts[1]) > w + hyphenBufferWidth) {
            parts = hyphenate(parts[1], w, addHyphen, hyphenChar);
          }
          else {
            outputLines.append(parts[1]);
            break;
          }
        }
      } 
      else {
        outputLines.append(currentLine.toString());
      }
    }
  }

  // trim any unwanted newline chars
  String out = outputLines.toString().replaceAll("^\n+", "");
  //println(out.replace("\n", "\\n"));

  // use the usual text() command to draw the string!
  if (g.textAlign == LEFT) {
    text(out, x, y);
  } else if (g.textAlign == CENTER) {
    text(out, x+w/2, y);
  } else if (g.textAlign == RIGHT) {
    text(out, x+w, y);
  }

  // count linebreaks to determine the overall text box height
  int numLinebreaks = countLinebreaks(out);
  return g.textSize + (numLinebreaks * g.textLeading) - g.textDescent();
}

float wrapText(String s, float x, float y, float w) {
  return wrapText(s, x, y, w, 0, 0, false, "-");
}
float wrapText(String s, float x, float y, float w, int indentFirst) {
  return wrapText(s, x, y, w, indentFirst, 0, false, "-");
}
float wrapText(String s, float x, float y, float w, int indentFirst, int indentOthers) {
  return wrapText(s, x, y, w, indentFirst, indentOthers, false, "-");
}
float wrapText(String s, float x, float y, float w, int indentFirst, int indentOthers, boolean addHyphen) {
  return wrapText(s, x, y, w, indentFirst, indentOthers, addHyphen, "-");
}



// returns the number of linebreaks in a string
int countLinebreaks(String s) {
  return s.length() - s.replace("\n", "").length();
}


// splits long strings at a specified width, add hyphens
// if specified (they're left off by default)
String[] hyphenate(String currentLine, float w, boolean addHyphen, String hyphenChar) {  
  String firstHalf = currentLine;
  String secondHalf = "";
  for (int i=currentLine.length()-2; i>=0; i-=1) {
    firstHalf = currentLine.substring(0, i);
    secondHalf = currentLine.substring(i, currentLine.length()-1);
    if (textWidth(firstHalf) <= w) {

      // if hyphenating, move the last char from the first line to the start
      // of the second line before adding the hyphen character
      if (addHyphen) {
        secondHalf = firstHalf.charAt(firstHalf.length()-1) + secondHalf;
        firstHalf = firstHalf.substring(0, firstHalf.length()-1) + hyphenChar;
      }
      break;
    }
  }
  return new String[] { firstHalf, secondHalf };
}
1 Like

I believe the task you are trying to do is challenging as previously reported:
https://forum.processing.org/one/topic/finding-text-height-from-a-text-area.html

Other references:

Kf

1 Like

Do not want to spoil the joy, but changing for instance textWidth to 300 string 3 is not working.(sorry I’m reporting honestly)

1 Like

@noel: no worry, that’s helpful! It works for me. Are you using the indent values? Those do break it for me (or rather they add an extra line at the top).

This is incredible! But where do you enable/disable to hyphen and change its symbol?

1 Like

Also why is there no draw()?

The hyphen character is an optional argument (so you don’t have to manually add it if you want “normal” behavior).

If you want no hyphen, you can just do this:

float h = wrapText(s, posX,posY, textWidth, 0,0, false);

(The 0,0 above are for indenting the text)

If you want to change the hyphen symbol, you can do this:

float h = wrapText(s, posX,posY, textWidth, 0,0, true, "•");

(This will add a bullet character as the hyphen but you could also use an em-dash or whatever)

There’s no draw() in my code above because it runs once and isn’t animated – just makes the code simpler but sorry for the confusion!