为什么 `java.awt.Font.getStringBounds` 在不同机器上的结果不同?

huangapple go评论101阅读模式
英文:

Why does java.awt.Font.getStringBounds give different result on different machines?

问题

我有一个生成PDF报告的应用程序(使用JasperReports),但是如果我在开发笔记本电脑上运行我的应用程序,文本字段的大小与我在服务器上生成完全相同的报告时的大小略有不同。最终,我将问题缩小到以下代码:

final Font font = Font.createFont(
    Font.TRUETYPE_FONT,
    MyTest.class.getResourceAsStream("/fonts/europa/Europa-Bold.otf")
).deriveFont(10f);
System.out.println(font);
System.out.println(font.getStringBounds(
    "Text",
    0,
    4,
    new FontRenderContext(null, true, true)
));

在我的笔记本电脑上,这将打印出:

java.awt.Font[family=Europa-Bold,name=Europa-Bold,style=plain,size=10]
java.awt.geom.Rectangle2D$Float[x=0.0,y=-9.90999,w=20.080002,h=12.669988]

在服务器上,这将打印出:

java.awt.Font[family=Europa-Bold,name=Europa-Bold,style=plain,size=10]
java.awt.geom.Rectangle2D$Float[x=0.0,y=-7.6757812,w=20.06897,h=10.094452]

正如您所见,我实际上将字体文件随应用程序一起提供,因此我相信两台计算机实际上不可能使用不同的字体。

根据这些条件,我本以为getStringBounds的输出是与系统无关的。显然并非如此。可能是什么原因导致了这种差异呢?

英文:

I have an application that generates PDF reports (using JasperReports), however if I run my application on my development laptop the text fields have a slightly different size than when I generate the exact same report on the server. I eventually reduced the issue to the following code:

final Font font = Font.createFont(
    Font.TRUETYPE_FONT,
    MyTest.class.getResourceAsStream("/fonts/europa/Europa-Bold.otf")
).deriveFont(10f);
System.out.println(font);
System.out.println(font.getStringBounds(
    "Text",
    0,
    4,
    new FontRenderContext(null, true, true)
));

On my Laptop this prints:

java.awt.Font[family=Europa-Bold,name=Europa-Bold,style=plain,size=10]
java.awt.geom.Rectangle2D$Float[x=0.0,y=-9.90999,w=20.080002,h=12.669988]

On the server this prints:

java.awt.Font[family=Europa-Bold,name=Europa-Bold,style=plain,size=10]
java.awt.geom.Rectangle2D$Float[x=0.0,y=-7.6757812,w=20.06897,h=10.094452]

As you can see I actually ship the font file with the application, so I believe that there is no chance that both machines actually work with a different font.

I would have guessed that under these conditions the output of getStringBounds is system-independent. Obviously it is not. What could possibly cause the difference?

答案1

得分: 6

免责声明!:我不是字体开发专家,只是分享我的经验。

是的,它有点本地化。甚至像新的 JavaFX web view 一样,也依赖于 webkit

如果你深入调试 getStringBounds,你会意识到它会达到一个点,字体管理器需要决定加载一个具体的字体管理器,该类名应该是系统属性 sun.font.fontmanager

sun.font.FontManagerFactory 的源代码

...
private static final String DEFAULT_CLASS;
    static {
        if (FontUtilities.isWindows) {
            DEFAULT_CLASS = "sun.awt.Win32FontManager";
        } else if (FontUtilities.isMacOSX) {
            DEFAULT_CLASS = "sun.font.CFontManager";
            } else {
            DEFAULT_CLASS = "sun.awt.X11FontManager";
            }
    }
...
public static synchronized FontManager getInstance() {
...
String fmClassName = System.getProperty("sun.font.fontmanager", DEFAULT_CLASS);
}

这些 DEFAULT_CLASS 值可以验证你的 显然不是。可能是什么原因造成了差异? 标记。

sun.font.fontmanager 的值在某些类Unix系统上可能是 sun.awt.X11FontManager,但对于 Windows 来说可能是 null,所以管理器会是 sun.awt.Win32FontManager

现在,每个管理器可能会依赖于不同的底层的造型/渲染引擎/实现(这可能会有所帮助)。

主要原因可能是字体的性质。因为它们大多是矢量图形。因此基于平台/环境,渲染的文本可能会变大或变小。例如,Windows 可能应用了桌面 ClearType,以及屏幕文本大小(DPI)对请求的文本渲染。

似乎,即使你有完全相同的两个 sun.awt.X11FontManager 管理器,结果也会有所不同。这也可能有所帮助

如果你在在线编译器上尝试示例代码,你肯定会面临不同的结果。

ideaone 的结果(https://ideone.com/AuQvMV)可能不会发生,stderr 中有一些有趣的信息

java.lang.UnsatisfiedLinkError: /opt/jdk/lib/libfontmanager.so: libfreetype.so.6: cannot open shared object file: No such file or directory
	at java.base/java.lang.ClassLoader$NativeLibrary.load0(Native Method)
	at java.base/java.lang.ClassLoader$NativeLibrary.load(ClassLoader.java:2430)
	...

注意到 libfreetype 加载失败/丢失,这是一个本地的/C 字体渲染应用程序。

coding ground 的结果(https://www.tutorialspoint.com/compile_java_online.php)

fnt manager: sun.awt.X11FontManager
java.awt.Font[family=Dialog,name=tahoma,style=plain,size=10]
java.awt.geom.Rectangle2D$Float[x=0.0,y=-9.282227,w=22.09961,h=11.640625]

jdoodle 的结果(https://www.jdoodle.com/online-java-compiler/)

fnt manager: sun.awt.X11FontManager
java.awt.Font[family=Dialog,name=tahoma,style=plain,size=10]
java.awt.geom.Rectangle2D$Float[x=0.0,y=-9.839991,w=24.0,h=12.569988]

我的机器

fnt manager: null
java.awt.Font[family=Tahoma,name=tahoma,style=plain,size=10]
java.awt.geom.Rectangle2D$Float[x=0.0,y=-10.004883,w=19.399414,h=12.0703125]

我的故事可能有帮助,你可以尝试一下

几年前,我遇到了类似的问题,在某些年代的macOs/jdk8上,使用完全相同的嵌入字体进行文本渲染失败,对于复杂的文本渲染(许多连字)。不仅仅是大小,还有破碎的连字,字距等...

我通过另一种方法来解决这个问题(无法记住是否解决了大小问题,但肯定没有破碎的连字),如下所示

InputStream is = Main.class.getResourceAsStream(fontFile);
Font newFont = Font.createFont(Font.TRUETYPE_FONT, is);
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(newFont);
//稍后通过构造Font实例来加载字体
Font f = new Font(name/*嵌入字体的名称*/, style, size);

使用 GraphicsEnvironment 注册字体,然后使用 Font 实例化字体,解决了我们的问题。所以你也可以试试。

解决方案

最终,我放弃了 jdk 的东西(这真是一个大麻烦),转而采用了 harfbuzz(造型)+ freetype(渲染)的本地实现,确实让人心情舒畅。

所以...

• 你可以把生产服务器(简单的方法)作为渲染字体和渲染的参考,并基于它验证结果(而不是开发机器)
• 或者,使用跨平台和独立的(可能是本地的)造型/渲染字体引擎/实现,以确保开发和生产结果是一致的。

英文:

Disclaimer! : I'm not a font dev expert, just sharing my experience.

Yes, it's kind of native. Even new JavaFX web view for example, is depends on webkit.

If you dive into debugging for getStringBounds, you will realize it reaches a point, where font manager should decide to load a concreted font manager, where the class name is supposed to be system property sun.font.fontmanager.

Source code of sun.font.FontManagerFactory

...
private static final String DEFAULT_CLASS;
    static {
        if (FontUtilities.isWindows) {
            DEFAULT_CLASS = "sun.awt.Win32FontManager";
        } else if (FontUtilities.isMacOSX) {
            DEFAULT_CLASS = "sun.font.CFontManager";
            } else {
            DEFAULT_CLASS = "sun.awt.X11FontManager";
            }
    }
...
public static synchronized FontManager getInstance() {
...
String fmClassName = System.getProperty("sun.font.fontmanager", DEFAULT_CLASS);
}

Those DEFAULT_CLASS values could validate your Obviously it is not. What could possibly cause the difference? mark.

The val for sun.font.fontmanager may be sun.awt.X11FontManager for some nix systems, but could be null for windows for instance, so the manager will be sun.awt.Win32FontManager.

Now each manager, may depends on vary underlying shaping/rendering engine/impl for sure(this may help).

Main reason could be the nature of fonts. As they are mostly vector stuffs. So based on platform/env, a rendered text could be bigger, or smaller. e.g., maybe windows apply desktop cleartype, and screen text size(DPI) on requested text rendering.

It seems, even if you have exactly two sun.awt.X11FontManager manager, the results will be vary. this may help too

If you just try out the sample code, on online compilers, you will face with vary results for sure.

Result of ideaone (https://ideone.com/AuQvMV), could not be happened, stderr has some interesting info

java.lang.UnsatisfiedLinkError: /opt/jdk/lib/libfontmanager.so: libfreetype.so.6: cannot open shared object file: No such file or directory
	at java.base/java.lang.ClassLoader$NativeLibrary.load0(Native Method)
	at java.base/java.lang.ClassLoader$NativeLibrary.load(ClassLoader.java:2430)
	at java.base/java.lang.ClassLoader$NativeLibrary.loadLibrary(ClassLoader.java:2487)
	at java.base/java.lang.ClassLoader.loadLibrary0(ClassLoader.java:2684)
	at java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2638)
	at java.base/java.lang.Runtime.loadLibrary0(Runtime.java:827)
	at java.base/java.lang.System.loadLibrary(System.java:1902)
	at java.desktop/sun.font.FontManagerNativeLibrary$1.run(FontManagerNativeLibrary.java:57)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:310)
	at java.desktop/sun.font.FontManagerNativeLibrary.<clinit>(FontManagerNativeLibrary.java:32)
	at java.desktop/sun.font.SunFontManager$1.run(SunFontManager.java:270)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:310)
	at java.desktop/sun.font.SunFontManager.<clinit>(SunFontManager.java:266)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:415)
	at java.desktop/sun.font.FontManagerFactory$1.run(FontManagerFactory.java:82)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:310)
	at java.desktop/sun.font.FontManagerFactory.getInstance(FontManagerFactory.java:74)
	at java.desktop/java.awt.Font.getFont2D(Font.java:497)
	at java.desktop/java.awt.Font.getFamily(Font.java:1410)
	at java.desktop/java.awt.Font.getFamily_NoClientCode(Font.java:1384)
	at java.desktop/java.awt.Font.getFamily(Font.java:1376)
	at java.desktop/java.awt.Font.toString(Font.java:1869)
	at java.base/java.lang.String.valueOf(String.java:3042)
	at java.base/java.io.PrintStream.println(PrintStream.java:897)
	at Ideone.main(Main.java:19)

Note the failed/missed load of libfreetype which is a native/C font rendering app

Result of coding ground (https://www.tutorialspoint.com/compile_java_online.php)

fnt manager: sun.awt.X11FontManager
java.awt.Font[family=Dialog,name=tahoma,style=plain,size=10]
java.awt.geom.Rectangle2D$Float[x=0.0,y=-9.282227,w=22.09961,h=11.640625]

Result of jdoodle (https://www.jdoodle.com/online-java-compiler/)

fnt manager: sun.awt.X11FontManager
java.awt.Font[family=Dialog,name=tahoma,style=plain,size=10]
java.awt.geom.Rectangle2D$Float[x=0.0,y=-9.839991,w=24.0,h=12.569988]

My machine

fnt manager: null
java.awt.Font[family=Tahoma,name=tahoma,style=plain,size=10]
java.awt.geom.Rectangle2D$Float[x=0.0,y=-10.004883,w=19.399414,h=12.0703125]

My Story (may help, you may try)

I had similar issue back in some years ago, where text rendering using exactly same embedded font failed on macOs/jdk8, for complex text rendering(lots of ligatures). Not just sizes, but also broken ligatures, kerning, etc...

I could fix my issue(cannot remember if fixed the sizing, but no broken ligatures for sure), using another workground, as following

InputStream is = Main.class.getResourceAsStream(fontFile);
Font newFont = Font.createFont(Font.TRUETYPE_FONT, is);
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(newFont);
//later load the font by constructing a Font ins
Font f = new Font(name/*name of the embedded font*/, style, size);

Registering font using GraphicsEnvironment, and then instancing it using Font fixed our issue. So you may also give it a try.

Solution

Finally, I just step-down the jdk stuffs(it's really great pain in neck) for good, and came up with harfbuzz(shaping) + freetype(rendering) native impl that indeed was a peace in mind.

So...

• You may consider your production server(easy way) as reference for rendered font advance and rendering, and validate the result based on it(rather than dev machine)
• Or, use a cross and standalone (and probably native) shaping/rendering font engine/impl to make sure dev and production results will be the same.

huangapple
  • 本文由 发表于 2020年9月11日 22:54:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/63849522.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定