When rendering a chart with JFreeChart, I noticed a layout problem when the chart's category labels included Japanese characters. Although the text is rendered with the correct glyphs, the text was positioned in the wrong location, presumably because the font metrics were wrong.
使用JFreeChart渲染图表时,我注意到当图表的类别标签包含日文字符时出现布局问题。虽然文本使用正确的字形进行渲染,但文本位于错误的位置,可能是因为字体指标错误。
The chart was originally configured to use the Source Sans Pro Regular font for that text, which supports only Latin character sets. The obvious solution is to bundle an actual Japanese .TTF font and ask JFreeChart to use it. This works fine, in that the output text uses the correct glyphs and it is also laid out correctly.
该图表最初配置为使用Source Sans Pro Regular字体为该文本,该字体仅支持拉丁字符集。显而易见的解决方案是捆绑一个实际的日语.TTF字体并要求JFreeChart使用它。这样可以正常工作,因为输出文本使用了正确的字形,并且它也正确布局。
My questions
-
How did java.awt end up rendering the Japanese characters correctly in the first scenario, when using a source font that doesn't actually support anything except Latin characters? If it matters, I am testing on OS X 10.9 with JDK 1.7u45.
在使用除拉丁字符之外实际上不支持任何内容的源字体时,java.awt如何在第一个场景中正确呈现日文字符?如果重要的话,我正在使用JDK 1.7u45测试OS X 10.9。
-
Is there any way to render the Japanese characters without bundling a separate Japanese font? (This is my end goal!) Although the bundling solution works, I don't want to add 6 Mb of bloat to my application if it can be avoided. Java clearly knows how to render the Japanese glyphs somehow even without the font (at least in my local environment)--it's seemingly just the metrics that are busted. I am wondering if this is related to the "frankenfont" issue below.
有没有办法渲染日文字符而不捆绑单独的日文字体? (这是我的最终目标!)尽管捆绑解决方案有效,但如果可以避免,我不想在我的应用程序中添加6 Mb的膨胀。 Java清楚地知道如何在没有字体的情况下以某种方式呈现日语字形(至少在我的本地环境中) - 它看起来只是被破坏的指标。我想知道这是否与下面的“frankenfont”问题有关。
-
After the JRE performs an internal transformation, why does the Source Sans Pro font tell the caller (via canDisplayUpTo()) that it can display Japanese characters even though it cannot? (See below.)
在JRE执行内部转换之后,为什么Source Sans Pro字体告诉调用者(通过canDisplayUpTo())它可以显示日文字符,即使它不能? (见下文。)
Edited to clarify:
-
This is a server app, and the text we are rendering will show up in the client's browser and/or in PDF exports. The charts are always rasterized to PNGs on the server.
这是一个服务器应用程序,我们呈现的文本将显示在客户端的浏览器和/或PDF导出中。图表始终光栅化为服务器上的PNG。
-
I have no control over the server OS or environment, and as nice as it would be to use the Java-standard platform fonts, many platforms have poor font choices that are unacceptable in my use case, so I need to bundle my own (at least for the Latin fonts). Using a platform font for the Japanese text is acceptable.
我无法控制服务器操作系统或环境,并且使用Java标准平台字体一样好,许多平台的字体选择都很差,在我的用例中是不可接受的,所以我需要捆绑自己的(在至少对于拉丁字体)。可以使用日语文本的平台字体。
-
The app can potentially be asked to display a mix of Japanese and Latin text, without no a priori knowledge of the text type. I am ambivalent about what fonts get used if a string contains mixed languages, so long as the glyphs are rendered correctly.
可能会要求应用程序显示日语和拉丁文本的混合,而不会对文本类型有先验知识。如果字符串包含混合语言,只要字形正确呈现,我就会使用什么字体变得矛盾。
Details
I understand that java.awt.Font#TextLayout is smart, and that when trying to lay out text, it first asks the underlying fonts whether they can actually render the supplied characters. If not, it presumably swaps in a different font that knows how to render those characters, but this is not happening here, based on my debugging pretty far into the JRE classes. TextLayout#singleFont
always returns a non-null value for the font and it proceeds through the fastInit()
part of the constructor.
我知道java.awt.Font #TextLayout是聪明的,并且在尝试布局文本时,它首先询问底层字体是否可以实际呈现所提供的字符。如果没有,它可能会以不同的字体交换,知道如何渲染这些字符,但这不会发生在这里,基于我对JRE类的调试。 TextLayout#singleFont总是为字体返回一个非null值,它继续通过构造函数的fastInit()部分。
One very curious note is that the Source Sans Pro font somehow gets coerced into telling the caller that it does know how to render Japanese characters after the JRE performs a transformation on the font.
一个非常好奇的注意事项是Source Sans Pro字体以某种方式被强制告诉调用者它知道如何在JRE对字体执行转换后呈现日文字符。
For example:
// 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));
The output of this is:
这个输出是:
Font Source Sans Pro can display up to: 0
Font Source Sans Pro can display up to: -1
Note that the three lines of "magic manipulation" mentioned above are not something of my own doing; we pass in the true source font object to JFreeChart, but it gets munged by the JRE when drawing the glyphs, which is what the three lines of "magic manipulation" code above replicates. The manipulation shown above is the functional equivalent of what happens in the following sequence of calls:
请注意,上面提到的三条“魔法操纵”并不是我自己所做的;我们将真正的源字体对象传递给JFreeChart,但是在绘制字形时它会被JRE传送,这就是上面三行“魔术操作”代码复制的内容。上面显示的操作功能等同于以下调用序列中发生的操作:
- org.jfree.text.TextUtilities#drawRotatedString
- sun.java2d.SunGraphics2D#drawString
- java.awt.font.TextLayout#(constructor)
- java.awt.font.TextLayout#singleFont
When we call Font.getFont() in the last line of the "magic" manipulation, we still get a Source Sans Pro font back, but the underlying font's font2D
field is different than the original font, and this single font now claims that it knows how to render the entire string. Why? It appears that Java is giving us back some sort of "frankenfont" that knows how to render all kinds of glyphs, even though it only understands the metrics for the glyphs that are supplied in the underlying source font.
当我们在“魔术”操作的最后一行调用Font.getFont()时,我们仍然得到Source Sans Pro字体,但底层字体的font2D字段与原始字体不同,而这个单字体现在声称它知道如何渲染整个字符串。为什么?似乎Java正在向我们提供某种“frankenfont”,它知道如何呈现各种字形,即使它只了解底层源字体中提供的字形的度量。
A more complete example showing the JFreeChart rendering example is here, based off one of the JFreeChart examples: https://gist.github.com/sdudley/b710fd384e495e7f1439 The output from this example is shown below.
这里显示了一个更完整的示例,显示了JFreeChart呈现示例,基于JFreeChart示例之一:https://gist.github.com/sdudley/b710fd384e495e7f1439此示例的输出如下所示。
Example with the Source Sans Pro font (laid out incorrectly):
Example with the IPA Japanese font (laid out correctly):
2 个解决方案
#1
5
I finally figured it out. There were a number of underlying causes, which was further hindered by an added dose of cross-platform variability.
我终于弄明白了。存在许多潜在原因,其进一步受到跨平台变异剂量的阻碍。
JFreeChart Renders Text in the Wrong Location Because It Uses a Different Font Object
The layout problem occurred because JFreeChart was inadvertently calculating the metrics for the layout using a different Font object than the one AWT actually uses to render the font. (For reference, JFreeChart's calculation happens in org.jfree.text#getTextBounds
.)
出现布局问题是因为JFreeChart使用与AWT实际用于渲染字体的字体对象不同的Font对象无意中计算了布局的度量。 (作为参考,JFreeChart的计算发生在org.jfree.text#getTextBounds中。)
The reason for the different Font object is a result of the implicit "magic manipulation" mentioned in the question, which is performed inside of java.awt.font.TextLayout#singleFont
.
不同Font对象的原因是问题中提到的隐式“魔术操作”的结果,该操作在java.awt.font.TextLayout #singleFont中执行。
Those three lines of magic manipulation can be condensed to just this:
这三行魔术操作可以简化为:
font = Font.getFont(font.getAttributes())
In English, this asks the font manager to give us a new Font object based on the "attributes" (name, family, point size, etc) of the supplied font. Under certain circumstances, the Font
it gives back to you will be different from the Font
you originally started with.
在英语中,这要求字体管理器根据提供的字体的“属性”(名称,族,点大小等)为我们提供一个新的Font对象。在某些情况下,它给你的字体将与你最初开始使用的字体不同。
To correct the metrics (and thus fix the layout), the fix is to run the one-liner above on your own Font
object before setting the font in JFreeChart objects.
要更正指标(从而修复布局),修复方法是在JFreeChart对象中设置字体之前在您自己的Font对象上运行上面的单行。
After doing this, the layout worked fine for me, as did the Japanese characters. It should fix the layout for you too, although it may not show the Japanese characters correctly for you. Read below about native fonts to understand why.
这样做之后,布局对我来说很好,就像日文字符一样。虽然它可能无法正确显示日文字符,但它也应该为您修复布局。阅读下面有关原生字体的内容,了解原因
The Mac OS X Font Manager Prefers to Return Native Fonts Even If You Feed it a Physical TTF File
The layout of the text was fixed by the above change...but why does this happen? Under what circumstances would the FontManager actually give us back a different type of Font
object than the one we provided?
上述更改修正了文本的布局......但为什么会发生这种情况?在什么情况下,FontManager实际上会给我们一个不同类型的Font对象,而不是我们提供的那个?
There are many reasons, but at least on Mac OS X, the reason related to the problem is that the font manager seems to prefer to return native fonts whenever possible.
原因有很多,但至少在Mac OS X上,与问题相关的原因是字体管理器似乎更愿意尽可能地返回本机字体。
In other words, if you create a new font from a physical TTF font named "Foobar" using Font.createFont
, and then call Font.getFont() with attributes derived from your "Foobar" physical font...so long as OS X already has a Foobar font installed, the font manager will give you back a CFont
object rather than the TrueTypeFont
object you were expecting. This seems to hold true even if you register the font through GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont
.
换句话说,如果使用Font.createFont从名为“Foobar”的物理TTF字体创建新字体,然后使用从“Foobar”物理字体派生的属性调用Font.getFont()...只要OS X已经安装了Foobar字体,字体管理器会给你一个CFont对象而不是你期望的TrueTypeFont对象。即使您通过GraphicsEnvironment.getLocalGraphicsEnvironment()。registerFont注册字体,这似乎也适用。
In my case, this threw a red herring into the investigation: I already had the "Source Sans" font installed on my Mac, which meant that I was getting different results from people who did not.
在我的情况下,这引发了一个红色的鲱鱼进入调查:我已经在我的Mac上安装了“Source Sans”字体,这意味着我得到的结果不同于那些没有的人。
Mac OS X Native Fonts Always Support Asian Characters
The crux of the matter is that Mac OS X CFont
objects always support Asian character sets. I am unclear of the exact mechanism that allows this, but I suspect that it's some sort of fallback font feature of OS X itself and not Java. In either case, a CFont
always claims to (and is truly able to) render Asian characters with the correct glyphs.
问题的关键在于Mac OS X CFont对象始终支持亚洲字符集。我不清楚允许这种情况的确切机制,但我怀疑它是OS X本身而不是Java的某种后备字体功能。在任何一种情况下,CFont总是声称(并且确实能够)使用正确的字形呈现亚洲字符。
This makes clear the mechanism that allowed the original problem to occur:
这清楚地表明了允许原始问题发生的机制:
- we created a physical
Font
from a physical TTF file, which does not itself support Japanese. - the same physical font as above was also installed in my Mac OS X Font Book
- when calculating the layout of the chart, JFreeChart asked the physical
Font
object for the metrics of the Japanese text. The physicalFont
could not do this correctly because it does not support Asian character sets. - when actually drawing the chart, the magic manipulation in
TextLayout#singleFont
caused it to obtain aCFont
object and draw the glyph using the same-named native font, versus the physicalTrueTypeFont
. Thus, the glyphs were correct but they were not positioned properly.
我们从物理TTF文件创建了一个物理字体,它本身不支持日语。
我的Mac OS X字体书中也安装了与上面相同的物理字体
在计算图表的布局时,JFreeChart向物理Font对象询问日文文本的度量。物理Font无法正确执行此操作,因为它不支持亚洲字符集。
当实际绘制图表时,TextLayout#singleFont中的魔术操作导致它获取CFont对象并使用相同命名的本机字体绘制字形,而不是物理TrueTypeFont。因此,字形是正确的,但它们没有正确定位。
You Will Get Different Results Depending on Whether You Registered the Font and Whether You Have The Font Installed in Your OS
If you call Font.getFont()
with the attributes from a created TTF font, you will get one of three different results, depending on whether the font is registered and whether you have the same font installed natively:
如果使用创建的TTF字体中的属性调用Font.getFont(),则将获得三种不同结果之一,具体取决于字体是否已注册以及是否具有本机安装的相同字体:
- If you do have a native platform font installed with the same name as your TTF font (regardless of whether you registered the font or not), you will get an Asian-supporting
CFont
for the font you wanted. - If you registered the TTF
Font
in the GraphicsEnvironment but you do not have a native font of the same name, calling Font.getFont() will return a physicalTrueTypeFont
object back. This gives you the font you want, but you don't get Asian characters. - If you did not register the TTF
Font
and you also do not have a native font of the same name, calling Font.getFont() return an Asian-supporting CFont, but it will not be the font you requested.
如果您确实安装了与TTF字体同名的本机平台字体(无论您是否注册了字体),您将获得支持亚洲语的CFont以获得您想要的字体。
如果您在GraphicsEnvironment中注册了TTF字体但没有相同名称的本机字体,则调用Font.getFont()将返回一个物理TrueTypeFont对象。这为您提供了所需的字体,但您没有获得亚洲字符。
如果您没有注册TTF字体,并且您也没有相同名称的本机字体,则调用Font.getFont()将返回支持亚洲语的CFont,但它不是您请求的字体。
In hindsight, none of this is entirely surprising. Leading to:
事后看来,这一切都不足为奇。导致:
I Was Inadvertently Using the Wrong Font
In the production app, I was creating a font, but I forgot to initially register it with the GraphicsEnvironment. If you haven't registered a font when you perform the magic manipulation above, Font.getFont()
doesn't know how to retrieve it and you get a backup font instead. Oops.
在生产应用程序中,我正在创建一个字体,但我忘了最初使用GraphicsEnvironment注册它。如果在执行上述魔术操作时尚未注册字体,则Font.getFont()不知道如何检索它,而是获得备份字体。哎呀。
On Windows, Mac and Linux, this backup font generally seems to be Dialog, which is a logical (composite) font that supports Asian characters. At least in Java 7u72, the Dialog font defaults to the following fonts for Western alphabets:
在Windows,Mac和Linux上,这种备份字体通常似乎是Dialog,它是一种支持亚洲字符的逻辑(复合)字体。至少在Java 7u72中,Dialog字体默认为西方字母的以下字体:
- Mac: Lucida Grande
- Linux (CentOS): Lucida Sans
- Windows: Arial
Mac:Lucida Grande
Linux(CentOS):Lucida Sans
This mistake was actually a good thing for our Asian users, because it meant that their character sets rendered as expected with the logical font...although the Western users were not getting the character sets that we wanted.
这个错误对于我们的亚洲用户来说实际上是一件好事,因为它意味着他们的字符集以逻辑字体呈现为预期......尽管西方用户没有得到我们想要的字符集。
Since it had been rendering in the wrong fonts and we needed to fix the Japanese layout anyway, I decided that I would be better off trying to standardize on one single common font for future releases (and thus coming closer to trashgod's suggestions).
由于它使用错误的字体进行渲染,我们无论如何都需要修复日语布局,因此我决定在未来的版本中尝试标准化一种常用字体(因此更接近trashgod的建议)。
Additionally, the app has font rendering quality requirements that may not always permit the use of certain fonts, so a reasonable decision seemed to be to try to configure the app to use Lucida Sans, which is the one physical font that is included by Oracle in all copies of Java. But...
此外,该应用程序具有字体呈现质量要求,可能并不总是允许使用某些字体,因此一个合理的决定似乎是尝试配置应用程序使用Lucida Sans,这是Oracle包含的一种物理字体所有Java副本。但...
Lucida Sans Doesn't Play Well with Asian Characters on All Platforms
The decision to try using Lucida Sans seemed reasonable...but I quickly found out that there are platform differences in how Lucida Sans is handled. On Linux and Windows, if you ask for a copy of the "Lucida Sans" font, you get a physical TrueTypeFont
object. But that font doesn't support Asian characters.
尝试使用Lucida Sans的决定似乎是合理的......但我很快发现Lucida Sans的处理方式存在平台差异。在Linux和Windows上,如果要求提供“Lucida Sans”字体的副本,则会获得物理TrueTypeFont对象。但该字体不支持亚洲字符。
The same problem holds true on Mac OS X if you request "Lucida Sans"...but if you ask for the slightly different name "LucidaSans" (note the lack of space), then you get a CFont
object that supports Lucida Sans as well as Asian characters, so you can have your cake and eat it too.
如果你请求“Lucida Sans”,Mac OS X也会出现同样的问题...但是如果你要求稍微不同的名字“LucidaSans”(注意缺少空间),那么你得到一个支持Lucida Sans的CFont对象以及亚洲人物,所以你可以吃蛋糕,也可以吃。
On other platforms, requesting "LucidaSans" yields a copy of the standard Dialog font because there is no such font and Java is returning its default. On Linux, you are somewhat lucky here because Dialog actually defaults to Lucida Sans for Western text (and it also uses a decent fallback font for Asian characters).
在其他平台上,请求“LucidaSans”会生成标准Dialog字体的副本,因为没有这样的字体而Java正在返回其默认值。在Linux上,你在这里有点幸运,因为Dialog实际上默认使用Lucida Sans为西方文本(并且它也为亚洲字符使用了一个不错的后备字体)。
This gives us a path to get (almost) the same physical font on all platforms, and which also supports Asian characters, by requesting fonts with these names:
这为我们提供了一条路径,通过请求具有以下名称的字体,在所有平台上获得(几乎)相同的物理字体,并且还支持亚洲字符:
- Mac OS X: "LucidaSans" (yielding Lucida Sans + Asian backup fonts)
- Linux: "Dialog" (yielding Lucida Sans + Asian backup fonts)
- Windows: "Dialog" (yielding Arial + Asian backup fonts)
Mac OS X:“LucidaSans”(产生Lucida Sans +亚洲备份字体)
Linux:“Dialog”(产生Lucida Sans +亚洲备份字体)
Windows:“Dialog”(产生Arial +亚洲备份字体)
I've pored over the fonts.properties on Windows and I could not find a font sequence that defaulted to Lucida Sans, so it looks like our Windows users will need to get stuck with Arial...but at least it's not that visually different from Lucida Sans, and the Windows font rendering quality is reasonable.
我已经仔细研究了Windows上的fonts.properties,我找不到默认为Lucida Sans的字体序列,所以看起来我们的Windows用户需要卡住Arial ...但至少它不是那么视觉上不同来自Lucida Sans,以及Windows字体的渲染质量是合理的。
Where Did Everything End Up?
In sum, we're now pretty much just using platform fonts. (I am sure that @trashgod is having a good chuckle right now!) Both Mac and Linux servers get Lucida Sans, Windows gets Arial, the rendering quality is good, and everyone is happy!
总而言之,我们现在几乎只使用平台字体。 (我确信@trashgod现在笑得很开心!)Mac和Linux服务器都获得Lucida Sans,Windows获得Arial,渲染质量很好,每个人都很开心!
#2
3
Although it doesn't address your question directly, I thought it might provide a useful point of reference to show the result using the platform's default font in an unadorned chart. A simplified version of BarChartDemo1
, source, is shown below.
虽然它没有直接解决你的问题,但我认为它可能提供一个有用的参考点,用于在纯粹的图表中使用平台的默认字体显示结果。 BarChartDemo1的简化版源,如下所示。
Due to the vagaries of third-party font metrics, I try to avoid deviating from the platform's standard logical fonts, which are chosen based on the platform's supported locale's. Logical fonts are mapped to physical font's in the platform's configuration files. On Mac OS, the relevant file are in $JAVA_HOME/jre/lib/
, where $JAVA_HOME
is result of evaluating /usr/libexec/java_home -v 1.n
and n is your version. I see similar results with either version 7 or 8. In particular, fontconfig.properties.src
defines the font used to supply Japanese font family variations. All mappings appear to use MS Mincho
or MS Gothic
.
由于第三方字体指标的变幻莫测,我尽量避免偏离平台的标准逻辑字体,这些逻辑字体是根据平台支持的语言环境选择的。逻辑字体映射到平台配置文件中的物理字体。在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);
});
}
}
#1
5
I finally figured it out. There were a number of underlying causes, which was further hindered by an added dose of cross-platform variability.
我终于弄明白了。存在许多潜在原因,其进一步受到跨平台变异剂量的阻碍。
JFreeChart Renders Text in the Wrong Location Because It Uses a Different Font Object
The layout problem occurred because JFreeChart was inadvertently calculating the metrics for the layout using a different Font object than the one AWT actually uses to render the font. (For reference, JFreeChart's calculation happens in org.jfree.text#getTextBounds
.)
出现布局问题是因为JFreeChart使用与AWT实际用于渲染字体的字体对象不同的Font对象无意中计算了布局的度量。 (作为参考,JFreeChart的计算发生在org.jfree.text#getTextBounds中。)
The reason for the different Font object is a result of the implicit "magic manipulation" mentioned in the question, which is performed inside of java.awt.font.TextLayout#singleFont
.
不同Font对象的原因是问题中提到的隐式“魔术操作”的结果,该操作在java.awt.font.TextLayout #singleFont中执行。
Those three lines of magic manipulation can be condensed to just this:
这三行魔术操作可以简化为:
font = Font.getFont(font.getAttributes())
In English, this asks the font manager to give us a new Font object based on the "attributes" (name, family, point size, etc) of the supplied font. Under certain circumstances, the Font
it gives back to you will be different from the Font
you originally started with.
在英语中,这要求字体管理器根据提供的字体的“属性”(名称,族,点大小等)为我们提供一个新的Font对象。在某些情况下,它给你的字体将与你最初开始使用的字体不同。
To correct the metrics (and thus fix the layout), the fix is to run the one-liner above on your own Font
object before setting the font in JFreeChart objects.
要更正指标(从而修复布局),修复方法是在JFreeChart对象中设置字体之前在您自己的Font对象上运行上面的单行。
After doing this, the layout worked fine for me, as did the Japanese characters. It should fix the layout for you too, although it may not show the Japanese characters correctly for you. Read below about native fonts to understand why.
这样做之后,布局对我来说很好,就像日文字符一样。虽然它可能无法正确显示日文字符,但它也应该为您修复布局。阅读下面有关原生字体的内容,了解原因
The Mac OS X Font Manager Prefers to Return Native Fonts Even If You Feed it a Physical TTF File
The layout of the text was fixed by the above change...but why does this happen? Under what circumstances would the FontManager actually give us back a different type of Font
object than the one we provided?
上述更改修正了文本的布局......但为什么会发生这种情况?在什么情况下,FontManager实际上会给我们一个不同类型的Font对象,而不是我们提供的那个?
There are many reasons, but at least on Mac OS X, the reason related to the problem is that the font manager seems to prefer to return native fonts whenever possible.
原因有很多,但至少在Mac OS X上,与问题相关的原因是字体管理器似乎更愿意尽可能地返回本机字体。
In other words, if you create a new font from a physical TTF font named "Foobar" using Font.createFont
, and then call Font.getFont() with attributes derived from your "Foobar" physical font...so long as OS X already has a Foobar font installed, the font manager will give you back a CFont
object rather than the TrueTypeFont
object you were expecting. This seems to hold true even if you register the font through GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont
.
换句话说,如果使用Font.createFont从名为“Foobar”的物理TTF字体创建新字体,然后使用从“Foobar”物理字体派生的属性调用Font.getFont()...只要OS X已经安装了Foobar字体,字体管理器会给你一个CFont对象而不是你期望的TrueTypeFont对象。即使您通过GraphicsEnvironment.getLocalGraphicsEnvironment()。registerFont注册字体,这似乎也适用。
In my case, this threw a red herring into the investigation: I already had the "Source Sans" font installed on my Mac, which meant that I was getting different results from people who did not.
在我的情况下,这引发了一个红色的鲱鱼进入调查:我已经在我的Mac上安装了“Source Sans”字体,这意味着我得到的结果不同于那些没有的人。
Mac OS X Native Fonts Always Support Asian Characters
The crux of the matter is that Mac OS X CFont
objects always support Asian character sets. I am unclear of the exact mechanism that allows this, but I suspect that it's some sort of fallback font feature of OS X itself and not Java. In either case, a CFont
always claims to (and is truly able to) render Asian characters with the correct glyphs.
问题的关键在于Mac OS X CFont对象始终支持亚洲字符集。我不清楚允许这种情况的确切机制,但我怀疑它是OS X本身而不是Java的某种后备字体功能。在任何一种情况下,CFont总是声称(并且确实能够)使用正确的字形呈现亚洲字符。
This makes clear the mechanism that allowed the original problem to occur:
这清楚地表明了允许原始问题发生的机制:
- we created a physical
Font
from a physical TTF file, which does not itself support Japanese. - the same physical font as above was also installed in my Mac OS X Font Book
- when calculating the layout of the chart, JFreeChart asked the physical
Font
object for the metrics of the Japanese text. The physicalFont
could not do this correctly because it does not support Asian character sets. - when actually drawing the chart, the magic manipulation in
TextLayout#singleFont
caused it to obtain aCFont
object and draw the glyph using the same-named native font, versus the physicalTrueTypeFont
. Thus, the glyphs were correct but they were not positioned properly.
我们从物理TTF文件创建了一个物理字体,它本身不支持日语。
我的Mac OS X字体书中也安装了与上面相同的物理字体
在计算图表的布局时,JFreeChart向物理Font对象询问日文文本的度量。物理Font无法正确执行此操作,因为它不支持亚洲字符集。
当实际绘制图表时,TextLayout#singleFont中的魔术操作导致它获取CFont对象并使用相同命名的本机字体绘制字形,而不是物理TrueTypeFont。因此,字形是正确的,但它们没有正确定位。
You Will Get Different Results Depending on Whether You Registered the Font and Whether You Have The Font Installed in Your OS
If you call Font.getFont()
with the attributes from a created TTF font, you will get one of three different results, depending on whether the font is registered and whether you have the same font installed natively:
如果使用创建的TTF字体中的属性调用Font.getFont(),则将获得三种不同结果之一,具体取决于字体是否已注册以及是否具有本机安装的相同字体:
- If you do have a native platform font installed with the same name as your TTF font (regardless of whether you registered the font or not), you will get an Asian-supporting
CFont
for the font you wanted. - If you registered the TTF
Font
in the GraphicsEnvironment but you do not have a native font of the same name, calling Font.getFont() will return a physicalTrueTypeFont
object back. This gives you the font you want, but you don't get Asian characters. - If you did not register the TTF
Font
and you also do not have a native font of the same name, calling Font.getFont() return an Asian-supporting CFont, but it will not be the font you requested.
如果您确实安装了与TTF字体同名的本机平台字体(无论您是否注册了字体),您将获得支持亚洲语的CFont以获得您想要的字体。
如果您在GraphicsEnvironment中注册了TTF字体但没有相同名称的本机字体,则调用Font.getFont()将返回一个物理TrueTypeFont对象。这为您提供了所需的字体,但您没有获得亚洲字符。
如果您没有注册TTF字体,并且您也没有相同名称的本机字体,则调用Font.getFont()将返回支持亚洲语的CFont,但它不是您请求的字体。
In hindsight, none of this is entirely surprising. Leading to:
事后看来,这一切都不足为奇。导致:
I Was Inadvertently Using the Wrong Font
In the production app, I was creating a font, but I forgot to initially register it with the GraphicsEnvironment. If you haven't registered a font when you perform the magic manipulation above, Font.getFont()
doesn't know how to retrieve it and you get a backup font instead. Oops.
在生产应用程序中,我正在创建一个字体,但我忘了最初使用GraphicsEnvironment注册它。如果在执行上述魔术操作时尚未注册字体,则Font.getFont()不知道如何检索它,而是获得备份字体。哎呀。
On Windows, Mac and Linux, this backup font generally seems to be Dialog, which is a logical (composite) font that supports Asian characters. At least in Java 7u72, the Dialog font defaults to the following fonts for Western alphabets:
在Windows,Mac和Linux上,这种备份字体通常似乎是Dialog,它是一种支持亚洲字符的逻辑(复合)字体。至少在Java 7u72中,Dialog字体默认为西方字母的以下字体:
- Mac: Lucida Grande
- Linux (CentOS): Lucida Sans
- Windows: Arial
Mac:Lucida Grande
Linux(CentOS):Lucida Sans
This mistake was actually a good thing for our Asian users, because it meant that their character sets rendered as expected with the logical font...although the Western users were not getting the character sets that we wanted.
这个错误对于我们的亚洲用户来说实际上是一件好事,因为它意味着他们的字符集以逻辑字体呈现为预期......尽管西方用户没有得到我们想要的字符集。
Since it had been rendering in the wrong fonts and we needed to fix the Japanese layout anyway, I decided that I would be better off trying to standardize on one single common font for future releases (and thus coming closer to trashgod's suggestions).
由于它使用错误的字体进行渲染,我们无论如何都需要修复日语布局,因此我决定在未来的版本中尝试标准化一种常用字体(因此更接近trashgod的建议)。
Additionally, the app has font rendering quality requirements that may not always permit the use of certain fonts, so a reasonable decision seemed to be to try to configure the app to use Lucida Sans, which is the one physical font that is included by Oracle in all copies of Java. But...
此外,该应用程序具有字体呈现质量要求,可能并不总是允许使用某些字体,因此一个合理的决定似乎是尝试配置应用程序使用Lucida Sans,这是Oracle包含的一种物理字体所有Java副本。但...
Lucida Sans Doesn't Play Well with Asian Characters on All Platforms
The decision to try using Lucida Sans seemed reasonable...but I quickly found out that there are platform differences in how Lucida Sans is handled. On Linux and Windows, if you ask for a copy of the "Lucida Sans" font, you get a physical TrueTypeFont
object. But that font doesn't support Asian characters.
尝试使用Lucida Sans的决定似乎是合理的......但我很快发现Lucida Sans的处理方式存在平台差异。在Linux和Windows上,如果要求提供“Lucida Sans”字体的副本,则会获得物理TrueTypeFont对象。但该字体不支持亚洲字符。
The same problem holds true on Mac OS X if you request "Lucida Sans"...but if you ask for the slightly different name "LucidaSans" (note the lack of space), then you get a CFont
object that supports Lucida Sans as well as Asian characters, so you can have your cake and eat it too.
如果你请求“Lucida Sans”,Mac OS X也会出现同样的问题...但是如果你要求稍微不同的名字“LucidaSans”(注意缺少空间),那么你得到一个支持Lucida Sans的CFont对象以及亚洲人物,所以你可以吃蛋糕,也可以吃。
On other platforms, requesting "LucidaSans" yields a copy of the standard Dialog font because there is no such font and Java is returning its default. On Linux, you are somewhat lucky here because Dialog actually defaults to Lucida Sans for Western text (and it also uses a decent fallback font for Asian characters).
在其他平台上,请求“LucidaSans”会生成标准Dialog字体的副本,因为没有这样的字体而Java正在返回其默认值。在Linux上,你在这里有点幸运,因为Dialog实际上默认使用Lucida Sans为西方文本(并且它也为亚洲字符使用了一个不错的后备字体)。
This gives us a path to get (almost) the same physical font on all platforms, and which also supports Asian characters, by requesting fonts with these names:
这为我们提供了一条路径,通过请求具有以下名称的字体,在所有平台上获得(几乎)相同的物理字体,并且还支持亚洲字符:
- Mac OS X: "LucidaSans" (yielding Lucida Sans + Asian backup fonts)
- Linux: "Dialog" (yielding Lucida Sans + Asian backup fonts)
- Windows: "Dialog" (yielding Arial + Asian backup fonts)
Mac OS X:“LucidaSans”(产生Lucida Sans +亚洲备份字体)
Linux:“Dialog”(产生Lucida Sans +亚洲备份字体)
Windows:“Dialog”(产生Arial +亚洲备份字体)
I've pored over the fonts.properties on Windows and I could not find a font sequence that defaulted to Lucida Sans, so it looks like our Windows users will need to get stuck with Arial...but at least it's not that visually different from Lucida Sans, and the Windows font rendering quality is reasonable.
我已经仔细研究了Windows上的fonts.properties,我找不到默认为Lucida Sans的字体序列,所以看起来我们的Windows用户需要卡住Arial ...但至少它不是那么视觉上不同来自Lucida Sans,以及Windows字体的渲染质量是合理的。
Where Did Everything End Up?
In sum, we're now pretty much just using platform fonts. (I am sure that @trashgod is having a good chuckle right now!) Both Mac and Linux servers get Lucida Sans, Windows gets Arial, the rendering quality is good, and everyone is happy!
总而言之,我们现在几乎只使用平台字体。 (我确信@trashgod现在笑得很开心!)Mac和Linux服务器都获得Lucida Sans,Windows获得Arial,渲染质量很好,每个人都很开心!
#2
3
Although it doesn't address your question directly, I thought it might provide a useful point of reference to show the result using the platform's default font in an unadorned chart. A simplified version of BarChartDemo1
, source, is shown below.
虽然它没有直接解决你的问题,但我认为它可能提供一个有用的参考点,用于在纯粹的图表中使用平台的默认字体显示结果。 BarChartDemo1的简化版源,如下所示。
Due to the vagaries of third-party font metrics, I try to avoid deviating from the platform's standard logical fonts, which are chosen based on the platform's supported locale's. Logical fonts are mapped to physical font's in the platform's configuration files. On Mac OS, the relevant file are in $JAVA_HOME/jre/lib/
, where $JAVA_HOME
is result of evaluating /usr/libexec/java_home -v 1.n
and n is your version. I see similar results with either version 7 or 8. In particular, fontconfig.properties.src
defines the font used to supply Japanese font family variations. All mappings appear to use MS Mincho
or MS Gothic
.
由于第三方字体指标的变幻莫测,我尽量避免偏离平台的标准逻辑字体,这些逻辑字体是根据平台支持的语言环境选择的。逻辑字体映射到平台配置文件中的物理字体。在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);
});
}
}