|
1
|
|
|
import ctypes as ct |
|
2
|
|
|
|
|
3
|
|
|
import freetype.raw as ft |
|
4
|
|
|
|
|
5
|
|
|
from ed2d import texture |
|
6
|
|
|
from ed2d import mesh |
|
7
|
|
|
from ed2d import typeutils |
|
8
|
|
|
from ed2d.opengl import gl, pgl |
|
9
|
|
|
# from ed2d import glmath as cyglmath |
|
10
|
|
|
# from ed2d.glmath import cython as cyglmath |
|
11
|
|
|
from gem import matrix, vector |
|
12
|
|
|
|
|
13
|
|
|
|
|
14
|
|
|
# Hack to verify that freetype is properly destructed after everything |
|
15
|
|
|
# this code was also commited to freetype-py |
|
16
|
|
|
class _FT_Library_Wrapper(ft.FT_Library): |
|
17
|
|
|
'''Subclass of FT_Library to help with calling FT_Done_FreeType''' |
|
18
|
|
|
# for some reason this doesn't get carried over and ctypes complains |
|
19
|
|
|
_type_ = ft.FT_Library._type_ |
|
20
|
|
|
|
|
21
|
|
|
# Store ref to FT_Done_FreeType otherwise it will be deleted before needed. |
|
22
|
|
|
_ft_done_freetype = ft.FT_Done_FreeType |
|
23
|
|
|
|
|
24
|
|
|
def __del__(self): |
|
25
|
|
|
# call FT_Done_FreeType |
|
26
|
|
|
self._ft_done_freetype(self) |
|
27
|
|
|
|
|
28
|
|
|
|
|
29
|
|
|
def init_freetype(): |
|
30
|
|
|
handle = _FT_Library_Wrapper() |
|
31
|
|
|
|
|
32
|
|
|
if ft.FT_Init_FreeType(ct.byref(handle)): |
|
33
|
|
|
raise Exception('FreeType failed to initialize.') |
|
34
|
|
|
|
|
35
|
|
|
return handle |
|
36
|
|
|
|
|
37
|
|
|
|
|
38
|
|
|
freetype = init_freetype() |
|
39
|
|
|
|
|
40
|
|
|
# These are the usable fields of FT_GlyphSlotRec |
|
41
|
|
|
# field: data type: |
|
42
|
|
|
# library FT_Library |
|
43
|
|
|
# face FT_Face |
|
44
|
|
|
# next FT_GlyphSlot |
|
45
|
|
|
# generic FT_Generic |
|
46
|
|
|
# metrics FT_Glyph_Metrics |
|
47
|
|
|
# linearHoriAdvance FT_Fixed |
|
48
|
|
|
# linearVertAdvance FT_Fixed |
|
49
|
|
|
# advance FT_Vector |
|
50
|
|
|
# format FT_Glyph_Format |
|
51
|
|
|
# bitmap FT_Bitmap |
|
52
|
|
|
# bitmap_left FT_Int |
|
53
|
|
|
# bitmap_top FT_Int |
|
54
|
|
|
# outline FT_Outline |
|
55
|
|
|
# num_subglyphs FT_UInt |
|
56
|
|
|
# subglyphs FT_SubGlyph |
|
57
|
|
|
# control_data void* |
|
58
|
|
|
# control_len long |
|
59
|
|
|
# lsb_delta FT_Pos |
|
60
|
|
|
# rsb_delta FT_Pos |
|
61
|
|
|
|
|
62
|
|
|
|
|
63
|
|
|
class Font(object): |
|
64
|
|
|
def __init__(self, size, fontPath): |
|
65
|
|
|
|
|
66
|
|
|
self.size = size |
|
67
|
|
|
self.path = fontPath |
|
68
|
|
|
|
|
69
|
|
|
self.face = ft.FT_Face() |
|
70
|
|
|
|
|
71
|
|
|
# here is the general structure of the char data dict. |
|
72
|
|
|
# |
|
73
|
|
|
# It has |
|
74
|
|
|
self.charDataCache = {} |
|
75
|
|
|
|
|
76
|
|
|
# load font face |
|
77
|
|
|
if ft.FT_New_Face(freetype, typeutils.to_c_str(fontPath), 0, |
|
78
|
|
|
ct.byref(self.face)): |
|
79
|
|
|
raise Exception('Error loading font.') |
|
80
|
|
|
|
|
81
|
|
|
# For now the device dpi will be hard coded to 72 |
|
82
|
|
|
# later on if we want to do mobile stuff, or have dpi scaling |
|
83
|
|
|
# for high-dpi monitors this will need to be changed. |
|
84
|
|
|
if ft.FT_Set_Char_Size(self.face, 0, size * 64, 72, 72): |
|
85
|
|
|
raise Exception('Error setting character size.') |
|
86
|
|
|
|
|
87
|
|
|
def load_glyph(self, char): |
|
88
|
|
|
''' |
|
89
|
|
|
Loads glyph, and returns a dictionary containing glyph data. |
|
90
|
|
|
''' |
|
91
|
|
|
try: |
|
92
|
|
|
return self.charDataCache[char] |
|
93
|
|
|
|
|
94
|
|
|
except KeyError: |
|
95
|
|
|
index = ft.FT_Get_Char_Index(self.face, ord(char)) |
|
96
|
|
|
|
|
97
|
|
|
if ft.FT_Load_Glyph(self.face, index, ft.FT_LOAD_RENDER): |
|
98
|
|
|
raise Exception('Error loading glyph') |
|
99
|
|
|
|
|
100
|
|
|
glyphSlot = self.face.contents.glyph |
|
101
|
|
|
|
|
102
|
|
|
charData = {} |
|
103
|
|
|
bitmapStruct = glyphSlot.contents.bitmap |
|
104
|
|
|
texWidth = bitmapStruct.width |
|
105
|
|
|
texHeight = bitmapStruct.rows |
|
106
|
|
|
|
|
107
|
|
|
pixelData = [0.0 for x in range(texWidth * texHeight)] |
|
108
|
|
|
|
|
109
|
|
|
for item in range(texWidth * texHeight): |
|
110
|
|
|
pixelData[item] = bitmapStruct.buffer[item] |
|
111
|
|
|
|
|
112
|
|
|
if not pixelData: |
|
113
|
|
|
pixelData = [0] |
|
114
|
|
|
|
|
115
|
|
|
charData['pixelData'] = pixelData |
|
116
|
|
|
charData['bitmap_x'] = glyphSlot.contents.bitmap_left |
|
117
|
|
|
charData['bitmap_y'] = glyphSlot.contents.bitmap_top |
|
118
|
|
|
charData['texWidth'] = texWidth |
|
119
|
|
|
charData['texHeight'] = texHeight |
|
120
|
|
|
charData['advance'] = glyphSlot.contents.advance.x >> 6 |
|
121
|
|
|
|
|
122
|
|
|
self.charDataCache[char] = charData |
|
123
|
|
|
|
|
124
|
|
|
return charData |
|
125
|
|
|
|
|
126
|
|
|
def delete(self): |
|
127
|
|
|
'''Delete the freetype face''' |
|
128
|
|
|
ft.FT_Done_Face(self.face) |
|
129
|
|
|
|
|
130
|
|
|
|
|
131
|
|
|
class Text(object): |
|
132
|
|
|
def __init__(self, program, font): |
|
133
|
|
|
self.program = program |
|
134
|
|
|
self.texAtlas = texture.TextureAtlas(self.program, texFormat=gl.GL_RED) |
|
135
|
|
|
|
|
136
|
|
|
self.font = font |
|
137
|
|
|
|
|
138
|
|
|
self.vertLoc = self.program.get_attribute(b'position') |
|
139
|
|
|
self.UVLoc = self.program.get_attribute(b'vertexUV') |
|
140
|
|
|
|
|
141
|
|
|
self.data = [[0.0, 1.0], [1.0, 1.0], [0.0, 0.0], [1.0, 0.0] ] |
|
142
|
|
|
|
|
143
|
|
|
self.chrMap = {} |
|
144
|
|
|
|
|
145
|
|
|
self.basePos = 0.0 |
|
146
|
|
|
self.lineSpacing = 3 |
|
147
|
|
|
|
|
148
|
|
|
for texVal in range(32, 128): |
|
149
|
|
|
char = chr(texVal) |
|
150
|
|
|
fontData = self.font.load_glyph(char) |
|
151
|
|
|
|
|
152
|
|
|
# Find the fartherst position from the baseline |
|
153
|
|
|
if fontData['bitmap_y'] > self.basePos: |
|
154
|
|
|
self.basePos = fontData['bitmap_y'] |
|
155
|
|
|
|
|
156
|
|
|
self.chrMap[char] = Glyph(self.program, self.texAtlas, fontData, |
|
157
|
|
|
char, self) |
|
158
|
|
|
print(self.basePos) |
|
159
|
|
|
|
|
160
|
|
|
|
|
161
|
|
|
self.texAtlas.gen_atlas() |
|
162
|
|
|
|
|
163
|
|
|
self.vbo = mesh.buffer_object(self.data, gl.GLfloat) |
|
164
|
|
|
|
|
165
|
|
|
for glyph in self.chrMap.values(): |
|
166
|
|
|
glyph.init_gl() |
|
167
|
|
|
|
|
168
|
|
|
|
|
169
|
|
|
def draw_text(self, text, xPos, yPos): |
|
170
|
|
|
self.program.use() |
|
171
|
|
|
self.texAtlas.bind() |
|
172
|
|
|
|
|
173
|
|
|
# When you can dynamically add textures to an Atlas |
|
174
|
|
|
# this is where the glyph objects will be created. |
|
175
|
|
|
# Instead of taking a while on init to generate all |
|
176
|
|
|
# normal characters. |
|
177
|
|
|
|
|
178
|
|
|
|
|
179
|
|
|
textLines = text.split('\n') |
|
180
|
|
|
|
|
181
|
|
|
penPosX = xPos |
|
182
|
|
|
penPosY = self.basePos + yPos |
|
183
|
|
|
for txt in textLines: |
|
184
|
|
|
for c in txt: |
|
185
|
|
|
char = self.chrMap[c] |
|
186
|
|
|
char.render(penPosX, penPosY) |
|
187
|
|
|
penPosX += char.advance |
|
188
|
|
|
penPosY += self.basePos + self.lineSpacing |
|
189
|
|
|
penPosX = xPos |
|
190
|
|
|
|
|
191
|
|
|
# gl.glDisableVertexAttribArray(self.UVLoc) |
|
192
|
|
|
|
|
193
|
|
|
# gl.glDisableVertexAttribArray(self.vertLoc) |
|
194
|
|
|
# gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) |
|
195
|
|
|
gl.glBindVertexArray(0) |
|
196
|
|
|
|
|
197
|
|
|
|
|
198
|
|
|
class Glyph(object): |
|
199
|
|
|
def __init__(self, program, atlas, fontData, char, parent): |
|
200
|
|
|
self.atlas = atlas |
|
201
|
|
|
self.fontData = fontData |
|
202
|
|
|
self.program = program |
|
203
|
|
|
self.parent = parent |
|
204
|
|
|
self.nverts = 4 |
|
205
|
|
|
|
|
206
|
|
|
self.vertLoc = self.program.get_attribute(b'position') |
|
207
|
|
|
self.modelLoc = self.program.new_uniform(b'model') |
|
208
|
|
|
self.UVLoc = self.program.get_attribute(b'vertexUV') |
|
209
|
|
|
|
|
210
|
|
|
self.modelMatrix = matrix.Matrix(4) |
|
211
|
|
|
|
|
212
|
|
|
self.char = char |
|
213
|
|
|
|
|
214
|
|
|
self.pixelData = self.fontData['pixelData'] |
|
215
|
|
|
|
|
216
|
|
|
self.textureWidth = self.fontData['texWidth'] |
|
217
|
|
|
self.textureHeight = self.fontData['texHeight'] |
|
218
|
|
|
|
|
219
|
|
|
self.bitX = self.fontData['bitmap_x'] |
|
220
|
|
|
self.bitY = self.fontData['bitmap_y'] |
|
221
|
|
|
|
|
222
|
|
|
self.advance = self.fontData['advance'] |
|
223
|
|
|
self.uniform = self.program.get_uniform(self.modelLoc) |
|
224
|
|
|
|
|
225
|
|
|
self.textureID = self.atlas.add_texture(self.textureWidth, |
|
226
|
|
|
self.textureHeight, |
|
227
|
|
|
self.pixelData) |
|
228
|
|
|
|
|
229
|
|
|
def init_gl(self): |
|
230
|
|
|
self.vao = pgl.glGenVertexArrays(1) |
|
231
|
|
|
gl.glBindVertexArray(self.vao) |
|
232
|
|
|
|
|
233
|
|
|
self._uvCoords = self.atlas.get_uvcoords(self.textureID) |
|
234
|
|
|
self.vertexScale = self.atlas.get_vertex_scale(self.textureID) |
|
235
|
|
|
|
|
236
|
|
|
vecScale = vector.Vector( |
|
237
|
|
|
3, |
|
238
|
|
|
data=[self.atlas.maxSubTextureHeight * self.vertexScale[0], |
|
239
|
|
|
self.atlas.maxSubTextureHeight * self.vertexScale[1], 0.0]) |
|
240
|
|
|
|
|
241
|
|
|
self.scaleMat = matrix.Matrix(4).i_scale(vecScale) |
|
242
|
|
|
|
|
243
|
|
|
self.uvbo = mesh.buffer_object(self._uvCoords, gl.GLfloat) |
|
244
|
|
|
|
|
245
|
|
|
gl.glEnableVertexAttribArray(self.vertLoc) |
|
246
|
|
|
gl.glEnableVertexAttribArray(self.UVLoc) |
|
247
|
|
|
|
|
248
|
|
|
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.parent.vbo) |
|
249
|
|
|
pgl.glVertexAttribPointer(self.vertLoc, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, None) |
|
250
|
|
|
|
|
251
|
|
|
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.uvbo) |
|
252
|
|
|
pgl.glVertexAttribPointer(self.UVLoc, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, None) |
|
253
|
|
|
|
|
254
|
|
|
gl.glBindVertexArray(0) |
|
255
|
|
|
|
|
256
|
|
|
def render(self, posX, posY): |
|
257
|
|
|
gl.glBindVertexArray(self.vao) |
|
258
|
|
|
|
|
259
|
|
|
vecScale = vector.Vector( |
|
260
|
|
|
3, |
|
261
|
|
|
data=[posX + self.bitX, posY - self.bitY, 0.0]) |
|
262
|
|
|
self.modelMatrix = self.scaleMat.translate(vecScale) |
|
263
|
|
|
|
|
264
|
|
|
self.program.set_uniform_matrix(self.modelLoc, self.modelMatrix, |
|
265
|
|
|
uniform=self.uniform, |
|
266
|
|
|
size=4) |
|
267
|
|
|
|
|
268
|
|
|
|
|
269
|
|
|
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, self.nverts) |
|
270
|
|
|
|