破解字体反爬(二)
介绍
本篇文章描述通过程序解析字库文件中字体的方法。
背景知识
网页上使用的字库文件常用格式有:.ttf,.woff,.eot(认识 Iconfont 以及什么是 .eot、.woff、.ttf、.svg ——简书)。经过测试,以ttf文件格式为准编写的解析程序,其他格式一般也能解析。下面就以解析ttf格式作介绍。
TrueType字体
TrueType字体通常包含在单个TrueType字体文件中,其文件后缀为.TTF。OpenType字体是以类似 于TrueType字体的格式编码的POSTSCRIPT字体。OPENTYPE字体使用.OTF文件后缀。OPENTYPE还允许把多个OPENTYPE字体组合在一个文件中以利于数据共享。这些字体被称为TrueType字体集(TrueType collection),其文件后缀为.TTC
对于TrueType字体文件结构解析的帖子很多,我这里贴出一个格式比较清晰的。ttf文件结构解析.doc,这里对ttf文件结构解析.doc作简单概述。
一个ttf文件中包含了很多个表结构,大概有:
- head 字体头 字体的全局信息
- cmap 字符代码到图元的映射 把字符代码映射为图元索引
- glyf 图元数据 图元轮廓定义以及网格调整指令
- maxp 最大需求表 字体中所需内存分配情况的汇总数据
- mmtx 水平规格 图元水平规格
- loca 位置表索引 把元索引转换为图元的位置
- name 命名表 版权说明、字体名、字体族名、风格名等等
- hmtx 水平布局 字体水平布局星系:上高、下高、行间距、最大前进宽度、最小左支撑、最小右支撑
- kerm 字距调整表 字距调整对的数组
- post PostScript信息 所有图元的PostScript FontInfo目录项和PostScript名
- PCLT PCL 5数据 HP PCL 5Printer Language 的字体信息:字体数、宽度、x高度、风格、记号集等等
- OS/2 OS/2和Windows特有的规格 TrueType字体所需的规格集
其中,图元数据(glyf表)是TrueType字体的核心信息,因此通常它是最大的表。图元,全称为图形输出原语,也就是字体的图形数据。一个图元对应这个一个字体,一个图元有多条轮廓,一条轮廓有多个点。例如:一个字体文件中有字符“0”,那么“0”就对应一个图元,“0”的图元有两条轮廓线,一条是内部的轮廓,另一条是外部的轮廓。,外圈有21个点控制,内圈有11个点控制。这些点控制这轮廓的形状,但不是组成。因为TureType字体中的图元轮廓是用二阶Bezier曲线定义的,有三个点:一个曲线上的点,一个曲线外的点和另一个曲线上的点。多个连续的不在曲线上的点是允许的,但不是用来定义三阶或更高阶的Bezier曲线,而是为了减少控制点的数目。比如,对于on-off-off-on模式的四个点,会加入一个隐含的点使之成为on-off-on-off-on,因此定义的是两段二阶Bezier曲线。如下图,会发现“0”外圈上会超过21个点,内圈上会超过11个。
fontTools 字体文件解析库
fontTools是python语言编写的字体文件解析程序。上面“ttf文件解析.doc”中使用C语言描述ttf文件内部表的结构如图元头部信息表:
typedef struct
{
WORD numberOfContours; //contor number,negative if composite 图元轮廓线数量
FWord xMin; //Minimum x for coordinate data. // 图元位置x轴最小值
FWord yMin; //Minimum y for coordinate data. // 图元坐标y轴最小值
FWord xMax; //Maximum x for coordinate data. // 图元坐标x轴最大值
FWord yMax; //Maximum y for coordinate data. // 图元坐标y轴最大值
}GlyphHeader;
使用fontTools工具包会更方便,fontTools可以把字体文件中的表结构及数据以xml的格式保存到文件中。
from fontTools.ttLib import TTFont
# ttfFile是ttf文件的路径或ttf文件流
font = TTFont(ttfFile)
# path可以是保存xml文件的路径或io流,tables参数指定保存的表,为空则保存全部表。
# keys方法可以查看所有表名['GlyphOrder', 'head', 'hhea', 'maxp', 'OS/2', 'hmtx', 'cmap', 'loca', 'glyf', 'name', 'post', 'GSUB']
font.saveXML(path, tables=['glyf'])
字符“0”以xml格式描述上面C语言结构体为:
<TTGlyph name="uniE575" xMin="0" yMin="-12" xMax="508" yMax="719"></TTGlyph>
fontTools.ttLib.TTFont.getBestCmp方法可以输出字符序列与图元名称之间的映射关系
因为fontTools会把数据转存为xml,所以我们需要解析xml
xml.dom.minidom xml代码解析库
python提供的xml代码解析库有很多,这里只是随便使用其中的一种
from xml.dom.minidom import parseString
doc = parseString(io.getvalue())
root = doc.documentElement
xml.dom.minidom解析出的xml-dom对象支持基本的dom操作,我们要使用的也就element.getElementsByTag,element.getElementByName,element.getAttribute
matplotlib 绘图工具包
matplotlib让我们能够把ttf中的图元绘制出来。预览一个matplotlib
pytesseract ocr识别库
pytesseract是python语言编写的tesseract-ocr开发工具包,需要安装tesseract-ocr才能正常使用。Tesseract:开源的OCR识别引擎,初期Tesseract引擎由HP实验室研发,后来贡献给了开源软件业,后经由Google进行改进,消除bug,优化,重新发布。
tesseract-ocr可以把我们绘制的字体图形识别成文字。pytesseract的使用方式很简单:
import pytesseract
from PIL import Image
image = Image.open(imagePath)
word = pytesseract.image_to_string(image, lang='chi_sim', config='--psm 10')
实现
我们的是方案是:通过fontTools解析ttf文件转存为xml,xml.dom.minidom解析xml,matplotlib绘制字体图形,pytesseract识别字体图形为字符串
fontTools解析ttf文件转存为xml
from fontTools.ttLib import TTFont
from io import StringIO
class TTFParser:
ttfFile = ""
"""parser ttf font file"""
def __init__(self):
self.ttfFile = ""
# 解析字库文件
def parseFontFile(self, ttfFile):
self.ttfFile = ttfFile
try:
self.font = TTFont(ttfFile)
except Exception as e:
raise Exception("a exception occurred during instantiatting TTFParser["+ttfFile+"]("+e.message+")")
return;
try:
io = StringIO()
self.font.saveXML(io, tables=['glyf'])
return io
except Exception as e:
raise Exception("a exception occurred during saving TTFont["+ttfFile+"] to xml file("+e.message+")")
io的内容为:
xml.dom.minidom解析xml
from xml.dom.minidom import parseString
doc = parseString(io.getvalue())
root = doc.documentElement
matplotlib绘制字体图形
先贴出核心代码
class TTFPath(Path):
"""docstring for TTFPath."""
def __init__(self, glyph):
self.__verts = []
self.__codes = []
self.calculatePath(glyph)
super(TTFPath, self).__init__(self.__verts, self.__codes)
# 计算path
def calculatePath(self, glyph):
contours = glyph.getContours()
for contour in contours:
# 画笔移至第一个点
self.__moveTo(contour.getPoint(0))
# 遍历轮廓线上的点,从第二个点开始
for index in range(1,contour.size()):
point = contour.getPoint(index)
if(point.onCurve):
# onCurve为True标识为贝塞尔曲线的起止点,该点在曲线上
self.__lineTo(point)
continue
else:
# onCurve为False标识该点为控制点,该点不在曲线上
if contour.getPoint(index-1).onCurve:
self.__quadTo(point)
else:
# 连续两个点onCurve为控制点,添加两点中点作为起止点
self.__lineTo(self.__mindPoint(point, contour.getPoint(index-1)))
self.__quadTo(point)
# 曲线绘制完成,画笔回到起点
self.__moveTo(contour.getPoint(0))
def __moveTo(self, point):
self.__codes.append(Path.MOVETO)
self.__verts.append((point.x, point.y))
# path.append((Path.MOVETO, (point.x, point.y)))
def __lineTo(self, point):
self.__codes.append(Path.LINETO)
self.__verts.append((point.x, point.y))
# path.append((Path.LINETO, (point.x, point.y)))
def __quadTo(self, point):
self.__codes.append(Path.CURVE3)
self.__verts.append((point.x, point.y))
# path.append((Path.CURVE3, [(ctrlPoint.x, ctrlPoint.y), (point.x, point.y)]))
def __closePath(self, point):
self.__codes.append(Path.CLOSEPOLY)
self.__verts.append((point.x, point.y))
def __mindPoint(self, pointA, pointB):
return Point(pointA.x+(pointB.x-pointA.x)/2, pointA.y+(pointB.y-pointA.y)/2)
class TTFRender:
"""docstring for TTFRender."""
color = 'black'
def __init__(self):
self.fig, self.ax = plt.subplots()
def draw(self, glyph):
path = TTFPath(glyph)
patch = PathPatch(path, facecolor=self.color)
plt.cla()
# plt.clf()
self.ax.add_patch(patch)
# 设置坐标系
self.ax.grid()
# self.ax.axis('equal')
self.centralize(glyph.getXmin(),glyph.getXmax(),glyph.getYmin(),glyph.getYmax())
# 隐藏坐标轴刻度
plt.xticks([])
plt.yticks([])
plt.axis('off')
# 使图像居中。应对逗号,句号,小数点等特殊字体
def centralize(self, xMin,xMax,yMin,yMax):
width = 640
height = 480
self.ax.axis(xmin=int(xMin-width/2),xmax=int(xMax+width/2),ymin=int(yMin-height/2),ymax=int(yMax+height/2))
def getBufferImage(self, width=80, height=60):
io = BytesIO()
self.fig.savefig(io)
image = Image.open(io)
return image.resize((width,height))
pytesseract识别字体图形为字符串
# encoding: utf-8
import pytesseract
from PIL import Image
class Ocr:
"""docstring for Ocr"""
def __init__(self, image = None):
self.image = image
self.lang = 'chi_sim'
self.psm = '10'
def setImagePath(self, imagePath):
self.image = Image.open(imagePath)
def setImageFile(self, imageFile):
self.image = imageFile
def setLang(self, lang):
self.lang = lang
def getWords(self):
return pytesseract.image_to_string(self.image, lang=self.lang, config='--psm '+self.psm)
def recoginze(self, image):
return pytesseract.image_to_string(image, lang=self.lang, config='--psm '+self.psm)