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
|
|
|
|