1 module fluid.typeface;
2 
3 import bindbc.freetype;
4 
5 import std.range;
6 import std.traits;
7 import std.string;
8 import std.algorithm;
9 
10 import fluid.rope;
11 import fluid.utils;
12 import fluid.backend;
13 
14 
15 @safe:
16 
17 
18 version (BindFT_Static) {
19     debug (Fluid_BuildMessages) {
20         pragma(msg, "Fluid: Using static freetype");
21     }
22 }
23 else {
24     version = BindFT_Dynamic;
25     debug (Fluid_BuildMessages) {
26         pragma(msg, "Fluid: Using dynamic freetype");
27     }
28 }
29 
30 /// Low-level interface for drawing text. Represents a single typeface.
31 ///
32 /// Unlike the rest of Fluid, Typeface uses screen-space dots directly, instead of fixed-size pixels. Consequently, DPI
33 /// must be specified manually.
34 ///
35 /// See: [fluid.text.Text] for an interface on a higher level.
36 interface Typeface {
37 
38     /// List glyphs in the typeface.
39     long glyphCount() const;
40 
41     /// Get initial pen position.
42     Vector2 penPosition() const;
43 
44     /// Get line height.
45     int lineHeight() const;
46 
47     /// Width of an indent/tab character, in dots.
48     /// `Text` sets `indentWidth` automatically.
49     ref inout(int) indentWidth() inout;
50 
51     /// Get advance vector for the given glyph. Uses dots, not pixels, as the unit.
52     Vector2 advance(dchar glyph);
53 
54     /// Set font scale. This should be called at least once before drawing.
55     /// `Text` sets DPI automatically.
56     ///
57     /// Font renderer should cache this and not change the scale unless updated.
58     ///
59     /// Params:
60     ///     scale = Horizontal and vertical DPI value, for example (96, 96)
61     deprecated("Use setSize instead.")
62     Vector2 dpi(Vector2 scale);
63 
64     /// Get curently set DPI.
65     Vector2 dpi() const;
66 
67     /// Set the font size. This should be called at least once before drawing.
68     /// `Text`, if used, sets this automatically.
69     ///
70     /// Font renderer should cache this and not change the scale unless updated.
71     ///
72     /// Params:
73     ///     dpi  = Horizontal and vertical DPI value, for example (96, 96).
74     ///     size = Size of the font, in pixels.
75     void setSize(Vector2 dpi, float size);
76 
77     /// Draw a line of text.
78     /// Note: This API is unstable and might change over time.
79     /// Params:
80     ///     target       = Image to draw to.
81     ///     penPosition  = Pen position for the beginning of the line. Updated to the pen position at the end of th line.
82     ///     text         = Text to draw.
83     ///     paletteIndex = If the image has a palette, this is the index to get colors from.
84     void drawLine(ref Image target, ref Vector2 penPosition, Rope text, ubyte paletteIndex = 0) const;
85 
86     /// Instances of Typeface have to be comparable in a memory-safe manner.
87     bool opEquals(const Object object) @safe const;
88 
89     /// Get the default Fluid typeface.
90     static defaultTypeface() {
91         return FreetypeTypeface.defaultTypeface;
92     }
93 
94     /// Default word splitter used by measure/draw.
95     alias defaultWordChunks = .breakWords;
96 
97     /// Updated version of `std.string.lineSplitter` that includes trailing empty lines.
98     ///
99     /// `lineSplitterIndex` will produce a tuple with the index into the original text as the first element.
100     static lineSplitter(KeepTerminator keepTerm = No.keepTerminator, Range)(Range text)
101     if (isSomeChar!(ElementType!Range))
102     do {
103 
104         enum dchar lineSep = '\u2028';  // Line separator.
105         enum dchar paraSep = '\u2029';  // Paragraph separator.
106         enum dchar nelSep  = '\u0085';  // Next line.
107 
108         import std.utf : byDchar;
109 
110         const hasEmptyLine = byDchar(text).endsWith('\r', '\n', '\v', '\f', "\r\n", lineSep, paraSep, nelSep) != 0;
111         auto split = .lineSplitter!keepTerm(text);
112 
113         // Include the empty line if present
114         return hasEmptyLine.choose(
115             split.chain(only(typeof(text).init)),
116             split,
117         );
118 
119     }
120 
121     /// ditto
122     static lineSplitterIndex(Range)(Range text) {
123 
124         import std.typecons : tuple;
125 
126         auto initialValue = tuple(size_t.init, Range.init, size_t.init);
127 
128         return Typeface.lineSplitter!(Yes.keepTerminator)(text)
129 
130             // Insert the index, remove the terminator
131             // Position [2] is line end index
132             .cumulativeFold!((a, line) => tuple(a[2], line.chomp, a[2] + line.length))(initialValue)
133 
134             // Remove item [2]
135             .map!(a => tuple(a[0], a[1]));
136 
137     }
138 
139     unittest {
140 
141         import std.typecons : tuple;
142 
143         auto myLine = "One\nTwo\r\nThree\vStuff\nï\nö";
144         auto result = [
145             tuple(0, "One"),
146             tuple(4, "Two"),
147             tuple(9, "Three"),
148             tuple(15, "Stuff"),
149             tuple(21, "ï"),
150             tuple(24, "ö"),
151         ];
152 
153         assert(lineSplitterIndex(myLine).equal(result));
154         assert(lineSplitterIndex(Rope(myLine)).equal(result));
155 
156     }
157 
158     unittest {
159 
160         assert(lineSplitter(Rope("ą")).equal(lineSplitter("ą")));
161 
162     }
163 
164     /// Measure space the given text would span. Uses dots as the unit.
165     ///
166     /// If `availableSpace` is specified, assumes text wrapping. Text wrapping is only performed on whitespace
167     /// characters.
168     ///
169     /// Params:
170     ///     chunkWords = Algorithm to use to break words when wrapping text; separators must be preserved as separate
171     ///         words.
172     ///     availableSpace = Amount of available space the text can take up (dots), used to wrap text.
173     ///     text = Text to measure.
174     ///     wrap = Toggle text wrapping. Defaults to on, unless using the single argument overload.
175     ///
176     /// Returns:
177     ///     Vector2 representing the text size, if `TextRuler` is not specified as an argument.
178     final Vector2 measure(alias chunkWords = defaultWordChunks, String)
179         (Vector2 availableSpace, String text, bool wrap = true)
180     do {
181 
182         auto ruler = TextRuler(this, wrap ? availableSpace.x : float.nan);
183 
184         measure!chunkWords(ruler, text, wrap);
185 
186         return ruler.textSize;
187 
188     }
189 
190     /// ditto
191     final Vector2 measure(String)(String text) {
192 
193         // No wrap, only explicit in-text line breaks
194         auto ruler = TextRuler(this);
195 
196         measure(ruler, text, false);
197 
198         return ruler.textSize;
199 
200     }
201 
202     /// ditto
203     static void measure(alias chunkWords = defaultWordChunks, String)
204         (ref TextRuler ruler, String text, bool wrap = true)
205     do {
206 
207         // TODO don't fail on decoding errors
208         // TODO RTL layouts
209         // TODO vertical text
210 
211         // Split on lines
212         foreach (line; lineSplitter(text)) {
213 
214             ruler.startLine();
215 
216             // Split on words; do nothing in particular, just run the measurements
217             foreach (word, penPosition; eachWord!chunkWords(ruler, line, wrap)) { }
218 
219         }
220 
221     }
222 
223     /// Helper function
224     static auto eachWord(alias chunkWords = defaultWordChunks, String)
225         (ref TextRuler ruler, String text, bool wrap = true)
226     do {
227 
228         struct Helper {
229 
230             alias ElementType = CommonType!(String, typeof(chunkWords(text).front));
231 
232             // I'd use `choose` but it's currently broken
233             int opApply(int delegate(ElementType, Vector2) @safe yield) {
234 
235                 // Text wrapping on
236                 if (wrap) {
237 
238                     auto range = chunkWords(text);
239 
240                     // Empty line, yield an empty word
241                     if (range.empty) {
242 
243                         const penPosition = ruler.addWord(String.init);
244                         if (const ret = yield(String.init, penPosition)) return ret;
245 
246                     }
247 
248                     // Split each word
249                     else foreach (word; chunkWords(text)) {
250 
251                         const penPosition = ruler.addWord(word);
252                         if (const ret = yield(word, penPosition)) return ret;
253 
254                     }
255 
256                     return 0;
257 
258                 }
259 
260                 // Text wrapping off
261                 else {
262 
263                     const penPosition = ruler.addWord(text);
264                     return yield(text, penPosition);
265 
266                 }
267 
268             }
269 
270         }
271 
272         return Helper();
273 
274     }
275 
276     /// Draw text within the given rectangle in the image.
277     final void draw(alias chunkWords = defaultWordChunks, String)
278         (ref Image image, Rectangle rectangle, String text, ubyte paletteIndex, bool wrap = true)
279     const {
280 
281         auto ruler = TextRuler(this, rectangle.w);
282 
283         // TODO decoding errors
284 
285         // Split on lines
286         foreach (line; this.lineSplitter(text)) {
287 
288             ruler.startLine();
289 
290             // Split on words
291             foreach (word, penPosition; eachWord!chunkWords(ruler, line, wrap)) {
292 
293                 auto wordPenPosition = rectangle.start + penPosition;
294 
295                 drawLine(image, wordPenPosition, word, paletteIndex);
296 
297             }
298 
299         }
300 
301     }
302 
303     /// Helper function for typeface implementations, providing a "draw" function for tabs, adjusting the pen position
304     /// automatically.
305     protected final void drawTab(ref Vector2 penPosition) const {
306 
307         penPosition.x += _tabWidth(penPosition.x);
308 
309     }
310 
311     private final float _tabWidth(float xoffset) const {
312 
313         return indentWidth - (xoffset % indentWidth);
314 
315     }
316 
317 }
318 
319 /// Break words on whitespace and punctuation. Splitter characters stick to the word that precedes them, e.g.
320 /// `foo!! bar.` is split as `["foo!! ", "bar."]`.
321 auto breakWords(Range)(Range range) {
322 
323     import std.uni : isAlphaNum, isWhite;
324     import std.utf : decodeFront;
325 
326     /// Pick the group the character belongs to.
327     static int pickGroup(dchar a) {
328 
329         return a.isAlphaNum ? 0
330             : a.isWhite ? 1
331             : 2;
332 
333     }
334 
335     /// Splitter function that splits in any case two
336     static bool isSplit(dchar a, dchar b) {
337 
338         return !a.isAlphaNum && !b.isWhite && pickGroup(a) != pickGroup(b);
339 
340     }
341 
342     struct BreakWords {
343 
344         Range range;
345         Range front = Range.init;
346 
347         bool empty() const {
348             return front.empty;
349         }
350 
351         void popFront() {
352 
353             dchar lastChar = 0;
354             auto originalRange = range.save;
355 
356             while (!range.empty) {
357 
358                 if (lastChar && isSplit(lastChar, range.front)) break;
359                 lastChar = range.decodeFront;
360 
361             }
362 
363             front = originalRange[0 .. $ - range.length];
364 
365         }
366 
367     }
368 
369     auto chunks = BreakWords(range);
370     chunks.popFront;
371     return chunks;
372 
373 }
374 
375 unittest {
376 
377     const test = "hellö world! 123 hellö123*hello -- hello -- - &&abcde!a!!?!@!@#3";
378     const result = [
379         "hellö ",
380         "world! ",
381         "123 ",
382         "hellö123*",
383         "hello ",
384         "-- ",
385         "hello ",
386         "-- ",
387         "- ",
388         "&&",
389         "abcde!",
390         "a!!?!@!@#",
391         "3"
392     ];
393 
394     assert(breakWords(test).equal(result));
395     assert(breakWords(Rope(test)).equal(result));
396 
397     const test2 = "Аа Бб Вв Гг Дд Ее Ëë Жж Зз Ии "
398         ~ "Йй Кк Лл Мм Нн Оо Пп Рр Сс Тт "
399         ~ "Уу Фф Хх Цц Чч Шш Щщ Ъъ Ыы Ьь "
400         ~ "Ээ Юю Яя ";
401 
402     assert(breakWords(test2).equal(breakWords(Rope(test2))));
403 
404 }
405 
406 /// Low level interface for measuring text.
407 struct TextRuler {
408 
409     /// Typeface to use for the text.
410     Typeface typeface;
411 
412     /// Maximum width for a single line. If `NaN`, no word breaking is performed.
413     float lineWidth;
414 
415     /// Current pen position.
416     Vector2 penPosition;
417 
418     /// Total size of the text.
419     Vector2 textSize;
420 
421     /// Index of the word within the line.
422     size_t wordLineIndex;
423 
424     this(Typeface typeface, float lineWidth = float.nan) {
425 
426         this.typeface = typeface;
427         this.lineWidth = lineWidth;
428         this.penPosition = typeface.penPosition;
429 
430     }
431 
432     /// Get the caret as a 0 width rectangle.
433     Rectangle caret() const {
434 
435         return caret(penPosition);
436 
437     }
438 
439     /// Get the caret as a 0 width rectangle for the given pen position.
440     Rectangle caret(Vector2 penPosition) const {
441 
442         const start = penPosition - Vector2(0, typeface.penPosition.y);
443 
444         return Rectangle(
445             start.tupleof,
446             0, typeface.lineHeight,
447         );
448 
449     }
450 
451     /// Begin a new line.
452     void startLine() {
453 
454         const lineHeight = typeface.lineHeight;
455 
456         if (textSize != Vector2.init) {
457 
458             // Move the pen to the next line
459             penPosition.x = typeface.penPosition.x;
460             penPosition.y += lineHeight;
461 
462         }
463 
464         // Allocate space for the line
465         textSize.y += lineHeight;
466         wordLineIndex = 0;
467 
468     }
469 
470     /// Add the given word to the text. The text must be a single line.
471     /// Returns: Pen position for the word. It might differ from the original penPosition, because the word may be
472     ///     moved onto the next line.
473     Vector2 addWord(String)(String word) {
474 
475         import std.utf : byDchar;
476 
477         const maxWordWidth = lineWidth - penPosition.x;
478 
479         float wordSpan = 0;
480 
481         // Measure each glyph
482         foreach (glyph; byDchar(word)) {
483 
484             // Tab aligns to set indent width
485             if (glyph == '\t')
486                 wordSpan += typeface._tabWidth(penPosition.x + wordSpan);
487 
488             // Other characters use their regular advance value
489             else
490                 wordSpan += typeface.advance(glyph).x;
491 
492         }
493 
494         // Exceeded line width
495         // Automatically false if lineWidth is NaN
496         if (maxWordWidth < wordSpan && wordLineIndex != 0) {
497 
498             // Start a new line
499             startLine();
500 
501         }
502 
503         const wordPosition = penPosition;
504 
505         // Increment word index
506         wordLineIndex++;
507 
508         // Update pen position
509         penPosition.x += wordSpan;
510 
511         // Allocate space
512         if (penPosition.x > textSize.x) {
513 
514             textSize.x = penPosition.x;
515 
516             // Limit space to not exceed maximum width (false if NaN)
517             if (textSize.x > lineWidth) {
518 
519                 textSize.x = lineWidth;
520 
521             }
522 
523         }
524 
525         return wordPosition;
526 
527     }
528 
529 }
530 
531 /// Represents a freetype2-powered typeface.
532 class FreetypeTypeface : Typeface {
533 
534     public {
535 
536         /// Underlying face.
537         FT_Face face;
538 
539         /// Adjust line height. `1` uses the original line height, `2` doubles it.
540         float lineHeightFactor = 1;
541 
542     }
543 
544     protected {
545 
546         struct AdvanceCacheKey {
547             dchar ch;
548             float size;
549         }
550 
551         /// Cache for character sizes.
552         Vector2[AdvanceCacheKey] advanceCache;
553 
554     }
555 
556     private {
557 
558         /// If true, this typeface has been loaded using this class, making the class responsible for freeing the font.
559         bool _isOwner;
560 
561         /// Font size loaded (in pixels).
562         float _size;
563 
564         /// Current DPI set for the typeface.
565         int _dpiX, _dpiY;
566 
567         int _indentWidth;
568 
569     }
570 
571     static FreetypeTypeface defaultTypeface;
572 
573     static this() @trusted {
574 
575         // Set the default typeface
576         FreetypeTypeface.defaultTypeface = new FreetypeTypeface;
577 
578     }
579 
580     /// Load the default typeface.
581     this() @trusted {
582 
583         static typefaceFile = cast(ubyte[]) import("ruda-regular.ttf");
584         const typefaceSize = cast(int) typefaceFile.length;
585 
586         // Load the font
587         if (auto error = FT_New_Memory_Face(freetype, typefaceFile.ptr, typefaceSize, 0, &face)) {
588 
589             assert(false, format!"Failed to load default Fluid typeface, error no. %s"(error));
590 
591         }
592 
593         // Mark self as the owner
594         this.isOwner = true;
595         this.lineHeightFactor = 1.16;
596 
597     }
598 
599     /// Params:
600     ///     face = Existing freetype2 typeface to use.
601     this(FT_Face face) {
602 
603         this.face = face;
604 
605     }
606 
607     /// Load a font from a file.
608     /// Params:
609     ///     filename = Filename of the font file.
610     this(string filename) @trusted {
611 
612         this._isOwner = true;
613 
614         // TODO proper exceptions
615         if (auto error = FT_New_Face(freetype, filename.toStringz, 0, &this.face)) {
616 
617             throw new Exception(format!"Failed to load `%s`, error no. %s"(filename, error));
618 
619         }
620 
621     }
622 
623     ~this() @trusted {
624 
625         // Ignore if the resources used by the class have been borrowed
626         if (!_isOwner) return;
627 
628         FT_Done_Face(face);
629 
630     }
631 
632     /// Instances of Typeface have to be comparable in a memory-safe manner.
633     override bool opEquals(const Object object) @safe const {
634 
635         return this is object;
636 
637     }
638 
639     ref inout(int) indentWidth() inout {
640         return _indentWidth;
641     }
642     bool isOwner() const {
643         return _isOwner;
644     }
645     bool isOwner(bool value) @system {
646         return _isOwner = value;
647     }
648 
649     long glyphCount() const {
650 
651         return face.num_glyphs;
652 
653     }
654 
655     /// Get initial pen position.
656     Vector2 penPosition() const {
657 
658         return Vector2(0, face.size.metrics.ascender) / 64;
659 
660     }
661 
662     /// Line height.
663     int lineHeight() const {
664 
665         // +1 is an error margin
666         return cast(int) (face.size.metrics.height * lineHeightFactor / 64) + 1;
667 
668     }
669 
670     Vector2 dpi() const {
671 
672         return Vector2(_dpiX, _dpiY);
673 
674     }
675 
676     deprecated("Use setSize instead")
677     Vector2 dpi(Vector2 dpi) {
678 
679         setSize(dpi, _size);
680         return dpi;
681 
682     }
683 
684     void setSize(Vector2 dpi, float size) @trusted {
685 
686         const dpiX = cast(int) dpi.x;
687         const dpiY = cast(int) dpi.y;
688 
689         // Ignore if there's no change
690         if (dpiX == _dpiX && dpiY == _dpiY && size == _size) return;
691 
692         _dpiX = dpiX;
693         _dpiY = dpiY;
694         _size = size;
695 
696         // dunno why, but FT_Set_Char_Size yields better results, kerning specifically, than FT_Set_Pixel_Sizes
697         version (all) {
698             const error = FT_Set_Char_Size(face, 0, cast(int) (size.pxToPt * 64 + 1), dpiX, dpiY);
699         }
700 
701         // Load size
702         else {
703             const dotsX = cast(int) (size * dpi.x / 96);
704             const dotsY = cast(int) (size * dpi.y / 96);
705             const error = FT_Set_Pixel_Sizes(face, dotsX, dotsY);
706         }
707 
708         // Test for errors
709         if (error) {
710 
711             throw new Exception(
712                 format!"Failed to load font at size %s at DPI %sx%s, error no. %s"(size, dpiX, dpiY, error)
713             );
714 
715         }
716 
717     }
718     
719     Vector2 advance(dchar glyph) @trusted {
720 
721         assert(_dpiX && _dpiY, "Font DPI hasn't been set");
722 
723         const key = AdvanceCacheKey(glyph, _size);
724 
725         // Return the stored value if the result is cached
726         if (auto result = key in advanceCache) {
727 
728             return *result;
729 
730         }
731 
732         // Load the glyph
733         if (auto error = FT_Load_Char(cast(FT_Face) face, glyph, FT_LOAD_DEFAULT)) {
734 
735             return advanceCache[key] = Vector2(0, 0);
736 
737         }
738 
739         // Advance the cursor position
740         // TODO RTL layouts
741         return advanceCache[key] = Vector2(face.glyph.advance.tupleof) / 64;
742 
743     }
744 
745     /// Draw a line of text
746     void drawLine(ref Image target, ref Vector2 penPosition, const Rope text, ubyte paletteIndex) const @trusted {
747 
748         assert(_dpiX && _dpiY, "Font DPI hasn't been set");
749 
750         foreach (glyph; text.byDchar) {
751 
752             // Tab character
753             if (glyph == '\t') {
754 
755                 drawTab(penPosition);
756                 continue;
757 
758             }
759 
760             // Load the glyph
761             if (auto error = FT_Load_Char(cast(FT_Face) face, glyph, FT_LOAD_RENDER)) {
762 
763                 continue;
764 
765             }
766 
767             const bitmap = face.glyph.bitmap;
768 
769             assert(bitmap.pixel_mode == FT_PIXEL_MODE_GRAY);
770 
771             // Draw it to the image
772             foreach (y; 0..bitmap.rows) {
773 
774                 foreach (x; 0..bitmap.width) {
775 
776                     // Each pixel is a single byte
777                     const pixel = *cast(ubyte*) (bitmap.buffer + bitmap.pitch*y + x);
778 
779                     const targetX = cast(int) penPosition.x + face.glyph.bitmap_left + x;
780                     const targetY = cast(int) penPosition.y - face.glyph.bitmap_top + y;
781 
782                     // Don't draw pixels out of bounds
783                     if (targetX >= target.width || targetY >= target.height) continue;
784 
785                     // Choose the stronger color
786                     const ubyte oldAlpha = target.get(targetX, targetY).a;
787                     const ubyte newAlpha = ubyte.max * pixel / pixel.max;
788 
789                     if (newAlpha >= oldAlpha)
790                         target.set(targetX, targetY, PalettedColor(paletteIndex, newAlpha));
791 
792                 }
793 
794             }
795             
796             // Advance pen positon
797             penPosition += Vector2(face.glyph.advance.tupleof) / 64;
798 
799         }
800 
801     }
802 
803 }
804 
805 unittest {
806 
807     auto image = generateColorImage(10, 10, color("#fff"));
808     auto tf = FreetypeTypeface.defaultTypeface;
809     tf.setSize(Vector2(96, 96), 14.pt);
810     tf.indentWidth = cast(int) (tf.advance(' ').x * 4);
811 
812     Vector2 measure(string text) {
813 
814         Vector2 penPosition;
815         tf.drawLine(image, penPosition, Rope(text), 0);
816         return penPosition;
817 
818     }
819 
820     // Draw 4 spaces to use as reference in the test
821     const indentReference = measure("    ");
822 
823     assert(indentReference.x > 0);
824     assert(indentReference.x == tf.advance(' ').x * 4);
825     assert(indentReference.x == tf.indentWidth);
826 
827     assert(measure("\t") == indentReference);
828     assert(measure("a\t") == indentReference);
829 
830     const doubleAIndent = measure("aa").x > indentReference.x
831         ? 2
832         : 1;
833     const tripleAIndent = measure("aaa").x > doubleAIndent * indentReference.x
834         ? doubleAIndent + 1
835         : doubleAIndent;
836 
837     assert(measure("aa\t")  == indentReference * doubleAIndent);
838     assert(measure("aaa\t") == indentReference * tripleAIndent);
839     assert(measure("\t\t") == indentReference * 2);
840     assert(measure("a\ta\t") == indentReference * 2);
841     assert(measure("aa\taa\t") == 2 * indentReference * doubleAIndent);
842 
843 }
844 
845 version (BindFT_Dynamic)
846 shared static this() @system {
847 
848     // Ignore if freetype was already loaded
849     if (isFreeTypeLoaded) return;
850 
851     // Load freetype
852     FTSupport ret = loadFreeType();
853 
854     // Check version
855     if (ret != ftSupport) {
856 
857         if (ret == FTSupport.noLibrary) {
858 
859             assert(false, "freetype2 failed to load");
860 
861         }
862         else if (FTSupport.badLibrary) {
863 
864             assert(false, format!"found freetype2 is of incompatible version %s (needed %s)"(ret, ftSupport));
865 
866         }
867 
868     }
869 
870 }
871 
872 /// Get the thread-local freetype reference.
873 FT_Library freetype() @trusted {
874 
875     static FT_Library library;
876 
877     // Load the library
878     if (library != library.init) return library;
879 
880     if (auto error = FT_Init_FreeType(&library)) {
881 
882         assert(false, format!"Failed to load freetype2: %s"(error));
883 
884     }
885 
886     return library;
887 
888 }