为什么只有拉丁字符的Java字体声称支持亚洲字符,即使它不支持?

时间:2014-09-26 16:08:56

标签: java fonts awt jfreechart fontmetrics

使用JFreeChart渲染图表时,我注意到当图表的类别标签包含日文字符时出现布局问题。虽然文本使用正确的字形进行渲染,但文本位于错误的位置,可能是因为字体指标错误。

该图表最初配置为对该文本使用Source Sans Pro Regular字体,该字体仅支持拉丁字符集。显而易见的解决方案是捆绑实际的日文.TTF字体并要求JFreeChart使用它。这样可以正常工作,因为输出文本使用了正确的字形,并且它也正确布局。

我的问题

  • 当使用除拉丁字符之外实际上不支持任何内容的源字体时,java.awt如何在第一个场景中正确呈现日文字符?如果重要的话,我正在使用JDK 1.7u45测试OS X 10.9。

  • 有没有办法渲染日文字符而不捆绑单独的日文字体? (这是我的最终目标!)尽管捆绑解决方案有效,但如果可以避免,我不想在我的应用程序中添加6 Mb的膨胀。 Java清楚地知道如何在没有字体的情况下以某种方式呈现日语字形(至少在我的本地环境中) - 它看起来只是被破坏的指标。我想知道这是否与下面的“frankenfont”问题有关。

  • 在JRE执行内部转换之后,为什么Source Sans Pro字体告诉调用者(通过canDisplayUpTo())它可以显示日文字符,即使它不能? (见下文。)

编辑澄清:

  • 这是一个服务器应用程序,我们呈现的文本将显示在客户端的浏览器和/或PDF导出中。图表始终光栅化为服务器上的PNG。

  • 我无法控制服务器操作系统或环境,并且使用Java标准平台字体一样好,许多平台的字体选择都很差,在我的用例中是不可接受的,所以我需要捆绑我自己(至少对于拉丁字体)。可以使用日语文本的平台字体。

  • 可能会要求应用程序显示日语和拉丁文本的混合,而不会对文本类型有先验知识。如果字符串包含混合语言,只要字形正确呈现,我对使用什么字体感到矛盾。

详细

我知道java.awt.Font #TextLayout是智能的,并且在尝试布局文本时,它首先询问底层字体是否可以实际呈现所提供的字符。如果没有,它可能会以不同的字体交换,知道如何呈现这些字符,但这不会发生在这里,基于我对JRE类的调试。 TextLayout#singleFont始终返回字体的非null值,并通过构造函数的fastInit()部分继续。

一个非常好奇的注意事项是Source Sans Pro字体以某种方式被强制告诉调用者它 知道如何在JRE对字体执行转换后呈现日文字符。

例如:

// We load our font here (download from the first link above in the question)

File fontFile = new File("/tmp/source-sans-pro.regular.ttf");
Font font = Font.createFont(Font.TRUETYPE_FONT, new FileInputStream(fontFile));
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(font);

// Here is some Japanese text that we want to display
String str = "クローズ";

// Should say that the font cannot display any of these characters (return code = 0)

System.out.println("Font " + font.getName() + " can display up to: " + font.canDisplayUpTo(str));

// But after doing this magic manipulation, the font claims that it can display the
// entire string (return code = -1)

AttributedString as = new AttributedString(str, font.getAttributes());
Map<AttributedCharacterIterator.Attribute,Object> attributes = as.getIterator().getAttributes();
Font newFont = Font.getFont(attributes);

// Eeek, -1!    
System.out.println("Font " + newFont.getName() + " can display up to: " + newFont.canDisplayUpTo(str));

这个输出是:

Font Source Sans Pro can display up to: 0
Font Source Sans Pro can display up to: -1

请注意,上面提到的三条“魔法操纵”并不是我自己所做的;我们将真正的源字体对象传递给JFreeChart,但是在绘制字形时它会被JRE传送,这就是上面三行“魔术操作”代码复制的内容。上面显示的操作功能等同于以下调用序列中发生的操作:

  1. org.jfree.text.TextUtilities#drawRotatedString
  2. sun.java2d.SunGraphics2D drawString之#
  3. java.awt.font.TextLayout中#(构造)
  4. java.awt.font.TextLayout中#singleFont
  5. 当我们在“魔术”操作的最后一行调用Font.getFont()时,我们仍然会返回Source Sans Pro字体,但底层字体的font2D字段与原始字体不同,并且这个单一字体现在声称它知道如何呈现整个字符串。为什么?似乎Java正在向我们提供某种“frankenfont”,它知道如何渲染各种字形,即使它只能理解底层源字体中提供的字形的度量。

    此处显示JFreeChart渲染示例的更完整示例,基于JFreeChart示例之一:https://gist.github.com/sdudley/b710fd384e495e7f1439此示例的输出如下所示。

    Source Sans Pro字体的示例(布局不正确):

    enter image description here

    使用IPA日语字体(正确布局)的示例:

    enter image description here

1 个答案:

答案 0 :(得分:3)

虽然它没有直接解决您的问题,但我认为它可以提供一个有用的参考点,以使用平台图表中的平台默认字体显示结果。简体版BarChartDemo1source如下所示。

由于第三方字体指标的变幻莫测,我尽量避免偏离平台的标准logical fonts,这是根据平台支持的区域设置选择的。逻辑字体映射到平台的configuration files中的物理字体。在Mac OS上,相关文件位于$JAVA_HOME/jre/lib/,其中$JAVA_HOME是评估/usr/libexec/java_home -v 1.n的结果, n 是您的版本。我在版本7或8中看到了类似的结果。特别是,fontconfig.properties.src定义了用于提供日语字体系列变体的字体。所有映射似乎都使用MS MinchoMS Gothic

image

import java.awt.Dimension;
import java.awt.EventQueue;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.RefineryUtilities;

/**
 * @see http://stackoverflow.com/a/26090878/230513
 * @see http://www.jfree.org/jfreechart/api/javadoc/src-html/org/jfree/chart/demo/BarChartDemo1.html
 */
public class BarChartDemo1 extends ApplicationFrame {

    /**
     * Creates a new demo instance.
     *
     * @param title the frame title.
     */
    public BarChartDemo1(String title) {
        super(title);
        CategoryDataset dataset = createDataset();
        JFreeChart chart = createChart(dataset);
        ChartPanel chartPanel = new ChartPanel(chart){

            @Override
            public Dimension getPreferredSize() {
                return new Dimension(600, 400);
            }
        };
        chartPanel.setFillZoomRectangle(true);
        chartPanel.setMouseWheelEnabled(true);
        setContentPane(chartPanel);
    }

    /**
     * Returns a sample dataset.
     *
     * @return The dataset.
     */
    private static CategoryDataset createDataset() {

        // row keys...
        String series1 = "First";
        String series2 = "Second";
        String series3 = "Third";

        // column keys...
        String category1 = "クローズ";
        String category2 = "クローズ";
        String category3 = "クローズクローズクローズ";
        String category4 = "Category 4 クローズ";
        String category5 = "Category 5";

        // create the dataset...
        DefaultCategoryDataset dataset = new DefaultCategoryDataset();

        dataset.addValue(1.0, series1, category1);
        dataset.addValue(4.0, series1, category2);
        dataset.addValue(3.0, series1, category3);
        dataset.addValue(5.0, series1, category4);
        dataset.addValue(5.0, series1, category5);

        dataset.addValue(5.0, series2, category1);
        dataset.addValue(7.0, series2, category2);
        dataset.addValue(6.0, series2, category3);
        dataset.addValue(8.0, series2, category4);
        dataset.addValue(4.0, series2, category5);

        dataset.addValue(4.0, series3, category1);
        dataset.addValue(3.0, series3, category2);
        dataset.addValue(2.0, series3, category3);
        dataset.addValue(3.0, series3, category4);
        dataset.addValue(6.0, series3, category5);

        return dataset;

    }

    /**
     * Creates a sample chart.
     *
     * @param dataset the dataset.
     *
     * @return The chart.
     */
    private static JFreeChart createChart(CategoryDataset dataset) {

        // create the chart...
        JFreeChart chart = ChartFactory.createBarChart(
                "Bar Chart Demo 1", // chart title
                "Category", // domain axis label
                "Value", // range axis label
                dataset, // data
                PlotOrientation.HORIZONTAL, // orientation
                true, // include legend
                true, // tooltips?
                false // URLs?
        );
        return chart;
    }

    /**
     * Starting point for the demonstration application.
     *
     * @param args ignored.
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            BarChartDemo1 demo = new BarChartDemo1("Bar Chart Demo 1");
            demo.pack();
            RefineryUtilities.centerFrameOnScreen(demo);
            demo.setVisible(true);
        });
    }
}