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 }