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 }