7

使用 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总是为字体返回一个非空值,并通过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

当我们在“魔术”操作的最后一行调用 Font.getFont() 时,我们仍然得到一个 Source Sans Pro 字体,但底层字体的font2D字段与原始字体不同,这个单一字体现在声称它知道如何渲染整个字符串。为什么?看起来 Java 给了我们某种“frankenfont”,它知道如何渲染各种字形,即使它只理解底层源字体中提供的字形的度量。

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

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

在此处输入图像描述

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

在此处输入图像描述

4

2 回答 2

5

我终于弄明白了。有许多根本原因,而跨平台变异性的增加进一步阻碍了这些原因。

JFreeChart 在错误的位置呈现文本,因为它使用不同的字体对象

出现布局问题是因为 JFreeChart 无意中使用与 AWT 实际用于呈现字体的Font 对象不同的字体对象来计算布局的度量。(作为参考,JFreeChart 的计算发生在org.jfree.text#getTextBounds.)

不同 Font 对象的原因是问题中提到的隐式“魔术操作”的结果,该操作是在java.awt.font.TextLayout#singleFont.

这三行魔法操作可以浓缩为:

font = Font.getFont(font.getAttributes())

在英语中,这要求字体管理器根据所提供字体的“属性”(名称、系列、磅值等)为我们提供一个新的 Font 对象。在某些情况下,Font它给你的回报会与Font你最初开始的不同。

为了更正指标(从而修复布局),修复方法是在设置 JFreeChart objects 中的字体之前在您自己的对象上运行上面的单行Font

这样做之后,布局对我来说很好,日文字符也是如此。它也应该为您修复布局,尽管它可能无法为正确显示日文字符。阅读以下有关本机字体的内容以了解原因。

Mac OS X 字体管理器更喜欢返回本机字体,即使你给它提供一个物理 TTF 文件

文本的布局已通过上述更改修复...但为什么会发生这种情况?在什么情况下,FontManager 实际上会返回与Font我们提供的对象不同类型的对象?

原因有很多,但至少在 Mac OS X 上,与问题相关的原因是字体管理器似乎更愿意尽可能返回本机字体

换句话说,如果您使用名为“Foobar”的物理 TTF 字体创建新字体Font.createFont,然后使用从“Foobar”物理字体派生的属性调用 Font.getFont()...只要 OS X 已经有Foobar 字体安装后,字体管理器会给你一个CFont对象而不是TrueTypeFont你期望的对象。即使通过GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont.

在我的情况下,这引发了调查:我已经在我的 Mac 上安装了“Source Sans”字体,这意味着我从没有安装的人那里得到了不同的结果。

Mac OS X 原生字体始终支持亚洲字符

问题的关键在于 Mac OS XCFont对象总是支持亚洲字符集。我不清楚允许这样做的确切机制,但我怀疑这是 OS X 本身而不是 Java 的某种后备字体功能。在任何一种情况下,aCFont总是声称(并且确实能够)使用正确的字形呈现亚洲字符。

这清楚地表明了导致原始问题发生的机制:

  • 我们Font从一个物理 TTF 文件创建了一个物理文件,它本身不支持日语。
  • 我的 Mac OS X Font Book 中也安装了与上面相同的物理字体
  • 在计算图表布局时,JFreeChart 向物理Font对象询问日文文本的度量。物理Font无法正确执行此操作,因为它不支持亚洲字符集。
  • 在实际绘制图表时,魔术操作TextLayout#singleFont导致它获取一个CFont对象并使用同名的本机字体绘制字形,而不是物理的TrueTypeFont. 因此,字形是正确的,但它们的位置不正确。

根据您是否注册字体以及您的操作系统中是否安装了字体,您将获得不同的结果

如果您Font.getFont()使用创建的 TTF 字体的属性进行调用,您将获得三种不同的结果之一,具体取决于该字体是否已注册以及您是否在本机安装了相同的字体:

  • 如果您确实安装了与您的 TTF 字体同名的本机平台字体(无论您是否注册了该字体),您将获得所需字体的亚洲支持CFont字体。
  • 如果您Font在 GraphicsEnvironment 中注册了 TTF,但没有同名的本机字体,则调用 Font.getFont() 将返回一个物理TrueTypeFont对象。这会给你你想要的字体,但你不会得到亚洲字符。
  • 如果您没有注册 TTFFont并且您也没有同名的本地字体,则调用 Font.getFont() 会返回一个支持亚洲的 CFont,但它不会是您请求的字体。

事后看来,这一切都不足为奇。导致:

我无意中使用了错误的字体

在生产应用程序中,我正在创建一种字体,但我最初忘记在 GraphicsEnvironment 中注册它。如果您在执行上述魔术操作时尚未注册字体,Font.getFont()则不知道如何检索它,而是获得备用字体。哎呀。

在 Windows、Mac 和 Linux 上,这种备份字体一般看起来是 Dialog,它是一种支持亚洲字符的逻辑(复合)字体。至少在 Java 7u72 中,Dialog 字体默认为以下西方字母字体:

  • Mac:露西达格兰德
  • Linux(CentOS):Lucida Sans
  • 视窗:宋体

这个错误对我们的亚洲用户来说实际上是一件好事,因为这意味着他们的字符集使用逻辑字体按预期呈现……尽管西方用户没有得到我们想要的字符集。

由于它一直以错误的字体呈现,并且无论如何我们都需要修复日文布局,我决定我最好尝试为将来的版本标准化一种通用字体(从而更接近垃圾上帝的建议)。

此外,该应用程序具有字体渲染质量要求,可能并不总是允许使用某些字体,因此一个合理的决定似乎是尝试将应用程序配置为使用 Lucida Sans,这是 Oracle 包含的一种物理字体Java的所有副本。但...

Lucida Sans 在所有平台上都不能很好地处理亚洲角色

尝试使用 Lucida Sans 的决定似乎是合理的……但我很快发现在处理 Lucida Sans 的方式上存在平台差异。在 Linux 和 Windows 上,如果你要一份“Lucida Sans”字体的副本,你会得到一个物理TrueTypeFont对象。但该字体不支持亚洲字符。

如果您请求“Lucida Sans”,则在 Mac OS X 上同样的问题也是如此……但如果您请求稍微不同的名称“LucidaSans”(注意缺少空间),那么您也会得到一个CFont支持 Lucida Sans 的对象作为亚洲人物,所以你可以吃蛋糕也可以吃。

在其他平台上,请求“LucidaSans”会生成标准 Dialog 字体的副本,因为没有这样的字体并且 Java 正在返回其默认值。在 Linux 上,您在这里有点幸运,因为 Dialog 实际上默认为 Lucida Sans 用于西方文本(并且它还为亚洲字符使用了体面的后备字体)。

这为我们提供了一条在所有平台上获取(几乎)相同物理字体的路径,并且还支持亚洲字符,方法是请求具有以下名称的字体:

  • Mac OS X:“LucidaSans”(产生 Lucida Sans + 亚洲备份字体)
  • Linux:“Dialog”(产生 Lucida Sans + 亚洲备份字体)
  • Windows:“对话框”(产生Arial + 亚洲备份字体)

我仔细研究了 Windows 上的 fonts.properties,但找不到默认为 Lucida Sans 的字体序列,所以看起来我们的 Windows 用户需要使用 Arial ......但至少在视觉上并没有那么不同来自 Lucida Sans,Windows 字体渲染质量是合理的。

一切都在哪里结束?

总之,我们现在几乎只使用平台字体。(我相信@trashgod 现在笑得很开心!)Mac 和 Linux 服务器都获得了 Lucida Sans,Windows 获得了 Arial,渲染质量很好,每个人都很开心!

于 2014-11-21T22:27:19.440 回答
3

尽管它没有直接解决您的问题,但我认为它可能会提供一个有用的参考点,以在未修饰的图表中使用平台的默认字体显示结果。BarChartDemo1的简化版本source如下所示。

由于第三方字体指标的变幻莫测,我尽量避免偏离平台的标准逻辑字体,这些字体是根据平台支持的语言环境选择的。逻辑字体映射到平台配置文件中的物理字体。在 Mac OS 上,相关文件位于 中$JAVA_HOME/jre/lib/,其中$JAVA_HOME是评估结果 /usr/libexec/java_home -v 1.nn是您的版本。我在版本 7 或 8 中看到了类似的结果。特别是,fontconfig.properties.src定义了用于提供日文字体系列变体的字体。所有映射似乎都使用MS Minchoor 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);
        });
    }
}
于 2014-09-29T00:09:11.640 回答