Java 的字体替换

不知道什么时候起,中文的斜体和粗体开始流行起来,还有很多人还特意地去追求这种效果。中文写作的习惯是不用斜体和粗体的,而是使用完全不同的字体来区别不同的内容,一般是楷体对应斜体,黑体对应粗体。对于英文来讲,斜体粗体或者粗斜体和正常的字体也是不同的字体,需要重新设计,但都属于同一族(Family)的。对于汉字,这种效果一般是使用一定的算法将字体倾斜或者加粗(当然这种算法并不只是针对汉字)。汉字是方块字,歪斜的字完全破坏了字的间架结构,没有了汉字的那种美感;加粗相对来说好一些,但看起来还是不舒服。现在中英文混杂的情况是越来越多,可能斜体和粗体都是依照英文的标准来的,方块字中间夹杂一些斜体的英文看起来就不好看了,但我觉得斜体的中文更难看。

我自己从来都是拒绝使用中文的斜体和粗体,而是使用字体替换法将斜体替换成楷体,粗体替换成黑体。这在 Linux 下使用 [fontconfig] 可以很方便地实现,并且影响所有使用 [XFT] 的应用程序。LaTeX 中相对来说也很容易,跟 fontconfig 方法很像,具体请参考[TeX / LaTeX 中的字体](暂时不能访问)一文。Windows 下就不知道怎么做了,所以现在用浏览器看到斜体就很不舒服。

Java 一直以来有自己的一套使用字体的方法,具体的字体设置 1.4 以前在 $JRE_HOME/lib/font.properties.* 文件中,具体的文档可以在[这里]找到,1.5 以后是在 $JRE_HOME/lib/fontconfig.properties.*,具体的文档在[这里]。以下都以 1.6 Windows 为例。Java 中的字体分两种,一种是逻辑字体,共有五种,分别是 SerifSanserifMonospaceDialog 以及 DialogInput。另外一种就是物理字体,也就是我们一般意义上的各种字体。使用逻辑字体,在真正去渲染一个字符的时候,必须使用物理字体,这种逻辑字体和物理字体之间的映射就是在上述的配置文件中来进行设置。Java 本身已经提供了字体替换以及不同字符集使用不同物理字体的功能。下面我们用一个例子来测试一下:

  1. import java.awt.*;
  2. import javax.swing.*;
  3.  
  4. public class ChineseFontTest {
  5.     public static void main(String[] args) {
  6.         String fn = "Dialog";
  7.         int size = 20;
  8.  
  9.         Font plainFont = new Font(fn, Font.PLAIN, size);
  10.         Font italicFont = new Font(fn, Font.ITALIC, size);
  11.         Font boldFont = new Font(fn, Font.BOLD, size);
  12.         Font boldItalicFont = new Font(fn, Font.BOLD | Font.ITALIC, size);
  13.  
  14.         Font simheiFont = new Font("SimHei", Font.PLAIN, size);
  15.         Font kaitiFont = new Font("STKaiti", Font.PLAIN, size);
  16.         Font lisuFont = new Font("Lisu", Font.PLAIN, size);
  17.  
  18.         JFrame frame = new JFrame("中文字体测试");
  19.  
  20.         JLabel plainLabel = new JLabel("Plain 中文");
  21.         plainLabel.setFont(plainFont);
  22.         plainLabel.setHorizontalAlignment(JLabel.CENTER);
  23.  
  24.         JLabel italicLabel = new JLabel("Italic 中文");
  25.         italicLabel.setFont(italicFont);
  26.         italicLabel.setHorizontalAlignment(JLabel.CENTER);
  27.  
  28.         JLabel boldLabel = new JLabel("Bold 中文");
  29.         boldLabel.setFont(boldFont);
  30.         boldLabel.setHorizontalAlignment(JLabel.CENTER);
  31.  
  32.         JLabel boldItalicLabel = new JLabel("Bold Italic 中文");
  33.         boldItalicLabel.setFont(boldItalicFont);
  34.         boldItalicLabel.setHorizontalAlignment(JLabel.CENTER);
  35.  
  36.         JLabel kaitiLabel = new JLabel("STKai 中文");
  37.         kaitiLabel.setFont(kaitiFont);
  38.         kaitiLabel.setHorizontalAlignment(JLabel.CENTER);
  39.  
  40.         JLabel simheiLabel = new JLabel("SimHei 中文");
  41.         simheiLabel.setFont(simheiFont);
  42.         simheiLabel.setHorizontalAlignment(JLabel.CENTER);
  43.  
  44.         JLabel lisuLabel = new JLabel("Lisu 中文");
  45.         lisuLabel.setFont(lisuFont);
  46.         lisuLabel.setHorizontalAlignment(JLabel.CENTER);
  47.  
  48.         JPanel panel = new JPanel();
  49.         panel.add(plainLabel);
  50.         panel.add(italicLabel);
  51.         panel.add(boldLabel);
  52.         panel.add(boldItalicLabel);
  53.         panel.add(kaitiLabel);
  54.         panel.add(simheiLabel);
  55.         panel.add(lisuLabel);
  56.  
  57.         frame.getContentPane().add(panel);
  58.  
  59.         frame.setSize(180, 250);
  60.         frame.setVisible(true);
  61.     }
  62. }

这个例子中前面几个 label 用的是逻辑字体,后面几个用的是物理字体。最初的显示效果如下:

可以看到,前面几个英文的字体都是对的,而中文字体全都一样,看起来应该是 MingLiU,我这里是英文环境,缺省字体配置是 fontconfig.properties.src,那么最后是从 sequence.fallback 字体查找序列中找到 chinese-ms950 字符集所对应的字体,那么在前面的字体定义中就是 MingLiU。如果在简体中文环境下,根据 allfonts.chinese-ms936=SimSun 找到的字体就是 SimSun

那现在我们就要替换字体,一般字体是 SimSun,斜体替换为 STKaiti,粗体替换为 SimHei,粗斜体替换为 Lisu。修改的例子如下,其他可以照猫画虎修改一下:

  1. ...
  2.  
  3. dialog.plain.alphabetic=Arial
  4. dialog.plain.chinese-ms936=SimSun
  5.  
  6. dialog.bold.alphabetic=Arial Bold
  7. dialog.bold.chinese-ms936=SimHei
  8.  
  9. dialog.italic.alphabetic=Arial Italic
  10. dialog.italic.chinese-ms936=STKaiti
  11.  
  12. dialog.bolditalic.alphabetic=Arial Bold Italic
  13. dialog.bolditalic.chinese-ms936=Lisu
  14.  
  15. ...

从图中可以看到,汉字的斜体、粗体和粗斜体字体已经对了,但是显示出来还是斜体是斜的,粗体加粗了,所以还是使用了算法去修改样式,也就是说在渲染的时候字体的样式还是保留下来了。经过对 [openjdk] 相关代码的艰苦阅读,最后发现问题是在这里:

  1. sun.font.FileFontStrike:
  2.  
  3.     FileFontStrike(FileFont fileFont, FontStrikeDesc desc) {
  4.         ...
  5.         if (desc.style != fileFont.style) {
  6.           /* If using algorithmic styling, the base values are
  7.            * boldness = 1.0, italic = 0.0. The superclass constructor
  8.            * initialises these.
  9.            */
  10.             if ((desc.style & Font.ITALIC) == Font.ITALIC &&
  11.                 (fileFont.style & Font.ITALIC) == 0) {
  12.                 algoStyle = true;
  13.                 italic = 0.7f;
  14.             }
  15.             if ((desc.style & Font.BOLD) == Font.BOLD &&
  16.                 ((fileFont.style & Font.BOLD) == 0)) {
  17.                 algoStyle = true;
  18.                 boldness = 1.33f;
  19.             }
  20.         }
  21.         ...
  22.     }

也就是说在字体渲染时候应该使用斜体或者粗体而物理字体本身却不是斜体或者粗体时,就会使用算法去计算样式。以下下是补丁:

  1. --- FileFontStrike-jdk6.java    Sun Mar 30 13:28:25 2008
  2. +++ FileFontStrike.java Thu Mar 27 17:42:45 2008
  3. @@ -26,6 +26,8 @@
  4.       */
  5.      static final int INVISIBLE_GLYPHS = 0x0fffe;
  6.  
  7. +    private static boolean algorithmicStylingDisabled = (System.getProperty("sun.java2d.font.DisableAlgorithmicStyles") != null);
  8. +
  9.      private FileFont fileFont;
  10.  
  11.      /* REMIND: replace this scheme with one that installs a cache
  12. @@ -95,6 +97,7 @@
  13.            * boldness = 1.0, italic = 0.0. The superclass constructor
  14.            * initialises these.
  15.            */
  16. +            if (!algorithmicStylingDisabled) {
  17.             if ((desc.style & Font.ITALIC) == Font.ITALIC &&
  18.                 (fileFont.style & Font.ITALIC) == 0) {
  19.                 algoStyle = true;
  20. @@ -105,6 +108,7 @@
  21.                 algoStyle = true;
  22.                 boldness = 1.33f;
  23.             }
  24. +            }
  25.         }
  26.         double[] matrix = new double[4];
  27.         AffineTransform at = desc.glyphTx;

JDK 6 的源文件可以在[这里]拿到,然后将补丁和此文件放在同一个目录里面,用 patch -p0 < patch_file_name,然后编译:javac -classpath $JAVA_HOME/jre/lib/rt.jar FileFontStrike.java,将生成的 class 文件更新到 $JAVA_HOME/jre/lib/rt.jar/sun/font/ 目录中。运行的时候可以通过 -Dsun.java2d.font.DisableAlgorithmicStyles 来控制是否使用算法控制样式。只要在运行的时候加入这个属性,就不使用算法控制样式。效果如下:

但可能还会有问题,中日韩文字可能不需要斜体,而对于英文或者别的文字,选择的字体如果没有相应斜体粗体字体的话,算法去模拟粗体斜体还是能够达到一定的效果的,那在全局禁掉不是很合理。更好的解决方法可以针对不同的字符集甚至不同的字体分别启用或者禁止,这就需要对字体配置文件进行修改,可能会涉及到几个地方,不是很容易修改,以后再说吧。

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <blockquote>
  • You can use BBCode tags in the text.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>. The supported tag styles are: <foo>, [foo].
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
"ixo dahu copi esat zotewe xatu zixaj xatu"