Reputation: 23789
I'm using FontMetrics.getHeight()
to get the height of the string, but it gives me a wrong value, cutting off the descenders of string characters. Is there a better function I can use?
Upvotes: 21
Views: 34454
Reputation: 1306
The getStringBounds()
method below is based on the GlyphVector
for the current Graphics2D
font, which works very well for one line string of text:
public class StringBoundsPanel extends JPanel
{
public StringBoundsPanel()
{
setBackground(Color.white);
setPreferredSize(new Dimension(400, 247));
}
@Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// must be called before getStringBounds()
g2.setFont(getDesiredFont());
String str = "My Text";
float x = 140, y = 128;
Rectangle bounds = getStringBounds(g2, str, x, y);
g2.setColor(Color.red);
g2.drawString(str, x, y);
g2.setColor(Color.blue);
g2.draw(bounds);
g2.dispose();
}
private Rectangle getStringBounds(Graphics2D g2, String str,
float x, float y)
{
FontRenderContext frc = g2.getFontRenderContext();
GlyphVector gv = g2.getFont().createGlyphVector(frc, str);
return gv.getPixelBounds(null, x, y);
}
private Font getDesiredFont()
{
return new Font(Font.SANS_SERIF, Font.BOLD, 28);
}
private void startUI()
{
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(this);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) throws Exception
{
final StringBoundsPanel tb = new StringBoundsPanel();
SwingUtilities.invokeAndWait(new Runnable()
{
public void run()
{
tb.startUI();
}
});
}
}
Note that I've omitted the imports for clarity.
The result:
Upvotes: 24
Reputation: 973
I recently wrote the code below as I needed pixel perfect height measurements for specific ranges of the font (for example: all lower characters, or all numbers).
If you need faster code (mine has for loops) I would recommend running it once at the start-up to get all values (for example from 1 to 100) in an array and then use the array instead.
The code basically draws all characters from the input string at the same place overlapped on a 250x250 bitmap (increase or reduce if needed), it starts looking for pixels from top, then from bottom, then it returns the maximum height found. It works with normal strings even if it was designed for character ranges. This means there is a sort of redundancy when evaluating regular strings as some of the characters repeat. So if your imput string exceeds the alphabet count (26), use as 'tRange' imput: "abcd...z" and other characters that may be used. It is faster.
Hope that helps.
public int getFontPixelHeight(float inSize, Paint sourcePaint, String tRange)
{
// It is assumed that the font is already set in the sourcePaint
int bW = 250, bH = 250; // bitmap's width and height
int firstContact = -1, lastContact = -2; // Used when scanning the pixel rows. Initial values are set so that if no pixels found, the returned result is zero.
int tX = (int)(bW - inSize)/2, tY = (int)(bH - inSize)/2; // Used for a rough centering of the displayed characters
int tSum = 0;
// Preserve the original paint attributes
float oldSize = sourcePaint.getTextSize();
int oldColor = sourcePaint.getColor();
// Set the size/color
sourcePaint.setTextSize(inSize); sourcePaint.setColor(Color.WHITE);
// Create the temporary bitmap/canvas
Bitmap.Config bConf = Bitmap.Config.ARGB_8888;
Bitmap hld = Bitmap.createBitmap(250, 250, bConf);
Canvas canv = new Canvas(hld);
for (int i = 0; i < bH; i++)
{
for (int j = 0; j < bW; j++)
{
hld.setPixel(j, i, 0); // Zero all pixel values. This might seem redundant, but I am not quite sure that creating a blank bitmap means the pixel color value is indeed zero, and I need them to be zero so the addition performed below is correct.
}
}
// Display all characters overlapping at the same position
for (int i = 0; i < tRange.length(); i++)
{
canv.drawText("" + tRange.charAt(i), tX, tY, sourcePaint);
}
for (int i = 0; i < bH; i++)
{
for (int j = 0; j < bW; j++)
{
tSum = tSum + hld.getPixel(j, i);
}
if (tSum > 0) // If we found at least a pixel, save row index and exit loop
{
firstContact = i;
tSum = 0; // Reset
break;
}
}
for (int i = bH - 1; i > 0 ; i--)
{
for (int j = 0; j < bW; j++)
{
tSum = tSum + hld.getPixel(j, i);
}
if (tSum > 0) // If we found at least a pixel, save row index and exit loop
{
lastContact = i;
break;
}
}
// Restore the initial attributes, just in case the paint was passed byRef somehow
sourcePaint.setTextSize(oldSize);
sourcePaint.setColor(oldColor);
return lastContact - firstContact + 1;
}
Upvotes: 3
Reputation: 375484
getHeight()
can't cut off the descenders of a string, only drawing the string can do that. You are using the height returned from getHeight
to draw the string somehow, and likely you are misusing the height. For example, if you position the start point of the string at the bottom of a box that is getHeight() high, then the baseline of your text will sit on the bottom edge of the box, and very likely the descenders will be clipped.
Text geometry is a complex topic, infused with bizarre historical artifacts. As others have suggested, use getAscent
and getDescent
to try to position the baseline properly within your box.
Upvotes: 3
Reputation: 308001
What makes you think it returns the wrong value? It's far more probable that your expectation of what it returns does not match the specification. Note that it's perfectly fine if some glyphs in the Font go over or under those values.
getMaxDescent()
and getMaxAscent()
should tell you the absolute maximum values of those fields for any glyph in the Font.
If you want to know the metrics for a specific String, then you definitely want to call getLineMetrics()
.
Upvotes: 14
Reputation: 83847
FontMetrics.getAscent() and FontMetrics.getDescent() might do the trick.
Upvotes: 8