Text.draw_text()   B
last analyzed

Complexity

Conditions 3

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
dl 0
loc 27
rs 8.8571
c 1
b 0
f 0
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