2

If you just create your font using new Font("Arial", Font.PLAIN, 10), later, when you try to display missing glyphs in that font, you will get the familiar squares indicating missing glyphs.

A long time ago, we found a workaround for this - pass the font to FontUtilities.getCompositeFontUIResource(Font) and you get back a Font which handles fallback for characters which aren't in the font itself.

Problem is, that utility is in sun.font, and I would like to eliminate the compiler warning.

Given that many years have passed in the meantime, is there now a proper way to do this?

Demo:

import java.awt.Font;
import java.awt.GridLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

import sun.font.FontUtilities;

public class TestFonts implements Runnable
{
    @Override
    public void run()
    {
        Font font = new Font("Arial", Font.PLAIN, 20);

        JLabel label1 = new JLabel("Before \u30C6\u30B9\u30C8");
        label1.setFont(font);

        JLabel label2 = new JLabel("After \u30C6\u30B9\u30C8");
        label2.setFont(FontUtilities.getCompositeFontUIResource(font));

        JFrame frame = new JFrame("Font Test");
        frame.setLayout(new GridLayout(2, 1));
        frame.add(label1);
        frame.add(label2);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new TestFonts());
    }
}

Result:

enter image description here

Hakanai
  • 12,010
  • 10
  • 62
  • 132
  • Possible duplicate of [How do I specify fallback fonts in Java2D/Graphics2D](http://stackoverflow.com/questions/9482255/how-do-i-specify-fallback-fonts-in-java2d-graphics2d) – Mike 'Pomax' Kamermans Feb 03 '16 at 01:51
  • Maybe, although the answer in there wouldn't solve the issue for me, because I'm not keen on rewriting every single class in Swing that happens to render text. – Hakanai Feb 03 '16 at 04:46

2 Answers2

2

After digging in the JDK sources a bit, I found a public method which calls the internal method directly. I don't know how dodgy this is considered, but it's at least public API. :)

        label2.setFont(StyleContext.getDefaultStyleContext()
            .getFont(familyName, style, size));
Hakanai
  • 12,010
  • 10
  • 62
  • 132
0

You can exploit the fact that JComponents are able to display HTML, and override the font for characters which the JComponent's font cannot display, by placing those characters in a <span>:

import java.util.Formatter;

import java.awt.Font;
import java.awt.GridLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

import sun.font.FontUtilities;

public class TestFonts implements Runnable
{
    /**
     * Replaces plain text meant to be displayed in a JComponent with
     * HTML that forces the font to Dialog for any characters which the
     * specified font cannot natively display.
     *
     * @param originalText text to transform to HTML, with forced fonts
     *                     where needed
     * @param font default font which will be used to display text
     *
     * @return HTML version of original text, which forces fonts where
     *         necessary to ensure all characters will be displayed
     */
    static String toCompositeFontText(String originalText,
                                      Font font) {
        Formatter html = new Formatter();
        html.format("%s", "<html><body style='white-space: nowrap'>");

        boolean fontOverride = false;
        int len = originalText.length();
        for (int i = 0; i < len; i = originalText.offsetByCodePoints(i, 1)) {
            int c = originalText.codePointAt(i);

            if (font.canDisplay(c)) {
                if (fontOverride) {
                    html.format("%s", "</span>");
                    fontOverride = false;
                }
            } else {
                if (!fontOverride) {
                    html.format("<span style='font-family: \"%s\"'>",
                        Font.DIALOG);
                    fontOverride = true;
                }
            }

            if (c == '<' || c == '>' || c == '&' || c < 32 || c >= 127) {
                html.format("&#%d;", c);
            } else {
                html.format("%c", c);
            }
        }

        if (fontOverride) {
            html.format("%s", "</span>");
        }

        html.format("%s", "</body></html>");

        return html.toString();
    }

    /**
     * Replaces text of the specified JLabel with HTML that contains the
     * same text, but forcing the font to Dialog for any characters which
     * the JLabel's current font cannot display.
     *
     * @param label JLabel whose text will be adjusted and replaced
     */
    static void adjustText(JLabel label) {
        label.setText(toCompositeFontText(label.getText(), label.getFont()));
    }

    @Override
    public void run()
    {
        Font font = new Font("Arial", Font.PLAIN, 20);

        JLabel label1 = new JLabel("Before \u30C6\u30B9\u30C8");
        label1.setFont(font);

        JLabel label2 = new JLabel("After \u30C6\u30B9\u30C8");
        label2.setFont(FontUtilities.getCompositeFontUIResource(font));

        JLabel label3 = new JLabel("Corrected \u30C6\u30B9\u30C8");
        label3.setFont(font);
        adjustText(label3);

        JFrame frame = new JFrame("Font Test");
        frame.setLayout(new GridLayout(3, 1));
        frame.add(label1);
        frame.add(label2);
        frame.add(label3);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new TestFonts());
    }
}

Update:

In addition, you can monitor a label's text property, to make this happen automatically:

label.addPropertyChangeListener("text", new PropertyChangeListener() {
    @Override
    public void propertyChange(PropertyChangeEvent event) {
        String text = (String) event.getNewValue();
        if (text != null && !text.startsWith("<html>")) {
            adjustText((JLabel) event.getSource());
        }
    }
});

The obvious drawback is that there is no (easy) way to adjust text which already starts with <html>. (Actually I'm fairly sure even that could be done by loading the label's text as an HTMLDocument.)

VGR
  • 40,506
  • 4
  • 48
  • 63
  • I don't really want to have to do this on any component that might contain some variable text... :/ – Hakanai Feb 10 '16 at 03:32
  • I agree it's awkward. You could add a PropertyChangeListener to make it an automatic process. Answer updated accordingly. – VGR Feb 10 '16 at 14:14