用 fontconfig 和 TamperMonkey 解决 Chrome 中文粗体(以及常规字体)问题


Sat May 14 02:52:58 UTC 2016 - stecue@gmail.com

  • 更新脚本至 v0.8,增加“修复级别”选项并可方便的自定义中英文字体。详见二楼。

Sat Apr 16 02:36:25 UTC 2016 - stecue@gmail.com

  • 今天重新写了“二合一”的 TamperMonkey 脚本,详见一楼。

— 原贴的分割线 ( 注意!本楼代码已经过时并被编辑器自动加了空格,请不要试用。已有 TamperMonkey 要直接安装请移步 GitHub 或者 OpenUserJS )—

Linux 下 Chrome 中文支持不咋地已经众所周知了。自从 Chrome 30 年代的某次升级之后,我发现中文粗体一直存在两个问题: 第一,对于没有内置粗体的点阵字体(比如中易宋体),其伪粗体合成方式没有采用 freetype 默认的方法而是只在 x 方向加粗,模糊的一塌糊涂而且很难看。第二,对于提供了粗体的字体,比如微软雅黑,除非在 css 中显式地把"Microsoft YaHei" 包含在 fall-back 序列中,或者 html 的元素的 “lang” 属性为 “zh” 并在“高级字体设置”中对简体中文显式地指定了"Microsoft YaHei",否则 chrome 只会调用常规字体进行算法加粗,也很难看。

对于第一个问题,只需要在~/.config/fontconfig/fonts.conf (之前的 openSUSE 版本可能是 ~/.fonts.conf) 中加入字体替换配置就可以了。下面的例子是将宋体的粗体替换为微软雅黑的粗体。

<match target="pattern">
   <test qual="any" name="family"><string>SimSun</string></test>
   <test name="weight" compare="more_eq"><const>bold</const></test>
   <edit name="family" mode="assign" binding="same"><string>Microsoft YaHei</string></edit>
</match>
<match target="pattern">
   <test qual="any" name="family"><string> 宋体</string></test>
   <test name="weight" compare="more_eq"><const>bold</const></test>
   <edit name="family" mode="assign" binding="same"><string>Microsoft YaHei</string></edit>
 </match>
<match target="font">
    <edit name="embolden" mode="assign">
        <bool>false</bool>
    </edit>
</match>

效果如下:
字体替换之前


字体替换之后(我习惯于 hint full)



当然这样的缺点是所有的宋体粗体都被替换了,有些 overkill。我是专门写了个 fonts.conf,然后用 FONTCONFIG_FILE=/path/to/your/fonts.conf chromium 启动。KDE 的启动器里可以方便的修改程序的启动选项。

第二个问题就比较难解决。我很早以前就报过一个 bug (https://bugs.chromium.org/p/chromium/issues/detail?id=448478)但大概由于描述也不太精准,没什么回应。由于 chrome 对 fontconfig/freetype 的调用似乎很不标准,似乎也没法跟上个问题一样在 fontconfig 的配置文件中进行变通处理(workaround)。最近我发现,用 Tampermonkey 脚本(等价于 Firefox 的 Greasemonkey,http://tampermonkey.net/)可以比较完美的解决这个问题(顺便也学下 JavaScript),见下:

// ==UserScript==
// @name         RealCJKBold
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  try to take over the world!
// @author       You
// @match        http://*/*
// @match        https://*/*
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle('code,tt {font-family:"DejaVu Sans Mono", "Microsoft YaHei", sans-serif !important; }');

    var all = document.getElementsByTagName("*");
    for (var i=0, max=all.length; i < max; i++) {
        if (window.getComputedStyle(all*, null).getPropertyValue("font-weight") == "bold")
            if (all*.innerText.match(/\u3400-\u9FBF]/))
                all*.setAttribute("lang","zh");
    }
})();

这个脚本的思路就是,对于所有的包含 CJK 字符的 html 元素,如果显示字体是粗体,就直接指定该元素的 lang 属性为“zh”,配合“高级字体设置”,就可以调用真粗体了。效果如下:



当然这个脚本也并非完美。实际上 chrome 对于 lang 为“en”但实际包含 CJK 字符的 html 元素的 font-fallback 的处理也不太好,同样是必须要 css 显式地包含中文字体才能完全正确地 fallback 并调用内置的 hinting 信息,系统 fontconfig 设置的 fallback 它不怎么认(honor)——虽然效果上其实比较细微,看不大出来。总之,以上的办法都是临时的变通、权宜之计,还是希望有达人出手从源码上把 chromium (其实算是 blink 引擎了,opera 问题一样)的 bug 修了……***

1赞

本楼内容已过时,请看二楼。

今天研究出来的“二合一”脚本,粗体和非粗体都一起解决了,不需要再调整 font-config 的配置了。似乎还没有发现什么功能上问题(当然代码本身的优雅程度还差得很),欢迎广泛测试!

// ==UserScript==
// @name         RealCJKBold
// @namespace    https://forum.suse.org.cn/
// @version      0.7.1
// @description  Use CJK fonts explicitly!
// @author       stecue@gmail.com
// @match        http://*/*
// @match        https://*/*
// @grant        none
// ==/UserScript==
(function () {
    'use strict';
    var all = document.getElementsByTagName('*');
    for (var i = 0, max = all.length; i < max; i++) {
        var child = all*.firstChild;
        var texts = 
        ];
        var if_replace = false;
        var sig_sun='RealCJKBold 宋';
        var sig_hei='RealCJKBold 黑';
        var qsig_sun='"'+sig_sun +'"'; //Quoted sinagure;
        var qsig_hei='"'+sig_hei +'"'; //Quoted sinagure;
        var mainCJKfont = 'Microsoft YaHei'; //The "good main CJK font" to replace SimSun.
        var qpreCJK = '"' + mainCJKfont + '"'; //Quoted "CJK font".
        var qLatin = '"Trebuchet MS"';
        var qLatinSun = '"Ubuntu Mono","Liberation Serif","Times New Roman"';
        var qCJK = qLatin + ',' + qpreCJK+','+qsig_hei;
        var qSimSun = qLatinSun+','+qLatin + ','+'SimSun'+','+qsig_sun;
        var qsans = '"Lucida Grande",Arial,' + qCJK;
        var qserif = '"Times New Roman",' + qCJK;
        var qmono = '"DejaVu Sans Mono",' + qCJK;
        //Only change if current node (not child node) contains CJK characters.
        var font_str = window.getComputedStyle(all*, null).getPropertyValue('font-family');
        var fweight = window.getComputedStyle(all*, null).getPropertyValue('font-weight');
        while (child) {
            if (child.nodeType == 3) {
                if (font_str.match(/simsun|宋体 /gi)) {
                    //Change SimSun for all characters
                    //alert(window.getComputedStyle(all*, null).getPropertyValue('font-weight'));
                    //alert('SimSun detected');
                    if (fweight == 'bold' || fweight > 500) {
                        //all*.style.color="Blue";
                        if (font_str.match(sig_hei)) {
                            //do nothing if already replaced;
                            //all*.style.color="Red";
                            //continue;
                            //alert('Matched already');
                            if_replace=false;
                        }
                        else {
                            //all*.style.color="Green";
                            //alert(font_str);
                            all*.style.fontFamily = font_str.replace(/\'\"]?simsun|宋体\'\"]?/gi, qCJK);
                            if_replace=false;
                        }
                    }
                    else {
                        //all*.style.color="Orange";
                        if (font_str.match(sig_sun)) {
                            //do nothing if already replaced;
                            //all*.style.color="Grey";
                            if_replace=false;
                        }
                        else {
                            //all*.style.color="SeaGreen";
                            all*.style.fontFamily = font_str.replace(/\'\"]?simsun|宋体\'\"]?/gi, qSimSun);
                            //alert(font_str);
                            if_replace=false;
                            //continue;
                        }
                    }
                    break;
                }
                if (child.data.match(/\u3400-\u9FBF]/)) {
                    if_replace = true;
                    break;
                }
            }
            child = child.nextSibling;
        }

        if (if_replace === true) {
            //continue;
            //Adding ">" to the left and "<" to the
            //all*.lang="en";
            //alert("SimSun found!");
            if (font_str.match(mainCJKfont)) {
                continue; //Skip if already in Microsoft YaHei;
            }
            else if (font_str.match(',')) {
                var font_last = font_str.split(',').pop();
                //alert(font_str);
                if (font_last.match(/\'\"]?sans\'\"]?|\'\"]?sans-serif\'\"]?|\'\"]?sans serif\'\"]?/gi)) {
                    all*.style.fontFamily = font_str.replace(font_last, qsans + ',' + font_last);
                }
                else if (font_last.match(/\'\"]?serif\'\"]?/gi)) {
                    all*.style.fontFamily = font_str.replace(font_last, qserif + ',' + font_last);
                }
                //Mediawiki does not put monospace in the last...
                else if (font_str.match(/\'\"]?monospace\'\"]?/gi)) {
                    all*.style.fontFamily = font_str.replace(/monospace/gi, qmono + ',' + 'monospace');
                }
                else {
                    //Just add the main font for all other cases;
                    all*.style.fontFamily = font_str + ',' + qCJK;
                }
            }
            else
            {
                if (font_str == 'sans' || font_str == 'Sans' || font_str == 'sans-serif' || font_str == 'Sans-Serif' || font_str == 'sans-Serif' || font_str == 'Sans-erif') {
                    all*.style.fontFamily = qsans + ',' + font_str;
                }
                else if (font_str == 'serif' || font_str == 'Serif') {
                    all*.style.fontFamily = qserif + ',' + font_str;
                }
                else if (font_str == 'Mono' || font_str == 'mono' || font_str == 'Monospace' || font_str == 'monospace') {
                    all*.style.fontFamily = qmono + ',' + font_str;
                }
                else {
                    all*.style.fontFamily = font_str + ',' + qCJK;
                }
            }
        }
    }
}) ();

1赞

** 非常重要!!! 由于本论坛的编辑器总是“自作聪明”地在中英文之间加空格而我还没看到如何关掉,所以如果从下面的代码区直接拷贝粘贴的话, *var re_simsun=/ *simsun *| * 宋体 /gi 这一行是不可以直接使用的!代码粘贴到 TamperMonkey 后,你需要把这一行中“宋体”和它之前的那个“星号”之间的空格手动删去,脚本才能正常工作!紧跟着“宋体”之后的那个空格不要删掉!建议通过 github 获得代码 github.com/stecue/fixcjk/blob/master/FixCJK!.user.js 或者通过 OpenUserJS 网站直接安装 openuserjs.org/scripts/stecuegmail.com/FixCJK! **

------------------------------------------------ 正文的分隔线 ---------------------------------------------------

今天脚本升级到 v0.8。提供了定义修复级别的变量 ( FixRegular , FixRegularFixMore 都为 false,则仅仅修复中文粗体。设定 var FixRegular=true 将常规自重的中易宋体也一并替换,并且中英文混排中的英文字符采用英文字体。再设定 var FixMore=true 之后将开启强力中文字体修正。所有 JavaScript 能遍历到的页面元素全部附上中文字体设定。这对之前无法设定的输入框之类很可能也有效。由于脚本中采用“附加”的方式修改字体,一般不会破坏英文元素的原有字体。如有渲染异常请跟帖说明或者 email 本人。

用户可以在变量定义区域设定自己喜欢的字体。变量的名字非常直接,请参看后面的注释。以 CJK 开头的四个变量最为重要,请务必使用你的系统有的字体(可以用 fc-match 命令检查)。如果要在一个变量内指定多个字体(优先使用靠左边的字体),请参考 LatinSerif 的定义。简言之,如果字体名有空格,请用英文双引号将字体名围起来,用英文逗号做分隔符。整个字体列表再用英文单引号围起来。

var all = document.getElementsByTagName(’’);* 这一行以及之后的代码一般不需要用户修改。当然,十分欢迎反馈意见!

本次升级后,由于本脚本不仅仅可以设定中文粗体,所以名字也相应更改为“FixCJK!”。

// ==UserScript==
// @name         FixCJK!
// @namespace    https://forum.suse.org.cn/
// @version      0.8
// @description  1) Use real bold to replace synthetic SimSun bold; 2) Use Latin fonts for Latin part in Latin/CJK mixed texts; 3) Assign general CJK fonts.
// @author       stecue@gmail.com
// @match        http://*/*
// @match        https://*/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';
    // You can change the the following fonts/settings until the "var all=" line.
    var CJKdefault ='WenQuanYi Zen Hei Sharp'; //The default CJK font. Regular weight.
    var CJKserif='WenQuanYi Micro Hei'; //Serif fonts for CJK. "SimSun" with regular weight will be replaced by the font specified here. Although It is intended for regular weight but some element with bold weight still use the font here. Therefore "SimSun" itself is not a good choice because it does not have a real bold font.
    var CJKsans='Noto Sans CJK SC'; //Sans-serif fonts for CJK. Regular weight.
    var CJKBold='WenQuanYi Micro Hei'; //The "good CJK font" to replace SimSun bold. Note that some elements still use font in CJKserif defined above such as the menus on JD.com.
    var LatinInSimSun='Ubuntu Mono'; //The Latin font in a paragraph whose font was specified to "SimSun" only.
    var LatinSans='Open Sans'; //Sans-serif fonts for Latin script. It will be overridden by  a non-virtual font in the CSS font list if present.
    var LatinSerif='Constantia,"Liberation Serif","Times New Roman"'; //Serif fonts for Latin script. It will be overridden by  a non-virtual font in the CSS font list if present.
    var LatinMono='DejaVu Sans Mono'; //Monospace fonts for Latin script. It will be overridden by  a non-virtual font in the CSS font list if present.
    var FixRegular=true; //Also fix regular fonts. You need to keep this true if you want to use "LatinInSimSun" in Latin/CJK mixed context.
    var FixMore=true; //Appendent CJK fonts to all elements. Might have side effects ?
    var FixPunct=false; //If Latin punctions in CJK paragraph need to be fixed. Usually one needs full-width punctions in CJK context. Not implemented yet.
    //Do not change following code unless you know the results!
    var re_simsun=/ *simsun *| * 宋体 */gi;
    var all = document.getElementsByTagName('*');
    var sig_sun='RealCJKBold 宋'; // signature to check if change is sucssful or not.
    var sig_hei='RealCJKBold 黑'; // signature to check if change is sucssful or not.
    var sig_bold='RealCJKBold 粗'; // signature to check if change is sucssful or not.
    var sig_default='RealCJKBold 默'; // signature to check if change is sucssful or not.
    var qsig_sun='"'+sig_sun +'"'; //Quoted sinagure; Actually no need to quote.
    var qsig_hei='"'+sig_hei +'"'; //Quoted sinagure;
    var qsig_bold='"'+sig_bold+'"';
    var qsig_default='"'+sig_default+'"';
    var qpreCJK = '"' + CJKdefault + '"'; //Quoted "CJK font".
    var qCJK = qpreCJK+','+qsig_default;
    var qSimSun = LatinInSimSun+','+CJKserif+','+qsig_sun;
    var qHei = LatinInSimSun+','+CJKsans+','+qsig_hei;
    var qBold = LatinInSimSun+','+CJKBold+','+qsig_bold;
    var qsans = LatinSans+',' + CJKsans +','+qsig_hei+','+'sans-serif'; //To replace "sans-serif"
    var qserif = LatinSerif+','+ CJKserif+','+qsig_sun+','+'serif'; //To replace "serif"
    var qmono = LatinMono+','+qCJK+','+'monospace'; //qCJK comes with signature;
    var i=0;
    var max=all.length;
    var child = all*.firstChild;
    var if_replace = false;
    var font_str = window.getComputedStyle(all*, null).getPropertyValue('font-family');
    var font_last=];
    var fweight = window.getComputedStyle(all*, null).getPropertyValue('font-weight');
    var re_sans0=/^ ?sans ?$|^ ?sans-serif ?$/i ;
    var re_serif=/^ ?serif ?$/i;
    var re_mono0=/^ ?mono ?$|^ ?monospace ?$/i;
    //fucntion to check matches
    function list_has(font_str,family ) {
        var allfonts=font_str.split(',');
        for (var j=0,maxl=allfonts.length;j< maxl;j++) {
            if (allfonts[j].match(family)) {
                return j;
            }
        }
        return false;
    }
    //alert(list_has('sans-serif,sans011,serif',re_sans0) !== false);
    //return true;
    function replace_font(font_str,family,qBold) {
        var allfonts=font_str.split(',');
        var j=0;
        var maxl=allfonts.length;
        for (j=0;j< maxl;j++) {
            if (allfonts[j].match(family)) {
                allfonts[j]=qBold;
            }
        }
        var toReturn=allfonts[0];
        for (j=1;j< maxl;j++) {
            toReturn=toReturn+','+allfonts[j];
        }
        //alert(qBold);
        return toReturn;
    }
    function has_genfam(font_str) {
        //Test if font_str include general families.
        if (list_has(font_str,re_sans0)){
            return true;
        }
        else if (list_has(font_str,re_serif)){
            return true;
        }
        else if (list_has(font_str,re_mono0)){
            return true;
        }
        return false;
    }
    /// First round: Replace all bold fonts to CJKBold ///
    for (i=0; i < max; i++) {
        child = all*.firstChild;
        if_replace = false;
        //Only change if current node (not child node) contains CJK characters.
        font_str = window.getComputedStyle(all*, null).getPropertyValue('font-family');
        fweight = window.getComputedStyle(all*, null).getPropertyValue('font-weight');
        while (child) {
            if (child.nodeType == 3 && (child.data.match(/\u3400-\u9FBF]/)) && (fweight == 'bold' || fweight > 500) && (!(font_str.match(sig_bold)))) {
                //Test if contains SimSun
                //all*.style.color="Blue"; //Bold-->Blue;
                if (font_str.match(re_simsun)) {
                    //all*.style.color="Sienna"; //SimSun --> Sienna
                    all*.style.fontFamily = font_str.replace(re_simsun, qBold);
                    if (!(has_genfam(all*.style.fontFamily))) {
                        all*.style.fontFamily=all*.style.fontFamily+','+'sans-serif';
                    }
                }
                //Test if contains Sans
                else if (list_has(font_str, re_sans0) !== false ) {
                    //all*.style.color="Salmon";
                    all*.style.fontFamily = LatinSans+','+replace_font(font_str,re_sans0,qBold)+','+'sans-serif';
                }
                //Test if contains serif
                else if (list_has(font_str, re_serif) !== false ) {
                    //all*.style.color="SeaGreen";
                    all*.style.fontFamily = LatinSerif+','+replace_font(font_str,re_serif,qBold)+','+'serif';
                }
                //Test if contains monospace
                else if (list_has(font_str, re_mono0) !== false ) {
                    //all*.style.color="Maroon";
                    all*.style.fontFamily = LatinMono+','+replace_font(font_str,re_mono0,qBold)+','+'monospace';
                }
                //Just append the fonts to the font preference list.
                else {
                    //all*.style.color="Fuchsia"; //qBold+"false-safe" sans-serif;
                    all*.style.fontFamily = font_str+','+qBold+','+'  sans-serif';
                    //alert(all*.style.fontFamily);
                }
            }
            child = child.nextSibling;
        }
    }
    if (FixRegular===false) {
        return false;
    }
    /// Second Round: Deal with regular weight. ///
    max=all.length;
    for (i=0; i < max; i++) {
        child = all*.firstChild;
        if_replace = false;
        //Only change if current node (not child node) contains CJK characters.
        font_str = window.getComputedStyle(all*, null).getPropertyValue('font-family');
        fweight = window.getComputedStyle(all*, null).getPropertyValue('font-weight');
        //alert(child.nodeType);
        while (child) {
            if (child.nodeType == 3) {
                //all*.style.color='Teal'; //text-->teal;
                //Just check and fix the improper SimSun use
                if (font_str.match(re_simsun)) {
                    //all*.style.color="Sienna";
                    if (fweight == 'bold' || fweight > 500) {
                        //all*.style.color="Grey";
                        if_replace=false;
                        //alert(child.data);
                        //return false;
                    }
                    else {
                        //all*.style.color="Orange";
                        if (font_str.match(sig_sun) || font_str.match(sig_hei) || font_str.match(sig_bold) || font_str.match(sig_default)) {
                            //do nothing if already replaced;
                            //all*.style.color="Grey";
                            if_replace=false;
                        }
                        else {
                            //all*.style.color="Indigo"; //Improperly used SimSun. It shouldn't be used for non-CJK fonts.
                            all*.style.fontFamily = font_str.replace(re_simsun, qSimSun);
                            if (!(has_genfam(all*.style.fontFamily))){
                                all*.style.fontFamily=all*.style.fontFamily+','+'sans-serif';
                            }
                            //all*.style.color="Indigo"; //Improperly used SimSun. It shouldn't be used for non-CJK fonts.
                            if_replace=false;
                            //all*.style.color="Grey";
                        }
                    }
                }
                if (child.data.match(/\u3400-\u9FBF]/)) {
                    if_replace=true;
                    //all*.style.color="Cyan"; //CJK-->Cyan
                    if (font_str.match(sig_sun) || font_str.match(sig_hei) || font_str.match(sig_bold) || font_str.match(sig_default)) {
                        //do nothing if already replaced;
                        //all*.style.color="Black";
                        if_replace=false;
                    }
                    //break;
                }
            }
            child = child.nextSibling;
        }
        //continue;
        if (if_replace === true) {
            //Test if contains Sans
            if (list_has(font_str, re_sans0) !== false ) {
                //all*.style.color="Salmon";
                all*.style.fontFamily = replace_font(font_str,re_sans0,qsans);
            }
            //Test if contains serif
            else if (list_has(font_str, re_serif) !== false ) {
                //all*.style.color="SeaGreen";
                all*.style.fontFamily = replace_font(font_str,re_serif,qserif);
            }
            //Test if contains monospace
            else if (list_has(font_str, re_mono0) !== false ) {
                //all*.style.color="Maroon";
                all*.style.fontFamily = replace_font(font_str,re_mono0,qmono);
            }
            else {
                //all*.style.color='Fuchsia';
                if (font_str.match(re_simsun)) {
                    //all*.style.color='Fuchsia';
                    //This is needed because some elements cannot be captured in "child elements" processing. (Such as the menues on JD.com) No idea why.
                    all*.style.fontFamily = font_str.replace(re_simsun, qSimSun)+','+'serif';
                }
                else {
                    //all*.style.color='Fuchsia';
                    all*.style.fontFamily = font_str + ',' + qCJK +','+'sans-serif';
                }
            }
        }
    }
    /// The final round: Add CJKdefault to all elements ///
    if (FixMore===false) {
        return false;
    }
    max=all.length;
    for (i=0; i < max; i++) {
        font_str = window.getComputedStyle(all*, null).getPropertyValue('font-family');
        if (!(font_str.match(sig_sun) || font_str.match(sig_hei) || font_str.match(sig_bold) || font_str.match(sig_default))) {
            if (list_has(font_str, re_sans0) !== false ) {
                //all*.style.color="Salmon";
                all*.style.fontFamily = replace_font(font_str,re_sans0,qsans);
            }
            //Test if contains serif
            else if (list_has(font_str, re_serif) !== false ) {
                //all*.style.color="SeaGreen";
                all*.style.fontFamily = replace_font(font_str,re_serif,qserif);
            }
            //Test if contains monospace
            else if (list_has(font_str, re_mono0) !== false ) {
                //all*.style.color="Maroon";
                all*.style.fontFamily = replace_font(font_str,re_mono0,qmono);
            }
            else {
                //all*.style.color='Fuchsia';
                if (font_str.match(re_simsun)){
                    //all*.style.color='Fuchsia';
                    //This is needed because some elements cannot be captured in "child elements" processing. (Such as the menues on JD.com) No idea why.
                    all*.style.fontFamily = font_str.replace(re_simsun, qSimSun)+','+'serif';
                }
                else {
                    //all*.style.color='Fuchsia';
                    all*.style.fontFamily = font_str + ',' + qCJK +','+'sans-serif';
                }
            }
        }
    }
    /// NOT IMPLEMENTED YET ///
    if (FixPunct===false) {
        return false;
    }
    else {
        return true;
    }
}
) ();

技术贴是要支持的!只是我最近好久没用 Chrome 了,所以就不测试了。

支持技术贴

赞!这次几乎对网页中所有中文都有效了!

中英文之间自动加空格的原因可以看这个帖: [Phpbb 中英文间自动加空格)
不过代码段确实不应该加空格……

贴代码可以用 gist.github.com/ ,然后直接发代码的地址,这样就能避免中英文空格的问题。

OK! 我直接建立了一个 github 的 repository, 复制代码和提 issue 都很方便了。传送门: github.com/stecue/fixcjk/blob/master/FixCJK!.user.js

另外也发布到 OpenUserJS 上了。安装 TamperMonkey 和 GreaseMonkey 后,可以直接在 Chrome 和 Firefox 中安装这个脚本。一键安装地址: openuserjs.org/scripts/stecuegmail.com/FixCJK!

@stecue

你现在是用 javascript 直接遍历所有的元素,这样会不会在网页渲染上有性能问题?

如果只是调整 css 样式的话,可以使用 Stylish 这个扩展,firefox 和 chrome 都有这个扩展,现在我是用下面这段代码替换网页中的宋体:

@font-face {
    font-family: '宋体';
    src: local('Noto Sans CJK SC');
}
@font-face {
    font-family: 'simsun';
    src: local('Noto Sans CJK SC');
}

大概除非是上网本或者是 raspberry pi 之类,应该不会有性能问题。我一直用 Page Load Time 计时,虽然它本身好像不能把 Tampermonkey 的时间算进去,但是我网页正常 Load 本身就要花 0.5~2 秒,相比之下 Tampermonkey 脚本的运行时间可以忽略不计——决速步骤根本不在这里。

Stylish 我以前也用过,但是感觉控制的精细程度还不够。我以前是想让所有中文粗体显式地采用某个中文字体,其他照旧。这是因为,在 Linux 版本的 Chrome 中,如果 CSS 中指定的字体没有找到,Fallback 到另一个字体的话,Chrome 会强制使用合成粗体而不是真正的粗体。Stylish 替换之后,首先常规字重也被替换了,其次粗体仍然是合成的。我实在看不习惯,所以就放弃 Stylish 了。

Firefox 装了 Greasemonkey 也可以用哈。可以用来改个字体啥的,比如 Windows 下看宋体的伪粗体不爽可以重新改成雅黑之类嘿嘿。就是对一些有 Frame 的网页似乎无效,可能是 Tampermonkey 和 Greasemonkey 脚本注入位置的差异?……

我用了 performance.now() 并打开控制台来监控运行时间,在我的 i3 台式机和 Chromium 上基本上不超过 0.1-0.3 秒,占总加载时间的比例可以忽略。这个帖子的第一页算比较慢的了,花了大约 150 毫秒。京东首页 50 毫秒。
另外,京东直接在控制台打招聘广告,真是既无成本又精准,忒有创意了——也可能我见得太少啦。