package jexer.backend;
import java.awt.Font;
+import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import jexer.bits.Cell;
+import jexer.bits.StringUtils;
/**
- * GlyphMaker creates glyphs as bitmaps from a font.
+ * GlyphMakerFont creates glyphs as bitmaps from a font.
*/
-public class GlyphMaker {
+class GlyphMakerFont {
// ------------------------------------------------------------------------
// Constants --------------------------------------------------------------
// ------------------------------------------------------------------------
- /**
- * The mono font resource filename (terminus).
- */
- public static final String MONO = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
-
- /**
- * The CJK font resource filename.
- */
- public static final String CJK = "NotoSansMonoCJKhk-Regular.otf";
-
// ------------------------------------------------------------------------
// Variables --------------------------------------------------------------
// ------------------------------------------------------------------------
*/
private static boolean DEBUG = false;
- /**
- * The instance that has the mono (default) font.
- */
- private static GlyphMaker INSTANCE_MONO;
-
- /**
- * The instance that has the CJK font.
- */
- private static GlyphMaker INSTANCE_CJK;
-
/**
* If true, we were successful at getting the font dimensions.
*/
*/
private Font font = null;
- /**
- * The currently selected font size in points.
- */
- private int fontSize = 16;
-
/**
* Width of a character cell in pixels.
*/
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
- /**
- * Private constructor used by the static instance methods.
- *
- * @param font the font to use
- */
- private GlyphMaker(final Font font) {
- this.font = font;
- fontSize = font.getSize();
- }
-
/**
* Public constructor.
*
- * @param fontName the name of the font to use
+ * @param filename the resource filename of the font to use
* @param fontSize the size of font to use
*/
- public GlyphMaker(final String fontName, final int fontSize) {
- font = new Font(fontName, Font.PLAIN, fontSize);
- }
-
- // ------------------------------------------------------------------------
- // GlyphMaker -------------------------------------------------------------
- // ------------------------------------------------------------------------
-
- /**
- * Obtain the GlyphMaker instance that uses the default monospace font.
- *
- * @return the instance
- */
- public static GlyphMaker getDefault() {
+ public GlyphMakerFont(final String filename, final int fontSize) {
- synchronized (GlyphMaker.class) {
- if (INSTANCE_MONO != null) {
- return INSTANCE_MONO;
- }
-
- int fallbackFontSize = 16;
- Font monoRoot = null;
- try {
- ClassLoader loader = Thread.currentThread().getContextClassLoader();
- InputStream in = loader.getResourceAsStream(MONO);
- monoRoot = Font.createFont(Font.TRUETYPE_FONT, in);
- } catch (java.awt.FontFormatException e) {
- e.printStackTrace();
- monoRoot = new Font(Font.MONOSPACED, Font.PLAIN,
- fallbackFontSize);
- } catch (java.io.IOException e) {
- e.printStackTrace();
- monoRoot = new Font(Font.MONOSPACED, Font.PLAIN,
- fallbackFontSize);
- }
- INSTANCE_MONO = new GlyphMaker(monoRoot);
- return INSTANCE_MONO;
+ if (filename.length() == 0) {
+ // Fallback font
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
+ return;
}
- }
- /**
- * Obtain the GlyphMaker instance that uses the CJK font.
- *
- * @return the instance
- */
- public static GlyphMaker getCJK() {
-
- synchronized (GlyphMaker.class) {
- if (INSTANCE_CJK != null) {
- return INSTANCE_CJK;
- }
-
- int fallbackFontSize = 16;
- Font cjkRoot = null;
- try {
- ClassLoader loader = Thread.currentThread().getContextClassLoader();
- InputStream in = loader.getResourceAsStream(CJK);
- cjkRoot = Font.createFont(Font.TRUETYPE_FONT, in);
- } catch (java.awt.FontFormatException e) {
- e.printStackTrace();
- cjkRoot = new Font(Font.MONOSPACED, Font.PLAIN,
- fallbackFontSize);
- } catch (java.io.IOException e) {
- e.printStackTrace();
- cjkRoot = new Font(Font.MONOSPACED, Font.PLAIN,
- fallbackFontSize);
- }
- INSTANCE_CJK = new GlyphMaker(cjkRoot);
- return INSTANCE_CJK;
- }
- }
-
- /**
- * Obtain the GlyphMaker instance that uses the correct font for this
- * character.
- *
- * @param ch the character
- * @return the instance
- */
- public static GlyphMaker getInstance(final int ch) {
- if (((ch >= 0x4e00) && (ch <= 0x9fff))
- || ((ch >= 0x3400) && (ch <= 0x4dbf))
- || ((ch >= 0x20000) && (ch <= 0x2ebef))
- ) {
- return getCJK();
+ Font fontRoot = null;
+ try {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ InputStream in = loader.getResourceAsStream(filename);
+ fontRoot = Font.createFont(Font.TRUETYPE_FONT, in);
+ font = fontRoot.deriveFont(Font.PLAIN, fontSize - 2);
+ } catch (FontFormatException e) {
+ // Ideally we would report an error here, either via System.err
+ // or TExceptionDialog. However, I do not want GlyphMaker to
+ // know about available backends, so we quietly fallback to
+ // whatever is available as MONO.
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
+ } catch (IOException e) {
+ // See comment above.
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize - 2);
}
- return getDefault();
}
- /**
- * Get a derived font at a specific size.
- *
- * @param fontSize the size to use
- * @return a new instance at that font size
- */
- public GlyphMaker size(final int fontSize) {
- GlyphMaker maker = new GlyphMaker(font.deriveFont(Font.PLAIN,
- fontSize));
- return maker;
- }
-
- /**
- * Get a glyph image, using the font's idea of cell width and height.
- *
- * @param cell the character to draw
- * @return the glyph as an image
- */
- public BufferedImage getImage(final Cell cell) {
- return getImage(cell, textWidth, textHeight, true);
- }
+ // ------------------------------------------------------------------------
+ // GlyphMakerFont ---------------------------------------------------------
+ // ------------------------------------------------------------------------
/**
* Get a glyph image.
getFontDimensions();
}
+ if (DEBUG && !font.canDisplay(cell.getChar())) {
+ System.err.println("font " + font + " has no glyph for " +
+ String.format("0x%x", cell.getChar()));
+ }
+
BufferedImage image = null;
if (cell.isBlink() && !blinkVisible) {
image = glyphCacheBlink.get(cell);
Graphics2D gr2 = image.createGraphics();
gr2.setFont(font);
- Cell cellColor = new Cell();
- cellColor.setTo(cell);
+ Cell cellColor = new Cell(cell);
// Check for reverse
if (cell.isReverse()) {
|| (cell.isBlink() && blinkVisible)
) {
gr2.setColor(SwingTerminal.attrToForegroundColor(cellColor));
- char [] chars = new char[1];
- chars[0] = cell.getChar();
- gr2.drawChars(chars, 0, 1, textAdjustX,
+ char [] chars = Character.toChars(cell.getChar());
+ gr2.drawChars(chars, 0, chars.length, textAdjustX,
cellHeight - maxDescent + textAdjustY);
if (cell.isUnderline()) {
gr2.dispose();
// We need a new key that will not be mutated by invertCell().
- Cell key = new Cell();
- key.setTo(cell);
+ Cell key = new Cell(cell);
if (cell.isBlink() && !blinkVisible) {
glyphCacheBlink.put(key, image);
} else {
glyphCache.put(key, image);
}
+ /*
+ System.err.println("cellWidth " + cellWidth +
+ " cellHeight " + cellHeight + " image " + image);
+ */
+
return image;
}
glyphCacheBlink = new HashMap<Cell, BufferedImage>();
glyphCache = new HashMap<Cell, BufferedImage>();
- BufferedImage image = new BufferedImage(fontSize * 2, fontSize * 2,
- BufferedImage.TYPE_INT_ARGB);
+ BufferedImage image = new BufferedImage(font.getSize() * 2,
+ font.getSize() * 2, BufferedImage.TYPE_INT_ARGB);
Graphics2D gr = image.createGraphics();
gr.setFont(font);
FontMetrics fm = gr.getFontMetrics();
textHeight = fontTextHeight + textAdjustHeight;
textWidth = fontTextWidth + textAdjustWidth;
+ /*
+ System.err.println("font " + font);
+ System.err.println("fontTextWidth " + fontTextWidth);
+ System.err.println("fontTextHeight " + fontTextHeight);
+ System.err.println("textWidth " + textWidth);
+ System.err.println("textHeight " + textHeight);
+ */
gotFontDimensions = true;
}
+ /**
+ * Checks if this maker's Font has a glyph for the specified character.
+ *
+ * @param codePoint the character (Unicode code point) for which a glyph
+ * is needed.
+ * @return true if this Font has a glyph for the character; false
+ * otherwise.
+ */
+ public boolean canDisplay(final int codePoint) {
+ return font.canDisplay(codePoint);
+ }
+}
+
+/**
+ * GlyphMaker presents unified interface to all of its supported fonts to
+ * clients.
+ */
+public class GlyphMaker {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The mono font resource filename (terminus).
+ */
+ private static final String MONO = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
+
+ /**
+ * The CJK font resource filename.
+ */
+ private static final String cjkFontFilename = "NotoSansMonoCJKtc-Regular.otf";
+
+ /**
+ * The emoji font resource filename.
+ */
+ private static final String emojiFontFilename = "OpenSansEmoji.ttf";
+
+ /**
+ * The fallback font resource filename.
+ */
+ private static final String fallbackFontFilename = "";
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, enable debug messages.
+ */
+ private static boolean DEBUG = false;
+
+ /**
+ * Cache of font bundles by size.
+ */
+ private static HashMap<Integer, GlyphMaker> makers = new HashMap<Integer, GlyphMaker>();
+
+ /**
+ * The instance that has the mono (default) font.
+ */
+ private GlyphMakerFont makerMono;
+
+ /**
+ * The instance that has the CJK font.
+ */
+ private GlyphMakerFont makerCjk;
+
+ /**
+ * The instance that has the emoji font.
+ */
+ private GlyphMakerFont makerEmoji;
+
+ /**
+ * The instance that has the fallback font.
+ */
+ private GlyphMakerFont makerFallback;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Create an instance with references to the necessary fonts.
+ *
+ * @param fontSize the size of these fonts in pixels
+ */
+ private GlyphMaker(final int fontSize) {
+ makerMono = new GlyphMakerFont(MONO, fontSize);
+
+ String fontFilename = null;
+ fontFilename = System.getProperty("jexer.cjkFont.filename",
+ cjkFontFilename);
+ makerCjk = new GlyphMakerFont(fontFilename, fontSize);
+ fontFilename = System.getProperty("jexer.emojiFont.filename",
+ emojiFontFilename);
+ makerEmoji = new GlyphMakerFont(fontFilename, fontSize);
+ fontFilename = System.getProperty("jexer.fallbackFont.filename",
+ fallbackFontFilename);
+ makerFallback = new GlyphMakerFont(fontFilename, fontSize);
+ }
+
+ // ------------------------------------------------------------------------
+ // GlyphMaker -------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Obtain the GlyphMaker instance for a particular font size.
+ *
+ * @param fontSize the size of these fonts in pixels
+ * @return the instance
+ */
+ public static GlyphMaker getInstance(final int fontSize) {
+ synchronized (GlyphMaker.class) {
+ GlyphMaker maker = makers.get(fontSize);
+ if (maker == null) {
+ maker = new GlyphMaker(fontSize);
+ makers.put(fontSize, maker);
+ }
+ return maker;
+ }
+ }
+
+ /**
+ * Get a glyph image.
+ *
+ * @param cell the character to draw
+ * @param cellWidth the width of the text cell to draw into
+ * @param cellHeight the height of the text cell to draw into
+ * @return the glyph as an image
+ */
+ public BufferedImage getImage(final Cell cell, final int cellWidth,
+ final int cellHeight) {
+
+ return getImage(cell, cellWidth, cellHeight, true);
+ }
+
+ /**
+ * Get a glyph image.
+ *
+ * @param cell the character to draw
+ * @param cellWidth the width of the text cell to draw into
+ * @param cellHeight the height of the text cell to draw into
+ * @param blinkVisible if true, the cell is visible if it is blinking
+ * @return the glyph as an image
+ */
+ public BufferedImage getImage(final Cell cell, final int cellWidth,
+ final int cellHeight, final boolean blinkVisible) {
+
+ int ch = cell.getChar();
+ if (StringUtils.isCjk(ch)) {
+ if (makerCjk.canDisplay(ch)) {
+ return makerCjk.getImage(cell, cellWidth, cellHeight,
+ blinkVisible);
+ }
+ }
+ if (StringUtils.isEmoji(ch)) {
+ if (makerEmoji.canDisplay(ch)) {
+ // System.err.println("emoji: " + String.format("0x%x", ch));
+ return makerEmoji.getImage(cell, cellWidth, cellHeight,
+ blinkVisible);
+ }
+ }
+
+ // When all else fails, use the default.
+ if (makerMono.canDisplay(ch)) {
+ return makerMono.getImage(cell, cellWidth, cellHeight,
+ blinkVisible);
+ }
+
+ return makerFallback.getImage(cell, cellWidth, cellHeight,
+ blinkVisible);
+ }
+
}