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

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 的输出是系统无关的。显然不是。什么可能导致差异?

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

是的,有点像本地人。例如,即使是新的 JavaFX web 视图也取决于 webkit.

如果你深入调试 getStringBounds,你会意识到它达到了一个点,字体管理器应该决定加载一个具体的字体管理器,其中 class 名称应该是系统属性 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 值可以验证您的 显然不是。什么可能导致差异?标记。

对于某些 nix 系统,sun.font.fontmanager 的 val 可能是 sun.awt.X11FontManager,但对于 windows 可能是 null,因此管理器将是 sun.awt.Win32FontManager.

现在每个经理,可能取决于不同的基础 shaping/rendering engine/impl 肯定(this may help)。

主要原因可能是字体的性质。因为它们大多是矢量的东西。所以基于 platform/env,呈现的文本可能更大,也可能更小。例如,也许 windows 在请求的文本呈现上应用桌面 cleartype 和屏幕文本大小 (DPI)。

看来,即使你正好有两个sun.awt.X11FontManager经理,结果也会有所不同。 this may help too

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

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)
    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.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.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.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)

注意 libfreetype 的 failed/missed 负载,它是原生/C 字体渲染应用程序

编码结果(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);
//later load the font by constructing a Font ins
Font f = new Font(name/*name of the embedded font*/, style, size);

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

解决方案

最后,我只是 step-down jdk 东西(脖子真的很痛),想出了 harfbuzz(塑形)+ freetype (rendering) native impl 这确实是一种内心的平静。

所以...

• 您可以考虑将您的生产服务器(简单方法)作为渲染字体推进和渲染的参考,并基于它(而不是开发机器)验证结果
• 或者,使用交叉和独立(可能是本机)shaping/rendering 字体engine/impl 以确保开发和生产结果相同。