I know it's a simple concept but I'm struggling with the font metrics. Centering horizontally isn't too hard but vertically seems a bit difficult.
I've tried using the FontMetrics getAscent, getLeading, getXXXX methods in various combinations but no matter what I've tried the text is always off by a few pixels. Is there a way to measure the exact height of the text so that it is exactly centered.
Note, you do need to consider precisely what you mean by vertical centering.
Fonts are rendered on a baseline, running along the bottom of the text. The vertical space is allocated as follows:
---
^
| leading
|
--
^ Y Y
| Y Y
| Y Y
| ascent Y y y
| Y y y
| Y y y
-- baseline ______Y________y_________
| y
v descent yy
--
The leading is simply the font's recommended space between lines. For the sake of centering vertically between two points, you should ignore leading (it's ledding, BTW, not leeding; in general typography it is/was the lead spacing inserted between lines in a printing plate).
So for centering the text ascenders and descenders, you want the
baseline=(top+((bottom+1-top)/2) - ((ascent + descent)/2) + ascent;
Without the final "+ ascent", you have the position for the top of the font; therefore adding the ascent goes from the top to the baseline.
Also, note that the font height should include leading, but some fonts don't include it, and due to rounding differences, the font height may not exactly equal (leading + ascent + descent).
I found a recipe here.
The crucial methods seem to be getStringBounds() and getAscent()
// Find the size of string s in font f in the current Graphics context g.
FontMetrics fm = g.getFontMetrics(f);
java.awt.geom.Rectangle2D rect = fm.getStringBounds(s, g);
int textHeight = (int)(rect.getHeight());
int textWidth = (int)(rect.getWidth());
int panelHeight= this.getHeight();
int panelWidth = this.getWidth();
// Center text horizontally and vertically
int x = (panelWidth - textWidth) / 2;
int y = (panelHeight - textHeight) / 2 + fm.getAscent();
g.drawString(s, x, y); // Draw the string.
(note: above code is covered by the MIT License as noted on the page.)
Not sure this helps, but drawString(s, x, y) sets the baseline of the text at y.
I was working with doing some vertical centering and couldn't get the text to look right until I noticed that behavior mentioned in the docs. I was assuming the bottom of the font was at y.
For me, the fix was to subtract fm.getDescent() from the y-coordinate.
Another option is the getBounds method from the TextLayout class.
Font f;
// code to create f
String TITLE = "Text to center in a panel.";
FontRenderContext context = g2.getFontRenderContext();
TextLayout txt = new TextLayout(TITLE, f, context);
Rectangle2D bounds = txt.getBounds();
int xString = (int) ((getWidth() - bounds.getWidth()) / 2.0 );
int yString = (int) ((getHeight() + bounds.getHeight()) / 2.0);
// g2 is the graphics object
g2.setFont(f);
g2.drawString(TITLE, xString, yString);
Related
I am jumping back into an old bunch of code and my Java is very rough. Please be kind.
Problem: I have an application that draws on the canvas. The placement of the screen objects works well. Even Text attached to other objects. However when I place a Text object on the canvas the scale of the canvas halves. I have fiddled off and on for months and can't seem to find the resolution. Any advice would be helpful.
Below is the code to draw the text on screen it is in a class Visualise2D with the other drawing method. All other objects use the same scale etc. This only occurred since I upgraded to Java 15, last java I used was java 8 and it worked fine.
//TEXT
public void paintText(Graphics2D t2D, Color color,Text t, Font font, double bearing, Rectangle2D bounds, double scale, boolean selected, boolean isRotationTool, double enhance) {
//Draws text where ever the user clicks
FontMetrics fm = t2D.getFontMetrics();
t2D.setFont(default_FONT);
AffineTransform at = new AffineTransform();
int x = (int) ((t.getX() - bounds.getX())*(scale));
int y = (int) ((bounds.getHeight() + bounds.getY() - t.getY()) *(scale));
at.setToRotation(Math.toRadians(bearing+270), x,y);
FontRenderContext frc = t2D.getFontRenderContext();
TextLayout layout = new TextLayout(t.getText(), t2D.getFont(), frc);
t2D.setTransform(at);
if (!(selected)) {
t2D.setColor(color);
}
else
{
//pixel size of the circle
float size = 20;//(float) (fm.stringWidth(t.getText())*0.5);
t2D.setColor(p_selectedObjectsColour);
t2D.setStroke(LINE_100);
//Highlight and origin indicator when selected - START
t2D.setColor(p_selectedObjectsColour);
t2D.draw(new Ellipse2D.Double((((t.getX() - bounds.getX())*scale) - size), (((bounds.getHeight() + bounds.getY() - t.getY())*scale) - size), (size*2), (size*2)));
if(isRotationTool){
t2D.drawString(" : \uu27f3 "+dec1P.format(bearing)+"\u00b0",(float) (x + (fm.stringWidth(t.getText())*1.05)),y);
}
t2D.setColor(p_selectedObjectsColour);
t2D.draw(new Rectangle2D.Double(
(t.getX() - bounds.getX())* scale,
((bounds.getHeight() + bounds.getY() - t.getY())*scale)-fm.getStringBounds(t.toString(), t2D).getHeight(),
t.getBounds().getWidth(),
t.getBounds().getHeight()
));
t2D.drawLine((int) (((t.getX() - bounds.getX())) * scale),
(int)(((bounds.getHeight() + bounds.getY())-(t.getY()))*scale),
(int)(((t.getX())- bounds.getX())*scale)+fm.stringWidth(t.getText()),
(int)(((bounds.getHeight() + bounds.getY())-(t.getY()))*scale));
}
t2D.setColor(color);
//t2D.drawString(t.getText(), x, y);
layout.draw(t2D, x, y);
at.setToRotation(0, x, y);
t2D.setTransform(at);
//This error is to remind you that the Affine transform is not working and the text is in the collection still after it is moved.
}
Below are two images that describe the issue.
Image 1 is the Normal View at Normal Scale
Image 2 is the Alter after Text addition Scale.
If the text is deleted the Scale returns to the first image.
Normal Scale:
Added Text Scale Changes:
I'm trying to write a code to place a text in the middle of a circle such that the center of the string is on the center of the circle. But the text appears to start from the center , if the font size of the string and the diameter of the circle are large.
Here is my code,
import java.applet.Applet;
import java.awt.Graphics;
import java.awt.*;
//extending Applet class is necessary
public class Main extends Applet{
public void paint(Graphics g)
{
g.setColor(Color.yellow);
int diameter=500;
int xpos=100,ypos=100;
g.fillOval(xpos,ypos,diameter,diameter);
Font f1 = new Font("Arial",Font.BOLD,24);
g.setColor(Color.black);
g.setFont(f1);
String s="Text inside a circle";
g.drawString(s,xpos+(diameter/2)-(s.length()/2),ypos+(diameter/2));
}
}
This is the output I'm getting:
But I want text to be in the middle.
But the text appears to start from the center
The length of the String variable only returns the number of characters in the String, not the width (in pixels) of the String.
You need to know the width of the text in pixels so it can be centered in the circle. For this you use the FontMetrics class.
FontMetrics fm = g.getFontMetrics();
int width = fm.stringWidth(s);
int offset = (diameter - width ) / 2;
g.drawString(s, xpos + offset, ypos + (diameter / 2));
//g.drawString(s,xpos+(diameter/2)-(s.length()/2),ypos+(diameter/2));
Note the above will only center based on the width. You should also center based on the height. For this you can use the getStringBounds(...) method. This will allow you to better approximated the vertical centering as well. This will return a Rectangle so you could use the width and height of the Rectangle for both the horizontal and vertical centering.
You shift string output to the left from circle center, but shift value is too small
-(s.length()/2
uses string length in chars. You can multiply this value by coefficient depending on font size (24 in your case), but different chars have different width ("Arial" is not fixed width font).
If you need more exact positioning, consider calculation of string width in pixels. Perhaps getFontMetrics + stringWidth. Arbitrary found example
I am trying to create a Shape with the centre of the ship being in the middle.
one.x and one.z is the X and Z positions of the ship. The ship size is about 100 on the X, and 50 on the Z.
Shape my = new Rectangle(
(int) one.x - disToLeft, // upper-left corner X
(int) one.z - disToTop, // upper-left corner Y
disToLeft + disToRight, // width
disToTop + disToBottom // height
);
I'm then rotating the Shape, to of course be facing the correct way. This appears to work:
int rectWidth = (disToLeft + disToRight);
int rectHeight = (disToTop + disToBottom);
AffineTransform tr = new AffineTransform();
// rotating in central axis
tr.rotate(
Math.toRadians(one.rotation),
x + (disToLeft + disToRight) / 2,
z + (disToTop + disToBottom) / 2
);
my = tr.createTransformedShape(my);
I am then doing the exact same thing with another Shape, and testing for intersection. This works.
However, it feels like the Shape is the incorrect dimensions. Or something. My ship is colliding very far out to one side (outside where it graphically exists), but through the other side, I can almost go right through the ship before any collision is detected!
Basically the Shapes are simply intersecting at the wrong location. And I cannot work out why. Either the shape, the location, or the rotation must be wrong.
int disToLeft = 100;
int disToRight = 100;
int disToTop = 150;
int disToBottom = 100;
These are the distance from the centre to the left, right, top, and bottom sides.
I am using Z instead of Y because my game is in a 3D world and the sea level is pretty much the same (hence I don't need to worry about Y axis collision!).
Update:
After doing a lot of testing, I have discovered that the top of the rectangle is in the middle! I have done a lot of messing around, but without being able to graphically see the squares, it's been very hard to test.
This means that the box is on the side of the ship, like this:
Obviously when the ship on the left rotates to what it's like in this picture, a collision is detected.
It seems that your rotation is wrong. From my understanding of math it should be
tr.rotate(Math.toRadians(one.rotation), x + (disToRight - disToLeft) /2, z + (disToBottom - disToTop) /2);
Note the signs and the order of the variables
Edit:
Let's take apart the formula:
Your Rectangle is defined like this:
x-coordinate (x): one.x-disToLeft
y-coordinate (y): one.z-disToTop
width: disToLeft+disToRight
height: disToTop+disToBottom
The centre of the Rectangle (where you are rotating) is therefore:
(x+width/2)
(y+height/2)
if you replace x, width, y and height with the declarations above you get
(one.x-disToLeft + (disToLeft+disToRight)/2)
(one.z-disToTop + (disToTop+disToBottom)/2)
This is already the point you need, but it can be simplyfied:
one.x- disToLeft + (disToLeft+disToRight)/2
is equal to
one.x-(2*disToLeft/2)+(disToLeft/2)+(disToRight/2)
is equal to
one.x-(distoLeft/2) + (disToRight/2)
is equal to
one.x+(disToRight-disToLeft)/2
The other coordinate works exactly the same.
There is a method in PDFBox's font class, PDFont, named getFontHeight which sounds simple enough. However I don't quite understand the documentation and what the parameters stand for.
getFontHeight
This will get the font width for a character.
Parameters:
c - The character code to get the width for.
offset - The offset into the array. length
The length of the data.
Returns: The width is in 1000 unit of text space, ie 333 or 777
Is this method the right one to use to get the height of a character in PDFBox and if so how? Is it some kind of relationship between font height and font size I can use instead?
I believe the answer marked right requires some additional clarification. There are no "error" per font for getHeight() and hence I believe it is not a good practice manually guessing the coefficient for each new font.
Guess it could be nice for your purposes simply use CapHeight instead of Height.
float height = ( font.getFontDescriptor().getCapHeight()) / 1000 * fontSize;
That will return the value similar to what you are trying to get by correcting the Height with 0.865 for Helvetica. But it will be universal for any font.
PDFBox docs do not explain too much what is it. But you can look at the image in the wikipedia Cap_height article to understand better how it is working and choose the parameter fit to your particular task.
https://en.wikipedia.org/wiki/Cap_height
EDIT: Cap height was what I was looking for. See the accepted answer.
After digging through the source of PDFBox I found that this should do the trick of calculating the font height.
int fontSize = 14;
PDFont font = PDType1Font.HELVETICA;
font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize
The method isn't perfect though. If you draw a rectangle with the height 200 and a Y with the font size 200 you get the font height 231.2 calculated with the above method even though it actually is printed smaller then the rectangle.
Every font has a different error but with helvetica it is close to 13.5 precent too much independently of font size. Therefore, to get the right font height for helvetica this works...
font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize * 0.865
Maybe use this?
http://pdfbox.apache.org/apidocs/org/apache/pdfbox/util/TextPosition.html
Seems like a wrap-around util for text. I haven't looked in the source if it accounts for font error though.
this is a working method for splitting the text and finding the height
public float heightForWidth(float width) throws IOException {
float height = 0;
String[] split = getTxt().split("(?<=\\W)");
int[] possibleWrapPoints = new int[split.length];
possibleWrapPoints[0] = split[0].length();
for (int i = 1; i < split.length; i++) {
possibleWrapPoints[i] = possibleWrapPoints[i - 1] + split[i].length();
}
float leading = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize;
int start = 0;
int end = 0;
for (int i : possibleWrapPoints) {
float w = font.getStringWidth(getTxt().substring(start, i)) / 1000 * fontSize;
if (start < end && w > width) {
height += leading;
start = end;
}
end = i;
}
height += leading;
return height + 3;
}
For imported True Type Fonts the total height of the font is
(org.apache.pdfbox.pdmodel.font.PDFont.getFontDescriptor().getDescent() + org.apache.pdfbox.pdmodel.font.PDFont.getFontDescriptor().getAscent() + org.apache.pdfbox.pdmodel.font.PDFont.getFontDescriptor().getLeading()) * point size * org.apache.pdfbox.pdmodel.font.PDFont.getFontMatrix().getValue(0, 0)
You will find that font.getFontDescriptor().getFontBoundingBox().getHeight() is 20% larger than the above value as it includes a 20% leading on the above value, but if you take the top value and remove 20%, the font will be right next too each other
I have an application in JavaSE and i want my application always starts at center of screen. If two monitors are plugged, the right one should be used. So i wrote a code like this:
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
if(ge.getScreenDevices().length == 2) {
int w_1 = ge.getScreenDevices()[0].getDisplayMode().getWidth();
int h_1 = ge.getScreenDevices()[0].getDisplayMode().getHeight();
int w_2 = ge.getScreenDevices()[1].getDisplayMode().getWidth();
int h_2 = ge.getScreenDevices()[1].getDisplayMode().getHeight();
int x = w_1 + w_2 / 2 - getWidth() / 2;
int y = h_2 / 2 - getHeight() / 2;
setLocation(x, y);
}
Unfortunately if monitor is rotated at 90°, width and height should be flipped. Is there any way to detect such rotation?
You don't need to know whether the second monitor is in portrait mode. Just find the bounds of the screen in device coordinates and use the center. (If it is in portrait mode, then height>width, but that isn't an important piece of information.)
Your formula to determine the center point of the second device is wrong. You are assuming that the coordinates of the second screen runs from (w_1,0) to (w_1 + w_2, h_2), but that isn't necessarily true. You need to find the GraphicsConfiguration object of the second screen and call GraphicsConfiguration.getBounds() on it. You can then calculate the center point of that rectangle.
If you want to know which device is on the left or the right (or top or bottom), you can compare the x (or y) values of their bounding rectangles. Note that the x or y values may be negative.
You should consider if the height is bigger than the width(portrait). I haven't heard anyone using portrait monitors yet, though.
Here is the code that works fine in most cases (from answer of Enwired).
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
if(ge.getScreenDevices().length == 2) {
int x = (int)ge.getScreenDevices()[1].getDefaultConfiguration().getBounds().getCenterX() - frame.getWidth() / 2;
int y = (int)ge.getScreenDevices()[1].getDefaultConfiguration().getBounds().getCenterY() - frame.getHeight() / 2;
setLocation(x, y);
}
The only problem is that device index is not always 0 - left, 1 - right.