字的研究(3)fontTools-TrueType輪廓座標的獲取以及基於TrueType的Glyph例項的構建

多事鬼間人 發表於 2022-01-28

前言

本文主要介紹如果使用Python第三方庫fontTools提取OpenType字型檔案中的TrueType輪廓座標以及如何構建基於TrueType的Glyph例項

TrueType輪廓座標的獲取

對於TrueType輪廓描述的OpenType檔案,除了前文提到的利用ttx元件將表結構轉化為XML檔案方法,利用如下程式碼也可以直接獲取具體的輪廓資料:

from fontTools.ttLib import TTFont

font = TTFont("Resources/simsun.ttf")
glyph = font.getGlyphSet()["uni70E0"] # 獲取_TTGlyph例項
print(glyph._glyph.coordinates) # 座標
print(glyph._glyph.endPtsOfContours) # 輪廓結束點
print(list(glyph._glyph.flags)) # 點型別flag

執行結果如下:

GlyphCoordinates([(138, 118),(138, 86),(206, 86),(206, 118),(138, 80),(138, 49),(206, 49),(206, 80),(138, 43),(138, -19),(123, -26),(124, -5),(124, 16),(124, 99),(110, 81),(86, 67),(84, 70),(118, 100),(142, 158),(125, 158),(112, 158),(101, 155),(92, 164),(144, 164),(154, 192),(156, 209),(176, 197),(169, 192),(161, 170),(159, 164),(207, 164),(221, 177),(238, 158),(157, 158),(151, 142),(140, 124),(205, 124),(214, 134),(229, 119),(220, 114),(220, 1),(220, -17),(199, -25),(197, -9),(168, -4),(168, 0),(195, -2),(206, 0),(206, 8),(206, 43),(52, 206),(74, 194),(67, 187),(67, 123),(87, 148),(91, 161),(105, 147),(99, 146),(90, 137),(81, 128),(67, 115),(67, 91),(64, 57),(87, 46),(103, 29),(103, 22),(103, 18),(99, 7),(92, 9),(87, 22),(82, 34),(63, 52),(56, 8),(12, -26),(11, -23),(41, 13),(53, 74),(53, 149),(33, 140),(34, 126),(33, 104),(25, 92),(13, 88),(10, 95),(10, 97),(10, 102),(14, 105),(19, 109),(28, 128),(29, 140)])
[3, 7, 49, 77, 89]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1]

描述TrueType輪廓的資料主要由座標、輪廓結束點以及各點的型別flag組成。其中,輪廓結束點為各輪廓的最後一個點的座標;點型別flag則和座標一一對應,說明該點是普通點還是貝塞爾曲線的控制點,0為控制點,1為普通點(注意,TrueType輪廓只包含二次貝塞爾曲線)。

相比之下,我個人更傾向將資料轉化為如下由三元組組成的二維陣列的形式,更方便理解和處理:

coordinates = list(glyph._glyph.coordinates)
endPts = glyph._glyph.endPtsOfContours
flags = list(glyph._glyph.flags)

contours = []
contour = []
for i, (x,y) in enumerate(coordinates):
    contour.append((x,y,flags[i]))
    if i in endPts:
        contours.append(contour)
        contour = []

print(contours)

執行結果如下:

[[(138, 118, 1), (138, 86, 1), (206, 86, 1), (206, 118, 1)], 
[(138, 80, 1), (138, 49, 1), (206, 49, 1), (206, 80, 1)], 
[(138, 43, 1), (138, -19, 1), (123, -26, 1), (124, -5, 0), (124, 16, 1), (124, 99, 1), (110, 81, 0), (86, 67, 1), (84, 70, 1), (118, 100, 0), (142, 158, 1), (125, 158, 1), (112, 158, 0), (101, 155, 1), (92, 164, 1), (144, 164, 1), (154, 192, 0), (156, 209, 1), (176, 197, 1), (169, 192, 1), (161, 170, 0), (159, 164, 1), (207, 164, 1), (221, 177, 1), (238, 158, 1), (157, 158, 1), (151, 142, 0), (140, 124, 1), (205, 124, 1), (214, 134, 1), (229, 119, 1), (220, 114, 1), (220, 1, 1), (220, -17, 0), (199, -25, 1), (197, -9, 0), (168, -4, 1), (168, 0, 1), (195, -2, 0), (206, 0, 0), (206, 8, 1), (206, 43, 1)], 
[(52, 206, 1), (74, 194, 1), (67, 187, 1), (67, 123, 1), (87, 148, 0), (91, 161, 1), (105, 147, 1), (99, 146, 0), (90, 137, 1), (81, 128, 0), (67, 115, 1), (67, 91, 0), (64, 57, 1), (87, 46, 0), (103, 29, 0), (103, 22, 1), (103, 18, 0), (99, 7, 0), (92, 9, 0), (87, 22, 1), (82, 34, 0), (63, 52, 1), (56, 8, 0), (12, -26, 1), (11, -23, 1), (41, 13, 0), (53, 74, 0), (53, 149, 0)],
[(33, 140, 1), (34, 126, 0), (33, 104, 0), (25, 92, 0), (13, 88, 0), (10, 95, 0), (10, 97, 1), (10, 102, 0), (14, 105, 1), (19, 109, 0), (28, 128, 0), (29, 140, 1)]]

基於TrueType的Glyph例項的構建

構建fontTools中的Glyph例項主要可以用於後續建立新的基於TrueType輪廓的字型檔案。所採用的方法是基於前文所提到的Pen物件的子類TTGlyphPointPen,輸入座標、輪廓結束點以及各點的型別flag三項資料,輸出Glyph例項:

from fontTools.pens.ttGlyphPen import TTGlyphPointPen

coordinates = [(138, 118),(138, 86),(206, 86),(206, 118),(138, 80),(138, 49),(206, 49),(206, 80),(138, 43),(138, -19),(123, -26),(124, -5),(124, 16),(124, 99),(110, 81),(86, 67),(84, 70),(118, 100),(142, 158),(125, 158),(112, 158),(101, 155),(92, 164),(144, 164),(154, 192),(156, 209),(176, 197),(169, 192),(161, 170),(159, 164),(207, 164),(221, 177),(238, 158),(157, 158),(151, 142),(140, 124),(205, 124),(214, 134),(229, 119),(220, 114),(220, 1),(220, -17),(199, -25),(197, -9),(168, -4),(168, 0),(195, -2),(206, 0),(206, 8),(206, 43),(52, 206),(74, 194),(67, 187),(67, 123),(87, 148),(91, 161),(105, 147),(99, 146),(90, 137),(81, 128),(67, 115),(67, 91),(64, 57),(87, 46),(103, 29),(103, 22),(103, 18),(99, 7),(92, 9),(87, 22),(82, 34),(63, 52),(56, 8),(12, -26),(11, -23),(41, 13),(53, 74),(53, 149),(33, 140),(34, 126),(33, 104),(25, 92),(13, 88),(10, 95),(10, 97),(10, 102),(14, 105),(19, 109),(28, 128),(29, 140)]
endPts = [3, 7, 49, 77, 89]
flags = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1]

pen = TTGlyphPointPen(None)
beginflag = 0
for i, pt in enumerate(coordinates):
    if pen._isClosed():
        pen.beginPath()
    if flags[i] == 1:
        pen.addPoint(pt,segmentType="line")
    else:
        pen.addPoint(pt)
    if i in endPts:
        pen.endPath()
glyph = pen.glyph()

返回的glyph即為Glyph例項,可直接用於構建基於TrueType輪廓的字型檔案。注意,Glyph物件只包含字形輪廓資料,屬於glyf表,對應前一節提到的_TTGlyph中的_glyph屬性,_TTGlyph中的字寬和上下沿等資料則來自在字型檔案的其他表格。