OpenJDK 21改变字体字符宽度的问题
在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导出到其他系统时),使用逻辑字体也许本身并不理想。