From 01e5eab0ca3db7faeaa863e7468e677d1849d2fa Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 30 Jun 2017 22:27:44 +0200 Subject: [PATCH] Added more accurate font height utility methods. Moved font utility methods into FontUtil class. --- src/dorkbox/util/FontUtil.java | 279 ++++++++++++++++++++++++++++++++ src/dorkbox/util/SwingUtil.java | 226 +------------------------- 2 files changed, 282 insertions(+), 223 deletions(-) create mode 100644 src/dorkbox/util/FontUtil.java diff --git a/src/dorkbox/util/FontUtil.java b/src/dorkbox/util/FontUtil.java new file mode 100644 index 0000000..5e7e4a7 --- /dev/null +++ b/src/dorkbox/util/FontUtil.java @@ -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 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 + *

+ * 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 + *

+ * For example: + *

+ * + * 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; + } +} diff --git a/src/dorkbox/util/SwingUtil.java b/src/dorkbox/util/SwingUtil.java index ec8995b..7441f99 100644 --- a/src/dorkbox/util/SwingUtil.java +++ b/src/dorkbox/util/SwingUtil.java @@ -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 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 - *

- * 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 - *

- * For example: - *

- * - * 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.