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

标签 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 标准平台字体很好,但许多平台的字体选择不佳,这在我的用例中是 Not Acceptable ,因此我需要捆绑自己的 (至少对于拉丁字体)。日语文本使用平台字体是可以接受的。
  • 该应用程序可能会被要求显示日语和拉丁语文本的混合,而无需先验了解文本类型。如果字符串包含混合语言,我对使用什么字体持矛盾态度,只要字形正确呈现。

  • 细节

    我知道 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 处理,这就是上面三行“魔术操作”代码所复制的内容。上面显示的操作与以下调用序列中发生的功能等效:
  • org.jfree.text.TextUtilities#drawRotatedString
  • sun.java2d.SunGraphics2D#drawString
  • java.awt.font.TextLayout#(构造函数)
  • java.awt.font.TextLayout#singleFont

  • 当我们在“魔术”操作的最后一行调用 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

    最佳答案

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

    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 总是声称(并且确实能够)使用正确的字形呈现亚洲字符。

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

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

    如果您使用创建的 TTF 字体的属性调用 Font.getFont(),您将获得三种不同的结果之一,具体取决于字体是否已注册以及您是否在 native 安装了相同的字体:
  • 如果您 do 安装了与您的 TTF 字体同名的 native 平台字体(无论您是否注册了该字体),您将获得支持亚洲的 CFont 字体,用于您想要的字体。
  • 如果您在 GraphicsEnvironment 中注册了 TTF Font 但您没有同名的 native 字体,则调用 Font.getFont() 将返回一个物理 TrueTypeFont 对象。这为您提供了您想要的字体,但您没有得到亚洲字符。
  • 如果您没有注册 TTF Font 并且您也没有同名的 native 字体,则调用 Font.getFont() 返回一个支持亚洲的 CFont,但它不会是您请求的字体。

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

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

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

    在 Windows、Mac 和 Linux 上,这种备用字体一般看起来是 Dialog,这是一种支持亚洲字符的逻辑(复合)字体。至少在 Java 7u72 中,Dialog 字体默认为以下西方字母字体:
  • Mac:露西达格兰德
  • Linux (CentOS):Lucida Sans
  • Windows:Arial

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

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

    此外,该应用程序具有字体渲染质量要求,可能并不总是允许使用某些字体,因此合理的决定似乎是尝试将应用程序配置为使用 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(并且它还为亚洲字符使用了一个不错的后备字体)。

    这为我们提供了一条在所有平台上获得(几乎)相同物理字体的路径,并且还支持亚洲字符,通过请求具有以下名称的字体:
  • Mac OS X:“LucidaSans”(产生 Lucida Sans + 亚洲备份字体)
  • Linux:“对话”(产生 Lucida Sans + 亚洲备份字体)
  • Windows:“对话框”(产生 Arial + 亚洲备用字体)

  • 我仔细研究了 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/

    相关文章:

    java - 我可以在一个类中使用多个 ActionListener 吗?

    java - 如何阻止 Proguard 删除返回类型?

    Java返回语句优先级

    css - 字体变体 : small-caps; shows different font sizes using Chrome or Firefox

    html - 在 CSS 中显示 @font-face 的替代字体

    java - Wicket CaptchaImageResource 在 Linux 服务器上创建零长度图像

    java - 通用 : extends Number & Comparable Basic understanding

    java - 从不同线程访问数据时丢失对象引用

    eclipse - Netbeans 字体渲染

    java - 多屏幕上的 VolatileImage JFrame