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