Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d'
[fanfix.git] / src / jexer / backend / GlyphMaker.java
index 0094145dafa811c34242530a94c64404f3639e7a..0da2918def6c8d9d0f57d7a77506828039994d69 100644 (file)
@@ -38,26 +38,17 @@ import java.io.IOException;
 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 --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -67,16 +58,6 @@ public class GlyphMaker {
      */
     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.
      */
@@ -87,11 +68,6 @@ public class GlyphMaker {
      */
     private Font font = null;
 
-    /**
-     * The currently selected font size in points.
-     */
-    private int fontSize = 16;
-
     /**
      * Width of a character cell in pixels.
      */
@@ -153,132 +129,41 @@ public class GlyphMaker {
     // 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);
+            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);
+        } catch (java.awt.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);
+        } catch (java.io.IOException e) {
+            // See comment above.
+            font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
         }
-        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.
@@ -311,6 +196,11 @@ public class GlyphMaker {
             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);
@@ -327,8 +217,7 @@ public class GlyphMaker {
         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()) {
@@ -345,9 +234,8 @@ public class GlyphMaker {
             || (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()) {
@@ -357,14 +245,18 @@ public class GlyphMaker {
         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;
     }
 
@@ -375,8 +267,8 @@ public class GlyphMaker {
         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();
@@ -393,8 +285,188 @@ public class GlyphMaker {
 
         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);
+    }
+
 }