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 }