1 /++ 2 Module for rendering text using the `FreeType` library. 3 4 Macros: 5 LREF = <a href="#$1">$1</a> 6 HREF = <a href="$1">$2</a> 7 PHOBREF = <a href="https://dlang.org/phobos/$1.html#$2">$2</a> 8 9 Authors: $(HREF https://github.com/TodNaz,TodNaz) 10 Copyright: Copyright (c) 2020 - 2021, TodNaz. 11 License: $(HREF https://github.com/TodNaz/Tida/blob/master/LICENSE,MIT) 12 +/ 13 module tida.text; 14 15 import bindbc.freetype; 16 import std.traits; 17 import tida.drawable; 18 import tida.color; 19 20 __gshared FT_Library _FTLibrary; 21 22 /++ 23 Loads a library for loading and rendering fonts. 24 25 $(PHOBREF object,Exception) if the library was not found on the system. 26 +/ 27 void initFontLibrary() 28 { 29 import std.exception : enforce; 30 31 enforce!Exception(loadFreeType() != FTSupport.noLibrary, "Not find FreeType library!"); 32 enforce!Exception(!FT_Init_FreeType(&_FTLibrary), "Not a initialize FreeType Library!"); 33 } 34 35 struct FontSymbolInfo 36 { 37 import tida.image; 38 import tida.vector; 39 40 int bitmapLeft; 41 int bitmapTop; 42 Vector!float advance; 43 Image image; 44 } 45 46 /++ 47 The object to load and use the font. 48 +/ 49 class Font 50 { 51 import std.exception : enforce; 52 import std.string : toStringz; 53 import std.file : exists; 54 import tida.image; 55 56 private: 57 FT_Face _face; 58 size_t _size; 59 60 public: 61 FontSymbolInfo[uint] cache; 62 63 @trusted: 64 /// Font face object. 65 @property FT_Face face() nothrow pure 66 { 67 return _face; 68 } 69 70 /// Font size. 71 @property size_t size() nothrow pure 72 { 73 return _size; 74 } 75 76 auto charIndex(T)(T symbol, int flags) 77 { 78 FT_Load_Char(_face, symbol, flags); 79 return FT_Get_Char_Index(_face, symbol); 80 } 81 82 FontSymbolInfo renderSymbol(uint index, int fload, int frender) 83 { 84 import tida.vector; 85 import tida.color; 86 87 if (index in cache) 88 { 89 return cache[index]; 90 } else 91 { 92 FontSymbolInfo syinfo; 93 Image image; 94 FT_GlyphSlot glyph; 95 96 FT_Load_Glyph(_face, index, fload); 97 FT_Render_Glyph(_face.glyph, frender); 98 99 glyph = _face.glyph; 100 101 if (glyph.bitmap.width > 0 && glyph.bitmap.rows > 0) 102 { 103 auto bitmap = glyph.bitmap; 104 105 image = new Image(); 106 image.allocatePlace(bitmap.width, bitmap.rows); 107 108 auto pixels = image.pixels; 109 110 foreach(j; 0 .. bitmap.width * bitmap.rows) 111 { 112 pixels[j] = rgba(255, 255, 255, bitmap.buffer[j]); 113 } 114 } 115 116 syinfo.bitmapLeft = glyph.bitmap_left; 117 syinfo.bitmapTop = glyph.bitmap_top; 118 syinfo.advance = vec!float(glyph.advance.x, glyph.advance.y); 119 syinfo.image = image; 120 121 cache[index] = syinfo; 122 123 return syinfo; 124 } 125 } 126 127 /++ 128 Loads a font. 129 130 Params: 131 path = The path to the font. 132 size = Font size. 133 134 Throws: 135 $(PHOBREF object,Exception) if the font was not found in the file system, 136 or if the font is damaged. 137 +/ 138 Font load(string path, size_t size) 139 { 140 enforce!Exception(exists(path), "Not file file `"~path~"`"); 141 142 enforce!Exception(!FT_New_Face(_FTLibrary, path.toStringz, 0, &_face), "Font damaged!"); 143 144 FT_CharMap found; 145 146 foreach(i; 0 .. _face.num_charmaps) 147 { 148 FT_CharMap charmap = _face.charmaps[i]; 149 if ((charmap.platform_id == 3 && charmap.encoding_id == 1) 150 || (charmap.platform_id == 3 && charmap.encoding_id == 0) 151 || (charmap.platform_id == 2 && charmap.encoding_id == 1) 152 || (charmap.platform_id == 0)) { 153 found = charmap; 154 break; 155 } 156 } 157 158 FT_Set_Charmap(_face, found); 159 FT_Set_Char_Size(_face, 0, cast(int) size*32, 300, 300); 160 FT_Set_Pixel_Sizes(_face, 0, cast(int) size*2); 161 162 this._size = size; 163 164 return this; 165 } 166 167 /++ 168 Changes the font size to the specified parameter. 169 170 Params: 171 newSize = Font new size. 172 +/ 173 void resize(size_t newSize) @trusted 174 { 175 this._size = newSize; 176 177 FT_Set_Char_Size(_face, 0, cast(int) _size*32, 300, 300); 178 FT_Set_Pixel_Sizes(_face, 0, cast(int) size*2); 179 180 cache.clear(); 181 } 182 183 /// Free memory 184 void free() @trusted 185 { 186 FT_Done_Face(_face); 187 } 188 189 ~this() @safe 190 { 191 free(); 192 } 193 } 194 195 /++ 196 Determining the type of character depending on the type of the string. 197 +/ 198 template TypeChar(T) 199 { 200 static if (is(T : string)) 201 alias TypeChar = char; 202 else 203 static if (is(T : wstring)) 204 alias TypeChar = wchar; 205 else 206 static if (is(T : dstring)) 207 alias Typechar = dchar; 208 } 209 210 /// The unit for rendering the text. 211 class Symbol 212 { 213 import tida.image; 214 import tida.color; 215 import tida.vector; 216 217 public: 218 Image image; /// Symbol image. 219 Vecf position; /// Symbol releative position. 220 Color!ubyte color; /// Symbol color. 221 Vecf advance; /// Symbol releative position. 222 size_t size; /// Symbol size. 223 float offsetY = 0.0f; /// Offset symbol 224 225 @safe: 226 this( Image image, 227 Vecf position, 228 Vecf rel, 229 size_t size, 230 Color!ubyte color = rgb(255,255,255)) 231 { 232 this.image = image; 233 this.position = position; 234 this.color = color; 235 this.advance = rel; 236 this.size = size; 237 238 if (this.image !is null) 239 if (this.image.texture is null) 240 this.image.toTexture(); 241 } 242 } 243 244 /++ 245 Cuts off formatting blocks. 246 247 Params: 248 symbols = Symbols. 249 +/ 250 inout(T) cutFormat(T)(inout T symbols) @safe /*nothrow*/ /*pure*/ 251 if (isSomeString!T) 252 { 253 T result; 254 int previous = 0; 255 256 for(int i = 0; i < symbols.length; i++) 257 { 258 if(symbols[i] == '$') 259 { 260 if(symbols[i+1] == '<') 261 { 262 __symEachCutter: for(int j = i; j < symbols.length; j++) 263 { 264 if(symbols[j] == '>') 265 { 266 result = result ~ symbols[previous .. i]; 267 previous = j + 1; 268 break __symEachCutter; 269 } 270 } 271 } 272 } 273 } 274 275 result ~= symbols[previous .. $]; 276 277 return result; 278 } 279 280 unittest 281 { 282 assert("Hello, $<FF0000>World!".cutFormat == ("Hello, World!")); 283 } 284 285 /++ 286 Returns the size of the rendered text for the given font. 287 288 Params: 289 T = String type. 290 text = Text. 291 font = Font. 292 +/ 293 inout(int) widthText(T)(T text, Font font) @safe 294 { 295 import std.algorithm : reduce; 296 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 297 298 return new Text(font) 299 .toSymbols!T(cutFormat!T(text)) 300 .widthSymbols; 301 } 302 303 /++ 304 Shows the width of the displayed characters. 305 306 Params: 307 text = Displayed characters. 308 +/ 309 inout(int) widthSymbols(inout Symbol[] text) @safe 310 { 311 import std.algorithm : fold; 312 import std.conv : to; 313 314 int width = int.init; 315 316 foreach(s; text) 317 width += (cast(int) s.advance.x) >> 6; 318 319 return width; 320 } 321 322 /++ 323 Draw text object. An array of letters and offset attributes already rendered 324 into the image is fed into the constructor, and the text is output 325 from these data. 326 +/ 327 class SymbolRender : IDrawable, IDrawableEx 328 { 329 import tida.render; 330 import tida.vector; 331 import tida.color; 332 import tida.shader; 333 import std.conv : to; 334 335 private: 336 Symbol[] symbols; 337 338 public @safe: 339 340 /++ 341 Constructor of the text rendering object. 342 343 Params: 344 symbols = Already drawn ready-made in image text with offset 345 parameters for correct presentation. 346 +/ 347 this(Symbol[] symbols) 348 { 349 this.symbols = symbols; 350 } 351 352 override void draw(IRenderer render, Vecf position) 353 { 354 Shader!Program currShader; 355 356 if (render.type != RenderType.software) 357 { 358 if (render.currentShader !is null) 359 { 360 currShader = render.currentShader; 361 } 362 } 363 364 position.y += (symbols[0].size + (symbols[0].size / 2)); 365 366 foreach (s; symbols) 367 { 368 if (s.image !is null) 369 { 370 if (render.type != RenderType.software) 371 render.currentShader = currShader; 372 373 s.image.drawEx(render, position - vecf(0, s.position.y), 0.0f, 374 vecfNaN, vecfNaN, s.color.a, s.color); 375 } 376 377 position = position + vecf(s.advance.x.to!int >> 6, s.offsetY); 378 } 379 } 380 381 override void drawEx( IRenderer render, 382 Vecf position, 383 float angle, 384 Vecf center, 385 Vecf size, 386 ubyte alpha, 387 Color!ubyte color) 388 { 389 Shader!Program currShader; 390 391 if (render.type != RenderType.software) 392 { 393 if (render.currentShader !is null) 394 { 395 currShader = render.currentShader; 396 } 397 } 398 399 position.y += (symbols[0].size + (symbols[0].size / 2)); 400 401 foreach (s; symbols) 402 { 403 if (s.image !is null) 404 { 405 if (render.type != RenderType.software) 406 render.currentShader = currShader; 407 408 s.image.drawEx(render, position, angle, center, vecfNaN, alpha, 409 s.color); 410 } 411 412 position = position + vecf(s.advance.x.to!int >> 6, 0); 413 } 414 } 415 } 416 417 Symbol fromSymInfo(Font font, FontSymbolInfo syInfo, Color!ubyte color) @safe 418 { 419 import tida.vector; 420 421 return new Symbol(syInfo.image, 422 vec!float(syInfo.bitmapLeft, syInfo.bitmapTop), 423 vec!float(syInfo.advance.x, 0), 424 font.size, 425 color); 426 } 427 428 /++ 429 An object for rendering drawing symbols. 430 +/ 431 class Text 432 { 433 import tida.color; 434 import tida.image; 435 import tida.vector; 436 437 private: 438 Font font; 439 440 public @trusted: 441 this(Font font) 442 { 443 this.font = font; 444 } 445 446 /++ 447 Draws each character and gives them a relative position to position 448 them correctly in the text. 449 450 Params: 451 T = String type. 452 text = Text. 453 color = Color text. 454 +/ 455 Symbol[] toSymbols(T)(T text, Color!ubyte color = rgb(0, 0, 0)) 456 { 457 import std.algorithm : map; 458 import std.range : array; 459 460 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 461 462 Symbol[] symbols; 463 464 symbols = text 465 .map!(a => font.charIndex(a, FT_LOAD_RENDER | FT_LOAD_TARGET_NORMAL)) 466 .map!(a => font.renderSymbol(a, FT_LOAD_DEFAULT, FT_RENDER_MODE_NORMAL)) 467 .map!(a => font.fromSymInfo(a, color)) 468 .array; 469 470 return symbols; 471 } 472 473 /++ 474 Outputs text with color and positional formatting. 475 476 Using the special block `$ <...>`, you can highlight text with color, 477 for example: 478 --- 479 new Text(font).toSymbolsFormat("Black text! $<ffffff> white text!\nNew line!"); 480 --- 481 482 There must be a hex color inside the special block, which will be used later. 483 Also, line wrapping is supported. 484 485 Params: 486 T = string type. 487 symbols = Text. 488 defaultColor = Default color. 489 +/ 490 Symbol[] toSymbolsFormat(T)(T symbols, 491 Color!ubyte defaultColor = rgba(255,255,255,255)) 492 { 493 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 494 495 Symbol[] result; 496 int previous = 0; 497 int j = 0; 498 499 Color!ubyte color = defaultColor; 500 501 for (int i = 0; i < symbols.length; i++) 502 { 503 if (symbols[i] == '$') 504 { 505 if (symbols[i+1] == '<') { 506 T colorText; 507 508 __symEach: for (j = i; j < symbols.length; j++) 509 { 510 if (symbols[j] == '>') 511 { 512 colorText = symbols[i+2 .. j]; 513 break __symEach; 514 } 515 } 516 517 if (colorText != "") 518 { 519 result ~= this.toSymbols(symbols[previous .. i], color); 520 521 color = Color!ubyte(colorText); 522 i = j + 1; 523 previous = i; 524 } 525 } 526 } else 527 if (symbols[i] == '\n') 528 { 529 Symbol[] temp = this.toSymbols(symbols[previous .. i], color); 530 Symbol last = temp[$ - 1]; 531 last.offsetY += last.size * 2; 532 float tc = -(result.widthSymbols + widthSymbols(temp)); 533 int ic = cast(int) tc; 534 last.advance = vecf((ic << 6) + last.advance.x , 0); 535 536 previous = i + 1; 537 538 result ~= temp; 539 } 540 } 541 542 result ~= this.toSymbols(symbols[previous .. $], color); 543 544 return result; 545 } 546 547 /++ 548 Creates symbols for rendering and immediately returns the object of 549 their renderer. 550 551 Params: 552 T = String type. 553 text = Text. 554 color = Color text. 555 +/ 556 SymbolRender renderSymbols(T)(T text, Color!ubyte color = rgb(0, 0, 0)) 557 { 558 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 559 return new SymbolRender(toSymbols(text, color)); 560 } 561 562 /++ 563 Outputs characters with color formatting and sequel, renders such text. 564 565 Params: 566 T = String type. 567 text = Text. 568 color = Color text. 569 +/ 570 SymbolRender renderFormat(T)(T text, Color!ubyte color = rgb(0, 0, 0)) 571 { 572 static assert(isSomeString!T, T.stringof ~ " is not a string!"); 573 return new SymbolRender(toSymbolsFormat(text, color)); 574 } 575 }