Search code examples
javafontsawtjfreechart

Why do I have to call GraphicsEnvorinment.registerFont() even if I my Font were created from file?


I'm developing a web application which use JFreeChart to render chart. However, when server dose not have any Chinese font installed, JFreeChart dose not display Chinese character even if I have set the font.

Then I write a small testing code and find out that add this line of code before drawing the chart can solve the problem.

GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(font);

So my questions are -

  1. Why do I have to register font into JVM even if I create my font from File? Does that mean JFreeChart do not use the font I set directly?

  2. When I deploy my program into server, even if I add this line of code, it dose not display Chinese characters. How do I make it always use the font I set in order to display character properly in all environment?

I know I can make a fallback directory in $JAVA_HOME/jre/lib and put my font into it. But that dose not explain why JFreeChart cannot display with the font I set.

UPDATE

I'm pretty sure the font were load correctly, and so does registerFont() return true when I deploy the program into Tomcat.

UPDATE 2

According to JAVA 2D FAQ, now I realize I have to call registerFont() in order to make my own font "installed" into JVM, and my font will be available through Font constructor.

As of Java SE 6, there is a method : GraphicsEnvironment.registerFont() which gives you the ability to make a "created" font available to Font constructors and to be listed via Font enumeration APIs. Font.createFont() and this method combine to provide a way to "install" a Font into the running JRE so it is available just as O/S installed fonts are. However this Font does not persist across JRE invocations.

But, since I already have Font instances created/derived from createFont(), why doesn't my program still need to create other Font?


Following is the code I used, it simply output a chart in PNG format. If you want to run the code, you should change the output location and font to fit your need, and here is the SourceForge link for Chinese font I use in code.

import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.io.File;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.StandardChartTheme;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;

public class Problem {

  public static void main(String[] args) throws Exception {
    setJFreeChartTheme();

    PieDataset dataset = createDataSet();
    JFreeChart chart = ChartFactory.createPieChart(
        "Chinese Testing", dataset, true, true, false);
    ChartUtilities.saveChartAsJPEG(new File("/tmp/output.png"), 
        chart, 800, 600);

    System.out.println("Done");
  }

  private static void setJFreeChartTheme() throws Exception {
    Font font = loadFont();
    //==================================================================
    GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(font);
    //==================================================================
    StandardChartTheme theme = new StandardChartTheme("Chinese font", true);
    theme.setExtraLargeFont(font.deriveFont(Font.BOLD, 20));
    theme.setLargeFont(font.deriveFont(Font.BOLD, 16));
    theme.setRegularFont(font.deriveFont(Font.PLAIN, 14));
    theme.setSmallFont(font.deriveFont(Font.PLAIN, 12));
    ChartFactory.setChartTheme(theme);
  }

  private static Font loadFont() throws Exception {
    File file = new File("/tmp/wqy-zenhei.ttc");
    return Font.createFont(Font.TRUETYPE_FONT, file);
  }

  private static PieDataset createDataSet() {
    DefaultPieDataset dataset = new DefaultPieDataset();
    dataset.setValue("種類1", Integer.valueOf(1));
    dataset.setValue("種類2", Integer.valueOf(2));
    dataset.setValue("種類3", Integer.valueOf(3));
    return dataset;
  }
}

Solution

  • When you create a Font directly from the TTF, Java obviously knows where to get a copy of the font file itself from within that single Font object. So why does the font also need to be registered in order to use it? The answer is that it does not always have to be registered, or at least not so long as the entire chain of control uses the original Font object directly.

    What really happens when Java tries to render the font?

    The nuance is in how JFreeChart asks to render the text. Rendering of text is performed in the TextUtilities#drawRotatedString method from jcommon. On JDK7, this method will, by default:

    • create an AttributedString based on the "attributes" of the font that you have passed in,
    • call Graphics2D#drawString on the attributed string, which then
    • creates a new TextLayout object.

    TextLayout is the class that selects the actual Font object to be provided to Graphics2D. TextLayout is designed to support the rendering of multilingual text using various fonts (even if the same single source String needs to be rendered in multiple fonts) by using automatic font selection to find an appropriate font for each piece of the string.

    The "attributes" mentioned above are simple facts about the font (like font family name, size, etc) that are derived from the Font that you provide. In the event that the font you've provided is unable to render all of the characters in the input string, the attributes are used to select similar fonts to use for those runs of text that need to use a different font.

    When JFreeChart invokes TextLayout, it always functions like this:

    • extract the attributes from the Font object you supplied,
    • call the static Font#getFont to get a font that matches the supplied attributes (see TextLayout#singleFont), and
    • uses the returned (possibly different) Font object to draw the text.

    If you have not statically registered your font somewhere (like with GraphicsEnvironment#registerFont), then all that the static Font#getFont method will have to go on is the attribute string containing the font family name. It won't know where to access the Font object that contains the reference to the TTF, let alone any of the data needed to actually render the font.

    OK, but I thought you said that I didn't need to register the font?

    If you don't want to register the font, then the trick is in making sure that your text is rendered only with the Font object that you provide. It so happens that there is another constructor for TextLayout that accepts a Font object directly, rather than a set of attributes that are used to look up a font.

    Helpfully, JFreeChart even provides a way to force it to use this constructor instead. In TextUtilities#drawRotatedString, a special configuration parameter can be used to force JFreeChart to construct the TextLayout object itself using the exact Font object you provide.

    To do this, you can either set up a jcommon.properties file like this:

    • create a resource file named jcommon.properties (which should end up in the root level of your classpath/JAR), and
    • add the following line to it:

    org.jfree.text.UseDrawRotatedStringWorkaround=true

    or else simply make a call to the static function:

    TextUtilities.setUseDrawRotatedStringWorkaround(true)
    

    This will ask JFreeChart to render the text using your font directly, and...voila! It works even without having registered the font. This was tested in the context of the question above, which is that of using JFreeChart to render the text directly to a raster image. Your mileage might vary if you are trying to render to a display device (which I did not try).

    Is it wise not to register the font?

    I can't say for sure. One of my applications runs inside an OSGi container and I was wary of creating PermGen classloader leaks by statically registering a Font that can never be unregistered. Using the Font object directly avoids that problem, which is why I wanted to go this route. I suppose it's always possible that a specific Java platform might have trouble if you did this, but this works fine in my tests on at least Windows, Linux, and OS X hosts with Oracle JDK 7.