使用 JFreeChart 渲染图表时,我注意到图表的类别标签包含日文字符时出现布局问题。尽管使用正确的字形呈现文本,但文本定位在错误的位置,可能是因为字体规范错误。
该图表最初配置为对该文本使用 Source Sans Pro Regular 字体,该字体仅支持拉丁字符集。显而易见的解决方案是捆绑一个实际的日语 .TTF 字体并要求 JFreeChart 使用它。这工作正常,因为输出文本使用正确的字形,并且布局也正确。
我的问题
编辑以澄清:
细节
我知道 java.awt.Font#TextLayout 很聪明,并且在尝试布局文本时,它首先询问底层字体是否可以实际呈现提供的字符。如果没有,它可能会换入一种知道如何呈现这些字符的不同字体,但根据我对 JRE 类的调试,这不会发生在这里。
TextLayout#singleFont
总是为字体返回一个非空值,它继续通过构造函数的 fastInit()
部分。一个非常奇怪的注意事项是,在 JRE 对字体执行转换后,Source 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));
这个的输出是:
Font Source Sans Pro can display up to: 0
Font Source Sans Pro can display up to: -1
请注意,上面提到的三行“魔法操纵”不是我自己做的;我们将真正的源字体对象传递给 JFreeChart,但是在绘制字形时它会被 JRE 处理,这就是上面三行“魔术操作”代码所复制的内容。上面显示的操作与以下调用序列中发生的功能等效:
当我们在“魔术”操作的最后一行调用 Font.getFont() 时,我们仍然得到一个 Source Sans Pro 字体,但底层字体的
font2D
字段与原始字体不同,并且这个单一字体现在声称它知道如何渲染整个字符串。为什么?看起来 Java 给了我们某种“frankenfont”,它知道如何呈现各种字形,即使它只理解底层源字体中提供的字形的度量。显示 JFreeChart 呈现示例的更完整示例位于此处,基于 JFreeChart 示例之一: https://gist.github.com/sdudley/b710fd384e495e7f1439 此示例的输出如下所示。
Source Sans Pro 字体示例(布局错误):
使用 IPA 日语字体的示例(布局正确):
最佳答案
我终于弄明白了。有许多根本原因,而增加的跨平台可变性进一步阻碍了这些原因。
JFreeChart 在错误的位置呈现文本,因为它使用不同的字体对象
出现布局问题是因为 JFreeChart 不经意间使用了与 AWT 实际用于呈现字体的 Font 对象不同的 Font 对象来计算布局的度量。 (作为引用,JFreeChart 的计算发生在 org.jfree.text#getTextBounds
中。)
不同 Font 对象的原因是问题中提到的隐式“魔法操作”的结果,这是在 java.awt.font.TextLayout#singleFont
内部执行的。
这三行魔法操作可以浓缩为:
font = Font.getFont(font.getAttributes())
在英语中,这要求字体管理器根据所提供字体的“属性”(名称、系列、磅值等)为我们提供一个新的 Font 对象。在某些情况下,它返回给您的
Font
将与您最初使用的 Font
不同。为了更正指标(从而修复布局), 修复是在设置 JFreeChart 对象 中的字体之前,在您自己的
Font
对象上运行上面的单行。这样做之后,布局对我来说效果很好,日语字符也是如此。它也应该为您修复布局,尽管它可能无法为您正确显示日语字符。阅读以下有关 native 字体的内容以了解原因。
Mac OS X 字体管理器更喜欢返回原生字体,即使你给它一个物理 TTF 文件
文本的布局已通过上述更改固定......但为什么会发生这种情况?在什么情况下 FontManager 实际上会返回与我们提供的对象类型不同的
Font
对象?原因有很多,但至少在 Mac OS X 上,与问题相关的原因是字体管理器似乎更喜欢尽可能返回原生字体。
换句话说,如果您使用
Font.createFont
从名为“Foobar”的物理 TTF 字体创建新字体,然后使用从“Foobar”物理字体派生的属性调用 Font.getFont()...只要 OS X 已经具有安装了 Foobar 字体,字体管理器将返回一个 CFont
对象,而不是您期望的 TrueTypeFont
对象。即使您通过 GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont
注册字体,这似乎也成立。就我而言,这给调查带来了麻烦:我的 Mac 上已经安装了“Source Sans”字体,这意味着我得到的结果与没有安装的人不同。
Mac OS X 原生字体始终支持亚洲字符
问题的关键在于 Mac OS X
CFont
对象始终支持亚洲字符集。我不清楚允许这样做的确切机制,但我怀疑这是 OS X 本身的某种后备字体功能,而不是 Java。在任何一种情况下,CFont
总是声称(并且确实能够)使用正确的字形呈现亚洲字符。这明确了允许原始问题发生的机制:
Font
,它本身不支持日语。 Font
对象询问日文文本的度量。物理 Font
无法正确执行此操作,因为它不支持亚洲字符集。 TextLayout#singleFont
中的魔术操作导致它获得 CFont
对象并使用同名的原生字体绘制字形,而不是物理 TrueTypeFont
。因此,字形是正确的,但它们的位置不正确。 根据您是否注册字体以及您的操作系统中是否安装了字体,您将获得不同的结果
如果您使用创建的 TTF 字体的属性调用
Font.getFont()
,您将获得三种不同的结果之一,具体取决于字体是否已注册以及您是否在 native 安装了相同的字体:CFont
字体,用于您想要的字体。 Font
但您没有同名的 native 字体,则调用 Font.getFont() 将返回一个物理 TrueTypeFont
对象。这为您提供了您想要的字体,但您没有得到亚洲字符。 Font
并且您也没有同名的 native 字体,则调用 Font.getFont() 返回一个支持亚洲的 CFont,但它不会是您请求的字体。 事后看来,这一切都不足为奇。导致:
我无意中使用了错误的字体
在生产应用程序中,我正在创建一种字体,但我忘记了最初将它注册到 GraphicsEnvironment。如果您在执行上述魔术操作时尚未注册字体,则
Font.getFont()
不知道如何检索它,而您将获得备用字体。哎呀。在 Windows、Mac 和 Linux 上,这种备用字体一般看起来是 Dialog,这是一种支持亚洲字符的逻辑(复合)字体。至少在 Java 7u72 中,Dialog 字体默认为以下西方字母字体:
这个错误对于我们的亚洲用户 来说实际上是一件好事,因为这意味着他们的字符集使用逻辑字体按预期呈现......尽管西方用户没有得到我们想要的字符集。
由于它一直以错误的字体呈现,无论如何我们都需要修复日文布局,我决定最好尝试将一种通用字体标准化以备将来发布(从而更接近垃圾神的建议)。
此外,该应用程序具有字体渲染质量要求,可能并不总是允许使用某些字体,因此合理的决定似乎是尝试将应用程序配置为使用 Lucida Sans,这是 Oracle 中包含的一种物理字体Java的所有副本。但...
Lucida Sans 在所有平台上都无法与亚洲角色完美搭配
尝试使用 Lucida Sans 的决定似乎是合理的……但我很快发现在处理 Lucida Sans 的方式上存在平台差异。在 Linux 和 Windows 上,如果您索要“Lucida Sans”字体的副本,您会得到一个物理对象
TrueTypeFont
。但该字体不支持亚洲字符。如果您请求“Lucida Sans”,同样的问题在 Mac OS X 上也成立……但是如果您要求稍微不同的名称“LucidaSans”(注意缺少空间),那么您会得到一个支持 Lucida Sans 的
CFont
对象以及亚洲人物,所以你可以吃蛋糕也可以吃。在其他平台上,请求“LucidaSans”会生成标准 Dialog 字体的副本,因为没有此类字体并且 Java 将返回其默认值。在 Linux 上,你在这里有点幸运,因为 Dialog 实际上默认为西方文本使用 Lucida Sans(并且它还为亚洲字符使用了一个不错的后备字体)。
这为我们提供了一条在所有平台上获得(几乎)相同物理字体的路径,并且还支持亚洲字符,通过请求具有以下名称的字体:
我仔细研究了 Windows 上的 fonts.properties,但我找不到默认为 Lucida Sans 的字体序列,所以看起来我们的 Windows 用户需要使用 Arial ......但至少它在视觉上没有那么不同来自 Lucida Sans,Windows 字体渲染质量合理。
一切都到哪里去了?
总之,我们现在几乎只使用平台字体。 (我相信@trashgod 现在笑得很开心!)Mac 和 Linux 服务器都获得了 Lucida Sans,Windows 获得了 Arial,渲染质量很好,每个人都很高兴!
关于java - 为什么只有拉丁字符的 Java 字体声称支持亚洲字符,即使它不支持?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26063828/