Frank
Frank

Reputation: 521

JavaFX FontMetrics

I'm trying to place text accurately in the centre of a pane both horizontally and vertically. Using fontmetrics and a test program I get the following results:

enter image description here

This test raises the following questions:

  1. Why is the ascent value (top black line) so high? I expected it to go across the top of the 'T'. The magenta line is the 'lineheight' value, so I assume that's the baseline for any text above it.
  2. If the black line includes line spacing, why is there no measurement for the top of the 'T'?
  3. Is there a way to get an accurate bounding box or do I have to graphically linescan a text image to find the boundaries? Obviously the left and right values also include some sort of spacing, so a scan would seem to be the only solution.

Upvotes: 8

Views: 5434

Answers (2)

jewelsea
jewelsea

Reputation: 159281

Here is an alternate implementation of Frank's reportSize function:

public void reportSize(String s, Font myFont) {
    Text text = new Text(s);
    text.setFont(myFont);
    Bounds tb = text.getBoundsInLocal();
    Rectangle stencil = new Rectangle(
            tb.getMinX(), tb.getMinY(), tb.getWidth(), tb.getHeight()
    );

    Shape intersection = Shape.intersect(text, stencil);

    Bounds ib = intersection.getBoundsInLocal();
    System.out.println(
            "Text size: " + ib.getWidth() + ", " + ib.getHeight()
    );
}

This implementation uses shape intersection to determine the size of the bounding box of the rendered shape with no whitespace. The implementation does not rely on com.sun package classes which may not be directly accessible to user application code in Java 9+.

Upvotes: 13

Frank
Frank

Reputation: 521

After a bit of experimenting I've come up with this solution:

enter image description here

Here is the code that produces it:

public void getBoundingBox(String s, Font myFont) {                             

    final FontMetrics fm = Toolkit.getToolkit().getFontLoader().getFontMetrics(myFont); 

    final Canvas canvas = new Canvas(fm.computeStringWidth(s), fm.getAscent() + fm.getDescent());        
    final GraphicsContext gc = canvas.getGraphicsContext2D();

    gc.setFill(Color.RED);                  // Just an abitrary color
    gc.setTextBaseline(VPos.TOP);           // This saves having to scan the bottom
    gc.setFont(myFont);

    gc.fillText(s, -fm.getLeading(), 0);    // This saves having to scan the left

    // Get a snapshot of the canvas
    final WritableImage image = canvas.snapshot(null, null);
    final PixelReader pr = image.getPixelReader();

    final int h = (int) canvas.getHeight();
    final int w = (int) canvas.getWidth();

    int x;
    int y = 0;

    // Scan from the top down until we find a red pixel

    boolean found = false;
    while (y < h && !found) {
        x = 0;
        while (x < w && !found) {
            found = pr.getColor(x, y).equals(Color.RED);
            x++;
        }
        y++;
    }
    int yPos = y - 2;

    // Scan from right to left until we find a red pixel

    x = w;        
    found = false;
    while (x > 0 && !found) {
        y = 0;           
        while (y < h && !found) {
            found = pr.getColor(x, y).equals(Color.RED);
            y++;
        }
        x--;
    }
    int xPos = x + 3;

    // Here is a visible representation of the bounding box

    Rectangle mask = new Rectangle(0, yPos, xPos, h - yPos);

    mask.setFill(Color.rgb(0, 0, 255, 0.25));       
    root.getChildren().addAll(canvas, mask);   // root is a global AnchorPane

    System.out.println("The width of the bounding box is " + xPos);
    System.out.println("The height of the bounding box is " + (h - yPos));
}

Two imports are required for FontMetrics:

 import com.sun.javafx.tk.FontMetrics;
 import com.sun.javafx.tk.Toolkit;

and call the boundingbox like this for example:

 Font myFont = new Font("Arial", 100.0); 
 getBoundingBox("Testing", myFont);

It solves my problem and I hope this is useful for others as well.

Upvotes: 4

Related Questions