/** * Copyright 2012 Jason Sorensen (sorensenj@smert.net) * * 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 net.smert.frameworkgl.opengl.font; import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.font.TextAttribute; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JScrollPane; import net.smert.frameworkgl.opengl.Texture; import net.smert.frameworkgl.opengl.renderable.AbstractRenderable; import net.smert.frameworkgl.opengl.renderable.Renderable; import net.smert.frameworkgl.utils.HashMapIntGeneric; /** * * @author Jason Sorensen <sorensenj@smert.net> */ public class AwtFont implements GLFont { private final Builder builder; private Glyph missingGlyph; private final HashMapIntGeneric<CodePage> codePageIndexToCodePage; private final List<CodePointRange> glyphsToLoad; public AwtFont(Font font) { this(true, true, font); } public AwtFont(boolean leftToRight, Font font) { this(true, leftToRight, font); } public AwtFont(boolean antiAliasing, boolean leftToRight, Font font) { builder = new Builder(antiAliasing, leftToRight, font); builder.createDefault(); codePageIndexToCodePage = new HashMapIntGeneric<>(); glyphsToLoad = new ArrayList<>(); } public void addAscii7BitGlyphs() { addGlyphs(32, 127); } public void addAscii8BitGlyphs() { addGlyphs(32, 255); } public void addGlyphs(int startCodePoint, int endCodePoint) { glyphsToLoad.add(new CodePointRange(startCodePoint, endCodePoint)); } public void addUsAsciiGlyphs() { addAscii7BitGlyphs(); } public void destroy() { Iterator<CodePage> iterator = codePageIndexToCodePage.values().iterator(); while (iterator.hasNext()) { CodePage codePage = iterator.next(); String filename = codePage.getFontTextureFilename(); codePage.destroy(); Texture texture = Renderable.texturePool.remove(filename); if (texture != null) { texture.destroy(); } } codePageIndexToCodePage.clear(); glyphsToLoad.clear(); } public int getFontHeight() { return builder.getFontHeight(); } public int getFontPaddingX() { return builder.getFontPaddingX(); } public void setFontPaddingX(int fontPaddingX) { builder.setFontPaddingX(fontPaddingX); } public int getFontPaddingY() { return builder.getFontPaddingY(); } public void setFontPaddingY(int fontPaddingY) { builder.setFontPaddingY(fontPaddingY); } public int getFontSpacing() { return builder.getFontSpacing(); } public void setFontSpacing(int fontSpacing) { builder.setFontSpacing(fontSpacing); } public int getFontWidth() { return builder.getFontHeight(); } public Glyph getGlyph(int codePoint) { // Get the code page for the code point int codePageIndex = CodePage.GetPageIndex(codePoint); CodePage codePage = codePageIndexToCodePage.get(codePageIndex); if (codePage == null) { return missingGlyph; } // Get the glyph for the code point int glyphIndex = Glyph.GetGlyphIndex(codePoint); Glyph glyph = codePage.getGlyph(glyphIndex); if (glyph == null) { return missingGlyph; } return glyph; } public int getImageDimensions() { return builder.getImageDimensions(); } public void setImageDimensions(int imageDimensions) { builder.setImageDimensions(imageDimensions); } public HashMapIntGeneric<CodePage> getCodePages() { return codePageIndexToCodePage; } public Glyph getMissingGlyph() { return missingGlyph; } public void setMissingGlyph(Glyph missingGlyph) { this.missingGlyph = missingGlyph; } public String getFilename(int codePoint) { int codePageIndex = CodePage.GetPageIndex(codePoint); return "__Internal_AwtFont_" + builder.hashCode() + "_CPI_" + codePageIndex; } public void loadGlyphs() { for (CodePointRange codePointRange : glyphsToLoad) { int startCodePoint = codePointRange.getStartCodePoint(); int endCodePoint = codePointRange.getEndCodePoint(); for (int codePoint = startCodePoint; codePoint <= endCodePoint; codePoint++) { char[] chars = Character.toChars(codePoint); String text = new String(chars); GlyphVector vector = builder.createGlyphVector(chars); for (int i = 0; i < vector.getNumGlyphs(); i++) { int fontCodePoint = text.codePointAt(vector.getGlyphCharIndex(i)); // Make sure this is legit if (!Character.isValidCodePoint(fontCodePoint) || !Character.isDefined(fontCodePoint)) { continue; } // Get the pixel bounds for the current character Rectangle bounds = vector.getGlyphPixelBounds(i, builder.getDefaultFontRenderContext(), 0, 0); // Do not define a glyph for zero length characters double charWidth = bounds.getWidth(); if (charWidth == 0) { continue; } // Create a code page if it does not exist int codePageIndex = CodePage.GetPageIndex(fontCodePoint); CodePage codePage = codePageIndexToCodePage.get(codePageIndex); if (codePage == null) { codePage = new CodePage(builder); codePageIndexToCodePage.put(codePageIndex, codePage); } // Create a glphy if it does not exist int glyphIndex = Glyph.GetGlyphIndex(fontCodePoint); Glyph glyph = codePage.getGlyph(glyphIndex); if (glyph == null) { glyph = new Glyph(); codePage.setGlyph(fontCodePoint, glyph); } // Set glyph values glyph.codePage = codePage; glyph.codePoint = fontCodePoint; glyph.h = builder.getFontHeight(); glyph.w = builder.getCharWidth(fontCodePoint); // Ensure the min height and width are at least 1 if (glyph.h <= 0) { glyph.h = 1; } if (glyph.w <= 0) { glyph.w = 1; } // Draw the glyph inside the code page image codePage.drawGlyph(builder, glyph); } } } glyphsToLoad.clear(); } public void showBufferedImagesFromCodePages() { Iterator<CodePage> iterator = codePageIndexToCodePage.values().iterator(); while (iterator.hasNext()) { CodePage codePage = iterator.next(); JFrame frame = new JFrame(); JScrollPane scrollPane = new JScrollPane(new JLabel(new ImageIcon(codePage.image))); frame.getContentPane().add(scrollPane, BorderLayout.CENTER); frame.pack(); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.setLocationRelativeTo(null); frame.setVisible(true); } } @Override public int getCharacterAdvance(char currentCharacter, char nextCharacter, float sizeX) { throw new UnsupportedOperationException("Not supported yet."); } @Override public int getWidth(String text, float sizeX) { throw new UnsupportedOperationException("Not supported yet."); } public static class Builder { private final boolean antiAliasing; private int fontAscent; private int fontDescent; private final int fontDirection; private int fontHeight; private int fontLeading; private int fontPaddingX; private int fontPaddingY; private int fontSpacing; private int imageDimensions; private BufferedImage defaultImage; private Font font; private FontMetrics defaultFontMetrics; private FontRenderContext defaultFontRenderContext; private Graphics2D defaultGraphics; public Builder(boolean antiAliasing, boolean leftToRight, Font font) { this.antiAliasing = antiAliasing; // Font direction if (leftToRight) { fontDirection = Font.LAYOUT_LEFT_TO_RIGHT; } else { fontDirection = Font.LAYOUT_RIGHT_TO_LEFT; } Map attributes = font.getAttributes(); attributes.put(TextAttribute.KERNING, TextAttribute.KERNING_ON); this.font = font.deriveFont(attributes); } public void createDefault() { defaultImage = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB); defaultGraphics = createGraphics(defaultImage); defaultFontMetrics = defaultGraphics.getFontMetrics(); defaultFontRenderContext = defaultGraphics.getFontRenderContext(); fontAscent = defaultFontMetrics.getAscent(); fontDescent = defaultFontMetrics.getDescent(); fontHeight = defaultFontMetrics.getHeight(); fontLeading = defaultFontMetrics.getLeading(); fontPaddingX = 5; fontPaddingY = 2; fontSpacing = defaultFontMetrics.charWidth(' '); imageDimensions = 64 * ((font.getSize() / 5) + 1) + 32; } public BufferedImage createImage() { return new BufferedImage(imageDimensions, imageDimensions, BufferedImage.TYPE_INT_ARGB); } public GlyphVector createGlyphVector(char[] chars) { return font.layoutGlyphVector(defaultFontRenderContext, chars, 0, chars.length, fontDirection); } public Graphics2D createGraphics(BufferedImage image) { Graphics2D graphics = image.createGraphics(); graphics.setComposite(AlphaComposite.SrcOver); graphics.setFont(font); if (antiAliasing) { graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } return graphics; } public void destroy() { defaultImage = null; font = null; defaultFontMetrics = null; defaultFontRenderContext = null; if (defaultGraphics != null) { defaultGraphics.dispose(); defaultGraphics = null; } } public int getCharWidth(int codePoint) { return defaultFontMetrics.charWidth(codePoint); } public int getFontAscent() { return fontAscent; } public int getFontDescent() { return fontDescent; } public int getFontHeight() { return fontHeight; } public int getFontLeading() { return fontLeading; } public int getFontPaddingX() { return fontPaddingX; } public void setFontPaddingX(int fontPaddingX) { this.fontPaddingX = fontPaddingX; } public int getFontPaddingY() { return fontPaddingY; } public void setFontPaddingY(int fontPaddingY) { this.fontPaddingY = fontPaddingY; } public int getFontSpacing() { return fontSpacing; } public void setFontSpacing(int fontSpacing) { this.fontSpacing = fontSpacing; } public int getImageDimensions() { return imageDimensions; } public void setImageDimensions(int imageDimensions) { this.imageDimensions = imageDimensions; } public FontRenderContext getDefaultFontRenderContext() { return defaultFontRenderContext; } @Override public int hashCode() { int hash = 7; hash = 71 * hash + (this.antiAliasing ? 1 : 0); hash = 71 * hash + this.fontAscent; hash = 71 * hash + this.fontDescent; hash = 71 * hash + this.fontDirection; hash = 71 * hash + this.fontHeight; hash = 71 * hash + this.fontLeading; hash = 71 * hash + this.fontPaddingX; hash = 71 * hash + this.fontPaddingY; hash = 71 * hash + this.imageDimensions; hash = 71 * hash + Objects.hashCode(this.font); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Builder other = (Builder) obj; if (this.antiAliasing != other.antiAliasing) { return false; } if (this.fontAscent != other.fontAscent) { return false; } if (this.fontDescent != other.fontDescent) { return false; } if (this.fontDirection != other.fontDirection) { return false; } if (this.fontHeight != other.fontHeight) { return false; } if (this.fontLeading != other.fontLeading) { return false; } if (this.fontPaddingX != other.fontPaddingX) { return false; } if (this.fontPaddingY != other.fontPaddingY) { return false; } if (this.imageDimensions != other.imageDimensions) { return false; } return Objects.equals(this.font, other.font); } } public static class CodePage { public final static int GLYPHS_PER_PAGE = 256; public final static int MAX_PAGES = Character.MAX_CODE_POINT / GLYPHS_PER_PAGE; private int imageX; private int imageY; private BufferedImage image; private Graphics2D graphics; private final HashMapIntGeneric<Glyph> indexToGlyph; private String fontTextureFilename; public CodePage(Builder builder) { imageX = 0; imageY = 0; image = builder.createImage(); graphics = builder.createGraphics(image); indexToGlyph = new HashMapIntGeneric<>(); } public void destroy() { image = null; if (graphics != null) { graphics.dispose(); graphics = null; } destroyGlyphsAndTexture(); } public void destroyGlyphsAndTexture() { Iterator<Glyph> iterator = indexToGlyph.values().iterator(); while (iterator.hasNext()) { Glyph glyph = iterator.next(); glyph.destroy(); } indexToGlyph.clear(); fontTextureFilename = null; } public void drawGlyph(Builder builder, Glyph glyph) { // Create new image and graphics from glyph BufferedImage glyphImage = new BufferedImage(glyph.w, glyph.h, BufferedImage.TYPE_INT_ARGB); Graphics2D glyphGraphics = builder.createGraphics(glyphImage); // Render the glyph String charString = new String(Character.toChars(glyph.codePoint)); glyphGraphics.setColor(Color.WHITE); glyphGraphics.drawString(charString, 0, glyph.h - builder.getFontDescent()); glyphGraphics.dispose(); // Form a new row if the current image will be past the bounds in the X direction if ((imageX + glyphImage.getWidth() + builder.getFontPaddingX()) >= image.getWidth()) { imageX = 0; imageY += glyph.h + builder.getFontPaddingY(); } // Set glyph to the current X, Y glyph.x = imageX; glyph.y = imageY; // Draw the glyph image in the code page image graphics.drawImage(glyphImage, imageX, imageY, null); // Advance to the next position imageX += glyph.w + builder.getFontPaddingX(); } public int getImageX() { return imageX; } public int getImageY() { return imageY; } public BufferedImage getImage() { return image; } public Glyph getGlyph(int index) { return indexToGlyph.get(index); } public void setGlyph(int index, Glyph glyph) { indexToGlyph.put(index, glyph); } public String getFontTextureFilename() { return fontTextureFilename; } public void setFontTextureFilename(String fontTextureFilename) { this.fontTextureFilename = fontTextureFilename; } public void resetImage() { // Clear image graphics.setComposite(AlphaComposite.Clear); graphics.fillRect(0, 0, image.getWidth(), image.getHeight()); graphics.setComposite(AlphaComposite.SrcOver); // Set image X, Y back to origin imageX = 0; imageY = 0; // Destroy any existing glyphs and textures since they won't match anymore destroyGlyphsAndTexture(); } public static int GetPageIndex(int codePoint) { return codePoint / GLYPHS_PER_PAGE; } } public static class CodePointRange { private final int endCodePoint; private final int startCodePoint; public CodePointRange(int startCodePoint, int endCodePoint) { this.endCodePoint = endCodePoint; this.startCodePoint = startCodePoint; } public int getEndCodePoint() { return endCodePoint; } public int getStartCodePoint() { return startCodePoint; } } public static class Glyph { public int codePoint; public int x, y, w, h; public AbstractRenderable renderable; public CodePage codePage; public void destroy() { if (renderable != null) { renderable.destroy(); renderable = null; } } public static int GetGlyphIndex(int codePoint) { return codePoint % CodePage.GLYPHS_PER_PAGE; } } }