1 module prova.graphics.text.font;
2 
3 import derelict.freetype,
4        prova.graphics,
5        prova.math,
6        std.algorithm,
7        std.conv,
8        std.math,
9        std..string;
10 
11 ///
12 final class Font
13 {
14   package(prova) static FT_Library ftlibrary;
15   ///
16   immutable int size;
17   immutable int ascentLine;
18   immutable int descentLine;
19   immutable bool hasKerning;
20   ///
21   float lineHeight;
22   private Glyph[int] glyphs;
23   private Texture _texture;
24   private FT_Face fontface;
25   private int largestGlyphLength;
26   private int marginLeft = 0;
27   private int left = 0;
28   private int top = 0;
29   private int right = 512;
30 
31   ///
32   this(string path, int size)
33   {
34     int error = FT_New_Face(
35       ftlibrary,
36       toStringz(path),
37       0,
38       &fontface
39     );
40 
41     if(error != 0)
42       throw new Exception("Failed to load font: " ~ path);
43 
44     error = FT_Set_Char_Size(
45       fontface,
46       0, size * 64,
47       0, 0
48     );
49 
50     if(error != 0)
51       throw new Exception("Failed to set size, using a fixed-sized font?");
52 
53     this._texture = new Texture(512, 512);
54     this.lineHeight = fontface.size.metrics.height / 64;
55     this.ascentLine = cast(int) fontface.size.metrics.ascender / 64;
56     this.descentLine = cast(int) fontface.size.metrics.descender / 64;
57     this.hasKerning = FT_HAS_KERNING(fontface);
58     this.size = size;
59 
60     // preload the glyphs for 0-9
61     foreach(int i; 0 .. 9)
62       loadGlyph(48 + i);
63   }
64 
65   @property Texture texture()
66   {
67     return _texture;
68   }
69 
70   ///
71   bool hasGlyph(int character)
72   {
73     uint glyphIndex = FT_Get_Char_Index(fontface, character);
74 
75     return glyphIndex != 0;
76   }
77 
78   ///
79   Glyph getGlyph(int character)
80   {
81     // return early if the character was already generated
82     if(character in glyphs)
83       return glyphs[character];
84     return loadGlyph(character);
85   }
86 
87   ///
88   Vector2 getKerning(int leftChar, int rightChar)
89   {
90     if(!hasKerning)
91       return Vector2();
92 
93     uint leftIndex = FT_Get_Char_Index(fontface, leftChar);
94     uint rightIndex = FT_Get_Char_Index(fontface, rightChar);
95 
96     FT_Vector kerning;
97 
98     int err = FT_Get_Kerning(
99       fontface,
100       leftChar,
101       rightChar,
102       FT_Kerning_Mode.FT_KERNING_DEFAULT,
103       &kerning
104     );
105 
106     return Vector2(kerning.x / 64, kerning.y / 64);
107   }
108 
109   ///
110   Vector2 measureString(string text)
111   {
112     Vector2 position;
113     float width = 0;
114     float height = 0;
115 
116     foreach(char c; text)
117     {
118       if(!hasGlyph(c))
119         continue;
120 
121       const Glyph glyph = getGlyph(c);
122 
123       // get the bottom of the glyph
124       const float bottom = position.y + glyph.offset.y - glyph.height;
125 
126       // update the height if the inverted value of bottom is larger
127       if(-bottom > height)
128         height = -bottom;
129 
130       // update the width
131       width = position.x + glyph.width;
132 
133       // update the position
134       position += glyph.advance;
135     }
136 
137     return Vector2(width, height);
138   }
139 
140   private Glyph loadGlyph(int character)
141   {
142     // character was not previously loaded, time to generate it
143 
144     uint glyphIndex = FT_Get_Char_Index(fontface, character);
145 
146     int error = FT_Load_Glyph(
147       fontface,
148       glyphIndex,
149       FT_LOAD_DEFAULT
150     );
151 
152     if(error != 0)
153       throw new Exception(
154         "Could not find '" ~ to!string(character) ~ "' in this font, use hasGlyph to check for this"
155       );
156 
157     FT_GlyphSlot slot = fontface.glyph;
158 
159     Glyph glyph;
160     glyph.code = character;
161     glyph.width = cast(int) (slot.metrics.width / 64);
162     glyph.height = cast(int) (slot.metrics.height / 64);
163     glyph.offset = Vector2(slot.bitmap_left, slot.bitmap_top - size);
164     glyph.advance = Vector2(slot.advance.x / 64, slot.advance.y / 64);
165 
166     // get the clip from stamping the glyph
167     glyph.clip = stampGlyph(glyph, slot);
168 
169     glyphs[character] = glyph;
170 
171     return glyph;
172   }
173 
174   // returns the clip
175   private Rect stampGlyph(Glyph glyph, FT_GlyphSlot slot)
176   {
177     optimizeTexture(glyph);
178     moveStamper(glyph);
179 
180     ubyte[] data = getBitmap(slot);
181 
182     _texture.update(data, left, _texture.height - top - glyph.height, glyph.width, glyph.height);
183 
184     return Rect(left, top, glyph.width, glyph.height);
185   }
186 
187   // optimizes the texture to be able to support every glyph
188   // assures that the texture's size will be a power of two
189   private void optimizeTexture(Glyph glyph)
190   {
191     largestGlyphLength = max(largestGlyphLength, glyph.width, glyph.height);
192     int glyphCount = cast(int) glyphs.length + 1;
193 
194     int combinedLength = largestGlyphLength * glyphCount;
195     int size = cast(int) sqrt(cast(float) combinedLength);
196 
197     int powerOfTwo = cast(int) ceil(log2(size));
198 
199     size = 2 ^^ powerOfTwo;
200 
201     if(size > _texture.width)
202       resizeTexture(size);
203   }
204 
205   // custom resize function, replaces old data at the top left
206   // Texture.resize places it at the bottom left (0,0)
207   private void resizeTexture(int size)
208   {
209     ubyte[] data = _texture.getData();
210     int oldWidth = _texture.width;
211     int oldHeight = _texture.height;
212 
213     _texture.recreate(null, size, size);
214     _texture.update(data, 0, _texture.height - oldHeight, oldWidth, oldHeight);
215   }
216 
217   private void moveStamper(Glyph glyph)
218   {
219     left += largestGlyphLength;
220 
221     // past right edge, move down a row
222     if(left + glyph.width > right) {
223       left = marginLeft;
224       top += largestGlyphLength;
225     }
226 
227     // past bottom, texture was resized
228     if(top + glyph.height > _texture.height) {
229       left = right;
230       top = 0;
231       right = _texture.width;
232     }
233   }
234 
235   private ubyte[] getBitmap(FT_GlyphSlot slot)
236   {
237     if(slot.format != FT_GLYPH_FORMAT_BITMAP)
238       FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL);
239 
240     switch(slot.bitmap.pixel_mode)
241     {
242       case FT_PIXEL_MODE_MONO:
243         return convertMonoBitmap(slot);
244       case FT_PIXEL_MODE_GRAY:
245         return convertGrayScaleBitmap(slot);
246       default:
247         throw new Exception("Bitmap format for rendering glyph is not supported");
248     }
249   }
250 
251   private ubyte[] convertMonoBitmap(FT_GlyphSlot slot)
252   {
253     ubyte[] data;
254     data.length = slot.bitmap.width * slot.bitmap.rows * 4;
255 
256     foreach(int row; 0 .. slot.bitmap.rows) {
257       foreach(int col; 0 .. slot.bitmap.width) {
258         int byteIndex = col / 8 + row * slot.bitmap.pitch;
259         int bitIndex = col % 8;
260         ubyte bit = (slot.bitmap.buffer[byteIndex] >> (7 - bitIndex)) & 1;
261         ubyte color = bit == 1 ? 255 : 0;
262 
263         int destinationRow = slot.bitmap.rows - row - 1;
264         int idx = (slot.bitmap.width * destinationRow + col) * 4;
265 
266         data[idx .. idx + 4] = color;
267       }
268     }
269 
270     return data;
271   }
272 
273   private ubyte[] convertGrayScaleBitmap(FT_GlyphSlot slot)
274   {
275     ubyte[] data;
276     data.length = slot.bitmap.width * slot.bitmap.rows * 4;
277 
278     foreach(int row; 0 .. slot.bitmap.rows) {
279       foreach(int col; 0 .. slot.bitmap.width) {
280         int sourceIdx = slot.bitmap.width * row + col;
281 
282         int destinationRow = slot.bitmap.rows - row - 1;
283         int destinationIdx = (slot.bitmap.width * destinationRow + col) * 4;
284 
285         data[destinationIdx .. destinationIdx + 3] = 255;
286         data[destinationIdx + 3] = slot.bitmap.buffer[sourceIdx];
287       }
288     }
289 
290     return data;
291   }
292 }