Added more accurate font height utility methods. Moved font utility

methods into FontUtil class.
This commit is contained in:
nathan 2017-06-30 22:27:44 +02:00
parent 61f4f5a4de
commit 01e5eab0ca
2 changed files with 282 additions and 223 deletions

View File

@ -0,0 +1,279 @@
/*
* Copyright 2014 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.util;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Enumeration;
/**
* Java Font utilities
*/
public
class FontUtil {
/** Default location where all the fonts are stored */
@Property
public static String FONTS_LOCATION = "resources/fonts";
/** All of the fonts in the {@link #FONTS_LOCATION} will be loaded by the Font manager */
public static
void loadAllFonts() {
boolean isJava6 = OS.javaVersion == 6;
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
Enumeration<URL> fonts = LocationResolver.getResources(FONTS_LOCATION);
if (fonts.hasMoreElements()) {
// skip the FIRST one, since we always know that the first one is the directory we asked for
fonts.nextElement();
while (fonts.hasMoreElements()) {
URL url = fonts.nextElement();
InputStream is = null;
//noinspection TryWithIdenticalCatches
try {
String path = url.toURI()
.getPath();
// only support TTF fonts (java6) and OTF fonts (7+).
if (path.endsWith(".ttf") || (!isJava6 && path.endsWith(".otf"))) {
is = url.openStream();
Font newFont = Font.createFont(Font.TRUETYPE_FONT, is);
// fonts that ALREADY exist are not re-registered
ge.registerFont(newFont);
}
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (FontFormatException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
/**
* Gets (or creates) a Font based on a specific system property. Remember: the FontManager caches system/loaded fonts, so we don't need
* to ALSO cache them as well. see: https://stackoverflow.com/questions/6102602/java-awt-is-font-a-lightweight-object
* <p>
* Also remember that if requesting a BOLD hint for a font, the system will look for a font that is BOLD. If none are found, it
* will then apply transforms to the specified font to create a font that is bold. Specifying a bold name AND a bold hint will not
* "double bold" the font
* <p></p>
* For example:
* <p>
*
* Font titleTextFont = SwingUtil.parseFont("Source Code Pro Bold 16");
*
* @param fontInfo This is the font "name style size", as a string. For example "Source Code Pro Bold BOLD 16"
*
* @return the specified font
*/
public static
Font parseFont(final String fontInfo) {
try {
final int sizeIndex = fontInfo.lastIndexOf(" ");
String size = fontInfo.substring(sizeIndex + 1);
// hint is at most 6 (ITALIC) before sizeIndex - we can use this to our benefit.
int styleIndex = fontInfo.indexOf(" ", sizeIndex - 7);
String styleString = fontInfo.substring(styleIndex + 1, sizeIndex);
int style = Font.PLAIN;
if (styleString.equalsIgnoreCase("bold")) {
style = Font.BOLD;
}
else if (styleString.equalsIgnoreCase("italic")) {
style = Font.ITALIC;
}
String fontName = fontInfo.substring(0, styleIndex);
// this can be WRONG, in which case it will just error out
//noinspection MagicConstant
return new Font(fontName, style, Integer.parseInt(size));
} catch (Exception e) {
throw new RuntimeException("Unable to load font info from '" + fontInfo + "'", e);
}
}
/**
* Gets the correct font (in GENERAL) for a specified pixel height.
*
* @param font the font we are checking
* @param height the height in pixels we want to get as close as possible to
*
* @return the font (derived from the specified font) that is as close as possible to the requested height. If our font-size is less
* than the height, then the approach is from the low size (so the returned font will always fit inside the box)
*/
public static
Font getFontForSpecificHeight(final Font font, final int height) {
int size = font.getSize();
Boolean lastAction = null;
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
while (true) {
Font fontCheck = new Font(font.getName(), Font.PLAIN, size);
FontMetrics metrics = g.getFontMetrics(fontCheck);
Rectangle2D rect = metrics.getStringBounds("`Tj|┃", g); // `Tj| are glyphs that are at the top/bottom of the fontset (usually)
double testHeight = rect.getHeight();
if (testHeight < height && lastAction != Boolean.FALSE) {
size++;
lastAction = Boolean.TRUE;
} else if (testHeight > height && lastAction != Boolean.TRUE) {
size--;
lastAction = Boolean.FALSE;
} else {
// either we are the exact size, or we are ONE font size to big/small (depending on what our initial guess was)
g.dispose();
return fontCheck;
}
}
}
/**
* Gets the specified font height for a specific string
*
* @param font the font to use
* @param string the string to get the size of
*
* @return the height of the string
*/
public static
int getFontHeight(final Font font, final String string) {
BufferedImage image = new BufferedImage(1, 1, 1);
Graphics2D g = image.createGraphics();
FontRenderContext frc = g.getFontRenderContext();
GlyphVector gv = font.createGlyphVector(frc, string);
int height = gv.getPixelBounds(null, 0, 0).height;
g.dispose();
return height;
}
/**
* Gets the maximum font height used by alpha-numeric characters ONLY
*/
public static
int getAlphaNumbericFontHeight(final Font font) {
// Because font metrics is based on a graphics context, we need to create a small, temporary image to determine the width and height
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
FontMetrics metrics = g.getFontMetrics(font);
int height = metrics.getAscent() + metrics.getDescent();
g.dispose();
return height;
}
/**
* Gets the maximum font height used by of ALL characters.
*/
public static
int getFontHeight(final Font font) {
// Because font metrics is based on a graphics context, we need to create a small, temporary image to determine the width and height
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
FontMetrics metrics = g.getFontMetrics(font);
int height = metrics.getMaxAscent() + metrics.getMaxDescent();
g.dispose();
return height;
}
/**
* Gets the specified text (with a font) and as an image
*
* @param font the specified font to render the image
* @return a BufferedImage of the specified text, font, and color
*/
public static
BufferedImage getFontAsImage(final Font font, String text, Color foregroundColor) {
// Because font metrics is based on a graphics context, we need to create a small, temporary image to determine the width and height
BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.setFont(font);
FontMetrics fm = g2d.getFontMetrics();
int width = fm.stringWidth(text);
int height = fm.getHeight();
g2d.dispose();
// make it square
if (width > height) {
height = width;
} else {
width = height;
}
img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
g2d = img.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
g2d.setFont(font);
fm = g2d.getFontMetrics();
g2d.setColor(foregroundColor);
// width/4 centers the text in the image
g2d.drawString(text, width/4.0f, fm.getAscent());
g2d.dispose();
return img;
}
}

View File

@ -15,38 +15,27 @@
*/
package dorkbox.util;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowListener;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Locale;
import javax.swing.AbstractButton;
@ -57,19 +46,11 @@ import javax.swing.UIManager;
public
class SwingUtil {
/** Default location where all the fonts are stored */
@Property
public static String FONTS_LOCATION = "resources/fonts";
static {
/*
* hack workaround for starting the Toolkit thread before any Timer stuff
* javax.swing.Timer uses the Event Dispatch Thread, which is not
* created until the Toolkit thread starts up. Using the Swing
* Timer before starting this stuff starts up may get unexpected
* results (such as taking a long time before the first timer
* event).
* hack workaround for starting the Toolkit thread before any Timer stuff javax.swing.Timer uses the Event Dispatch Thread, which is not
* created until the Toolkit thread starts up. Using the Swing Timer before starting this stuff starts up may get unexpected
* results (such as taking a long time before the first timer event).
*/
Toolkit.getDefaultToolkit();
}
@ -173,100 +154,6 @@ class SwingUtil {
new Exception("Could not load " + lookAndFeel + ", it was not available.").printStackTrace();
}
/** All of the fonts in the {@link #FONTS_LOCATION} will be loaded by the Font manager */
public static
void loadAllFonts() {
boolean isJava6 = OS.javaVersion == 6;
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
Enumeration<URL> fonts = LocationResolver.getResources(FONTS_LOCATION);
if (fonts.hasMoreElements()) {
// skip the FIRST one, since we always know that the first one is the directory we asked for
fonts.nextElement();
while (fonts.hasMoreElements()) {
URL url = fonts.nextElement();
InputStream is = null;
//noinspection TryWithIdenticalCatches
try {
String path = url.toURI()
.getPath();
// only support TTF fonts (java6) and OTF fonts (7+).
if (path.endsWith(".ttf") || (!isJava6 && path.endsWith(".otf"))) {
is = url.openStream();
Font newFont = Font.createFont(Font.TRUETYPE_FONT, is);
// fonts that ALREADY exist are not re-registered
ge.registerFont(newFont);
}
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (FontFormatException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
/**
* Gets (or creates) a Font based on a specific system property. Remember: the FontManager caches system/loaded fonts, so we don't need
* to ALSO cache them as well. see: https://stackoverflow.com/questions/6102602/java-awt-is-font-a-lightweight-object
* <p>
* Also remember that if requesting a BOLD hint for a font, the system will look for a font that is BOLD. If none are found, it
* will then apply transforms to the specified font to create a font that is bold. Specifying a bold name AND a bold hint will not
* "double bold" the font
* <p></p>
* For example:
* <p>
*
* Font titleTextFont = SwingUtil.parseFont("Source Code Pro Bold 16");
*
* @param fontInfo This is the font "name style size", as a string. For example "Source Code Pro Bold BOLD 16"
*
* @return the specified font
*/
public static
Font parseFont(final String fontInfo) {
try {
final int sizeIndex = fontInfo.lastIndexOf(" ");
String size = fontInfo.substring(sizeIndex + 1);
// hint is at most 6 (ITALIC) before sizeIndex - we can use this to our benefit.
int styleIndex = fontInfo.indexOf(" ", sizeIndex - 7);
String styleString = fontInfo.substring(styleIndex + 1, sizeIndex);
int style = Font.PLAIN;
if (styleString.equalsIgnoreCase("bold")) {
style = Font.BOLD;
}
else if (styleString.equalsIgnoreCase("italic")) {
style = Font.ITALIC;
}
String fontName = fontInfo.substring(0, styleIndex);
// this can be WRONG, in which case it will just error out
//noinspection MagicConstant
return new Font(fontName, style, Integer.parseInt(size));
} catch (Exception e) {
throw new RuntimeException("Unable to load font info from '" + fontInfo + "'", e);
}
}
/** used when setting various icon components in the GUI to "nothing", since null doesn't work */
public static final Image BLANK_ICON = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB_PRE);
@ -318,113 +205,6 @@ class SwingUtil {
}
}
/**
* Gets the correct font (in GENERAL) for a specified pixel height.
*
* @param font the font we are checking
* @param height the height in pixels we want to get as close as possible to
*
* @return the font (derived from the specified font) that is as close as possible to the requested height. If our font-size is less
* than the height, then the approach is from the low size (so the returned font will always fit inside the box)
*/
public static
Font getFontForSpecificHeight(final Font font, final int height) {
int size = font.getSize();
Boolean lastAction = null;
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
while (true) {
Font fontCheck = new Font(font.getName(), Font.PLAIN, size);
FontMetrics metrics = g.getFontMetrics(fontCheck);
Rectangle2D rect = metrics.getStringBounds("`Tj|┃", g); // `Tj| are glyphs that are at the top/bottom of the fontset (usually)
double testHeight = rect.getHeight();
if (testHeight < height && lastAction != Boolean.FALSE) {
size++;
lastAction = Boolean.TRUE;
} else if (testHeight > height && lastAction != Boolean.TRUE) {
size--;
lastAction = Boolean.FALSE;
} else {
// either we are the exact size, or we are ONE font size to big/small (depending on what our initial guess was)
g.dispose();
return fontCheck;
}
}
}
/**
* Gets the specified font height
* @param font
* @return
*/
public static
int getFontHeight(final Font font) {
// Because font metrics is based on a graphics context, we need to create a small, temporary image to determine the width and height
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
FontMetrics metrics = g.getFontMetrics(font);
Rectangle2D rect = metrics.getStringBounds("`Tj|┃", g); // `Tj| are glyphs that are at the top/bottom of the fontset (usually)
int testHeight = (int) rect.getHeight();
g.dispose();
return testHeight;
}
/**
* Gets the specified text (with a font) and as an image
*
* @param font the specified font to render the image
* @return a BufferedImage of the specified text, font, and color
*/
public static
BufferedImage getFontAsImage(final Font font, String text, Color foregroundColor) {
// Because font metrics is based on a graphics context, we need to create a small, temporary image to determine the width and height
BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.setFont(font);
FontMetrics fm = g2d.getFontMetrics();
int width = fm.stringWidth(text);
int height = fm.getHeight();
g2d.dispose();
// make it square
if (width > height) {
height = width;
} else {
width = height;
}
img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
g2d = img.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
g2d.setFont(font);
fm = g2d.getFontMetrics();
g2d.setColor(foregroundColor);
// width/4 centers the text in the image
g2d.drawString(text, width/4.0f, fm.getAscent());
g2d.dispose();
return img;
}
/**
* Gets the largest icon/image for a button (or other JComponent that has .setIcon(image) method) without affecting the size of the
* button. An image that is any larger will require that the button increases it's height or width.