化学式利用上下标来表示物质中的原子组成和电荷状态。在通常情况下,在排版时输入化学式需要使用额外的Unicode字符(⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻₀₁₂₃₄₅₆₇₈₉),或者手动应用上下标样式,或者使用专门的语法(比如TeX)。

然而,使用常规键盘输入上下标字符并不方便,渲染TeX则往往需要繁重的依赖。有没有办法可以在不用TeX的情况下使用类似TeX语法绘制上下标呢?答案是有的,我们可以借助TrueType/OpenType字体的连字功能实现。

使用连字渲染化学式

现代字体技术允许我们通过字体的连字(ligature)功能来根据上下文替换字符的字形。该功能最初是用于 f + i = fi 这样的排版渲染需求,以及用于渲染像阿拉伯语这样字形和上下文强相关的文字。许多编程字体中也通过连字来渲染多字符操作符(比如将!=渲染为 )。

通过连字特性,我们可以实现纯ASCII化学式的渲染,只要定义以下连字规则:

  • 当数字出现下标标记符_之后时,使用下标字形
  • 当数字或+-符号出现在上标标记符^之后时,使用上标字形

为了方便,我们还可以规定

  • 当数字出现在下标字形之后时,维持下标字形
  • 当数字或+-符号出现在上标字形的数字之后时,维持上标字形

我们可以通过FontTools库来为现有字体添加这些连字规则。下面的Python脚本实现了这个功能:

build_chem_font.py
#!/usr/bin/env python3
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables._g_l_y_f import Glyph, GlyphComponent
from fontTools.feaLib.builder import addOpenTypeFeatures
from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.transformPen import TransformPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
import io


def build_chem_font(font):
    glyf, hmtx = font["glyf"], font["hmtx"]
    gord, cmap = font.getGlyphOrder(), font.getBestCmap()
    gset = font.getGlyphSet()
    upm = font["head"].unitsPerEm

    def register_glyph(name, glyph_obj, width, lsb):
        glyf[name] = glyph_obj
        hmtx[name] = (width, lsb)
        if name not in gord:
            gord.append(name)

    empty_glyph = Glyph()
    empty_glyph.numberOfContours = 0
    register_glyph("hide.glyph", empty_glyph, 0, 0)

    def build_derivative(src, scale=1, xoff=0, yoff=0):
        rec = RecordingPen()
        gset[src].draw(rec)
        tt_pen = TTGlyphPen(gset)
        t_pen = TransformPen(tt_pen, (scale, 0, 0, scale, xoff, yoff))
        rec.replay(t_pen)
        return tt_pen.glyph()

    caret, lowline = cmap.get(ord("^")), cmap.get(ord("_"))
    nums = [cmap[ord(c)] for c in "0123456789"]
    pm = [cmap[ord(c)] for c in "+-"]

    for g in nums + pm:
        gl = build_derivative(g, 0.6, 0, int(upm * 0.35))
        register_glyph(f"{g}.sup", gl, int(hmtx[g][0] * 0.6), 0)
    for g in nums:
        gl = build_derivative(g, 0.6, 0, int(upm * -0.1))
        register_glyph(f"{g}.sub", gl, int(hmtx[g][0] * 0.6), 0)
    font.setGlyphOrder(gord)

    feature = f"""
    @supprefix = [{caret} {" ".join([c+ ".sup" for c in nums])}];
    @subprefix = [{lowline} {" ".join([c+ ".sub" for c in nums])}];
 
    @supchar0 = [{" ".join(nums + pm)}];
    @subchar0 = [{" ".join(nums)}];
    @supchar = [{" ".join([c+ ".sup" for c in nums + pm])}];
    @subchar = [{" ".join([c+ ".sub" for c in nums])}];

    feature calt {{
        lookup SUB_CHAIN {{ sub @subprefix @subchar0' by @subchar; }} SUB_CHAIN;
        lookup SUP_CHAIN {{ sub @supprefix @supchar0' by @supchar; }} SUP_CHAIN;
 
        lookup HIDE_CARET {{ sub {caret}' @supchar by hide.glyph; }} HIDE_CARET;
        lookup HIDE_LOWLINE {{ sub {lowline}' @subchar by hide.glyph; }} HIDE_LOWLINE;
    }} calt;
    """

    with io.StringIO(feature) as fea:
        addOpenTypeFeatures(font, fea)

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Create chemical symbols font.")
    parser.add_argument("input", help="Path to input file (TTF font)")
    parser.add_argument("output", help="Path to output file")
    args = parser.parse_args()

    font = TTFont(args.input)
    build_chem_font(font)
    font.save(args.output)

通过python3 build_chem_font.py input.ttf chem.ttf命令运行该脚本,即可生成一个支持化学式渲染的字体文件chem.ttf。之后可以在浏览器环境中通过css@font-face加载字体查看结果:

preview.html



    
    font preview
    


    


最终的渲染结果如下:

image

注:本方法修改了字体文件,操作前请先参考字体的授权规则和用户协议,避免未授权的编辑。


原文地址: https://www.cveoy.top/t/topic/qFUH 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录