精华内容
下载资源
问答
  • 前面我在《2万字硬核剖析网页自定义字体解析》一文中,讲解了通过图像识别来解析自定义字体,但是图像识别的缺点在于准确率并不能达到100%,还需要二次修改。 前面将字体的称为点阵图,其实根据TrueType字体实际采用...

    前面我在《2万字硬核剖析网页自定义字体解析》一文中,讲解了通过图像识别来解析自定义字体,但是图像识别的缺点在于准确率并不能达到100%,还需要二次修改。

    前面将字体的称为点阵图,其实根据TrueType字体实际采用的技术,称为轮廓图更为合适,所以本文所说的轮廓图就是上篇的点阵图。

    由于目前几个大厂的网站的自定义字体的轮廓图都是那个固定的顺序,所以上文只处理了所有字体文件轮廓图顺序都一致的情况,并没有继续深挖去处理轮廓图顺序出现随机的情况。

    本文就将针对未来自定义字体的轮廓图顺序出现随机的情况进行处理。

    具体处理思路就是,提取字体的图元数据,包括控制点位置和标志位,转成二进制字节进行唯一标识,与现有的已知的字符集进行映射。后续任何Unicode代码点顺序随机和轮廓图顺序随机的字体文件,都可以提取图元数据转换后进行唯一匹配从而解码出唯一正确的字符。

    不过上述思路还只是处理了轮廓图顺序随机,其实还可以再变态点以多个基础字形制作自定义字体取随机,意味着每个字符的图元数据都会发生较大变化,上面的匹配方法就会直接失效。此时便只能通过机器学习计算字符间的相似度,从而识别出图元对应的真实字符。

    字体格式类型介绍

    字体格式类型主要有几个大分类:TrueType、Embedded Open Type 、OpenType、WOFF 、SVG。

    TrueType

    Windows和Mac系统最常用的字体格式,基于轮廓技术的数学模式来进行定义,比基于矢量的字体更容易处理,保证了屏幕与打印输出的一致性。同时,这类字体和矢量字体一样可以随意缩放、旋转而不必担心会出现锯齿。

    EOT – Embedded Open Type (.eot):

    微软开发的嵌入式字体,允许OpenType字体用@font-face嵌入到网页并下载至浏览器渲染,存储在临时安装文件夹下。

    OpenType (.otf)

    微软和Adobe共同开发的字体,微软的IE浏览器全部采用这种字体,致力于替代TrueType字体。

    WOFF – Web Open Font Format (.woff):

    专门为了Web而设计的字体格式标准,实际上是对TrueType/OpenType等字体格式的封装,每个字体文件中含有字体以及针对字体的元数据(Metadata),字体文件被压缩,以便于网络传输。

    SVG (Scalable Vector Graphics) Fonts (.svg)

    使用SVG技术来呈现字体,支持gzip压缩格式。

    在上次从css的@font-face提取出字体URL链接时,就包含了eot和woff两种格式。鉴于woff字体更容易被分析,所以我们上次选择了只下载woff字体格式,今天这篇文章也一样。

    字体格式转换工具:

    可以生成自定义字体的网站:

    如何生成自定义字体

    先生成svg字体,再导入到自定义字体生成网站,再定义字体映射关系,最后导入字体即可。

    由于https://everythingfonts.com/对文件较大的字体转换需要收费,这里我使用https://www.fontsquirrel.com/tools/webfont-generator将系统自带的arial.ttf字体文件转换为svg字体:

    image-20210803180943071

    下载并解压得到一个arial-webfont.svg文件。

    接下来打开https://icomoon.io/app/#/select,选择需要被自定义的字符:

    image-20210803181332460

    本例选择了0-9作为被自定义的字符,然后点击右下角 Generate Font 按钮准备设置字符映射:

    image-20210803182341180

    设置好映射关系后,点击下载字体。

    下载的压缩包包含多种字体,解压出其中的icomoon.woff字体文件。

    用FontCreator字体设计工具打开后可以看到如下结果:

    image-20210803182352168

    可以看到与我们前面在网站中自定义的映射一致。

    woff字体的解析

    首先,我们用python的fontTools库读取上次下载的字体文件:

    from fontTools.ttLib import TTFont
    
    font = TTFont("tagName.woff")
    

    可以一次性将相关数据保存到本地:

    font.saveXML("tagName.xml")
    

    字体文件都包含了一个TableDirectory结构,保存了多张表,每个表保存了不同的信息。

    TrueType字体中常见的表有:

    英文表名中文表名详细描述
    head字体头字体的全局信息
    cmap字符代码到图元的映射把字符代码映射为图元索引
    glyf图元数据图元轮廓定义以及网格调整指令
    loca位置表索引把元索引转换为图元的位置
    maxp最大需求表字体中所需内存分配情况的汇总数据
    name命名表版权说明、字体名、字体族名、风格名等等
    hmtx水平布局字体水平布局:上高、下高、行间距、最大前进宽度、最小左支撑、最小右支撑

    字体头表(head表)

    字体头表(head表)中包含了TrueType字体的全局信息,在c语言中的结构定义如下:

    typedef   sturct
    {
        Fixed   Table;//x00010000   ro   version   1.0
        Fixed   fontRevision;//Set   by   font   manufacturer.
        ULONG   checkSumAdjustment;
        ULONG   magicNumer;   //Set   to   0x5f0f3cf5
        USHORT   flags;
        USHORT   unitsPerEm;   //Valid   range   is   from   16   to   16384
        longDT   created;   //International   date   (8-byte   field).
        longDT   modified;   //International   date   (8-byte   field).
        FWord   xMin;   //For   all   glyph   bounding   boxes.
        FWord   yMin;   //For   all   glyph   bounding   boxes.
        FWord   xMax;   //For   all   glyph   bounding   boxes.
        FWord   xMax;   //For   all   glyph   bounding   boxes.
        USHORT   macStyle;
        USHORT   lowestRecPPEM;   //Smallest   readable   size   in   pixels.
        SHORT   fontDirctionHint;
        SHORT   indexToLocFormat;   //0   for   short   offsets   ,1   for   long.
        SHORT   glyphDataFormat;     //0   for   current   format.
    }Table_head;
    

    上面各个字段定义基本都能直接在python中读取,其中日期字段有created和modified,分别表示字体创建时间和字体最后修改时间,使用8个字节记录从1904年1月1日午夜12:00开始的秒数。

    获取字体的创建时间和字体最后修改时间:

    import datetime
    head = font['head']
    base = datetime.datetime(1904, 1, 1, 0, 0, 0)
    create_time = base+datetime.timedelta(seconds=head.created)
    modifie_time = base+datetime.timedelta(seconds=head.modified)
    print(f"创建时间:{create_time},最后修改时间:{modifie_time}")
    
    创建时间:2021-08-02 15:00:30,最后修改时间:2021-08-02 15:00:30
    

    字体是针对一个被称为em-square的参考网格设计的,字体中的图元用网格中的坐标表示。em-squrare的大小决定字体的图元被缩放的方式和质量。字体头中保存了每个em-square的格数和能 包含所有图元的边界框。Em-square的有效值是从16到16384。

    读取每个em-square的格数和图元边界框范围:

    print(f"每个em-square的格数:{head.unitsPerEm},边界框范围x: {head.xMin} - {head.xMax},y: {head.yMin} - {head.yMax}")
    
    每个em-square的格数:1000,边界框范围x: 0 - 1136,y: -112 - 833
    

    字体头表中的其他信息包括最小可读像素大小、字体方向、在位置表中图元索引的格式和图元数据格式等:

    head.lowestRecPPEM, head.fontDirectionHint, head.indexToLocFormat, head.glyphDataFormat
    
    (8, 2, 0, 0)
    

    字符到图元索引的映射表(cmap表)

    字符到图元索引的映射表(cmap表)定义了从不同代码页中的字符代码到图元索引的映射关系。cmap表包含几个子表以支持不同的平台和不同的字符编码方案。cmap表在c语言中的定义较为复杂,不作展示。

    在python中我们可以通过cmap表获取字符代码到图元索引的映射关系:

    cmap = font['cmap']
    cmap.getBestCmap()
    
    {120: 'x',
     57360: 'unie010',
     57369: 'unie019',
     57370: 'unie01a',
     ...
     63699: 'unif8d3',
     63718: 'unif8e6',
     63724: 'unif8ec'}
    

    不过获取这个关系也并没有太大的意义,因为我们可以很轻松的进行相互转换:

    "uni"+chr(57360).encode("unicode_escape").decode()[2:]
    

    就可以得到对应的unie010,反过来也可以:

    char = 'unie010'
    ord(("\\u"+char[3:]).encode().decode("unicode_escape"))
    

    即可得到57360

    当然fontTools本身也提供了反向获取的API:

    cmap.buildReversed()
    
    {'x': {120},
     'unie010': {57360},
     'unie019': {57369},
     'unie01a': {57370},
     ...
     'unif8d3': {63699},
     'unif8e6': {63718},
     'unif8ec': {63724}}
    

    图元数据(glyf表)

    图元数据(glyf表)是我们所需要的字体核心信息,以序列形式保存了图元数据,每个图元以图元头(GlyphHeader)结构开始,在c语言中的定义为:

    typedef   struct   
    {
    WORD numberOfContours;   //contor number,negative if   composite
    FWord xMin;       //Minimum x for coordinate data.
    FWord yMin;       //Minimum y for coordinate data.
    FWord xMax;       //Maximum x for coordinate data.
    FWord yMax;       //Maximum y for coordinate data.
    }GlyphHeader;
    

    合成图元由多个简单图元或合成图元组成,简单图元的numberOfContours字段保存了当前图元的轮廓线的数目。而合成图元的numberOfContours字段为负值,表示需要基于组成该合成图元的所有简单图元的轮廓线的数目计算得到。后四个字段记录了图元的边界框。

    简单图元的图元描述信息紧跟在其GlyphHeader结构之后,c语言定义为:

    USHORT endPtsOfContours[n];   //n=number of contours
    USHORT instructionlength;
    BYTE instruction[i];     //i = instructionlength
    BYTE flags[];            //variable size
    BYTE xCoordinates[];     //variable size
    BYTE yCoordinates[];     //variable size
    

    包括所有轮廓线结束点的索引、图元指令和一系列的控制点,每个控制点包括包括一个标志和xy轴坐标。

    endPtsOfContours数组保存了每一条轮廓线终点的索引,通过该索引可以计算出每条轮廓线中点的数量。比如,endPtsOfContours[0]+1是第一条轮廓线上点的数量,endPtsOfContours[1]-endPtsOfContours[0]是第二条轮廓线上点的数量。

    图元的控制点保存在三个数组中:标志获得组、x坐标数组和y坐标数组。为了节省存储空间,图元中保存的是相对坐标。第一个点的坐标是相对原点(0, 0)记录的,随后的点记录和上一个点的坐标差值。标志数组保存了每个坐标的编码信息以及其他一些信息。下面是标志中各个位的含义(c语言定义):

    typedef enum
    {
        G_ONCURVE=0x01,    // on curve ,off curve
        G_REPEAT=0x08,     // next byte is flag repeat count   
        G_XMASK=0x12,   
        G_XADDBYTE=0x12,   //X is positive byte
        G_XSUBBYTE=0x12,   //X is negative byte   
        G_XSAME=0x10,      //X is same
        G_XADDINT=0x00,    //X is signed word   
    
        G_YMASK=0x24,
        G_YADDBYTE=0x24,   //Y is positive byte   
        G_YSUBBYTE=0x04,   //Y is negative byte
        G_YSAME=0x20,      //Y is same
    	G_YADDINT=0x00,    //Y is signed word
    };
    

    在轮廓技术的数学模式中,一段三阶的Bezier曲线由四个控制点定义:位于曲线上的起始点、两个不在曲线上(off-curve)的控制点和一个曲线上的结束点。

    字体中的图元轮廓用二阶Bezier曲线定义,有三个点:一个曲线上的点,一个曲线外的点和另一个曲线上的点。对于多个连续不在曲线上的点,会隐式加入一些点使其符合二阶Bezier曲线曲线的定义。例如,on-off-off-on模式的四个点,会隐式加入一个点使之成为on-off-on-off-on的五个点。

    G_ONCURVE位表示控制点是否在曲线上,设置G_REPEAT位表示标志数组的下一字节表示重复次数,当前标志被重复指定的次数。解码图元的描述需要两次扫描起始点,然后再遍历图元定义中的每一个点进行转换。

    图元指令具体细节比较复杂,主要是为了控制图元轮廓从em-square到栅格网格的缩放过程,通过网格调整技术使缩放后的渲染不失真,而记录控制值的一张表。

    整体来说渲染图元是一个非常复杂的算法,咱们不再继续深究。


    下面看看fontTools库能够读取到的图元数据,首先读取glyf表:

    glyf = font["glyf"]
    

    我们以字符0为例进行演示,查看到该字体中数字0对应的代码点为unif82e

    首先查看图元头信息:

    glyph = glyf['unif82e']
    print(f"轮廓线数目:{glyph.numberOfContours},边界范围:({glyph.xMin},{glyph.yMin})-({glyph.xMax},{glyph.yMax})")
    
    轮廓线数目:2,边界范围:(0,-14)-(550,729)
    

    前面已经提到,每个点记录的是和上一个点的坐标差值,所以边界范围存在负数很好理解。

    获取每条轮廓线终点的索引:

    glyph.endPtsOfContours
    
    [12, 25]
    

    可以计算出两条轮廓线点的数量:

    num1 = glyph.endPtsOfContours[0]+1
    num2 = glyph.endPtsOfContours[1]-glyph.endPtsOfContours[0]
    print(f"第一条轮廓线上点的数量为{num1},第二条轮廓线上点的数量为{num2}")
    
    第一条轮廓线上点的数量为13,第二条轮廓线上点的数量为13
    

    对于控制点数据中的标志,python的fontTools库似乎只能读取G_ONCURVE标志位,即是否存在于曲线上。

    首先查看控制点的坐标coordinates:

    glyph.coordinates
    
    GlyphCoordinates([(300, 728),(171, 729),(107, 615),(50, 519),(50, 195),(107, 99),(171, -14),(427, -14),(493, 99),(550, 195),(550, 519),(493, 615),(427, 729),(300, 658),(396, 658),(438, 555),(469, 483),(469, 233),(438, 159),(396, 57),(204, 57),(162, 159),(132, 233),(132, 483),(162, 555),(204, 658)])
    

    可以借助numpy计算出偏移后的实际坐标:

    coordinates = np.array(glyph.coordinates).cumsum(axis=0)
    print(coordinates.shape, coordinates.tolist())
    
    (26, 2) [[300, 728], [471, 1457], [578, 2072], [628, 2591], [678, 2786], [785, 2885], [956, 2871], [1383, 2857], [1876, 2956], [2426, 3151], [2976, 3670], [3469, 4285], [3896, 5014], [4196, 5672], [4592, 6330], [5030, 6885], [5499, 7368], [5968, 7601], [6406, 7760], [6802, 7817], [7006, 7874], [7168, 8033], [7300, 8266], [7432, 8749], [7594, 9304], [7798, 9962]]
    

    控制点是否存在于曲线上:

    glyph.flags
    
    bytearray(b'\x01\x00\x01\x00\x00\x01\x00\x00\x01\x00\x00\x01\x00\x01\x00\x01\x00\x00\x01\x00\x00\x01\x00\x00\x01\x00')
    

    可以用numpy横向拼接,方便查看:

    data = np.c_[coordinates, glyph.flags].astype("int16")
    print(data)
    
    [[ 300  728    1]
     [ 471 1457    0]
     [ 578 2072    1]
     [ 628 2591    0]
     [ 678 2786    0]
     [ 785 2885    1]
     [ 956 2871    0]
     [1383 2857    0]
     [1876 2956    1]
     [2426 3151    0]
     [2976 3670    0]
     [3469 4285    1]
     [3896 5014    0]
     [4196 5672    1]
     [4592 6330    0]
     [5030 6885    1]
     [5499 7368    0]
     [5968 7601    0]
     [6406 7760    1]
     [6802 7817    0]
     [7006 7874    0]
     [7168 8033    1]
     [7300 8266    0]
     [7432 8749    0]
     [7594 9304    1]
     [7798 9962    0]]
    

    对于连续不在曲线上的点都会自动添加隐式的点。

    如何将这些控制点数据用最简化的2进制的形式描述呢?

    np.array(glyph.coordinates).astype("int16").tobytes()+glyph.flags
    
    b',\x01\xd8\x02\xab\x00\xd9\x02k\x00g\x022\x00\x07\x022\x00\xc3\x00k\x00c\x00\xab\x00\xf2\xff\xab\x01\xf2\xff\xed\x01c\x00&\x02\xc3\x00&\x02\x07\x02\xed\x01g\x02\xab\x01\xd9\x02,\x01\x92\x02\x8c\x01\x92\x02\xb6\x01+\x02\xd5\x01\xe3\x01\xd5\x01\xe9\x00\xb6\x01\x9f\x00\x8c\x019\x00\xcc\x009\x00\xa2\x00\x9f\x00\x84\x00\xe9\x00\x84\x00\xe3\x01\xa2\x00+\x02\xcc\x00\x92\x02\x01\x00\x01\x00\x00\x01\x00\x00\x01\x00\x00\x01\x00\x01\x00\x01\x00\x00\x01\x00\x00\x01\x00\x00\x01\x00'
    

    位置索引(loca表)

    前面在读取glyf表中的图元数据时就需要读取loca表的图元索引的偏移量。

    位置索引表中保存了n+1个图元数据表的索引,其中的n是保存在最大需求表中的图元数量。最后一个额外的偏移量指向最后一个图元的偏移量和当前图元的偏移量间的差值得到的图元长度。

    python中能够读取到:

    loca = font["loca"]
    loca.locations
    
    array('I', [0, 0, 24, 68, 168, 304, 364, 480, 612, 652, 824, 948, 1040, 1164, 1252, 1432, 1660, 1856, 1944, 2052, 2140, 
    ......
    97488, 97624, 97776, 98036, 98180, 98320, 98480, 98676, 98832, 99020, 99308])
    

    最大需求表(maxp表)

    最大需求表的目的是告知字体栅格器(rasterizer)对内存的需求,以便 在出来字体前分配合适大小的内存。下面是maxp表的结构在c语言中的定义:

    typedef  struct
    {
    Fixed  Version;//0x00010000  for  version  1.0.
    USHORT  numGlypha;  //Number  of  glyphs  in  the  font  .
    USHORT  maxPoints;  //Max  points  in  noncomposite  glyph  .
    RSHORT  maxContours;  //Max  contours  in  noncomposite  glyph.
    USHORT  maxCompositePoints;//Max  points  in  a  composite  glyph.
    USHORT  maxCompositeContours;  //Max  contours  in  a  composite  glyph.
    USHORT  maxZones;//  1  if  not  use  the  twilight  zone  [Z0],
                         //or  2  if  so  use  Z0;2  in  most  cases.
    USHORT  max  TwilightPoints  ;/  Maximum  points  used  in  Z0.
    USHORT  maxStorage;  //Number  of  storage  area  locations.
    USHORT  maxFunctionDefs;  //Number  of  FDEFs.
    USHORT  maxStackElements;  //Number  of  depth.
    USHORT  maxSizeOfInstructions;  //Max  byte  count  for  glyph  inst.
    USHORT  maxComponentElements;  //Max  number  top  components  refernced.
    USHORT  maxComponentDepth;    //Max  levels  of  recursion.
    }Table_maxp;
    

    numGlyphs字段保存了字体中图元的总数,这决定了到位置表的图元索引的数量,可以验证图元索引的有效性。maxPoints\maxCountors\maxCompositePoints maxCompositeContours这几个字段说明了图元定义的复杂度。

    python中的读取一下:

    maxp = font["maxp"]
    maxp.numGlyphs, maxp.maxPoints, maxp.maxContours, maxp.maxCompositePoints, maxp.maxCompositeContours
    
    (603, 134, 11, 0, 0)
    

    命名表(name)

    包含版权说明、字体名、字体族名、风格名等,直接通过python查看:

    for n in font["name"].names:
        print(repr(n), n)
        print(n.platformID, n.nameID, n.string)
        print("----------------")
    
    1 0 b'\n  Created by font-carrier\n  '
    ----------------
    ......
    1 10 b'Generated by svg2ttf from Fontello project.'
    ----------------
    1 11 b'http://fontello.com'
    ----------------
    3 0 b'\x00\n\x00 \x00 \x00C\x00r\x00e\x00a\x00t\x00e\x00d\x00 \x00b\x00y\x00 \x00f\x00o\x00n\x00t\x00-\x00c\x00a\x00r\x00r\x00i\x00e\x00r\x00\n\x00 \x00 '
    ......
    

    截取了部分结果,可以看到该自定义字体通过fontello.com生成。

    水平布局(hmtx)

    Python查看字体的水平布局:

    for code, width in hmtx.metrics.items():
        print(code, width)
    
    glyph00000 (1136, 0)
    x (100, 0)
    uniec3e (600, 0)
    ...
    unif82e (600, 0)
    unie7c5 (1000, 0)
    ...
    unif69c (1000, 0)
    

    二进制匹配解析轮廓图顺序随机的woff字体

    有了前面的基础,现在对于乱序了轮廓图顺序的woff字体,已经变得非常简单。

    我们使用上次下载的address.woff文件作为已知训练集,然后将shopNum.woff字体文件的轮廓图,进行一定的乱序处理,看看能否正确的提取出需要的文字。

    首先使用FontCreator.exe打开shopNum.woff字体文件,然后修改轮廓图顺序。

    最终在我一顿操作后,形成下面的顺序:

    image-20210804152725277

    再将字体导出为random.woff

    那么我们能否通过address.woff文件和已知字符列表作为训练集,正确匹配出random.woff文件每个Unicode代码点对应的字符呢?

    首先读取address.woff文件的每个图元数据转成二进制后和之前已经识别出来的字符列表建立映射关系:

    from fontTools.ttLib import TTFont
    import numpy as np
    
    def get_glyphBytes(glyph):
        coordinates = np.array(glyph.coordinates).astype("int16")
        return coordinates.tobytes()+glyph.flags
    
    
    font = TTFont("address.woff")
    glyf = font["glyf"]
    
    chars = ' `1234567890店中美家馆小车大市公酒行国品发电金心业商司超生装园场食有新限天面工服海华水房饰城乐汽香部利子老艺花专东肉菜学福饭人百餐茶务通味所山区门药银农龙停尚安广鑫一容动南具源兴鲜记时机烤文康信果阳理锅宝达地儿衣特产西批坊州牛佳化五米修爱北养卖建材三会鸡室红站德王光名丽油院堂烧江社合星货型村自科快便日民营和活童明器烟育宾精屋经居庄石顺林尔县手厅销用好客火雅盛体旅之鞋辣作粉包楼校鱼平彩上吧保永万物教吃设医正造丰健点汤网庆技斯洗料配汇木缘加麻联卫川泰色世方寓风幼羊烫来高厂兰阿贝皮全女拉成云维贸道术运都口博河瑞宏京际路祥青镇厨培力惠连马鸿钢训影甲助窗布富牌头四多妆吉苑沙恒隆春干饼氏里二管诚制售嘉长轩杂副清计黄讯太鸭号街交与叉附近层旁对巷栋环省桥湖段乡厦府铺内侧元购前幢滨处向座下澩凤港开关景泉塘放昌线湾政步宁解白田町溪十八古双胜本单同九迎第台玉锦底后七斜期武岭松角纪朝峰六振珠局岗洲横边济井办汉代临弄团外塔杨铁浦字年岛陵原梅进荣友虹央桂沿事津凯莲丁秀柳集紫旗张谷的是不了很还个也这我就在以可到错没去过感次要比觉看得说常真们但最喜哈么别位能较境非为欢然他挺着价那意种想出员两推做排实分间甜度起满给热完格荐喝等其再几只现朋候样直而买于般豆量选奶打每评少算又因情找些份置适什蛋师气你姐棒试总定啊足级整带虾如态且尝主话强当更板知己无酸让入啦式笑赞片酱差像提队走嫩才刚午接重串回晚微周值费性桌拍跟块调糕'
    glyphBytes2char = {}
    for code, char in zip(glyf.glyphOrder, chars):
        glyph = glyf[code]
        if not hasattr(glyph, 'coordinates'):
            continue
        glyphBytes2char[get_glyphBytes(glyph)] = char
    

    有了映射关系,我们再开始尝试匹配random.woff文件每个Unicode代码点对应的字符:

    font = TTFont("random.woff")
    glyf = font["glyf"]
    
    code2char = {}
    for code in glyf.glyphOrder:
        glyph = glyf[code]
        if not hasattr(glyph, 'coordinates'):
            continue
        glyphBytes = get_glyphBytes(glyph)
        if glyphBytes not in glyphBytes2char:
            print("不在资料库的代码点:", code)
            continue
        code2char[code] = glyphBytes2char[glyphBytes]
    code2char
    

    结果:

    image-20210804155034484

    可以看到每一个代码点都一一精准的匹配出正确的结果。

    可以将上述过程封装成类,方便以后随时调用使用:

    from fontTools.ttLib import TTFont
    import numpy as np
    
    
    class FontMatch:
        """用于字体图元数据匹配的类"""
    
        @staticmethod
        def get_glyphBytes(glyph):
            coordinates = np.array(glyph.coordinates).astype("int16")
            return coordinates.tobytes() + glyph.flags
    
        def __init__(self, sample_font="sample.woff", chars=None, dest_font=None):
            """
            传入已知轮廓图顺序的字体文件和真实字符作为训练集,去匹配目标字体,后面能够得到该目标字体映射字符对应的真实字符
            :param sample_font: 已知轮廓图顺序的字体文件
            :param chars: 该字体文件每个轮廓图对应的真实字符
            :param dest_font: 要进行匹配的目标字体,可以后面再调用 load_dest_font 传入
            """
            sample_font = TTFont(sample_font)
            glyf = sample_font["glyf"]
            if chars is None:
                chars = ' `1234567890店中美家馆小车大市公酒行国品发电金心业商司超生装园场食有新限天面工服海华水房饰城乐汽香部利子老艺花专东肉菜学福饭人百餐茶务通味所山区门药银农龙停尚安广鑫一容动南具源兴鲜记时机烤文康信果阳理锅宝达地儿衣特产西批坊州牛佳化五米修爱北养卖建材三会鸡室红站德王光名丽油院堂烧江社合星货型村自科快便日民营和活童明器烟育宾精屋经居庄石顺林尔县手厅销用好客火雅盛体旅之鞋辣作粉包楼校鱼平彩上吧保永万物教吃设医正造丰健点汤网庆技斯洗料配汇木缘加麻联卫川泰色世方寓风幼羊烫来高厂兰阿贝皮全女拉成云维贸道术运都口博河瑞宏京际路祥青镇厨培力惠连马鸿钢训影甲助窗布富牌头四多妆吉苑沙恒隆春干饼氏里二管诚制售嘉长轩杂副清计黄讯太鸭号街交与叉附近层旁对巷栋环省桥湖段乡厦府铺内侧元购前幢滨处向座下澩凤港开关景泉塘放昌线湾政步宁解白田町溪十八古双胜本单同九迎第台玉锦底后七斜期武岭松角纪朝峰六振珠局岗洲横边济井办汉代临弄团外塔杨铁浦字年岛陵原梅进荣友虹央桂沿事津凯莲丁秀柳集紫旗张谷的是不了很还个也这我就在以可到错没去过感次要比觉看得说常真们但最喜哈么别位能较境非为欢然他挺着价那意种想出员两推做排实分间甜度起满给热完格荐喝等其再几只现朋候样直而买于般豆量选奶打每评少算又因情找些份置适什蛋师气你姐棒试总定啊足级整带虾如态且尝主话强当更板知己无酸让入啦式笑赞片酱差像提队走嫩才刚午接重串回晚微周值费性桌拍跟块调糕'
            glyphBytes2char = {}
            for code, char in zip(glyf.glyphOrder, chars):
                glyph = glyf[code]
                if not hasattr(glyph, 'coordinates'):
                    continue
                glyphBytes2char[FontMatch.get_glyphBytes(glyph)] = char
            self.glyphBytes2char = glyphBytes2char
            sample_font.close()
            if dest_font is not None:
                self.load_dest_font(dest_font)
    
        def load_dest_font(self, dest_font):
            """传入要进行匹配的目标字体,之前已经传入的目标字体会被覆盖"""
            font = TTFont(dest_font)
            self.code2name = font.getBestCmap()
            self.glyf = font["glyf"]
    
        def getRealChar(self, char):
            code = ord(char)
            if code not in self.code2name:
                return
            name = self.code2name[code]
            glyphBytes = FontMatch.get_glyphBytes(self.glyf[name])
            return self.glyphBytes2char.get(glyphBytes)
    

    调用方式:下面的代码将前面已经下载的任意一个字体文件重命名为sample.woff作为训练集,random.woff是要处理的目标字体。对于任何给点的映射字符都可以匹配出正确结果:

    from FontMatch import FontMatch
    
    font = FontMatch(sample_font="sample.woff", dest_font="random.woff")
    print(font.getRealChar("\uEE9B"))
    
    '4'
    

    对前面我们自行乱序后的自定义字体前面几个字符批量匹配测试一下:

    real_map = {'\uE0A7': '1', '\uEBF3': '2', '\uEE9B': '4', '\uE7E4': '3', '\uF5F8': '店', '\uE7A1': '中', '\uEF49': '7', '\uEEF7': '8', '\uF7E0': '9', '\uE633': '小', '\uE5DE': '车', '\uE67F': '6', '\uF2C3': '美', '\uF012': '家', '\uE0B8': '馆', '\uE438': '5'}
    for char, real in real_map.items():
        r = font.getRealChar(char)
        print("真实结果与匹配结果:", real, "|", r)
    
    真实结果与匹配结果: 1 | 1
    真实结果与匹配结果: 2 | 2
    真实结果与匹配结果: 4 | 4
    真实结果与匹配结果: 3 | 3
    真实结果与匹配结果: 店 | 店
    真实结果与匹配结果: 中 | 中
    真实结果与匹配结果: 7 | 7
    真实结果与匹配结果: 8 | 8
    真实结果与匹配结果: 9 | 9
    真实结果与匹配结果: 小 | 小
    真实结果与匹配结果: 车 | 车
    真实结果与匹配结果: 6 | 6
    真实结果与匹配结果: 美 | 美
    真实结果与匹配结果: 家 | 家
    真实结果与匹配结果: 馆 | 馆
    真实结果与匹配结果: 5 | 5
    

    一样也是完全正确。

    图像识别解析字形随机的woff字体

    上述代码解决了轮廓图顺序随机的问题,但是假如字形也发生随机怎么破呢?例如用10套基础字体随机生成自定义字体。那么之前的获取到的图元数据就无法直接匹配。

    此时我们需要使用机器学习或深度学习相关的算法,或者能够完成图元数据渲染字体图形的大佬可以直接使用逻辑算法完成。

    自己尝试了一些分类模型发现效果并不比图像识别算法好,所以最终我们依然还是决定使用一开始采用的图像识别来解决这个问题,优点是通用性强,但缺点是准确率再也无法达到100%。

    前面下载的字体文件定义最常用的601个字符,这里我们也只对这601个字符进行测试。

    首先,创建文字识别类:

    from ddddocr import DdddOcr, np
    
    
    class OCR(DdddOcr):
        def __init__(self):
            super().__init__()
    
        def ocr(self, image):
            image = np.array(image).astype(np.float32)
            image = np.expand_dims(image, axis=0) / 255.
            image = (image - 0.5) / 0.5
            ort_inputs = {'input1': np.array([image])}
            ort_outs = self._DdddOcr__ort_session.run(None, ort_inputs)
            result = []
            last_item = 0
            for item in ort_outs[0][0]:
                if item == 0 or item == last_item:
                    continue
                result.append(self._DdddOcr__charset[item])
                last_item = item
            return ''.join(result)
    
    ocr = OCR()
    

    定义需要被测试的正确字符:

    chars = '1234567890店中美家馆小车大市公酒行国品发电金心业商司超生装园场食有新限天面工服海华水房饰城乐汽香部利子老艺花专东肉菜学福饭人百餐茶务通味所山区门药银农龙停尚安广鑫一容动南具源兴鲜记时机烤文康信果阳理锅宝达地儿衣特产西批坊州牛佳化五米修爱北养卖建材三会鸡室红站德王光名丽油院堂烧江社合星货型村自科快便日民营和活童明器烟育宾精屋经居庄石顺林尔县手厅销用好客火雅盛体旅之鞋辣作粉包楼校鱼平彩上吧保永万物教吃设医正造丰健点汤网庆技斯洗料配汇木缘加麻联卫川泰色世方寓风幼羊烫来高厂兰阿贝皮全女拉成云维贸道术运都口博河瑞宏京际路祥青镇厨培力惠连马鸿钢训影甲助窗布富牌头四多妆吉苑沙恒隆春干饼氏里二管诚制售嘉长轩杂副清计黄讯太鸭号街交与叉附近层旁对巷栋环省桥湖段乡厦府铺内侧元购前幢滨处向座下澩凤港开关景泉塘放昌线湾政步宁解白田町溪十八古双胜本单同九迎第台玉锦底后七斜期武岭松角纪朝峰六振珠局岗洲横边济井办汉代临弄团外塔杨铁浦字年岛陵原梅进荣友虹央桂沿事津凯莲丁秀柳集紫旗张谷的是不了很还个也这我就在以可到错没去过感次要比觉看得说常真们但最喜哈么别位能较境非为欢然他挺着价那意种想出员两推做排实分间甜度起满给热完格荐喝等其再几只现朋候样直而买于般豆量选奶打每评少算又因情找些份置适什蛋师气你姐棒试总定啊足级整带虾如态且尝主话强当更板知己无酸让入啦式笑赞片酱差像提队走嫩才刚午接重串回晚微周值费性桌拍跟块调糕'
    

    先对系统自带的微软雅黑字体进行测试:

    from PIL import ImageFont, Image, ImageDraw
    
    size = 64
    font = ImageFont.truetype("msyh.ttc", size-24)
    error = 0
    for char in chars:
        im = Image.new(mode='L', size=(size, size), color=255)
        draw = ImageDraw.Draw(im=im)
        w, h = draw.textsize(char, font)
        o1, o2 = font.getoffset(char)
        fontx, fonty = (size-w-o1)/2, (size-h-o2)/2
        draw.text(xy=(fontx, fonty), text=char, fill=0, font=font)
        result = ocr.ocr(im)[0]
        if result != char:
            print("正确结果:", char, ",识别结果:", result)
            error += 1
    print("识别错误的字符数量:", error)
    
    正确结果: 二 ,识别结果: 一
    正确结果: 澩 ,识别结果: 檗
    正确结果: 昌 ,识别结果: 目
    正确结果: 町 ,识别结果: 盯
    正确结果: 丁 ,识别结果: j
    正确结果: 入 ,识别结果: 人
    识别错误的字符数量: 6
    

    可以看到对该字体601字符的识别只存在6个错误,其他都正确。

    再对之前下载的自定义字体进行测试:

    from fontTools.ttLib import TTFont
    
    font = TTFont("shopNum.woff")
    name2char = dict(zip(font.getGlyphOrder()[2:], chars))
    
    size = 64
    imageFont = ImageFont.truetype("shopNum.woff", size-24)
    error = 0
    for code, name in font.getBestCmap().items():
        if name not in name2char:
            continue
        char = chr(code)
        real_char = name2char[name]
        im = Image.new(mode='L', size=(size, size), color=255)
        draw = ImageDraw.Draw(im=im)
        w, h = draw.textsize(char, imageFont)
        o1, o2 = imageFont.getoffset(char)
        fontx, fonty = (size-w-o1)/2, (size-h-o2)/2
        draw.text(xy=(fontx, fonty), text=char, fill=0, font=imageFont)
        result = ocr.ocr(im)[0]
        if result != real_char:
            print("正确结果:", real_char, "识别结果:", result)
            error += 1
    print("识别错误的字符数量:", error)
    
    正确结果: 町 识别结果: 盯
    正确结果: 二 识别结果: 一
    正确结果: 澩 识别结果: 嗅
    识别错误的字符数量: 3
    

    可以看到对该字体601字符的识别只存在3个错误,其他都正确。

    那么对于任何一个未知的自定义字体,如何通过图像识别技术知道真实字符是什么呢?

    我们改造一下前面的ocr类,封装一下:

    from ddddocr import DdddOcr, np
    from PIL import ImageFont, Image, ImageDraw
    
    
    class FontOCR(DdddOcr):
    
        def __init__(self, font_name, font_size=40):
            super().__init__()
            self.font = ImageFont.truetype(font_name, font_size)
            self.cache = {}
            self.im_cache = {}
    
        def ocr(self, image):
            image = np.array(image).astype(np.float32)
            image = np.expand_dims(image, axis=0) / 255.
            image = (image - 0.5) / 0.5
            ort_inputs = {'input1': np.array([image])}
            ort_outs = self._DdddOcr__ort_session.run(None, ort_inputs)
            for item in ort_outs[0][0]:
                if item == 0:
                    continue
                return self._DdddOcr__charset[item]
    
        def ocrFontChar(self, char):
            if char in self.cache:
                return self.cache[char]
            im = self.getCharImage(char)
            return self.cache.setdefault(char, self.ocr(im))
    
        def getCharImage(self, char):
            if char in self.im_cache:
                return self.im_cache[char]
            im = Image.new(mode='L', size=(64, 64), color=255)
            draw = ImageDraw.Draw(im=im)
            w, h = draw.textsize(char, self.font)
            o1, o2 = self.font.getoffset(char)
            fontx, fonty = (64-w-o1)/2, (64-h-o2)/2
            draw.text(xy=(fontx, fonty), text=char, fill=0, font=self.font)
            return self.im_cache.setdefault(char, im)
    

    调用方式:

    fontocr = FontOCR("shopNum.woff")
    fontocr.getRealChar("\uF7F2")
    
    '让'
    

    于是可以通过以下代码可以对自定义字体的全部unicode代码点识别一遍:

    from fontTools.ttLib import TTFont
    
    font_name = "address.woff"
    fontocr = FontOCR(font_name)
    font = TTFont(font_name)
    for name, real_char in zip(font.getGlyphOrder(), chars):
        if not name.startswith("uni"):
            continue
        char = f"\\u{name[3:]}".encode().decode("unicode_escape")
        ocr_char = fontocr.ocrFontChar(char)
        print(name, real_char, ocr_char)
    
    uniec3e 1 1
    unif3fc 2 2
    uniea1f 3 3
    unie7f7 4 4
    unie258 5 5
    unif5aa 6 6
    unif48c 7 7
    unif088 8 8
    unif588 9 9
    unif82e 0 0
    unie7c5 店 店
    unie137 中 中
    unie2cb 美 美
    unif767 家 家
    ...
    

    image-20210805102046484

    可以看到这些数据都被正确的解析出来,至此我们就完成了对任意自定义字体的智能解析。

    总结

    今天,我首先演示了如何生成自定义字体,并对字体的格式结构进行了较为详细的讲解,顺便演示如何通过python的fontools库获取相应的字体数据。

    在上一篇文章中,我们通过二级缓存解决了cssURL和fontURL随机以及Unicode代码点顺序点随机的问题,本文进一步考虑针对自定义字体文件内部,轮廓图甚至基础字形也随机怎么处理。

    本文针对轮廓图顺序随机,开发了FontMatch,传入已知字体的轮廓图顺序,能处理任何针对该字体进行轮廓图顺序随机的匹配,准确率能达到100%。

    但针对字形也可能随机的情况,中间个人进行了很多基础研究,写了很多算法,但最终都还不如图像识别的效果更好。所以最终我封装了一个基于图像识别的OCR处理类,能够针对任何自定义字体传入输入字符识别出相应的结果字符。目前测试的600个高频字符,准确率达到98%以上,针对未来的不确定性,牺牲这一点准确率个人感觉也很值。

    展开全文
  • Vue-cli4前端自定义字体、iconfont,你学废了吗? 文章目录Vue-cli4前端自定义字体、iconfont,你学废了吗?一、自定义字体简介二、字体格式三、字体下载地址四、使用实例五、前端实现字体压缩六、字体加载七、web端...

    前端自定义字体、iconfont,你学废了吗?

    一、自定义字体简介

    @font-faceCSS3中的一个模块,他主要是把自己定义的Web字体嵌入到你的网页中,@font-face这个功能早在IE4就已经支持。指定的字体可以从远程服务器或者用户本地安装的字体加载,语法规则:

    @font-face {
       font-family: <YourWebFontName>;
       src: <source> [<format>][,<source> [<format>]]*;
       [font-weight: <weight>];
       [font-style: <style>];
     }
    

    参数说明:

    1、YourWebFontName:自定义的字体名称,最好是使用你下载的默认字体。

    2、source:自定义的字体的存放路径,可以是相对路径也可以是绝路径;

    3、format:自定义的字体的格式,主要用来帮助浏览器识别,其值主要有以下几种类型:truetype,opentype,truetype-aat,embedded-opentype,avg等;

    4、weight和style:weight定义字体是否为粗体,style主要定义字体样式,如斜体。

    浏览器兼容:

    在这里插入图片描述

    二、字体格式

    TureTpe(.ttf)格式: Windows和Mac的最常见的字体,是一种RAW格式,因此他不为网站优化,可以任意缩放和旋转不会出现锯齿,支持这种字体的浏览器有:IE9+,Firefox3.5+,Chrome4+,Safari3+,Opera10+,iOS Mobile Safari4.2+

    OpenType(.otf)格式:.otf字体被认为是一种原始的字体格式,是可缩放性的电脑字体类型,是微软与Adobe共同开发用来替代TrueType的新字形,其内置在TureType的基础上,所以也提供了更多的功能,支持这种字体的浏览器有:

    Firefox3.5+,Chrome4.0+,Safari3.1+,Opera10.0+,iOS Mobile Safari4.2+

    Web Open Font Format(.woff)格式:.woff字体是Web字体中最佳格式,专门为了We设计的字体格式标准,他是一个开放的TrueType/OpenType的压缩版本,并针对网络使用进行了优化,同时也支持元数据包的分离,支持这种字体的浏览器有:

    IE9+,Firefox3.5+,Chrome6+,Safari3.6+,Opera11.1+

    Embedded Open Type(.eot)格式: 微软开发的IE专用嵌入字体格式,可以从TrueType创建此格式字体,支持这种字体的浏览器有:IE4+

    SVG(.svg)格式:.svg字体是基于SVG字体渲染的一种格式,其中.svgz是使用了Gzip压缩的SVG字体,但 IE 或 Firefox 从不支持它,并且现在 Chrome 也放弃了对它的支持。因此,它的用途很有限,本指南中有意将其忽略。支持这种字体的浏览器有:

    Chrome4+,Safari3.1+,Opera10.0+,iOS Mobile Safari3.2+

    我们可以根据项目需求来判定使用哪种字体,一般其实.ttf格式就可以满足需求,但如果项目想要兼容多种低版本浏览器,这就意味着在@font-face中我们至少需要.woff,.eot两种格式字体,甚至还需要.svg等字体达到更多种浏览版本的支持。Paul Irish写了一个独特的@font-face语法叫Bulletproof @font-face,让各多的浏览器支持,可以写成:

    @font-face {
    	font-family: 'YourWebFontName';
    	src: url('YourWebFontName.eot'); /* IE9 Compat Modes */
    	src: url('YourWebFontName.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
           url('YourWebFontName.woff') format('woff'), /* Modern Browsers */
           url('YourWebFontName.ttf')  format('truetype'), /* Safari, Android, iOS */
           url('YourWebFontName.svg#YourWebFontName') format('svg'); /* Legacy iOS */
       }
    

    三、字体下载地址

    查阅网上资料,可以在Webfonts,Typekit,Kernest,Google Web Fonts,Kernest,Dafont,Niec Web Type,寻找自己需要的字体,可根据需求自行瞎子啊,但个人推荐使用字客网
    注意:字体下载时,如果是应用于商业项目一定要注意一下版权问题,避免不必要的商业纠纷。

    字体下载往往会只有其中一种格式字体,当我们需要.eot,.woff,.ttf,.svg字体格式。我给大家推荐一款好用的工具fontsquirrel,点这里进入到下面这个界面吧。

    在这里插入图片描述

    四、使用实例

    以项目中Let’s go Digital Regular 字体为例,使用场景为vue-cli4,style-resources-loader预加载

    function addStyleResource(rule) {
        rule.use('style-resource')
            .loader('style-resources-loader')
            .options({
                patterns: [
                    path.resolve(__dirname, './src/styles/fonts.less'),
                ]
            });
    }
    

    使用这种形式,因为业务场景,.ttf格式便可以满足需求,在引入文件地址时会有路径问题,因为style-resources-loader使用绝对路径导致,所以这里我们采用这种方式来引入:

    @font-face {
        font-family: 'LetsgoDigital-Regular';
        src: url('~@/assets/fonts/digital-regular.ttf')  format('truetype');
    }
    

    最终页面上效果:

    在这里插入图片描述

    五、前端实现字体压缩

    当项目中需要引入很多自定义字体时,会发现字体库体积很大,比如常用的PingFangSC-Regular字体库就有14M,因此压缩是必不可少的。

    推荐使用font-spider,也可以使用Fontmin,相对于font-spider而言比较方便的是有一个客户端操作界面。

    font-spider的工作原理是这样的: 中文字体文件之所以很大,是因为英语只有26个字母,而中文的汉字有好多好多个,所以文件相对来说就会大很多。font-spider就是从你的css文件的@font-face入手,去查找字体,然后遍历你的html文件,通过分析本地 CSS 与 HTML 文件获取 WebFont 中没有使用的字符,并将这些字符数据从字体中删除以实现压缩,同时生成跨浏览器使用的格式。

    注意:这种是把fonts.html中没有引用的字体删除,从而达到压缩的效果!!!!!如果是整个中文包压缩,这种方式就很不适用了!!!

    在vue中使用时,可创建一个fonts.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <style>
            @font-face {
                font-family: 'lcdd';
                src: url('./src/assets/fonts/pingfangsc-regular.ttf');
                src: url('./src/assets/fonts/pingfangsc-regular.ttf') format('truetype');
                font-weight: normal;
                font-style: normal;
            }
            .test {
                font-family: 'lcdd';
            }
        </style>
    </head>
    <body>
    <h2 class="test">12345678.980%</h2>
    </body>
    

    1.安装font-spider :npm install font-spider -g

    2.进入fonts.html所属文件夹下,执行font-swiper fonts.html

    3.生成.font-spider存储之前未压缩字体,压缩后字体仅有4k

    在这里插入图片描述

    压缩后:

    在这里插入图片描述

    压缩前:

    在这里插入图片描述

    六、字体加载

    虽然有压缩的功能,但必须提供出所有使用过的字体,而且我想的是我的项目中就默认一个好看的字体。

    这样就遇到一个问题,在第一次加载的时候,浏览器就会用一些时间来加载这个字体文件。

    而在加载完成之前,页面就会空白,也就是FOIT(Flash of Invisible Text)

    1. FOUT(不推荐)

    FOUT(Flash of Unstyled Text)大意就是在字体加载完成前,浏览器会显示font-family的顺序字体

    当加载完成后,才会替换成定义的字体,设置如下:

    @font-face {
        ...
        font-display: swap;
        ...
    }
    

    这样设置之后依旧会有字体闪动的效果,只是初始由白屏变为了默认字体,体验依旧不好,不推荐。

    1. FontFaceObserver

      判断字体加载完添加loading动画,使用fontfaceobserver,安装:npm i fontfaceobserver

      b.

      // css 中 @font-face 已定义好
      import FontFaceObserver from 'fontfaceobserver'
      
      loadfont(){
          console.time("字体加载用时")
          var ooo = new FontFaceObserver('Regular')
          ooo.load().then(() =>{
              document.getElementById('index').style.fontFamily = 'Regular'
              console.timeEnd("字体加载用时")
          })
      },
      

    同时可以结合通过 HTTP 缓存优化字体重复使用

    字体资源通常是不会频繁更新的静态资源。因此,它们非常适合较长的 max-age 到期 - 确保您为所有字体资源同时指定了条件 ETag 标头和最佳 Cache-Control 策略。

    无需在 localStorage 中或通过其他机制存储字体,其中的每一种机制都有各自的性能缺陷。 浏览器的 HTTP 缓存与 Font Loading API 或 webfontloader 内容库相结合,实现了最佳并且最可靠的机制来向浏览器提供字体资源。

    七、web端使用iconfont自定义图标

    阿里iconfont相信大家都是用过,这里不对注册等流程做过多赘述,直接说一下使用方式。

    使用途径一:icon单个使用(不推荐)

    单个图标用户可以自行选择下载不同的格式使用,包括png,ai,svg。此种方式适合用在图标引用特别少,以后也不需要特别维护的场景。比如设计师用来做demo原型、前端临时做个活动页、下载图标做PPT等。在项目中不推荐使用此种方式

    在这里插入图片描述

    使用途径二:unicode引用(不推荐,主要原因:不支持多色图标,书写不直观,语意不明确)

    unicode是字体在网页端最原始的应用方式,特点是:

    • 兼容性最好,支持ie6+,及所有现代浏览器。
    • 支持按字体的方式去动态调整图标大小,颜色等等。
    • 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。
    • 在不同的设备浏览器字体的渲染会略有差别,在不同的浏览器或系统中对文字的渲染不同,其显示的位置和大小可能会受到font-size、line-height、word-spacing等CSS属性的影响,而且这种影响调整起来较为困难

    注意:新版iconfont支持多色图标,这些多色图标在unicode模式下将不能使用,如果有需求建议使用symbol的引用方式

    第一步:拷贝项目下面生成的font-face

    @font-face {font-family: 'iconfont';
        src: url('iconfont.eot');
        src: url('iconfont.eot?#iefix') format('embedded-opentype'),
        url('iconfont.woff') format('woff'),
        url('iconfont.ttf') format('truetype'),
        url('iconfont.svg#iconfont') format('svg');
    }
    

    第二步:定义使用iconfont的样式

    .iconfont{
        font-family:"iconfont" !important;
        font-size:16px;font-style:normal;
        -webkit-font-smoothing: antialiased;
        -webkit-text-stroke-width: 0.2px;
        -moz-osx-font-smoothing: grayscale;
    }
    

    第三步:挑选相应图标并获取字体编码,应用于页面

    <i class="iconfont">&#x33;</i>
    

    使用途径三:font-class引用(无多色图标,可根据需要判断使用场景)

    font-class是unicode使用方式的一种变种,主要是解决unicode书写不直观,语意不明确的问题。

    但一定要注意命名空间的问题,否则一上线引发了各种雪崩问题。与unicode使用方式相比,具有如下特点:

    • 兼容性良好,支持ie8+,及所有现代浏览器。
    • 相比于unicode语意明确,书写更直观。可以很容易分辨这个icon是什么。
    • 因为使用class来定义图标,所以当要替换图标时,只需要修改class里面的unicode引用。
    • 不过因为本质上还是使用的字体,所以多色图标还是不支持的。

    使用步骤如下:

    第一步:拷贝项目下面生成的fontclass代码:

    //at.alicdn.com/t/font_8d5l8fzk5b87iudi.css
    

    第二步:挑选相应图标并获取类名,应用于页面:

    <i class="iconfont icon-xxx"></i>
    

    使用途径四:symbol引用(多色图标推荐使用,也是目前使用人数比较多的方式)

    svgsymbol提供了类似于雪碧图的功能,让svg的使用变得更简单,也可以满足做图标系统的需求,这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 这种用法其实是做了一个svg的集合,与上面两种相比具有如下特点:

    • 支持多色图标了,不再受单色限制。
    • 通过一些技巧,支持像字体那样,通过font-size,color来调整样式。
    • 兼容性较差,支持 ie9+,及现代浏览器。
    • 浏览器渲染svg的性能一般,还不如png。

    使用步骤如下:

    第一步:拷贝项目下面生成的symbol代码:

    //at.alicdn.com/t/font_8d5l8fzk5b87iudi.js
    

    在这里插入图片描述

    第二步:加入通用css代码(引入一次就行):

    <style type="text/css">
        .icon {
           width: 1em; height: 1em;
           vertical-align: -0.15em;
           fill: currentColor;
           overflow: hidden;
        }
    </style>
    

    第三步:挑选相应图标并获取类名,应用于页面:

    <svg class="icon" aria-hidden="true">
        <use xlink:href="#icon-xxx"></use>
    </svg>
    

    八、简单使用iconfont

    a. 创建 icon-component 组件

    <template>
        <svg class="svg-icon" aria-hidden="true">
            <use :xlink:href="iconName"></use>
        </svg>
    </template>
    
    <script lang="ts">
    import {Component, Vue, Prop} from 'vue-property-decorator';
    
    @Component
    export default class IconSvg extends Vue {
        @Prop({required: true}) iconClass!: string;
    
        get iconName() {
            return `#icon-${this.iconClass}`;
        }
    }
    </script>
    
    <style scoped lang="less">
    .svg-icon {
        width: 1em;
        height: 1em;
        vertical-align: -0.15em;
        fill: currentColor;
        overflow: hidden;
    }
    </style>
    

    b. 引用组件,可根据需求判定是否注册为全局组件

    <icon-svg icon-class="icon-name"/>
    

    九、 更加优雅的导入svg

    上面的方式使用的iconfont官方推荐的方式引用的。这种方式有很多缺陷,比如

    • 他是通过js来生成svg的代码,icon映射关系不明了,是以下面形式展示

    • 做不到按需加载,不能根据我们使用了那些 svg 动态的生成 svg-sprite

    • 自定义性差,通常导出的svg包含大量的无用信息,例如编辑器源信息、注释等。通常包含其它一些不会影响渲染结果或可以移除的内容。

    • 重点!!!!添加方式很不友好,icon更新需要每次重新生成iconfont.js,很麻烦

    svg-sprite-loader按需加载

    所以,我们可以借助 svg-sprite-loader 来实现按需加载。 svg-sprite-loader 它是一个 webpack loader ,可以将多个 svg 打包成 svg-sprite 。根据导入的 svg 文件自动生成 symbol 标签并插入 html。

    svg-sprite-loader优点:

    • 页面代码清爽
    • 可使用 ID 随处重复调用
    • 每个 SVG 图标都可以更改大小颜色

    但是我们发现vue-cli默认情况下会使用 url-loader 对svg进行处理,会将它放在/img 目录下,所以这时候我们引入svg-sprite-loader 会引发一些冲突。参考文章vue-cli3中webpack 相关配置,其中配置了include和exclude,请参考文章vue-cli3中webpack 相关配置。本来只添加svg-sprite-loader就行了,但是svg也是图片的一种,所以file-loader也会对其进行处理,所以就会冲突,解决的办法就是,在项目中新建一个文件icons,使用file-loader编译svg的时候不编译icons里面的图标。

    // vue.config.js
    const path = require('path')
    module.exports = {
      chainWebpack: config => {
        const svgRule = config.module.rule('svg')
        // 清除已有的所有 loader。
        // 如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。
        svgRule.uses.clear()
        svgRule
          .test(/\.svg$/)
          .include.add(path.resolve(__dirname, './src/icons'))
          .end()
          .use('svg-sprite-loader')
          .loader('svg-sprite-loader')
          .options({
            symbolId: 'icon-[name]'
          })
        const fileRule = config.module.rule('file')
        fileRule.uses.clear()
        fileRule
          .test(/\.svg$/)
          .exclude.add(path.resolve(__dirname, './src/icons'))
          .end()
          .use('file-loader')
          .loader('file-loader')
      }
    }
    

    配置完成之后,可以检查一下配置是否生效:

    • vue inspect:控制台会显示你的webpack所有的配置

    • vue inspect --rules 显示所有的rule配置规则

    • vue inspect --rule svg (我们在上面配置了svg)

    <icon-svg icon-class="icon-geren-copy" /> // 使用方式
    
    import '@/icons/icon-Shapecopy.svg';  // 引入方式
    

    自动导入所有图标

    通过上面的方式,每次引入图标都需要

    import '@/icons/icon-Shapecopy.svg';
    

    import引入我们需要的图标,很麻烦,这里我们可以使用自动导入所有图标。之后我们就要使用到 webpack 的 require.context。简单的说就是可以正则匹配引入相应的文件模块。例如:

    require.context("./test", false, /.test.js$/);
    这行代码就会去 test 文件夹(不包含子目录)下面的找所有文件名以 .test.js 结尾的文件能被 require 的文件。
    

    require.context有三个参数:

    • directory:说明需要检索的目录
    • useSubdirectories:是否检索子目录
    • regExp: 匹配文件的正则表达式

    直接上配置代码:

    import Vue from 'vue';
    import IconSvg from '@/components/icon/icon-svg.vue';
    Vue.component('icon-svg', IconSvg);
    
    const requireAll = (requireContext) => requireContext.keys().map(requireContext);
    const req = require.context('../../icons', false, /\.svg$/)
    requireAll(req);
    

    通过这种方式我们使用图标时就不需要import啦~ 是不是很方便?

    这里附上SVG在线压缩合并工具SVG Sprites还原与管理在线工具。另外附上好用的工具svgo 官方文档 张鑫旭大大的文章

    svg延伸

    写到这里我产生了一个疑问,为什么一定要用svg?png与iconfont字体他不香吗?这里做一下简单的介绍,更多延伸可以参考:这篇文章

    先说一下与icon font对比:

    • 渲染方式不同,icon font采用的是字体渲染,icon font在一倍屏幕下渲染效果并不好,在细节部分锯齿还是很明显的,SVG上面我说过它是图形所以在浏览器中使用的是图形渲染,所以SVG却没有这种问题,请看下图对比:

    在这里插入图片描述

    • icon font只能支持单色,因此这也成为了icon font的一个瓶颈。
    • icon font主要在页面用Unicode符号调用对应的图标,这种方式不管是浏览器,搜索引擎和对无障碍方面的能力都没有SVG好

    所以,根据我的理解简单的说,svg更适合做图标,因为它更加高清且支持多色。

    十、字体加密,反爬虫

    大众点评评论数据抓取 反爬虫措施有css文字映射和字体库反爬虫。

    大众点评的反爬虫手段有那些?比如:封ip,封账号,字体库反爬虫,css文字映射,图形滑动验证码。

    目前很多主流网站其实用到的很少,极为典型的就是大众点评,这里只做一下简单的介绍。具体的一些内容可参考文献:css反爬虫。目前反爬虫大概为两类,第一种是svg引用的方式,第二种是字体库反爬虫。

    字体加密原理

    字体加密其实是将一种特定的字体库来代替浏览器本身的字体库显示的过程。用凯撒加密的方式将明文(下图的原文)替换为密文(下图的阴书),加密的密钥(也就是下图字验)就是上面font-face声明的自定义字体,加密好之后,HTML源码中是密文,使用自定义字体作为密钥进行CSS解密,渲染在页面上的就是解密后的正常的明文了。

    在这里插入图片描述

    svg加密

    这一种对应文本为空,只能通过 class 属性区分,点击检查,可以看到右边属性中有一个 background-image url ,这个以 svg 为后缀的文件是一种可伸缩矢量图形,通过他来对应字体。然后在svg中找到对应的文字。但这种方式现在已经被摒弃掉了,目前已经没有网站在使用了。

    在这里插入图片描述

    字体库加密

    字体库反爬虫是大众点评目前在使用的手段,主要在页面上展示的效果如下:
    在这里插入图片描述
    我们可以从对应资源中查找字体资源

    在这里插入图片描述

    然后通过工具FontCreator 打开该字体文件。

    在这里插入图片描述

    由于大众点评对相应的 unicode 码进行了处理,所以就只能使用一些识图的 api 或者工具,识别出其中的内容,并保存构造相应的字典。因为大众点评的字体文件会更新,所以建议可以保存到 reids 中,方便处理。

    因为这种方式少数注重数据的网站会采用,这里就不详细赘述了。如果以数据为核心资产的项目,可以尝试使用这种加密方式。

    参考文献:https://www.w3cplus.com/content/css3-font-face

    https://developer.mozilla.org/zh-CN/docs/Web/CSS/@font-face

    https://segmentfault.com/a/1190000019262268

    https://juejin.im/post/59bb864b5188257e7a427c09

    展开全文
  • Android 在通知栏使用自定义字体

    千次阅读 2016-11-16 13:56:26
    在通知栏使用自定义布局时,经常会有各种奇葩的需求,更改字体,更改字体颜色,更改图片等等。。。 实践: 让我们一条一条来分析: 1,动态设置文字:remoteViews.setTextViewText(R.id.fn_flow_noti_hotword_...

    背景:

    在通知栏使用自定义布局时,经常会有各种奇葩的需求,更改字体,更改字体颜色,更改图片等等。。。

    实践:

    让我们一条一条来分析:

    1,动态设置文字:remoteViews.setTextViewText(R.id.fn_flow_noti_hotword_content_black, keyWord);  get;

    2.动态设置文字颜色:emoteViews.setTextColor(R.id.fn_flow_noti_text_int_flow_black, Color.parseColor("#CD3E3E"));  get

    3.动态设置文字大小:remoteViews.setTextViewTextSize(R.id.fn_flow_noti_info_layout_time_black,13); but   查看api这个方法只支持Android 16 以上,但是我们的应用要支持到14啊!!!!!,

     解决方案:

                SpannableString mspInt= null;
                mspInt = new SpannableString(leftString[1]);
                mspInt.setSpan(new AbsoluteSizeSpan(sp2px(context,25)), 0, leftString[1].length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                remoteViews.setTextViewText(R.id.fn_flow_noti_text_int_flow_black,mspInt);
    
    这里用到了SpannableString 这个黑科技,具体与String的关系,大家自行百度。

    我们看下remoteViews.setText()第二个参数,发现是CharSequence;

    只要支持CharSequence那么就可以使用SpannableString。get

    4.通知栏使用自定义字体

    通知栏使用大号字体会出现字体太粗,字体间隔太大的问题,我们尝试使用SpannableString来解决这个问题,但是发现,及时你给SpannableString 设定了自定义字体,运行发现并没有生效,简单看下源码会发现通知栏屏蔽了textview的字体效果。

    TextView行不通,那么就只能考虑imgaeview,imageview也有两种方案:

    1) 在uc浏览器的常驻通知栏上有一个温度的数字显示,也是大号字体,但是确实没有特别粗,间距也不大,粗布分析时使用了图片,解压缩一下apk,在res下可以找到0-9的10张图片,这是一种实现方式

    2)使用画布的方式来手动绘制,废话不多上,上代码:

    /**
         * 使用自定义字体绘制流量信息以及低流量图片
         * @param context
         * @param str
         * @param flowStatus
         * @return
         */
        private static Bitmap buildUpdate(Context context,SpannableString str,int flowStatus)     {
            //初始化画笔
            Paint paint = new Paint();
            Typeface clock = Typeface.createFromAsset(context.getAssets(),"iconfont/light.otf");
            paint.setAntiAlias(true);
            paint.setSubpixelText(true);
            paint.setTextAlign(Paint.Align.LEFT);
            paint.setTypeface(clock);
            paint.setStyle(Paint.Style.FILL);
            paint.setColor(Color.RED);
            
    
            /**
             * 提取出来作为方法,65写出来 fixed me
             */
            int mCanvasWidth = 0;
            //计算整数位上的宽度
            paint.setTextSize(sp2px(context,40));
            Rect boundsLeft = new Rect();
            paint.getTextBounds(str.toString(), 0, 3, boundsLeft);
            mCanvasWidth += boundsLeft.width();
            //计算小数位上的宽度
            paint.setTextSize(sp2px(context,20));
            Rect boundsRight = new Rect();
            paint.getTextBounds(str.toString(), 3, str.length(), boundsRight);
            
            //根据文字宽度生成画布,比较图片和小数位宽度,取最大值
            Bitmap flowLowBmp = BitmapFactory.decodeResource(context.getResources(), R.mipmap.img_flow_low);
            if (flowLowBmp.getWidth()>boundsRight.width()) {
                mCanvasWidth += flowLowBmp.getWidth();
            } else {
                mCanvasWidth += boundsRight.width();
            }
            //生成画布  宽度为文字宽度,加30保证不被压缩到,高度为通知栏高度
            Bitmap mBitmap = Bitmap.createBitmap(mCanvasWidth+30,dip2px(context,65), Bitmap.Config.ARGB_4444);
            Canvas mCanvas = new Canvas(mBitmap);
            //计算文字的左下起点y值,画布中线家加上文字高度的一半
            float baseline = dip2px(context,65)/2 + paint.getTextSize()/2;
            //绘制整数位
            paint.setTextSize(sp2px(context,40));
            mCanvas.drawText(str,0,3, 10, baseline, paint);
            //绘制小数位
            paint.setTextSize(sp2px(context,20));
            mCanvas.drawText(str,3,str.length(), boundsLeft.width()+15,baseline, paint);
    
            //流量不足时显示低流量图片
            if(flowStatus == FlowDataUtils.STATE_WARNING) {
                mCanvas.drawBitmap(flowLowBmp, boundsLeft.width()+20, dip2px(context, 10), paint);
            }
            return mBitmap;
        }




    展开全文
  • 如何改变word文档结构图字体大小

    千次阅读 2012-12-19 21:44:38
    如何改变word文档结构图字体大小 1,在“格式”菜单上,单击“样式和格式”。 , 2,在“样式和格式”任务窗格中,单击“显示”框中的“自定义”。  3,在“类别”列表中,单击“所有样式”。 4,在“可见...

    如何改变word文档结构图字体大小

    1,在“格式”菜单上,单击“样式和格式”。 ,
    2,在“样式和格式”任务窗格中,单击“显示”框中的“自定义”。 
    3,在“类别”列表中,单击“所有样式”。
    4,在“可见样式”列表中,选中“文档结构图”复选框,再单击“确定”。 
    5,在“样式和格式”任务窗格中,指向“请选择要应用的格式”下的“文档结构图”,
    6,单击箭头,再单击“修改”。 
    7,若要设置字体和字号,请单击“字体”,然后在字体和字号框内进行所需更改。

    展开全文
  • word文档中,设置字体大小是再普通、再常见不过的事了,我们常常根据相关部门的不同要求来设置它们,如“宋体四号”、“楷体14磅”等。 谁料想,一次为了特殊需要,竟要求打印出的字体高度非10厘米不可。高度是...
  • 设置词云图中文字字体大小的范围。 完整示例 完整代码 #!/usr/bin/env python # -*- coding: utf-8 -*- # @Author : AwesomeTang # @File : wordcloud_custom_word_size.py # @Version : Python 3.7 # @Time : 2020-...
  • word2007修改文档结构图的字体大小

    千次阅读 2014-03-31 17:19:00
    然而看到的文档结构图的字体实在太小了,根本无法快速定位。如下图:   在网上找到不少解决办法,但大都是文字描述,自己感觉不直观。一下是我的解决步骤,图文一起: 1.先把样式
  • 其中要整体修改文字的字体大小和字体,可以用以下方法:newfile = docx.Document()newfile.styles['Normal'].font.name = 'Times New Roman'newfile.styles['Normal']._element.rPr.rFonts.set(qn...
  • 2、修改 wangEditor.js 源码后,用户可以输入自定义大小。例如,50%效果如下: 再例如,20%效果如下: 修改方式 在 wangEditor.js 中进行修改,整体修改内容如下图: 主要分为两个部分的修改:输入框、事件方法...
  • Word2010怎样设置自定义的编号人们在使用Word2010编辑文档的过程中,除了可以使用Word2010本身包含的编号以外,还可以自己设置一些自定义的编号格式,这样就能更加符合人们的要求了。下面就来介绍一下设置自定义编号...
  • 1、点击word中插入的任意一个公式进入MathType编辑界面,点击大小(Z),选择定义,如下图,设置好所需尺寸之后点确定; 2、点击预置–>公式预置–>保存到文件,保存到自定义位置;(假设该文件为1.eqp) 3、在...
  • Word2007的使用中发现“文档结构图”部分字体太大,结构较多时窗口不是很方便,于是想把文档结构图放小点,网上搜索了一下,结合网络介绍的内容,自己试验了一下,把方法总结了一下。  一般关于“文档结构图”的...
  • 如何实现用户自定义Word模板

    千次阅读 2014-08-15 11:19:32
    导读: 在涉及到word文档生成的项目中,一般采用编程将数据填充到word模板中生成文件的实现方式,如果模板由开发人员自己设计,那么编程填充数据是相对容易实现的;但如果用户希望可以自己修改模板或自己定义新模板...
  • mathtype怎么调公式字体大小

    千次阅读 2020-02-15 13:11:35
    使用mathtype键入公式比word直接插入公式更加方便,但是mathtype键入公式调节字体稍微麻烦点,那么,mathtype怎么调公式字体大小呢?下面让我来给大家介绍下吧,希望对大家有所帮助。 解决方法: 1、如图所示,...
  • VUE中tinymce设置字体大小、字体选择(就没有一篇文章能说的清楚的,那么我就说清楚这个问题) ...toolbar 增加自定义字体大小配置fontsizeselect及字体选择配置项fontselect 第二步...
  • PageOffice--实现用户自定义Word模板

    千次阅读 2018-09-12 14:19:50
    导读: 在涉及到word文档生成的项目中,一般采用编程将数据填充到word模板中生成文件的实现方式,如果模板由开发人员自己设计,那么编程填充数据是相对容易实现的;但如果用户希望可以自己修改模板或自己定义新模板...
  • 如何在 Word 中使用自定义样式生成文章目录 概要本文介绍如何在 Microsoft Word 2002 和 Microsoft Office Word 2003 中使用自定义样式创建目录。在 Word 中创建目录分为两步。
  • 问题描述:修改自定义目录之后,目录的字体样式仍没有变化,而且随着正文相应的文字字体变化。 原因:正文中的标题的样式不是“标题”,只是改变了大纲级别 解决方法:将正文中的相关段落改为相应的“标题” ...
  • 写下这篇文章是不为更好的去理解自定义view机制和原理。 有时候会遇到这种需求,就是一个textview有多行(大于两行),但是只是title和同容不一致,而第二行又需要顶格来,遇到这种情况怎么处理呢? 下面是给出实现...
  • 幸好博客园在博客设置中提供了自定义的css设置,让我们可以最大限度的设定博客风格。 一、寻找样式名称 (如果你仅仅希望知道如何修改,那么可以调过本节,直接进入修改一节) 进入一篇文章,在Chrome浏览器中...
  • 幸好博客园在博客设置中提供了自定义的css设置,让我们可以最大限度的设定博客风格。 一、寻找样式名称 如果你仅仅希望知道如何修改,那么可以调过本节,直接进入修改一节。 要修改代码样式就必须找到原本的样式,...
  • jq实现字体大小随字数改变而变化 提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录jq实现字体大小随字数改变而变化前言一、pandas是什么?二、使用步骤1.引入库2.读入数据总结 前言 ...
  • 解决word生成目录-页码-字体问题

    千次阅读 2019-10-25 12:02:59
    1.word独立生成目录页码 2.word从正文第一页开始自动生成目录 1.word独立生成目录页码 (1). 一般而言,我们写好的文档会是这样的 (2). 给文档的各级标题添加 标题样式 ,根据需要设置的标题等级依次选用不同的...
  • 其中要整体修改文字的字体大小和字体,可以用以下方法: newfile = docx.Document() newfile.styles['Normal'].font.name = 'Times New Roman' newfile.styles['Normal']._element.rPr.rFont...
  • w(宽),h(高) //位置、宽、高的单位都为mm doc.addImage(logoData, 10, 5, 21, 21) doc.setFontSize(38)//字体大小配置,在需要配置的文本上方设置。 doc.setTextColor('red')//字体颜色配置 //添加文本,参数为('文本...
  • Android WebView(属性)缩放 字体大小

    千次阅读 2015-09-25 12:35:18
    * 自定义的WebViewClient类,将特殊链接从WebView打开,其他链接仍然用默认浏览器打开 * * @author 1 * */ private class MyWebViewClient extends WebViewClient { @Override public ...
  • Itext设置导出word文件的字体

    千次阅读 2017-02-24 16:27:18
    首先,要导入Itext的三个jar...其次,Itext里面设置导出word文件的字体,用RtfFont这个类的初始化方法进行设置;  RtfFont font =new RtfFont("仿 宋", 12, Font.NORMAL, Color.BLACK); 这个构造方法里面:第一个参数
  • Word论文用的各级标题大小

    千次阅读 2019-09-08 23:08:57
    标题的格式的作用就是为了生成目录的(可以参考我前一篇文章有讲这个与如何制作目录,注:不能跨级用标题,只能第一级标题用Word的标题1,第二级标题用Word的标题2,以此类推,因为Word就是依靠标题格式顺序建立目录...
  • 幸好博客园在博客设置中提供了自定义的css设置,让我们可以最大限度的设定博客风格。 一、寻找样式名称 如果你仅仅希望知道如何修改,那么可以调过本节,直接进入修改一节。 要修改代码样式就必须找到原本的样式...
  • ActionsPane 接口 表示 Word 或 Excel 的文档级自定义项中“文档操作”任务窗格的自定义用户界面。 命名空间: Microsoft.Office.Tools

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 17,327
精华内容 6,930
关键字:

word自定义字体大小