woff字体图元结构剖析,自定义字体的制作与匹配和识别
前面我在《2萬字硬核剖析網(wǎng)頁自定義字體解析》一文中,講解了通過圖像識(shí)別來解析自定義字體,但是圖像識(shí)別的缺點(diǎn)在于準(zhǔn)確率并不能達(dá)到100%,還需要二次修改。
前面將字體的稱為點(diǎn)陣圖,其實(shí)根據(jù)TrueType字體實(shí)際采用的技術(shù),稱為輪廓圖更為合適,所以本文所說的輪廓圖就是上篇的點(diǎn)陣圖。
由于目前幾個(gè)大廠的網(wǎng)站的自定義字體的輪廓圖都是那個(gè)固定的順序,所以上文只處理了所有字體文件輪廓圖順序都一致的情況,并沒有繼續(xù)深挖去處理輪廓圖順序出現(xiàn)隨機(jī)的情況。
本文就將針對(duì)未來自定義字體的輪廓圖順序出現(xiàn)隨機(jī)的情況進(jìn)行處理。
具體處理思路就是,提取字體的圖元數(shù)據(jù),包括控制點(diǎn)位置和標(biāo)志位,轉(zhuǎn)成二進(jìn)制字節(jié)進(jìn)行唯一標(biāo)識(shí),與現(xiàn)有的已知的字符集進(jìn)行映射。后續(xù)任何Unicode代碼點(diǎn)順序隨機(jī)和輪廓圖順序隨機(jī)的字體文件,都可以提取圖元數(shù)據(jù)轉(zhuǎn)換后進(jìn)行唯一匹配從而解碼出唯一正確的字符。
不過上述思路還只是處理了輪廓圖順序隨機(jī),其實(shí)還可以再變態(tài)點(diǎn)以多個(gè)基礎(chǔ)字形制作自定義字體取隨機(jī),意味著每個(gè)字符的圖元數(shù)據(jù)都會(huì)發(fā)生較大變化,上面的匹配方法就會(huì)直接失效。此時(shí)便只能通過機(jī)器學(xué)習(xí)計(jì)算字符間的相似度,從而識(shí)別出圖元對(duì)應(yīng)的真實(shí)字符。
文章目錄
- 字體格式類型介紹
- 如何生成自定義字體
- woff字體的解析
- 字體頭表(head表)
- 字符到圖元索引的映射表(cmap表)
- 圖元數(shù)據(jù)(glyf表)
- 位置索引(loca表)
- 最大需求表(maxp表)
- 命名表(name)
- 水平布局(hmtx)
- 二進(jìn)制匹配解析輪廓圖順序隨機(jī)的woff字體
- 圖像識(shí)別解析字形隨機(jī)的woff字體
- 總結(jié)
字體格式類型介紹
字體格式類型主要有幾個(gè)大分類:TrueType、Embedded Open Type 、OpenType、WOFF 、SVG。
TrueType:
Windows和Mac系統(tǒng)最常用的字體格式,基于輪廓技術(shù)的數(shù)學(xué)模式來進(jìn)行定義,比基于矢量的字體更容易處理,保證了屏幕與打印輸出的一致性。同時(shí),這類字體和矢量字體一樣可以隨意縮放、旋轉(zhuǎn)而不必?fù)?dān)心會(huì)出現(xiàn)鋸齒。
EOT – Embedded Open Type (.eot):
微軟開發(fā)的嵌入式字體,允許OpenType字體用@font-face嵌入到網(wǎng)頁并下載至瀏覽器渲染,存儲(chǔ)在臨時(shí)安裝文件夾下。
OpenType (.otf):
微軟和Adobe共同開發(fā)的字體,微軟的IE瀏覽器全部采用這種字體,致力于替代TrueType字體。
WOFF – Web Open Font Format (.woff):
專門為了Web而設(shè)計(jì)的字體格式標(biāo)準(zhǔn),實(shí)際上是對(duì)TrueType/OpenType等字體格式的封裝,每個(gè)字體文件中含有字體以及針對(duì)字體的元數(shù)據(jù)(Metadata),字體文件被壓縮,以便于網(wǎng)絡(luò)傳輸。
SVG (Scalable Vector Graphics) Fonts (.svg):
使用SVG技術(shù)來呈現(xiàn)字體,支持gzip壓縮格式。
在上次從css的@font-face提取出字體URL鏈接時(shí),就包含了eot和woff兩種格式。鑒于woff字體更容易被分析,所以我們上次選擇了只下載woff字體格式,今天這篇文章也一樣。
字體格式轉(zhuǎn)換工具:
- https://www.fontsquirrel.com/tools/webfont-generator
- https://everythingfonts.com/
可以生成自定義字體的網(wǎng)站:
- https://icomoon.io/app/#/select
- http://fontello.com
如何生成自定義字體
先生成svg字體,再導(dǎo)入到自定義字體生成網(wǎng)站,再定義字體映射關(guān)系,最后導(dǎo)入字體即可。
由于https://everythingfonts.com/對(duì)文件較大的字體轉(zhuǎn)換需要收費(fèi),這里我使用https://www.fontsquirrel.com/tools/webfont-generator將系統(tǒng)自帶的arial.ttf字體文件轉(zhuǎn)換為svg字體:
下載并解壓得到一個(gè)arial-webfont.svg文件。
接下來打開https://icomoon.io/app/#/select,選擇需要被自定義的字符:
本例選擇了0-9作為被自定義的字符,然后點(diǎn)擊右下角 Generate Font 按鈕準(zhǔn)備設(shè)置字符映射:
設(shè)置好映射關(guān)系后,點(diǎn)擊下載字體。
下載的壓縮包包含多種字體,解壓出其中的icomoon.woff字體文件。
用FontCreator字體設(shè)計(jì)工具打開后可以看到如下結(jié)果:
可以看到與我們前面在網(wǎng)站中自定義的映射一致。
woff字體的解析
首先,我們用python的fontTools庫讀取上次下載的字體文件:
from fontTools.ttLib import TTFontfont = TTFont("tagName.woff")可以一次性將相關(guān)數(shù)據(jù)保存到本地:
font.saveXML("tagName.xml")字體文件都包含了一個(gè)TableDirectory結(jié)構(gòu),保存了多張表,每個(gè)表保存了不同的信息。
TrueType字體中常見的表有:
| head | 字體頭 | 字體的全局信息 |
| cmap | 字符代碼到圖元的映射 | 把字符代碼映射為圖元索引 |
| glyf | 圖元數(shù)據(jù) | 圖元輪廓定義以及網(wǎng)格調(diào)整指令 |
| loca | 位置表索引 | 把元索引轉(zhuǎn)換為圖元的位置 |
| maxp | 最大需求表 | 字體中所需內(nèi)存分配情況的匯總數(shù)據(jù) |
| name | 命名表 | 版權(quán)說明、字體名、字體族名、風(fēng)格名等等 |
| hmtx | 水平布局 | 字體水平布局:上高、下高、行間距、最大前進(jìn)寬度、最小左支撐、最小右支撐 |
字體頭表(head表)
字體頭表(head表)中包含了TrueType字體的全局信息,在c語言中的結(jié)構(gòu)定義如下:
typedef sturct {Fixed Table;//x00010000 ro version 1.0Fixed fontRevision;//Set by font manufacturer.ULONG checkSumAdjustment;ULONG magicNumer; //Set to 0x5f0f3cf5USHORT flags;USHORT unitsPerEm; //Valid range is from 16 to 16384longDT 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;上面各個(gè)字段定義基本都能直接在python中讀取,其中日期字段有created和modified,分別表示字體創(chuàng)建時(shí)間和字體最后修改時(shí)間,使用8個(gè)字節(jié)記錄從1904年1月1日午夜12:00開始的秒數(shù)。
獲取字體的創(chuàng)建時(shí)間和字體最后修改時(shí)間:
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"創(chuàng)建時(shí)間:{create_time},最后修改時(shí)間:{modifie_time}") 創(chuàng)建時(shí)間:2021-08-02 15:00:30,最后修改時(shí)間:2021-08-02 15:00:30字體是針對(duì)一個(gè)被稱為em-square的參考網(wǎng)格設(shè)計(jì)的,字體中的圖元用網(wǎng)格中的坐標(biāo)表示。em-squrare的大小決定字體的圖元被縮放的方式和質(zhì)量。字體頭中保存了每個(gè)em-square的格數(shù)和能 包含所有圖元的邊界框。Em-square的有效值是從16到16384。
讀取每個(gè)em-square的格數(shù)和圖元邊界框范圍:
print(f"每個(gè)em-square的格數(shù):{head.unitsPerEm},邊界框范圍x: {head.xMin} - {head.xMax},y: {head.yMin} - {head.yMax}") 每個(gè)em-square的格數(shù):1000,邊界框范圍x: 0 - 1136,y: -112 - 833字體頭表中的其他信息包括最小可讀像素大小、字體方向、在位置表中圖元索引的格式和圖元數(shù)據(jù)格式等:
head.lowestRecPPEM, head.fontDirectionHint, head.indexToLocFormat, head.glyphDataFormat (8, 2, 0, 0)字符到圖元索引的映射表(cmap表)
字符到圖元索引的映射表(cmap表)定義了從不同代碼頁中的字符代碼到圖元索引的映射關(guān)系。cmap表包含幾個(gè)子表以支持不同的平臺(tái)和不同的字符編碼方案。cmap表在c語言中的定義較為復(fù)雜,不作展示。
在python中我們可以通過cmap表獲取字符代碼到圖元索引的映射關(guān)系:
cmap = font['cmap'] cmap.getBestCmap() {120: 'x',57360: 'unie010',57369: 'unie019',57370: 'unie01a',...63699: 'unif8d3',63718: 'unif8e6',63724: 'unif8ec'}不過獲取這個(gè)關(guān)系也并沒有太大的意義,因?yàn)槲覀兛梢院茌p松的進(jìn)行相互轉(zhuǎn)換:
"uni"+chr(57360).encode("unicode_escape").decode()[2:]就可以得到對(duì)應(yīng)的unie010,反過來也可以:
char = 'unie010' ord(("\\u"+char[3:]).encode().decode("unicode_escape"))即可得到57360。
當(dāng)然fontTools本身也提供了反向獲取的API:
cmap.buildReversed() {'x': {120},'unie010': {57360},'unie019': {57369},'unie01a': {57370},...'unif8d3': {63699},'unif8e6': {63718},'unif8ec': {63724}}圖元數(shù)據(jù)(glyf表)
圖元數(shù)據(jù)(glyf表)是我們所需要的字體核心信息,以序列形式保存了圖元數(shù)據(jù),每個(gè)圖元以圖元頭(GlyphHeader)結(jié)構(gòu)開始,在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;合成圖元由多個(gè)簡(jiǎn)單圖元或合成圖元組成,簡(jiǎn)單圖元的numberOfContours字段保存了當(dāng)前圖元的輪廓線的數(shù)目。而合成圖元的numberOfContours字段為負(fù)值,表示需要基于組成該合成圖元的所有簡(jiǎn)單圖元的輪廓線的數(shù)目計(jì)算得到。后四個(gè)字段記錄了圖元的邊界框。
簡(jiǎn)單圖元的圖元描述信息緊跟在其GlyphHeader結(jié)構(gòu)之后,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包括所有輪廓線結(jié)束點(diǎn)的索引、圖元指令和一系列的控制點(diǎn),每個(gè)控制點(diǎn)包括包括一個(gè)標(biāo)志和xy軸坐標(biāo)。
endPtsOfContours數(shù)組保存了每一條輪廓線終點(diǎn)的索引,通過該索引可以計(jì)算出每條輪廓線中點(diǎn)的數(shù)量。比如,endPtsOfContours[0]+1是第一條輪廓線上點(diǎn)的數(shù)量,endPtsOfContours[1]-endPtsOfContours[0]是第二條輪廓線上點(diǎn)的數(shù)量。
圖元的控制點(diǎn)保存在三個(gè)數(shù)組中:標(biāo)志獲得組、x坐標(biāo)數(shù)組和y坐標(biāo)數(shù)組。為了節(jié)省存儲(chǔ)空間,圖元中保存的是相對(duì)坐標(biāo)。第一個(gè)點(diǎn)的坐標(biāo)是相對(duì)原點(diǎn)(0, 0)記錄的,隨后的點(diǎn)記錄和上一個(gè)點(diǎn)的坐標(biāo)差值。標(biāo)志數(shù)組保存了每個(gè)坐標(biāo)的編碼信息以及其他一些信息。下面是標(biāo)志中各個(gè)位的含義(c語言定義):
typedef enum {G_ONCURVE=0x01, // on curve ,off curveG_REPEAT=0x08, // next byte is flag repeat count G_XMASK=0x12, G_XADDBYTE=0x12, //X is positive byteG_XSUBBYTE=0x12, //X is negative byte G_XSAME=0x10, //X is sameG_XADDINT=0x00, //X is signed word G_YMASK=0x24,G_YADDBYTE=0x24, //Y is positive byte G_YSUBBYTE=0x04, //Y is negative byteG_YSAME=0x20, //Y is sameG_YADDINT=0x00, //Y is signed word };在輪廓技術(shù)的數(shù)學(xué)模式中,一段三階的Bezier曲線由四個(gè)控制點(diǎn)定義:位于曲線上的起始點(diǎn)、兩個(gè)不在曲線上(off-curve)的控制點(diǎn)和一個(gè)曲線上的結(jié)束點(diǎn)。
字體中的圖元輪廓用二階Bezier曲線定義,有三個(gè)點(diǎn):一個(gè)曲線上的點(diǎn),一個(gè)曲線外的點(diǎn)和另一個(gè)曲線上的點(diǎn)。對(duì)于多個(gè)連續(xù)不在曲線上的點(diǎn),會(huì)隱式加入一些點(diǎn)使其符合二階Bezier曲線曲線的定義。例如,on-off-off-on模式的四個(gè)點(diǎn),會(huì)隱式加入一個(gè)點(diǎn)使之成為on-off-on-off-on的五個(gè)點(diǎn)。
G_ONCURVE位表示控制點(diǎn)是否在曲線上,設(shè)置G_REPEAT位表示標(biāo)志數(shù)組的下一字節(jié)表示重復(fù)次數(shù),當(dāng)前標(biāo)志被重復(fù)指定的次數(shù)。解碼圖元的描述需要兩次掃描起始點(diǎn),然后再遍歷圖元定義中的每一個(gè)點(diǎn)進(jìn)行轉(zhuǎn)換。
圖元指令具體細(xì)節(jié)比較復(fù)雜,主要是為了控制圖元輪廓從em-square到柵格網(wǎng)格的縮放過程,通過網(wǎng)格調(diào)整技術(shù)使縮放后的渲染不失真,而記錄控制值的一張表。
整體來說渲染圖元是一個(gè)非常復(fù)雜的算法,咱們不再繼續(xù)深究。
下面看看fontTools庫能夠讀取到的圖元數(shù)據(jù),首先讀取glyf表:
glyf = font["glyf"]我們以字符0為例進(jìn)行演示,查看到該字體中數(shù)字0對(duì)應(yīng)的代碼點(diǎn)為unif82e。
首先查看圖元頭信息:
glyph = glyf['unif82e'] print(f"輪廓線數(shù)目:{glyph.numberOfContours},邊界范圍:({glyph.xMin},{glyph.yMin})-({glyph.xMax},{glyph.yMax})") 輪廓線數(shù)目:2,邊界范圍:(0,-14)-(550,729)前面已經(jīng)提到,每個(gè)點(diǎn)記錄的是和上一個(gè)點(diǎn)的坐標(biāo)差值,所以邊界范圍存在負(fù)數(shù)很好理解。
獲取每條輪廓線終點(diǎn)的索引:
glyph.endPtsOfContours [12, 25]可以計(jì)算出兩條輪廓線點(diǎn)的數(shù)量:
num1 = glyph.endPtsOfContours[0]+1 num2 = glyph.endPtsOfContours[1]-glyph.endPtsOfContours[0] print(f"第一條輪廓線上點(diǎn)的數(shù)量為{num1},第二條輪廓線上點(diǎn)的數(shù)量為{num2}") 第一條輪廓線上點(diǎn)的數(shù)量為13,第二條輪廓線上點(diǎn)的數(shù)量為13對(duì)于控制點(diǎn)數(shù)據(jù)中的標(biāo)志,python的fontTools庫似乎只能讀取G_ONCURVE標(biāo)志位,即是否存在于曲線上。
首先查看控制點(diǎn)的坐標(biāo)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計(jì)算出偏移后的實(shí)際坐標(biāo):
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]]控制點(diǎn)是否存在于曲線上:
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]]對(duì)于連續(xù)不在曲線上的點(diǎn)都會(huì)自動(dòng)添加隱式的點(diǎn)。
如何將這些控制點(diǎn)數(shù)據(jù)用最簡(jiǎn)化的2進(jìn)制的形式描述呢?
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表中的圖元數(shù)據(jù)時(shí)就需要讀取loca表的圖元索引的偏移量。
位置索引表中保存了n+1個(gè)圖元數(shù)據(jù)表的索引,其中的n是保存在最大需求表中的圖元數(shù)量。最后一個(gè)額外的偏移量指向最后一個(gè)圖元的偏移量和當(dāng)前圖元的偏移量間的差值得到的圖元長(zhǎng)度。
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)對(duì)內(nèi)存的需求,以便 在出來字體前分配合適大小的內(nèi)存。下面是maxp表的結(jié)構(gòu)在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字段保存了字體中圖元的總數(shù),這決定了到位置表的圖元索引的數(shù)量,可以驗(yàn)證圖元索引的有效性。maxPoints\maxCountors\maxCompositePoints maxCompositeContours這幾個(gè)字段說明了圖元定義的復(fù)雜度。
python中的讀取一下:
maxp = font["maxp"] maxp.numGlyphs, maxp.maxPoints, maxp.maxContours, maxp.maxCompositePoints, maxp.maxCompositeContours (603, 134, 11, 0, 0)命名表(name)
包含版權(quán)說明、字體名、字體族名、風(fēng)格名等,直接通過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 ' ......截取了部分結(jié)果,可以看到該自定義字體通過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)二進(jìn)制匹配解析輪廓圖順序隨機(jī)的woff字體
有了前面的基礎(chǔ),現(xiàn)在對(duì)于亂序了輪廓圖順序的woff字體,已經(jīng)變得非常簡(jiǎn)單。
我們使用上次下載的address.woff文件作為已知訓(xùn)練集,然后將shopNum.woff字體文件的輪廓圖,進(jìn)行一定的亂序處理,看看能否正確的提取出需要的文字。
首先使用FontCreator.exe打開shopNum.woff字體文件,然后修改輪廓圖順序。
最終在我一頓操作后,形成下面的順序:
再將字體導(dǎo)出為random.woff。
那么我們能否通過address.woff文件和已知字符列表作為訓(xùn)練集,正確匹配出random.woff文件每個(gè)Unicode代碼點(diǎn)對(duì)應(yīng)的字符呢?
首先讀取address.woff文件的每個(gè)圖元數(shù)據(jù)轉(zhuǎn)成二進(jìn)制后和之前已經(jīng)識(shí)別出來的字符列表建立映射關(guān)系:
from fontTools.ttLib import TTFont import numpy as npdef get_glyphBytes(glyph):coordinates = np.array(glyph.coordinates).astype("int16")return coordinates.tobytes()+glyph.flagsfont = TTFont("address.woff") glyf = font["glyf"]chars = ' `1234567890店中美家館小車大市公酒行國(guó)品發(fā)電金心業(yè)商司超生裝園場(chǎng)食有新限天面工服海華水房飾城樂汽香部利子老藝花專東肉菜學(xué)福飯人百餐茶務(wù)通味所山區(qū)門藥銀農(nóng)龍停尚安廣鑫一容動(dòng)南具源興鮮記時(shí)機(jī)烤文康信果陽理鍋寶達(dá)地兒衣特產(chǎn)西批坊州牛佳化五米修愛北養(yǎng)賣建材三會(huì)雞室紅站德王光名麗油院堂燒江社合星貨型村自科快便日民營(yíng)和活童明器煙育賓精屋經(jīng)居莊石順林爾縣手廳銷用好客火雅盛體旅之鞋辣作粉包樓校魚平彩上吧保永萬物教吃設(shè)醫(yī)正造豐健點(diǎn)湯網(wǎng)慶技斯洗料配匯木緣加麻聯(lián)衛(wèi)川泰色世方寓風(fēng)幼羊燙來高廠蘭阿貝皮全女拉成云維貿(mào)道術(shù)運(yùn)都口博河瑞宏京際路祥青鎮(zhèn)廚培力惠連馬鴻鋼訓(xùn)影甲助窗布富牌頭四多妝吉苑沙恒隆春干餅氏里二管誠(chéng)制售嘉長(zhǎng)軒雜副清計(jì)黃訊太鴨號(hào)街交與叉附近層旁對(duì)巷棟環(huán)省橋湖段鄉(xiāng)廈府鋪內(nèi)側(cè)元購前幢濱處向座下澩鳳港開關(guān)景泉塘放昌線灣政步寧解白田町溪十八古雙勝本單同九迎第臺(tái)玉錦底后七斜期武嶺松角紀(jì)朝峰六振珠局崗洲橫邊濟(jì)井辦漢代臨弄團(tuán)外塔楊鐵浦字年島陵原梅進(jìn)榮友虹央桂沿事津凱蓮丁秀柳集紫旗張谷的是不了很還個(gè)也這我就在以可到錯(cuò)沒去過感次要比覺看得說常真?zhèn)兊钕补磩e位能較境非為歡然他挺著價(jià)那意種想出員兩推做排實(shí)分間甜度起滿給熱完格薦喝等其再幾只現(xiàn)朋候樣直而買于般豆量選奶打每評(píng)少算又因情找些份置適什蛋師氣你姐棒試總定啊足級(jí)整帶蝦如態(tài)且嘗主話強(qiáng)當(dāng)更板知己無酸讓入啦式笑贊片醬差像提隊(duì)走嫩才剛午接重串回晚微周值費(fèi)性桌拍跟塊調(diào)糕' glyphBytes2char = {} for code, char in zip(glyf.glyphOrder, chars):glyph = glyf[code]if not hasattr(glyph, 'coordinates'):continueglyphBytes2char[get_glyphBytes(glyph)] = char有了映射關(guān)系,我們?cè)匍_始嘗試匹配random.woff文件每個(gè)Unicode代碼點(diǎn)對(duì)應(yīng)的字符:
font = TTFont("random.woff") glyf = font["glyf"]code2char = {} for code in glyf.glyphOrder:glyph = glyf[code]if not hasattr(glyph, 'coordinates'):continueglyphBytes = get_glyphBytes(glyph)if glyphBytes not in glyphBytes2char:print("不在資料庫的代碼點(diǎn):", code)continuecode2char[code] = glyphBytes2char[glyphBytes] code2char結(jié)果:
可以看到每一個(gè)代碼點(diǎn)都一一精準(zhǔn)的匹配出正確的結(jié)果。
可以將上述過程封裝成類,方便以后隨時(shí)調(diào)用使用:
from fontTools.ttLib import TTFont import numpy as npclass FontMatch:"""用于字體圖元數(shù)據(jù)匹配的類"""@staticmethoddef get_glyphBytes(glyph):coordinates = np.array(glyph.coordinates).astype("int16")return coordinates.tobytes() + glyph.flagsdef __init__(self, sample_font="sample.woff", chars=None, dest_font=None):"""傳入已知輪廓圖順序的字體文件和真實(shí)字符作為訓(xùn)練集,去匹配目標(biāo)字體,后面能夠得到該目標(biāo)字體映射字符對(duì)應(yīng)的真實(shí)字符:param sample_font: 已知輪廓圖順序的字體文件:param chars: 該字體文件每個(gè)輪廓圖對(duì)應(yīng)的真實(shí)字符:param dest_font: 要進(jìn)行匹配的目標(biāo)字體,可以后面再調(diào)用 load_dest_font 傳入"""sample_font = TTFont(sample_font)glyf = sample_font["glyf"]if chars is None:chars = ' `1234567890店中美家館小車大市公酒行國(guó)品發(fā)電金心業(yè)商司超生裝園場(chǎng)食有新限天面工服海華水房飾城樂汽香部利子老藝花專東肉菜學(xué)福飯人百餐茶務(wù)通味所山區(qū)門藥銀農(nóng)龍停尚安廣鑫一容動(dòng)南具源興鮮記時(shí)機(jī)烤文康信果陽理鍋寶達(dá)地兒衣特產(chǎn)西批坊州牛佳化五米修愛北養(yǎng)賣建材三會(huì)雞室紅站德王光名麗油院堂燒江社合星貨型村自科快便日民營(yíng)和活童明器煙育賓精屋經(jīng)居莊石順林爾縣手廳銷用好客火雅盛體旅之鞋辣作粉包樓校魚平彩上吧保永萬物教吃設(shè)醫(yī)正造豐健點(diǎn)湯網(wǎng)慶技斯洗料配匯木緣加麻聯(lián)衛(wèi)川泰色世方寓風(fēng)幼羊燙來高廠蘭阿貝皮全女拉成云維貿(mào)道術(shù)運(yùn)都口博河瑞宏京際路祥青鎮(zhèn)廚培力惠連馬鴻鋼訓(xùn)影甲助窗布富牌頭四多妝吉苑沙恒隆春干餅氏里二管誠(chéng)制售嘉長(zhǎng)軒雜副清計(jì)黃訊太鴨號(hào)街交與叉附近層旁對(duì)巷棟環(huán)省橋湖段鄉(xiāng)廈府鋪內(nèi)側(cè)元購前幢濱處向座下澩鳳港開關(guān)景泉塘放昌線灣政步寧解白田町溪十八古雙勝本單同九迎第臺(tái)玉錦底后七斜期武嶺松角紀(jì)朝峰六振珠局崗洲橫邊濟(jì)井辦漢代臨弄團(tuán)外塔楊鐵浦字年島陵原梅進(jìn)榮友虹央桂沿事津凱蓮丁秀柳集紫旗張谷的是不了很還個(gè)也這我就在以可到錯(cuò)沒去過感次要比覺看得說常真?zhèn)兊钕补磩e位能較境非為歡然他挺著價(jià)那意種想出員兩推做排實(shí)分間甜度起滿給熱完格薦喝等其再幾只現(xiàn)朋候樣直而買于般豆量選奶打每評(píng)少算又因情找些份置適什蛋師氣你姐棒試總定啊足級(jí)整帶蝦如態(tài)且嘗主話強(qiáng)當(dāng)更板知己無酸讓入啦式笑贊片醬差像提隊(duì)走嫩才剛午接重串回晚微周值費(fèi)性桌拍跟塊調(diào)糕'glyphBytes2char = {}for code, char in zip(glyf.glyphOrder, chars):glyph = glyf[code]if not hasattr(glyph, 'coordinates'):continueglyphBytes2char[FontMatch.get_glyphBytes(glyph)] = charself.glyphBytes2char = glyphBytes2charsample_font.close()if dest_font is not None:self.load_dest_font(dest_font)def load_dest_font(self, dest_font):"""傳入要進(jìn)行匹配的目標(biāo)字體,之前已經(jīng)傳入的目標(biāo)字體會(huì)被覆蓋"""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:returnname = self.code2name[code]glyphBytes = FontMatch.get_glyphBytes(self.glyf[name])return self.glyphBytes2char.get(glyphBytes)調(diào)用方式:下面的代碼將前面已經(jīng)下載的任意一個(gè)字體文件重命名為sample.woff作為訓(xùn)練集,random.woff是要處理的目標(biāo)字體。對(duì)于任何給點(diǎn)的映射字符都可以匹配出正確結(jié)果:
from FontMatch import FontMatchfont = FontMatch(sample_font="sample.woff", dest_font="random.woff") print(font.getRealChar("\uEE9B")) '4'對(duì)前面我們自行亂序后的自定義字體前面幾個(gè)字符批量匹配測(cè)試一下:
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("真實(shí)結(jié)果與匹配結(jié)果:", real, "|", r) 真實(shí)結(jié)果與匹配結(jié)果: 1 | 1 真實(shí)結(jié)果與匹配結(jié)果: 2 | 2 真實(shí)結(jié)果與匹配結(jié)果: 4 | 4 真實(shí)結(jié)果與匹配結(jié)果: 3 | 3 真實(shí)結(jié)果與匹配結(jié)果: 店 | 店 真實(shí)結(jié)果與匹配結(jié)果: 中 | 中 真實(shí)結(jié)果與匹配結(jié)果: 7 | 7 真實(shí)結(jié)果與匹配結(jié)果: 8 | 8 真實(shí)結(jié)果與匹配結(jié)果: 9 | 9 真實(shí)結(jié)果與匹配結(jié)果: 小 | 小 真實(shí)結(jié)果與匹配結(jié)果: 車 | 車 真實(shí)結(jié)果與匹配結(jié)果: 6 | 6 真實(shí)結(jié)果與匹配結(jié)果: 美 | 美 真實(shí)結(jié)果與匹配結(jié)果: 家 | 家 真實(shí)結(jié)果與匹配結(jié)果: 館 | 館 真實(shí)結(jié)果與匹配結(jié)果: 5 | 5一樣也是完全正確。
圖像識(shí)別解析字形隨機(jī)的woff字體
上述代碼解決了輪廓圖順序隨機(jī)的問題,但是假如字形也發(fā)生隨機(jī)怎么破呢?例如用10套基礎(chǔ)字體隨機(jī)生成自定義字體。那么之前的獲取到的圖元數(shù)據(jù)就無法直接匹配。
此時(shí)我們需要使用機(jī)器學(xué)習(xí)或深度學(xué)習(xí)相關(guān)的算法,或者能夠完成圖元數(shù)據(jù)渲染字體圖形的大佬可以直接使用邏輯算法完成。
自己嘗試了一些分類模型發(fā)現(xiàn)效果并不比圖像識(shí)別算法好,所以最終我們依然還是決定使用一開始采用的圖像識(shí)別來解決這個(gè)問題,優(yōu)點(diǎn)是通用性強(qiáng),但缺點(diǎn)是準(zhǔn)確率再也無法達(dá)到100%。
前面下載的字體文件定義最常用的601個(gè)字符,這里我們也只對(duì)這601個(gè)字符進(jìn)行測(cè)試。
首先,創(chuàng)建文字識(shí)別類:
from ddddocr import DdddOcr, npclass 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.5ort_inputs = {'input1': np.array([image])}ort_outs = self._DdddOcr__ort_session.run(None, ort_inputs)result = []last_item = 0for item in ort_outs[0][0]:if item == 0 or item == last_item:continueresult.append(self._DdddOcr__charset[item])last_item = itemreturn ''.join(result)ocr = OCR()定義需要被測(cè)試的正確字符:
chars = '1234567890店中美家館小車大市公酒行國(guó)品發(fā)電金心業(yè)商司超生裝園場(chǎng)食有新限天面工服海華水房飾城樂汽香部利子老藝花專東肉菜學(xué)福飯人百餐茶務(wù)通味所山區(qū)門藥銀農(nóng)龍停尚安廣鑫一容動(dòng)南具源興鮮記時(shí)機(jī)烤文康信果陽理鍋寶達(dá)地兒衣特產(chǎn)西批坊州牛佳化五米修愛北養(yǎng)賣建材三會(huì)雞室紅站德王光名麗油院堂燒江社合星貨型村自科快便日民營(yíng)和活童明器煙育賓精屋經(jīng)居莊石順林爾縣手廳銷用好客火雅盛體旅之鞋辣作粉包樓校魚平彩上吧保永萬物教吃設(shè)醫(yī)正造豐健點(diǎn)湯網(wǎng)慶技斯洗料配匯木緣加麻聯(lián)衛(wèi)川泰色世方寓風(fēng)幼羊燙來高廠蘭阿貝皮全女拉成云維貿(mào)道術(shù)運(yùn)都口博河瑞宏京際路祥青鎮(zhèn)廚培力惠連馬鴻鋼訓(xùn)影甲助窗布富牌頭四多妝吉苑沙恒隆春干餅氏里二管誠(chéng)制售嘉長(zhǎng)軒雜副清計(jì)黃訊太鴨號(hào)街交與叉附近層旁對(duì)巷棟環(huán)省橋湖段鄉(xiāng)廈府鋪內(nèi)側(cè)元購前幢濱處向座下澩鳳港開關(guān)景泉塘放昌線灣政步寧解白田町溪十八古雙勝本單同九迎第臺(tái)玉錦底后七斜期武嶺松角紀(jì)朝峰六振珠局崗洲橫邊濟(jì)井辦漢代臨弄團(tuán)外塔楊鐵浦字年島陵原梅進(jìn)榮友虹央桂沿事津凱蓮丁秀柳集紫旗張谷的是不了很還個(gè)也這我就在以可到錯(cuò)沒去過感次要比覺看得說常真?zhèn)兊钕补磩e位能較境非為歡然他挺著價(jià)那意種想出員兩推做排實(shí)分間甜度起滿給熱完格薦喝等其再幾只現(xiàn)朋候樣直而買于般豆量選奶打每評(píng)少算又因情找些份置適什蛋師氣你姐棒試總定啊足級(jí)整帶蝦如態(tài)且嘗主話強(qiáng)當(dāng)更板知己無酸讓入啦式笑贊片醬差像提隊(duì)走嫩才剛午接重串回晚微周值費(fèi)性桌拍跟塊調(diào)糕'先對(duì)系統(tǒng)自帶的微軟雅黑字體進(jìn)行測(cè)試:
from PIL import ImageFont, Image, ImageDrawsize = 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)/2draw.text(xy=(fontx, fonty), text=char, fill=0, font=font)result = ocr.ocr(im)[0]if result != char:print("正確結(jié)果:", char, ",識(shí)別結(jié)果:", result)error += 1 print("識(shí)別錯(cuò)誤的字符數(shù)量:", error) 正確結(jié)果: 二 ,識(shí)別結(jié)果: 一 正確結(jié)果: 澩 ,識(shí)別結(jié)果: 檗 正確結(jié)果: 昌 ,識(shí)別結(jié)果: 目 正確結(jié)果: 町 ,識(shí)別結(jié)果: 盯 正確結(jié)果: 丁 ,識(shí)別結(jié)果: j 正確結(jié)果: 入 ,識(shí)別結(jié)果: 人 識(shí)別錯(cuò)誤的字符數(shù)量: 6可以看到對(duì)該字體601字符的識(shí)別只存在6個(gè)錯(cuò)誤,其他都正確。
再對(duì)之前下載的自定義字體進(jìn)行測(cè)試:
from fontTools.ttLib import TTFontfont = 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:continuechar = 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)/2draw.text(xy=(fontx, fonty), text=char, fill=0, font=imageFont)result = ocr.ocr(im)[0]if result != real_char:print("正確結(jié)果:", real_char, "識(shí)別結(jié)果:", result)error += 1 print("識(shí)別錯(cuò)誤的字符數(shù)量:", error) 正確結(jié)果: 町 識(shí)別結(jié)果: 盯 正確結(jié)果: 二 識(shí)別結(jié)果: 一 正確結(jié)果: 澩 識(shí)別結(jié)果: 嗅 識(shí)別錯(cuò)誤的字符數(shù)量: 3可以看到對(duì)該字體601字符的識(shí)別只存在3個(gè)錯(cuò)誤,其他都正確。
那么對(duì)于任何一個(gè)未知的自定義字體,如何通過圖像識(shí)別技術(shù)知道真實(shí)字符是什么呢?
我們改造一下前面的ocr類,封裝一下:
from ddddocr import DdddOcr, np from PIL import ImageFont, Image, ImageDrawclass 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.5ort_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:continuereturn 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)/2draw.text(xy=(fontx, fonty), text=char, fill=0, font=self.font)return self.im_cache.setdefault(char, im)調(diào)用方式:
fontocr = FontOCR("shopNum.woff") fontocr.getRealChar("\uF7F2") '讓'于是可以通過以下代碼可以對(duì)自定義字體的全部unicode代碼點(diǎn)識(shí)別一遍:
from fontTools.ttLib import TTFontfont_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"):continuechar = 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 家 家 ...可以看到這些數(shù)據(jù)都被正確的解析出來,至此我們就完成了對(duì)任意自定義字體的智能解析。
總結(jié)
今天,我首先演示了如何生成自定義字體,并對(duì)字體的格式結(jié)構(gòu)進(jìn)行了較為詳細(xì)的講解,順便演示如何通過python的fontools庫獲取相應(yīng)的字體數(shù)據(jù)。
在上一篇文章中,我們通過二級(jí)緩存解決了cssURL和fontURL隨機(jī)以及Unicode代碼點(diǎn)順序點(diǎn)隨機(jī)的問題,本文進(jìn)一步考慮針對(duì)自定義字體文件內(nèi)部,輪廓圖甚至基礎(chǔ)字形也隨機(jī)怎么處理。
本文針對(duì)輪廓圖順序隨機(jī),開發(fā)了FontMatch,傳入已知字體的輪廓圖順序,能處理任何針對(duì)該字體進(jìn)行輪廓圖順序隨機(jī)的匹配,準(zhǔn)確率能達(dá)到100%。
但針對(duì)字形也可能隨機(jī)的情況,中間個(gè)人進(jìn)行了很多基礎(chǔ)研究,寫了很多算法,但最終都還不如圖像識(shí)別的效果更好。所以最終我封裝了一個(gè)基于圖像識(shí)別的OCR處理類,能夠針對(duì)任何自定義字體傳入輸入字符識(shí)別出相應(yīng)的結(jié)果字符。目前測(cè)試的600個(gè)高頻字符,準(zhǔn)確率達(dá)到98%以上,針對(duì)未來的不確定性,犧牲這一點(diǎn)準(zhǔn)確率個(gè)人感覺也很值。
總結(jié)
以上是生活随笔為你收集整理的woff字体图元结构剖析,自定义字体的制作与匹配和识别的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CCF CSP 小明放学
- 下一篇: 【币值最大化问题】