Java: FontMetrics ascent incorrect?
Asked Answered
T

3

20

When I look at the javadoc for FontMetric.getAscent() I see:

The font ascent is the distance from the font's baseline to the top of most alphanumeric characters. Some characters in the Font might extend above the font ascent line.

But I wrote a quick demo program and I see this: enter image description here

where the 4 horizontal lines for each row of text are:

  • baseline position lowered by getDescent()
  • baseline position
  • baseline position raised by getAscent()
  • baseline position raised by getHeight()

Notice the space between the getAscent() line and the top of the characters. I've looked at most of the fonts and sizes, and there's always this gap. (Whereas the font descent looks just right.) What gives?

package com.example.fonts;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextPane;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

public class FontMetricsExample extends JFrame
{
    static final int marg = 10;
    public FontMetricsExample()
    {
        super(FontMetricsExample.class.getSimpleName());

        JPanel panel = new JPanel(new BorderLayout());
        JPanel fontPanel = new JPanel(new BorderLayout());
        final JTextPane textSource = new JTextPane();
        textSource.setText("ABCDEFGHIJKLMNOPQRSTUVWXYZ\n"
                +"abcdefghijklmnopqrstuvwxyz\n"
                +"0123456789!@#$%^&*()[]{}");
        final SpinnerNumberModel fontSizeModel = 
              new SpinnerNumberModel(18, 4, 32, 1);
        final String fonts[] = 
              GraphicsEnvironment.getLocalGraphicsEnvironment()
                .getAvailableFontFamilyNames();
        final JComboBox fontFamilyBox = new JComboBox(fonts);
        fontFamilyBox.setSelectedItem("Arial");

        final JPanel text = new JPanel() {
            @Override protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                String fontFamilyName = 
                         fonts[fontFamilyBox.getSelectedIndex()]; 
                int fontSize = fontSizeModel.getNumber().intValue();
                Font f = new Font(fontFamilyName, 0, fontSize);
                g.setFont(f);
                FontMetrics fm = g.getFontMetrics();
                int lineHeight = fm.getHeight();
                String[] s0 = textSource.getText().split("\n");
                int x0 = marg;
                int y0 = getHeight()-marg-(marg+lineHeight)*s0.length;
                for (int i = 0; i < s0.length; ++i)
                {
                    y0 += marg+lineHeight;
                    String s = s0[i];
                    g.drawString(s, x0, y0);
                    int w = fm.stringWidth(s);
                    for (int yofs : Arrays.asList(
                            0,   // baseline
                            -fm.getHeight(),
                            -fm.getAscent(),
                            fm.getDescent()))
                    {
                        g.drawLine(x0,y0+yofs,x0+w,y0+yofs);
                    }
                }
            }
        };
        final JSpinner fontSizeSpinner = new JSpinner(fontSizeModel);
        fontSizeSpinner.getModel().addChangeListener(
               new ChangeListener() {           
            @Override public void stateChanged(ChangeEvent e) {
                text.repaint();
            }
        });
        text.setMinimumSize(new Dimension(200,100));
        text.setPreferredSize(new Dimension(400,150));
        ActionListener repainter = new ActionListener() {
            @Override public void actionPerformed(ActionEvent e) {
                text.repaint();
            }           
        };
        textSource.getDocument().addDocumentListener(new DocumentListener() {
            @Override public void changedUpdate(DocumentEvent e) {
                text.repaint();             
            }
            @Override public void insertUpdate(DocumentEvent e) {}
            @Override public void removeUpdate(DocumentEvent e) {}
        });
        fontFamilyBox.addActionListener(repainter);

        fontPanel.add(fontFamilyBox, BorderLayout.CENTER);
        fontPanel.add(fontSizeSpinner, BorderLayout.EAST);
        fontPanel.add(textSource, BorderLayout.SOUTH);
        panel.add(fontPanel, BorderLayout.NORTH);
        panel.add(text, BorderLayout.CENTER);       
        setContentPane(panel);
        pack();
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {
        new FontMetricsExample().setVisible(true);
    }
}
Tartarus answered 1/6, 2011 at 13:48 Comment(2)
You might have better luck using TextLayout.Kraul
@Andrew -- care to elaborate?Tartarus
H
15

One possible reason is that this value takes into accounts letters with diacritics.

For example, adding the umlauts ÄÖÜ shows that their trema are much closer to the ascent (although they still don't quite reach it).

Looking for a more general definition of ascent I find the definition in Wikipedia:

[..] the ascent spans the distance between the baseline and the top of the glyph that reaches farthest from the baseline. The ascent and descent may or may not include distance added by accents or diacritical marks.

So it seems that even within typography there is no precise, absolute definition.

Heliotype answered 1/6, 2011 at 13:53 Comment(4)
I guess so... but why should it take into account diacritics? getAscent() says it's supposed to be to the top of most alphanumeric characters. If you want the maximum ascent, there's getMaxAscent().Tartarus
@Jason: I'm mostly guessing here. But since diacritics are many and can be applied to almost all letters, one could argue that most alphanumeric characters actually do have diacritics. And that's only partially tongue-in-cheek.Heliotype
It's not the most satisfying answer, but I'll accept, as there's some sense to it.Tartarus
I've had this problem too, when trying to center text on an image (baseline = imageHeight/2 - fontHeight/2 + fontAscent;), and it always seemed to be drawn too far down. But indeed, after some testing it turned out a string like "Èj" was centered correctly.Juneberry
L
10

I came across the same problem and it appears that the true upper bound of a character can be obtained by using GlyphVector class.

package graphics;

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class FontMetricsTest2 {

    public static void main(String[] args) throws IOException {
        //Draw the text to measure it with a drawing program
        BufferedImage img = new BufferedImage(
            500, 300, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = img.createGraphics();
        Font font = new Font(Font.SERIF, Font.PLAIN, 150);
        graphics.setFont(font);
        String text = "ABCxyz";
        graphics.drawString(text, 20, 180);
        ImageIO.write(img, "PNG", new File("G:\\someDir\\fontMetrics2.png"));

        //Failed attempts to determine ascent with FontMetrics
        FontMetrics fm = graphics.getFontMetrics();
        System.out.println("FM Ascent=" + fm.getAscent() + 
            ", FM descent=" + fm.getDescent());
        //returned ascent is too high
        System.out.println("FM string bounds: " + 
            fm.getStringBounds(text, graphics));
        //too high as well

        //The succesful way with glyph vector
        GlyphVector gv = font.layoutGlyphVector(
            graphics.getFontRenderContext(), text.toCharArray(),
            0, text.length(), Font.LAYOUT_LEFT_TO_RIGHT);
        Rectangle pixBounds = gv.getPixelBounds(
            graphics.getFontRenderContext(), 0, 0);
        System.out.println("GlyphVector - pixelBounds: " + pixBounds);
        Rectangle2D visBounds = gv.getVisualBounds();
        System.out.println("GlyphVector - visualBounds: " + visBounds);
    }

}

The y value in the rectangles returned by ascent of characters appearing in the string represented "text" variable.

The main difference between pixel bounds and visual bounds is that the pixelBounds are integers and the visualBounds are floats. Otherwise they seem almost equal.

Licence answered 27/12, 2012 at 13:58 Comment(0)
B
2

The TrueType Reference Manual says that the ascent of a font is stored in the 'hhea' table. The documentation for hhea states, "The values for ascent, descent and lineGap represent the design intentions of the font's creator rather than any computed value." The OpenType specification is an extension of the TrueType specification. It also stores the ascender in the hhea table and references the TrueType definition of ascent. Bottom line, the ascent property is a guide, not an absolute. The GlyphLayoutVector is the most accurate way to get the bounds of text.

Babbittry answered 17/1, 2013 at 20:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.