Search code examples
javafontsjava-2dgraphics2dfallback

How do I specify fallback fonts in Java2D/Graphics2D


I'm using g.drawString(str, x, y) to draw a String with a Graphics2D object g. The current font of g does not cover all the characters of str (I have e.g. Chinese chars in there). On Mac OS X, a fallback font seems to be automatically used, but not on Windows, where black square outlines appear instead of the wanted characters.

  • Why is the behavior different depending on the platform?
  • How do I specify a fallback font (or several fallback fonts) in case of missing characters?

(For instance, one of the nice fonts there.)

Update/More Info

So, the original font that doesn't support all characters is not one of the JVM's logical fonts, but is a bundled font that comes with my app and was obtained with Font.createFont(). So, adding fonts to the JRE's lib/fonts/fallback folder doesn't work here.


Solution

  • We could attribute string to switch font on "bad" symbols and use Graphics2D.drawString(AttributedCharacterIterator iterator, int x, int y) to render result. Advantage: this will work with any font. Drawback: without some sort of caching of intermediate objects this will work slower and dirtier.

    So, i suggest using AttributedString with main font attribute on whole string:

    AttributedString astr = new AttributedString(text);
    astr.addAttribute(TextAttribute.FONT, mainFont, 0, textLength);
    

    and with fallback font on specific parts:

    astr.addAttribute(TextAttribute.FONT, fallbackFont, fallbackBegin, fallbackEnd);
    

    The rendering itself:

    g2d.drawString(astr.getIterator(), 20, 30);
    

    The result (Physical "Segoe Print" as main font, logical "Serif" as fallback):

    enter image description here

    Complete supposed-to-be-SSCCE code:

    import java.awt.BorderLayout;
    import java.awt.Dimension;
    import java.awt.Font;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.font.TextAttribute;
    import java.text.AttributedString;
    
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    
    public class FontTest extends JFrame {
    
        public FontTest() {
            setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            getContentPane().setLayout(new BorderLayout());
            getContentPane().add(new TestStringComponent());
            pack();
        }
    
        public static void main(String[] args) {
            java.awt.EventQueue.invokeLater(new Runnable() {
                public void run() {
                    new FontTest().setVisible(true);
                }
            });
        }
    }
    
    class TestStringComponent extends JComponent {
    
        protected void paintComponent(Graphics g) {
            Graphics2D g2d = (Graphics2D) g;
            g.setColor(getBackground());
            g.fillRect(0, 0, getWidth(), getHeight());
    
            g.setColor(getForeground());
    
            Font mainFont = new Font("Segoe Print", Font.PLAIN, 25);
            Font fallbackFont = new Font("Serif", Font.PLAIN, 25);
    
            String s = "Test 漢鼎繁古印 Test 漢鼎繁古印 Test";
    
            g2d.drawString(createFallbackString(s, mainFont, fallbackFont).getIterator(), 20, 30);
        }
    
        public Dimension getPreferredSize() {
            return new Dimension(500, 40);
        }
    
        private AttributedString createFallbackString(String text, Font mainFont, Font fallbackFont) {
            AttributedString result = new AttributedString(text);
    
            int textLength = text.length(); 
            result.addAttribute(TextAttribute.FONT, mainFont, 0, textLength);
    
            boolean fallback = false;
            int fallbackBegin = 0;
            for (int i = 0; i < text.length(); i++) {
                boolean curFallback = !mainFont.canDisplay(text.charAt(i));
                if (curFallback != fallback) {
                    fallback = curFallback;
                    if (fallback) {
                        fallbackBegin = i;
                    } else {
                        result.addAttribute(TextAttribute.FONT, fallbackFont, fallbackBegin, i);
                    }
                }
            }
            return result;
        }
    }