OpenJDK 21改变字体字符宽度的问题

后端开发 2026-07-08

在Ubuntu上使用openjdk-21时,我遇到了一个问题:在“Serif”字体(对我来说解析为Nimbus Roman)下,字符宽度并未正确显示。这看起来并不稳定,仿佛Java对字符宽度有某种预先设定的概念,从而忽略了字体本身的标注。用fontforge查看字体时,一切看起来都正确。但当我使用Java的方法获取边界矩形时,宽度就错了。通常情况下,Java的宽度大于“真实字体”的宽度,虽然并非每次都如此。

我尝试了很多不同的方法,包括为小数宽度设置字体呈现提示。下面是我的字体测试程序的输出,显示Java返回的边界矩形(红色),矩形边界在上方打印,矩形内部绘制了字符,下面是来自实际字体文件(.afm)中的宽度。这个是以12磅缩放的,这是Java中创建的字体的大小。Java会在字体上附加前导,因此矩形高度是16.3而不是12。但是额外的宽度并不一致。对于所有定义宽度相同的字符,Java返回的宽度却不同。有时,Java的宽度甚至小于“真实字体”的宽度。

http://sebhc.durgadas.com/NimbusRoman-test.ps

我想弄清楚这类误算是如何/为什么被执行的,以及如何补偿。由于没有一个一致的误差系数,我似乎无法用一个简单的公式来调整宽度。

以下是用于生成字体测试输出的代码片段:

import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;

class FontTest
{
        static Font uf = new Font("Monospaced", Font.PLAIN, 6);

        // This is taken from /usr/share/fonts/type1/urw-base35/NimbusRoman-Regular.afm
        // and assumes we're using the "Serif", Font.PLAIN, 12pt rendering.
        static float[] wt = new float[]{
                0.250f,0.333f,0.408f,0.500f,0.500f,0.833f,0.778f,0.333f,
                0.333f,0.333f,0.500f,0.564f,0.250f,0.333f,0.250f,0.278f,
                0.500f,0.500f,0.500f,0.500f,0.500f,0.500f,0.500f,0.500f,
                0.500f,0.500f,0.278f,0.278f,0.564f,0.564f,0.564f,0.444f,
                0.921f,0.722f,0.667f,0.667f,0.722f,0.611f,0.556f,0.722f,
                0.722f,0.333f,0.389f,0.722f,0.611f,0.889f,0.722f,0.722f,
                0.556f,0.722f,0.667f,0.556f,0.611f,0.722f,0.722f,0.944f,
                0.722f,0.722f,0.611f,0.333f,0.278f,0.333f,0.469f,0.500f,
                0.333f,0.444f,0.500f,0.444f,0.500f,0.444f,0.333f,0.500f,
                0.500f,0.278f,0.278f,0.500f,0.278f,0.778f,0.500f,0.500f,
                0.500f,0.500f,0.333f,0.389f,0.278f,0.500f,0.500f,0.722f,
                0.500f,0.500f,0.444f,0.480f,0.200f,0.480f,0.541f
        };

        static public void paint(Graphics2D g2d, Font font) {
//2,3           FontMetrics fm = g2d.getFontMetrics();
//3             int[] ws = fm.getWidths();

                g2d.setStroke(new BasicStroke(0.25f));
                FontRenderContext frc = g2d.getFontRenderContext();
                char[] c = new char[1];
                c[0] = ' ';
                LineMetrics lm = font.getLineMetrics(c, 0, 1, frc);
                int fa = (int)Math.round(lm.getAscent());

                int x = 50;
                int y = 50;
                for (int b = 0; b < 95; ++b) {
                        c[0] = (char)(b + 32);
                        String s = new String(c);
                        Rectangle2D r = font.getStringBounds(s, frc);
//2                     Rectangle2D r = fm.getStringBounds(s, g2d);
                        g2d.setColor(Color.red);
                        Rectangle2D rr = new Rectangle2D.Double(x, y,
                                                r.getWidth(), r.getHeight());
//3                     Rectangle2D rr = new Rectangle2D.Double(x, y,
//3                                             (double)ws[b + 32], r.getHeight());
//2                     Rectangle2D rr = new Rectangle2D.Double(x, y,
//2                                             (double)fm.charWidth((char)(b + 32)), r.getHeight());
                        g2d.draw(rr);
                        g2d.setColor(Color.black);
                        g2d.setFont(font);
                        g2d.drawString(s, x, y + fa);
                        g2d.setFont(uf);
                        g2d.drawString(String.format("%.1f", r.getWidth()), x, y - 8);
                        g2d.drawString(String.format("%.1f", r.getHeight()), x, y - 2);
                        g2d.drawString(String.format("%.1f", wt[b] * 12f), x, y + fa + 8);

                        x +=  32;
                        if ((b & 0x0f) == 0x0f) {
                                x = 50;
                                y += 50;
                        }
                }
        }
}

解决方案

问题的原因在于Java对“Serif”所选择的字体与系统在PostScript渲染/打印时提供的字体不同。我还没能具体追踪到发生在何处,但很可能是在Java或 Ubuntu的字体配置文件中的某处。具体发生的是,Java实例化了“Noto Serif”或可能是“DejaVu Serif”,这两者都比PostScript选择的“Nimbus Roman”更“臃肿”。在这些情况下,字体度量也会出现一些奇怪的值,其中边界矩形的高度并非字体大小加上前导(实际上前导为0)。如果指定一个物理字体(例如“NimbusRoman-Regular”),那么Java会将字符字形嵌入为绘制命令,因此此后就不再依赖系统字体——然而PostScript文件会显著增大。当可移植性成为关注点(将PostScript导出到其他系统时),使用逻辑字体也许本身并不理想。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章