JDK-4522900 : Bad Font Metrics returned (ascent, descent) when using scaled fonts in Java2D
  • Type: Bug
  • Component: client-libs
  • Sub-Component: 2d
  • Affected Version: 1.4.0
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • OS: windows_2000
  • CPU: x86
  • Submitted: 2001-11-05
  • Updated: 2006-06-16
Related Reports
Relates :  
Description

Name: gm110360			Date: 11/05/2001


java version "1.4.0-beta2"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.0-beta2-b77)
Java HotSpot(TM) Client VM (build 1.4.0-beta2-b77, mixed mode)

import java.awt.*;
import java.awt.image.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.event.*;
import javax.swing.*;

/*

I have found a problem with Fonts and TextLayout when using
scaled fonts in Java2D JDK 1.4 Beta2. I am trying to adjust the
"user space" coordinates so that "1.0 units" means about the
same as 1 inch. Depending on your screen size, this means
applying a scale factor to the Graphics2D of about 100.

This works beautifully until I try to start displaying multi-line
text. Doing that requires knowing the Ascent, Descent and Leading;
when running with this scale factor these numbers are always
returned incorrectly as zero or close to it.

This test program shows the problem; I try to draw a line of text
using TextLayout in my scaled context. The output appears at the
expected size and in the expected position. However, when I ask
the TextLayout for Ascent, Descent and Leading it returns the
wrong answers (usually zero).

I tried to dig in deeper and get the same wrong answers when
going after the LineMetrics directly from the Font. Of course,
the FontMetrics returns zeros because it only knows about integers.
But I expected LineMetrics (which returns floats) to give the
right result.

I tried this under JDK 1.3.1 and the results are even worse.
Not only are the metrics wrong, but all the characters are piled
on top of each other, meaning that the "advance" is wrong as well.
But I only care about JDK 1.4...

This problem obviously stems from Font's checkered past; in the
deeps of time it only knew about integer metrics. I imagine there
is still some code hiding in Font or FontMetrics or LineMetrics
or whatever that hasn't been updated to respect the "fractional
metrics" hint. Some rogue roundoff error is throwing away my
metrics, which are perfectly correct for my user space (although
they would seem wrong in the old integer/pointsize view of Fonts).

This is pretty serious for us since we are trying to write
a sophisticated drawing program that can scale and zoom. Java2D
is perfect for that, except for this problem with Font metrics.
It certainly seems like Java2D *should* be able to do what we
want. A workaround would be fine, but it would be nice to see
a fix before 1.4 goes FCS.

  To run this test, just compile it

javac FontTest.java

and run it

java FontTest

The window looks correct; the (bad) metrics will be printed to stdout.

My results look something like this:

usesFractionalMetrics=true
TextLayout
  tl.Ascent=-0.0
  tl.Descent=0.0
  tl.Leading=-0.0
  tl.Bounds=java.awt.geom.Rectangle2D$Float[x=3.4028235E38,y=3.4028235E38,w=-
3.4028235E38,h=-3.4028235E38]
font.size2d=0.24
r2d.Bounds=java.awt.geom.Rectangle2D$Float[x=0.0,y=0.0,w=1.1471485,h=0.0]
r2d.MaxBounds=java.awt.geom.Rectangle2D$Float[x=0.0,y=0.0,w=0.0,h=0.0]
LineMetrics
  lm.Ascent=-0.0
  lm.Descent=0.0
  lm.Leading=-0.0
FontMetrics
  fm.Ascent=0
  fm.Descent=0
  fm.Leading=0

  
Steve Langley
###@###.###

*/
public class FontTest extends JFrame
{
    //
    //  Scaling like this makes 1 user space unit roughly equal to 1 inch
    //
    private static final double SCALE = 100.0;

    public FontTest()
    {
        super();

        WindowListener listener = new WindowAdapter()
        {
            public void windowClosing(WindowEvent e)
            {
                System.exit(0);
            }
        };

        this.setTitle("Font Test");
        this.setBackground(Color.white);
        this.getContentPane().setLayout(new BorderLayout());

        JPanel panel = new JPanel()
        {
            public void paint(Graphics g)
            {
                Graphics2D  g2d = (Graphics2D)g;
                Font        f = new Font("Arial",Font.PLAIN,24);

                g2d.setRenderingHint(   RenderingHints.KEY_FRACTIONALMETRICS,
                                        
RenderingHints.VALUE_FRACTIONALMETRICS_ON);

                g2d.setColor(Color.white);
                g2d.fillRect(0,0,getWidth(),getHeight());

                //
                //  Scale user space so "1.0 units" equals about 1 inch
                //
                g2d.scale(SCALE,SCALE);

                g2d.setColor(Color.black);

                //
                //  This is just a trick to create a "24 point font" in our user
                //  space. Since it's less than 1 we can't use the old
fashioned way
                //  using an integer.
                //
                f = f.deriveFont((float)(24.0 / SCALE));

                g2d.setFont(f);

                FontRenderContext frc = g2d.getFontRenderContext();

                System.out.println("usesFractionalMetrics=" +
frc.usesFractionalMetrics());

                TextLayout tl = new TextLayout("TextLayout ", f, frc);

                //
                //  Draw about 1 inch over, 1 inch down
                //
                tl.draw(g2d, (float)1.0, (float)1.0);

                //
                //  The font appears at the right place and at the right size.
                //  But all of the line metrics are wrong, which makes doing
                //  multi-line text impossible (since you don't know how
                //  far down to position the next line.)
                //
                System.out.println("TextLayout");
                System.out.println("  tl.Ascent=" + tl.getAscent());
                System.out.println("  tl.Descent=" + tl.getDescent());
                System.out.println("  tl.Leading=" + tl.getLeading());
                System.out.println("  tl.Bounds=" + tl.getBounds());

                //
                //  Let's just try something simpler; use drawString
                //

                //
                //  Draw about 1 inch over, 2 inches down
                //
                g2d.drawString("drawString", (float)1.0, (float)2.0);

                //
                //  This comes out as we expect (.24)
                //
                System.out.println("font.size2d=" + f.getSize2D());

                //
                //  This is wrong
                //
                Rectangle2D r = f.getStringBounds("drawString",frc);
                System.out.println("r2d.Bounds=" + r);

                //
                //  And this is wrong
                //
                r = f.getMaxCharBounds(frc);
                System.out.println("r2d.MaxBounds=" + r);

                //
                //  And this is wrong
                //
                LineMetrics lm = f.getLineMetrics("drawString",frc);

                System.out.println("LineMetrics");
                System.out.println("  lm.Ascent=" + lm.getAscent());
                System.out.println("  lm.Descent=" + lm.getDescent());
                System.out.println("  lm.Leading=" + lm.getLeading());

                //
                //  And this is wrong too
                //
                FontMetrics fm = g2d.getFontMetrics();

                System.out.println("FontMetrics");
                System.out.println("  fm.Ascent=" + fm.getAscent());
                System.out.println("  fm.Descent=" + fm.getDescent());
                System.out.println("  fm.Leading=" + fm.getLeading());

            }
        };

        this.getContentPane().add("Center", panel);
        this.addWindowListener(listener);
        this.pack();
        this.setSize(400,300);
        this.show();
    }

    public static void main(String argv[])
    {
        FontTest ft = new FontTest();
    }
}
(Review ID: 134618) 
======================================================================

Comments
EVALUATION The main problem is an 'optimization' I used to compute the visual bounds of a glyph. This is defined to be the bounding box of the glyph in user space, e.g. the minima and maxima of the glyph outline in user space. I 'optimized' this by not using the device transform, so that the outline and thus the bounds would always remain in user space. The alternative was to generate the outline in device space, then untransform the entire outline back into user space, and only then get the minima and maxima. The problem with not using device space is that at very small point sizes, the hinting to normal device pixels basically destroys the outline-- it becomes effectively empty when all points are at small fractional positions and so get hinted to 0,0. ###@###.### 2001-11-08
08-11-2001