Search code examples
javafontsbufferedimagegraphics2d

Graphics2D text - font size change causes shift on X axis


When writing text** to a Graphics2D object, I'm finding that the x position of the text changes when I change the size of the font. As the size increases, the x position increases. This happens no matter what method I use to write the text: drawString, fill or draw with a GlyphVector, or drawGlyphVector.

** both the beginning character of the text and the font type change this behavior: Sans Serif fonts, where the beginning character's left-most stroke is a vertical line (P, H, L, E, etc...) are the most affected. If the font is Serif and/or the beginning character's left-most stroke is not vertical (W, y, Y, T, X, etc...), this behavior is decreased or eliminated.

Here's a sample program that illustrates this behavior:

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.image.BufferedImage;

import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class FontSizeTest {
    public FontSizeTest() {
        super();
    }

    public static void main(String[] args) throws IOException {
        int width = 700, height = 120;

        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = image.createGraphics();

        g2d.setColor(Color.WHITE);
        g2d.fillRect(0, 0, width, height);

        FontRenderContext frc = g2d.getFontRenderContext();
        Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
        GlyphVector gv;
        Rectangle pixelBounds;

        g2d.setColor(Color.BLACK);
        g2d.translate(10, 100);

        for(float size : new float[]{40f, 80f, 120f}) {
            font = font.deriveFont(size);
            gv = font.createGlyphVector(frc, "Hello World");
            pixelBounds = gv.getPixelBounds(frc, 0, 0);
            System.out.printf("font: %s%npixel bounds: %s%n", font, pixelBounds);
            g2d.fill(gv.getOutline());
            g2d.draw(pixelBounds);
        }

        g2d.dispose();

        File pngFile = new File("FontSizeTest.png");
        ImageIO.write(image, "png", pngFile);
        System.out.printf("png file written: %s%n", pngFile.getCanonicalPath());
    }
}

The output of this program is info to the console:

font: java.awt.Font[family=SansSerif,name=SansSerif,style=plain,size=40]
pixel bounds: java.awt.Rectangle[x=4,y=-31,width=217,height=31]
font: java.awt.Font[family=SansSerif,name=SansSerif,style=plain,size=80]
pixel bounds: java.awt.Rectangle[x=7,y=-62,width=432,height=63]
font: java.awt.Font[family=SansSerif,name=SansSerif,style=plain,size=120]
pixel bounds: java.awt.Rectangle[x=11,y=-93,width=651,height=94]
png file written: FontSizeTest.png

And the image:

Output of FontSizeTest

Note how the larger font sizes draw offset from the smaller font sizes.

If this is expected behavior, I can adjust for it by getting the offset from the pixel bounds and adjusting the x coordinate using translate; I'm just not sure this is the expected behavior. I feel like I'm doing something wrong - missing some piece of the Graphics2D programming approach.


Solution

  • This effect is normal behaviour of Fonts. Even Microsoft Word has this offset in textlines with different sizes. As you can see here!
    If you want to align all of your textlines at the same pixel line you could use translate with your calculated pixel bounds as you mentioned.
    A way more elegant way is to shift your glyphs positions in the GlyphVector. That will also result in a shift of the getPixelBounds result.

    public static void adjust(GlyphVector gv){
        // calc the adjust Factor
        int adjust = (int)Math.round(gv.getGlyphVisualBounds(0).getBounds2D().getMinX());
        // Shift all the Glyphs in Vector
        for (int i = 0; i < gv.getNumGlyphs(); i++) {
            Point2D point = gv.getGlyphPosition(i);
            gv.setGlyphPosition( i, new Point2D.Double(
                point.getX() - adjust, 
                point.getY()));         
        }
    }
    

    Use it in your code after you create the Glyph Vector and you get the result you want.

    gv = font.createGlyphVector(frc, "Hello World");
    adjust(gv);