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

为什么只有拉丁字符的Java字体声称支持亚洲字符,尽管它不支持?,java,fonts,awt,jfreechart,fontmetrics,Java,Fonts,Awt,Jfreechart,Fontmetrics,在使用JFreeChart呈现图表时,我注意到当图表的类别标签包含日文字符时出现了布局问题。虽然文本是使用正确的图示符呈现的,但文本定位在错误的位置,可能是因为字体度量错误 图表最初配置为使用该文本的字体,该字体仅支持拉丁字符集。显而易见的解决方案是捆绑一个实际的日文.TTF字体,并让JFreeChart使用它。这很好,因为输出文本使用了正确的图示符,并且布局也正确 我的问题 在第一个场景中,当使用的源字体实际上不支持除拉丁字符以外的任何字符时,java.awt是如何正确呈现日语字符的?如果有

在使用JFreeChart呈现图表时,我注意到当图表的类别标签包含日文字符时出现了布局问题。虽然文本是使用正确的图示符呈现的,但文本定位在错误的位置,可能是因为字体度量错误

图表最初配置为使用该文本的字体,该字体仅支持拉丁字符集。显而易见的解决方案是捆绑一个实际的日文.TTF字体,并让JFreeChart使用它。这很好,因为输出文本使用了正确的图示符,并且布局也正确

我的问题
  • 在第一个场景中,当使用的源字体实际上不支持除拉丁字符以外的任何字符时,java.awt是如何正确呈现日语字符的?如果有必要的话,我正在使用JDK1.7u45在OSX10.9上进行测试

  • 有没有办法在不捆绑单独的日文字体的情况下呈现日文字符?(这是我的最终目标!)虽然捆绑解决方案有效,但如果可以避免的话,我不想在我的应用程序中添加6MB的膨胀。Java清楚地知道如何以某种方式呈现日文字形,即使没有字体(至少在我的本地环境中是这样)——看起来只是一些指标被破坏了。我想知道这是否与下面的“法兰克福”问题有关

  • 在JRE执行内部转换后,为什么源SAN Pro字体会告诉调用者(通过)它可以显示日语字符,尽管它不能?(见下文。)

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

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

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

细节 我知道java.awt.Font#TextLayout是智能的,当尝试布局文本时,它首先询问底层字体是否能够实际呈现提供的字符。如果不是,它可能会换成一种不同的字体,知道如何呈现这些字符,但根据我在JRE类中进行的调试,这里不会发生这种情况
TextLayout#singleFont
始终为字体返回非空值,并通过构造函数的
fastInit()
部分进行操作

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

例如:

// 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));
注意,上面提到的三行“魔术操作”不是我自己做的;我们将真正的源字体对象传递给JFreeChart,但在绘制glyphs时,它会被JRE屏蔽,这就是上面三行“魔术操作”代码所复制的内容。上面显示的操作在功能上等同于以下调用序列中发生的操作:

  • org.jfree.text.TextUtilities#drawRotatedString
  • sun.java2d.SunGraphics2D#拉丝
  • java.awt.font.TextLayout#(构造函数)
  • java.awt.font.TextLayout#singleFont
  • 在“magic”操作的最后一行中调用Font.getFont()时,我们仍然会得到一个源Sans Pro字体,但是基础字体的
    font2D
    字段与原始字体不同,并且这个单一字体现在声称它知道如何渲染整个字符串。为什么?Java似乎给了我们某种“frankenfont”,它知道如何呈现各种字形,尽管它只了解底层源字体中提供的字形的度量

    这里有一个更完整的JFreeChart渲染示例,它基于一个JFreeChart示例:该示例的输出如下所示

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

    IPA日文字体示例(布局正确):

    虽然它没有直接回答您的问题,但我认为它可能提供了一个有用的参考点,可以在未经修饰的图表中使用平台的默认字体显示结果。的简化版本如下所示

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

    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);
            });
        }
    }
    


    我终于明白了。有许多潜在的原因,这进一步受到跨平台变异剂量增加的阻碍

    JFreeChart在错误的位置渲染文本,因为它使用了不同的字体对象 出现布局问题是因为JFreeChart无意中使用了与AWT实际用于呈现字体的字体对象不同的字体对象来计算布局的度量。(用于
    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);
            });
        }
    }
    
    font = Font.getFont(font.getAttributes())