It’s easy to determine the rendered height of a font using FontMetrics, but what about the other way around? How can I obtain a font that will fit into a specific height in pixels?
"Give me Verdana in a size that is 30 pixels high from ascender to descender."
How do I ask Java for this?
I know this is a very old question, but someone might still find it:
The font height in Java (and many other places) is given in "typographic points", which are defined as roughly 1/72nd of an inch.
To calculate the points needed for a certain pixel height, you should be able to use the following:
double fontSize= 72.0 * pixelSize / Toolkit.getDefaultToolkit().getScreenResolution();
I haven't tested this extensively yet, but it seems to work for the monitors that I've used. I'll report back if I ever find a case where it doesn't work.
For the standard system fonts I've used this with, this sets the height of a capital letter (i.e. the ascent) to the provided pixel size. If you need to set the ascent+descent to the pixel size, you can correct the value using the FontMetrics:
FontMetrics m= g.getFontMetrics(font); // g is your current Graphics object
double totalSize= fontSize * (m.getAscent() + m.getDescent()) / m.getAscent();
Of course, the actual pixel-height of some specific letters will depend on the letter and the font used, so if you want to make sure that your "H" is some exact number of pixels tall, you might still want to use the trial-and-error methods mentioned in the other answers. Just keep in mind that if you use these methods to get the size for each specific text you want to display (as #Bob suggested), you might end up with a random font-size-mess on your screen where a text like "ace" will have much bigger letters than "Tag". To avoid this, I would pick one specific letter or letter sequence ("T" or "Tg" or something) and fix that one to your pixel height once and then use the font size you get from that everywhere.
I don't think there's a "direct" way to find a font by height; only an indirect way... by looping through the sizes, and testing the height of each is <= required height.
If you're doing this once, just loop through them... if you've doing it "on the fly" then do a binary search, it'll be quicker.
I'm not aware of a way to get a font by its actual height in pixels. It depends on the context it's used in so there's probably no shorter way than to sample for the best match. It should be pretty quick to look for sizes up or down from the designed height. Here's an example method that does that:
public Font getFont(String name, int style, int height) {
int size = height;
Boolean up = null;
while (true) {
Font font = new Font(name, style, size);
int testHeight = getFontMetrics(font).getHeight();
if (testHeight < height && up != Boolean.FALSE) {
size++;
up = Boolean.TRUE;
} else if (testHeight > height && up != Boolean.TRUE) {
size--;
up = Boolean.FALSE;
} else {
return font;
}
}
}
WhiteFang34's code is useful in combination with the following method that returns the actual height of a specific string. It might be a bit slow for real-time rendering, especially for large fonts/strings and I'm sure it can be further optimised, but for now it meets my own needs and is fast enough to run in a back-end process.
/*
* getFontRenderedHeight
* *************************************************************************
* Summary: Font metrics do not give an accurate measurement of the rendered
* font height for certain strings because the space between the ascender
* limit and baseline is not always fully used and descenders may not be
* present. for example the strings '0' 'a' 'f' and 'j' are all different
* heights from top to bottom but the metrics returned are always the same.
* If you want to place text that exactly fills a specific height, you need
* to work out what the exact height is for the specific string. This method
* achieves that by rendering the text and then scanning the top and bottom
* rows until the real height of the string is found.
*/
/**
* Calculate the actual height of rendered text for a specific string more
* accurately than metrics when ascenders and descenders may not be present
* <p>
* Note: this method is probably not very efficient for repeated measurement
* of large strings and large font sizes but it works quite effectively for
* short strings. Consider measuring a subset of your string value. Also
* beware of measuring symbols such as '-' and '.' the results may be
* unexpected!
*
* #param string
* The text to measure. You might be able to speed this process
* up by only measuring a single character or subset of your
* string i.e if you know your string ONLY contains numbers and
* all the numbers in the font are the same height, just pass in
* a single digit rather than the whole numeric string.
* #param font
* The font being used. Obviously the size of the font affects
* the result
* #param targetGraphicsContext
* The graphics context the text will actually be rendered in.
* This is passed in so the rendering options for anti-aliasing
* can be matched.
* #return Integer - the exact actual height of the text.
* #author Robert Heritage [mrheritage#gmail.com]
*/
public Integer getFontRenderedHeight(String string, Font font, Graphics2D targetGraphicsContext) {
BufferedImage image;
Graphics2D g;
Color textColour = Color.white;
// In the first instance; use a temporary BufferedImage object to render
// the text and get the font metrics.
image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
g = image.createGraphics();
FontMetrics metrics = g.getFontMetrics(font);
Rectangle2D rect = metrics.getStringBounds(string, g);
// now set up the buffered Image with a canvas size slightly larger than
// the font metrics - this guarantees that there is at least one row of
// black pixels at the top and the bottom
image = new BufferedImage((int) rect.getWidth() + 1, (int) metrics.getHeight() + 2, BufferedImage.TYPE_INT_RGB);
g = image.createGraphics();
// take the rendering hints from the target graphics context to ensure
// the results are accurate.
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, targetGraphicsContext.getRenderingHint(RenderingHints.KEY_ANTIALIASING));
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, targetGraphicsContext.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING));
g.setColor(textColour);
g.setFont(font);
g.drawString(string, 0, image.getHeight());
// scan the bottom row - descenders will be cropped initially, so the
// text will need to be moved up (down in the co-ordinates system) to
// fit it in the canvas if it contains any. This may need to be done a
// few times until there is a row of black pixels at the bottom.
boolean foundBottom, foundTop = false;
int offset = 0;
do {
g.setColor(Color.BLACK);
g.fillRect(0, 0, image.getWidth(), image.getHeight());
g.setColor(textColour);
g.drawString(string, 0, image.getHeight() - offset);
foundBottom = true;
for (int x = 0; x < image.getWidth(); x++) {
if (image.getRGB(x, image.getHeight() - 1) != Color.BLACK.getRGB()) {
foundBottom = false;
}
}
offset++;
} while (!foundBottom);
System.out.println(image.getHeight());
// Scan the top of the image downwards one line at a time until it
// contains a non-black pixel. This loop uses the break statement to
// stop the while loop as soon as a non-black pixel is found, this
// avoids the need to scan the rest of the line
int y = 0;
do {
for (int x = 0; x < image.getWidth(); x++) {
if (image.getRGB(x, y) != Color.BLACK.getRGB()) {
foundTop = true;
break;
}
}
y++;
} while (!foundTop);
return image.getHeight() - y;
}
Related
I am still a bit new to Java programming so sorry for the huge dump of text. I greatly appreciate you taking the time to read over my current problem!
I am working on software to help speed up the process of board game design using Java Swing. It takes a CSV file of cards for a game, lets you build a dummy card by placing where each column will render on the card and then automatically generating all of the cards in the CSV from these positions.
Many card games have symbols that represent something in the game and I want to be able to insert them in the middle of strings. I can currently replace an entire string with a symbol; as it checks if the string == a known rule, draw the symbol instead. However, I don't know how I would go about searching through a string for a specific set of characters. If it finds them, delete them from the string and then draw the corresponding symbol in it's place. A great example can be seen with the mana symbols on magic cards: https://cdn0.vox-cdn.com/uploads/chorus_asset/file/8039357/C0cIVZ5.png
So the string could be: Gain 1 {GOLD} at the start of each tun.
And it would need to replace {GOLD} with the picture of gold using the Rule class that contains the string to find and a buffered image to replace it with.
I would like this to work without using a hard limit on the size of the symbol, but that is not a hard requirement. The best solution would scale the symbol so it's height was the same of the text.
This method takes a buffered image (a card with no text) and overlays the text on top of the card.
//Will modify the buffered image with the placeables
static public BufferedImage buildCard(BufferedImage start, int whichCardID) {
//Copy so we don't lose our template
BufferedImage ni = deepCopy(start); //ni = new image
//The headers of the document
String[] headers = MainWindow.loadedCards.get(0);
//For each placeable, write down it's text
for(int i=0; i<headers.length; i++) {
//get current header
String currentHeader = headers[i];
//The Text
String theText = MainWindow.loadedCards.get(whichCardID)[i];
//The Settings
PlaceableSettings theSettings = MainWindow.placeableSettings.get(currentHeader);
//Make the change to the image
//ni = writeToImage(ni, theText, theSettings);
///////New below
boolean foundRule = false;
//see if we have a rule to draw a graphic instead
for(RuleMaster.Rule r : RuleMaster.rules) {
if(r.keyword.equals(theText)) {
//there is a rule for this!
ni = drawRuleToImage(ni, r, theSettings);
foundRule = true; //so we don't draw the current text
}
}
//No rules for this
//Make the change to the image if there are no rules
if(foundRule == false)
ni = writeToImage(ni, theText, theSettings);
}
return ni;
}
//Takes a buffered image and writes text into it at the location given
static public BufferedImage writeToImage(BufferedImage old, String text, PlaceableSettings setts) {
//make new blank graphics
BufferedImage bi = new BufferedImage(old.getWidth(), old.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = bi.createGraphics();
//write old image to it
g2d.drawImage(old, 0, 0, null); //null was set to "this" when this was not static | Note ion case this breaks
//write text on it
g2d.setPaint(setts.getColor());
g2d.setFont(setts.getFont());
//Setup word wrap
FontMetrics fm = g2d.getFontMetrics(setts.getFont());
// int rightSideBuffer = bi.getWidth() - 10;
//Rectangle2D rect = fm.getStringBounds(text, setts.getX(), rightSideBuffer, g2d); // try just -'ing the x slot from the width below
Rectangle2D rect = fm.getStringBounds(text, g2d); //this gets you the bounds for the entire image, need to remove space for x,y position
//TODO: Problem: this is always length 1
//Solution! No auto wrap, let the person define it as a setting
#SuppressWarnings("unchecked")
List<String> textList=StringUtils.wrap(text, fm, setts.getPixelsTillWrap() ); //width counted in # of characters
//g2d.drawString(text, setts.getX(), setts.getY()); //old draw with no wrap
for(int i=0; i< textList.size(); i++) {
g2d.drawString(textList.get(i), setts.getX(), setts.getY() + ( i*(setts.getFont().getSize() + 2/*Buffer*/)));
}
//!!DEBUG
if(EntryPoint.DEBUG) {
Random r = new Random();
g2d.setPaint(Color.RED);
g2d.drawString(Integer.toString(textList.size()), 100, 50+r.nextInt(250));
g2d.setPaint(Color.GREEN);
g2d.drawString(Double.toString(rect.getWidth()), 200, 50+r.nextInt(250));
g2d.setPaint(Color.PINK);
//g2d.drawString(Integer.toString(( ((int) rect.getWidth()) - setts.getX())), 100, 250+r.nextInt(100));
}
//cleanup
g2d.dispose();
return bi;
}
//Takes a buffered image and draws an image on it at the location given
static public BufferedImage drawRuleToImage(BufferedImage old, RuleMaster.Rule rule, PlaceableSettings theSettings) {
//make new blank graphics
BufferedImage bi = new BufferedImage(old.getWidth(), old.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = bi.createGraphics();
//write old image to it
g2d.drawImage(old, 0, 0, null); //null was set to "this" when this was not static | Note ion case this breaks
g2d.drawImage(rule.image, theSettings.getX(), theSettings.getY(), null);
//cleanup
g2d.dispose();
//System.exit(1);
return bi;
}
Each Rule just contains the string to replace and the image to replace it with.
static public class Rule{
//Text to look for
String keyword;
//image to replace it with
BufferedImage image;
public Rule (String key, BufferedImage img) {
keyword = key;
image = img;
}
}
I am attempting to design this as a tool for many people to use, so the text should be able to match whatever the user adds; though my current process has been to use strings such as "{M}" and that could be a standard.
Another big hurdle in this is that the text can wrap on the cards, which means that the strings and image need to wrap together in provided bounds.
Edit 1:
Had some thoughts and am going to try this approach. Still see a possible issue with the bounds when getting the 'next' half of the string drawn; but I believe this may work.
//If found a rule mid text:
//Split string in 2 at the rule match: strings 'start', and 'next'
//Calculate x and y for symbol
//x is the # of characters in ('start' % the word wrap width) +1 as the symbol is the next character, then multiply that by the character size of the font
//y is the integer of dividing the # of characters in 'start' by word wrap width multiplied by the font height
//Draw Start of String
//Draw symbol
//next x = sym.x + sym width //TODO next (x,y) math
I was able to solve the problem by warping ahead of time based on the text size. By using a known size of the image, it could be know ahead of time to get the wrap done right.
Then I looped through each line. I looked did a split in the string on the text that would be replaced. I them drew the first part of the string as normal. Calculated the width of the string using int pixelWidth = ni.getGraphics().getFontMetrics().stringWidth(splitString[j]);. Then drew the image at the same Y, but added the width of the previous string to the X. Then added the current width of the image to the X and continued looping; drawing images and strings as needed.
My app, which uses ZXing to scan QR codes, can't read a QR Code unless the phone is VERY far away from the code (see picture, 6-7+ inches away and still not reading). The code is centered and well within the framingRect, but the camera seems to only be picking up result points from the top 2 positioning squares. I have increased the size of the framing rectangle through some code which I found here, which does yield a much better result.
Code: (replaces getFramingRect from zxing.camera.cameramanager.Java)
public Rect getFramingRect() {
if (framingRect == null) {
if (camera == null) {
return null;
}
Point screenResolution = configManager.getScreenResolution();
int width = screenResolution.x * 3 / 4;
int height = screenResolution.y * 3 / 4;
Log.v("Framing rect is : ", "width is "+width+" and height is "+height);
int leftOffset = (screenResolution.x - width) / 2;
int topOffset = (screenResolution.y - height) / 2;
framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
Log.d(TAG, "Calculated framing rect: " + framingRect);
}
return framingRect;
}
For reasons beyond my comprehension, with this new larger framing rectangle, codes can be read as soon as they fit inside the rect width, whereas previously the code had to occupy a small region at the center of the rect (see pic).
My Question:
How can I make code scan as soon as it is within the bounds of the framing rect, without increasing the size of the rectangle? Why Is this happening?
Increase the width and height to 4/4 (just leave them as the screen resolution) and then change the framing rect visual representation to make it seem as if the scanner is only inside that. Worked for my app.
I have tried to make an algorithm in java to rotate a 2-d pixel array(Not restricted to 90 degrees), the only problem i have with this is: the end result leaves me with dots/holes within the image.
Here is the code :
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
int xp = (int) (nx + Math.cos(rotation) * (x - width / 2) + Math
.cos(rotation + Math.PI / 2) * (y - height / 2));
int yp = (int) (ny + Math.sin(rotation) * (x - width / 2) + Math
.sin(rotation + Math.PI / 2) * (y - height / 2));
int pixel = pixels[x + y * width];
Main.pixels[xp + yp * Main.WIDTH] = pixel;
}
}
'Main.pixels' is an array connected to a canvas display, this is what is displayed onto the monitor.
'pixels' and the function itself, is within a sprite class. The sprite class grabs the pixels from a '.png' image at initialization of the program.
I've tried looking at the 'Rotation Matrix' solutions. But they are too complicated for me. I have noticed that when the image gets closer to a point of 45 degrees, the image is some-what stretched ? What is going wrong? And what is the correct code; that adds the pixels to a larger scale array(E.g. Main.pixels[]).
Needs to be java! and relative to the code format above. I am not looking for complex examples, simply because i will not understand(As said above). Simple and straight to the point, is what i am looking for.
How id like the question to be answered.
Your formula is wrong because ....
Do this and the effect will be...
Simplify this...
Id recommend...
Im sorry if im asking to much, but i have looked for an answer relative to this question, that i can understand and use. But to always either be given a rotation of 90 degrees, or an example from another programming language.
You are pushing the pixels forward, and not every pixel is hit by the discretized rotation map. You can get rid of the gaps by calculating the source of each pixel instead.
Instead of
for each pixel p in the source
pixel q = rotate(p, theta)
q.setColor(p.getColor())
try
for each pixel q in the image
pixel p = rotate(q, -theta)
q.setColor(p.getColor())
This will still have visual artifacts. You can improve on this by interpolating instead of rounding the coordinates of the source pixel p to integer values.
Edit: Your rotation formulas looked odd, but they appear ok after using trig identities like cos(r+pi/2) = -sin(r) and sin(r+pi/2)=cos(r). They should not be the cause of any stretching.
To avoid holes you can:
compute the source coordinate from destination
(just reverse the computation to your current state) it is the same as Douglas Zare answer
use bilinear or better filtering
use less then single pixel step
usually 0.75 pixel is enough for covering the holes but you need to use floats instead of ints which sometimes is not possible (due to performance and or missing implementation or other reasons)
Distortion
if your image get distorted then you do not have aspect ratio correctly applied so x-pixel size is different then y-pixel size. You need to add scale to one axis so it matches the device/transforms applied. Here few hints:
Is the source image and destination image separate (not in place)? so Main.pixels and pixels are not the same thing... otherwise you are overwriting some pixels before their usage which could be another cause of distortion.
Just have realized you have cos,cos and sin,sin in rotation formula which is non standard and may be you got the angle delta wrongly signed somewhere so
Just to be sure here an example of the bullet #1. (reverse) with standard rotation formula (C++):
float c=Math.cos(-rotation);
float s=Math.sin(-rotation);
int x0=Main.width/2;
int y0=Main.height/2;
int x1= width/2;
int y1= height/2;
for (int a=0,y=0; y < Main.height; y++)
for (int x=0; x < Main.width; x++,a++)
{
// coordinate inside dst image rotation center biased
int xp=x-x0;
int yp=y-y0;
// rotate inverse
int xx=int(float(float(xp)*c-float(yp)*s));
int yy=int(float(float(xp)*s+float(yp)*c));
// coordinate inside src image
xp=xx+x1;
yp=yy+y1;
if ((xp>=0)&&(xp<width)&&(yp>=0)&&(yp<height))
Main.pixels[a]=pixels[xp + yp*width]; // copy pixel
else Main.pixels[a]=0; // out of src range pixel is black
}
Would it be possible to get the width of a string based on its font?
So for example, if the font size is 40, and the string was "hi", could you do 2*40.
An example of this would be
startX = 260; startWidth = "Start".length()*getFont().getSize();
startY = getHeight()-startWidth-20;
startHeight = getFont().getSize();
It really depends on the type of font, whether it's monospaced or proportional. Most font now are proportional so you would not simply be able to calculate num_of_chars * n.
The gui interface you are using should give you the width of the container once it has been populated with the given text.
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