为啥只有拉丁字符的 Java 字体声称支持亚洲字符,即使它不支持?
Posted
技术标签:
【中文标题】为啥只有拉丁字符的 Java 字体声称支持亚洲字符,即使它不支持?【英文标题】:Why does a Latin-characters-only Java font claim to support Asian characters, even though it does not?为什么只有拉丁字符的 Java 字体声称支持亚洲字符,即使它不支持? 【发布时间】:2014-11-21 17:12:11 【问题描述】:在使用 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 修改,这就是上面三行“魔术操作”代码所复制的内容。上面显示的操作与以下调用序列中发生的操作在功能上等价:
-
org.jfree.text.TextUtilities#drawRotatedString
sun.java2d.SunGraphics2D#drawString
java.awt.font.TextLayout#(构造函数)
java.awt.font.TextLayout#singleFont
当我们在“魔术”操作的最后一行调用 Font.getFont() 时,我们仍然得到一个 Source Sans Pro 字体,但是底层字体的 font2D
字段与原始字体不同,并且这个单一的font 现在声称它知道如何渲染整个字符串。为什么?似乎 Java 正在向我们提供某种知道如何渲染各种字形的“frankenfont”,尽管它只了解底层源字体中提供的字形的度量。
此处是显示 JFreeChart 呈现示例的更完整示例,基于其中一个 JFreeChart 示例:https://gist.github.com/sdudley/b710fd384e495e7f1439 此示例的输出如下所示。
Source Sans Pro 字体示例(布局不正确):
IPA 日文字体示例(布局正确):
【问题讨论】:
我还没有弄清楚这一点,但我对“魔法操纵”的一些调查产生了我的answer to this other SO question。遗憾的是,使用该 JFreeChart 解决方法故意强制 TextLayout 使用单一字体,它甚至不尝试查找需要以不同字体呈现的不可显示字符的运行。 【参考方案1】:我终于明白了。有许多根本原因,而跨平台变异性的增加进一步阻碍了这些原因。
JFreeChart 在错误的位置呈现文本,因为它使用不同的字体对象
出现布局问题是因为 JFreeChart 无意中使用了一个 与 AWT 实际用于呈现字体的字体对象不同的字体对象来计算布局的指标。 (供参考,JFreeChart 的计算发生在org.jfree.text#getTextBounds
。)
不同 Font 对象的原因是问题中提到的隐式“魔术操作”的结果,该操作在 java.awt.font.TextLayout#singleFont
内部执行。
这三行魔法操作可以浓缩成这样:
font = Font.getFont(font.getAttributes())
在英语中,这要求字体管理器根据所提供字体的“属性”(名称、系列、磅值等)为我们提供一个新的 Font 对象。在某些情况下,它返回给您的Font
会与您最初使用的Font
不同。
为了更正指标(从而修复布局),修复方法是在您自己的 Font
对象上运行上面的单行代码,然后再在 JFreeChart 对象中设置字体。
完成此操作后,布局对我来说很好,日文字符也是如此。它也应该为您修复布局,尽管它可能无法为 you 正确显示日文字符。阅读以下有关原生字体的内容以了解原因。
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 Font Book 中也安装了与上面相同的物理字体
在计算图表布局时,JFreeChart 向物理Font
对象询问日文文本的度量。物理 Font
无法正确执行此操作,因为它不支持亚洲字符集。
在实际绘制图表时,TextLayout#singleFont
中的魔术操作导致它获取CFont
对象并使用同名的本机字体绘制字形,而不是物理TrueTypeFont
。因此,字形是正确的,但位置不正确。
您将获得不同的结果,具体取决于您是否注册了字体以及您的操作系统中是否安装了该字体
如果您使用创建的 TTF 字体的属性调用 Font.getFont()
,您将获得三种不同结果之一,具体取决于该字体是否已注册以及您是否在本机安装了相同的字体:
CFont
for你想要的字体。
如果您在 GraphicsEnvironment 中注册了 TTF Font
,但您没有同名的本机字体,则调用 Font.getFont() 将返回一个物理 TrueTypeFont
对象。这会为您提供所需的字体,但不会获得亚洲字符。
如果您没有注册 TTF Font
并且您也没有同名的本地字体,则调用 Font.getFont() 会返回一个支持亚洲的 CFont,但它不会是您请求的字体。
事后看来,这一切都不足为奇。导致:
我无意中使用了错误的字体
在生产应用程序中,我正在创建一种字体,但我最初忘记在 GraphicsEnvironment 中注册它。如果您在执行上述魔术操作时还没有注册字体,Font.getFont()
不知道如何检索它,而是获得了备用字体。哎呀。
在 Windows、Mac 和 Linux 上,这种备用字体一般看起来是 Dialog,它是一种支持亚洲字符的逻辑(复合)字体。至少在 Java 7u72 中,Dialog 字体默认为以下西方字母字体:
Mac:露西达·格兰德 Linux (CentOS):Lucida Sans Windows:宋体这个错误对我们的亚洲用户来说实际上是一件好事,因为这意味着他们的字符集使用逻辑字体按预期呈现......尽管西方用户没有得到符合预期的字符集我们想要。
由于它一直以错误的字体呈现,并且无论如何我们都需要修复日文布局,我决定我最好尝试为将来的版本标准化一种通用字体(从而更接近垃圾上帝的建议) .
此外,应用程序具有字体渲染质量要求,可能并不总是允许使用某些字体,因此一个合理的决定似乎是尝试将应用程序配置为使用 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,渲染质量很好,每个人都很开心!
【讨论】:
我总是很高兴看到什么被证明能够获得良好的跨平台结果!【参考方案2】:虽然它没有直接解决您的问题,但我认为它可能会提供一个有用的参考点,以在未修饰的图表中使用平台的默认字体显示结果。 BarChartDemo1
的简化版source如下所示。
由于第三方字体指标的变幻莫测,我尽量避免偏离平台的标准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 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://***.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);
);
【讨论】:
感谢您的参考!如果我能够使用标准的逻辑字体,我会喜欢的。不幸的是,某些客户端平台(尤其是 CentOS)上的系统字体很丑陋,并且不适合演示质量图表,至少就拉丁文本而言。我不太关心日文字符的质量:如果我可以让它以漂亮的字体呈现拉丁文本,但回退到日文或其他不受支持的字符的平台字体,那很好。我怀疑这就是它在我的示例中所做的,+/- 指标问题。 我看到StandardChartTheme
默认为"Tahoma"
;你可以试试Font.SANS_SERIF
。
在“不丑”位之上,我想我还应该添加“一致性”,所以平台字体不会削减它。例如,我不希望图表仅仅因为 IT 部门从 Windows 切换到 Linux 而变得混乱。我什至尝试使用Lucida Sans,但仍然发现结果并不理想。 (我想我应该在我的个人资料中添加“字体势利小人”?我也很想仅仅因为你使用了分号就赞成你之前的评论。)
我会敦促您接受平台 UI 的一致性而不是跨平台的一致性。让用户choose 外观和感觉和persist 选择。
我同意你的最终用户程序,但这是一个服务器应用程序,输出在用户的浏览器中呈现和/或导出为 PDF(然后经常通过电子邮件发送给不太懂技术的人)。我很确定(比如说)现代 OS X Mavericks 用户不会接受一些古老的 Linux 发行版附带的蹩脚的平台字体,而 IT 部门恰好安装了该软件。 :-)以上是关于为啥只有拉丁字符的 Java 字体声称支持亚洲字符,即使它不支持?的主要内容,如果未能解决你的问题,请参考以下文章