1 /// freetype fonts support
2 module dlangui.graphics.ftfonts;
3 
4 import dlangui.graphics.fonts;
5 
6 import derelict.freetype.ft;
7 private import dlangui.core.logger;
8 private import std.algorithm;
9 private import std.file;
10 private import std.string;
11 private import std.utf;
12 
13 private struct FontDef {
14     immutable FontFamily _family;
15     immutable string _face;
16 	immutable bool _italic;
17 	immutable int _weight;
18 	@property FontFamily family() { return _family; }
19 	@property string face() { return _face; }
20 	@property bool italic() { return _italic; }
21 	@property int weight() { return _weight; }
22 	this(FontFamily family, string face, bool italic, int weight) {
23 		_family = family;
24 		_face = face;
25 		_italic = italic;
26         _weight = weight;
27 	}
28     const bool opEquals(ref const FontDef v) {
29         return _family == v._family && _italic == v._italic && _weight == v._weight && _face.equal(v._face);
30     }
31     const hash_t toHash() const nothrow @safe {
32         hash_t res = 123;
33         res = res * 31 + cast(hash_t)_italic;
34         res = res * 31 + cast(hash_t)_weight;
35         res = res * 31 + cast(hash_t)_family;
36         res = res * 31 + typeid(_face).getHash(&_face);
37         return res;
38     }
39 }
40 
41 private class FontFileItem {
42 	private FontList _activeFonts;
43     private FT_Library _library;
44     private FontDef _def;
45     string[] _filenames;
46     @property ref FontDef def() { return _def; }
47     @property string[] filenames() { return _filenames; }
48     @property FT_Library library() { return _library; }
49     void addFile(string fn) {
50         // check for duplicate entry
51         foreach (ref string existing; _filenames)
52             if (fn.equal(existing))
53                 return;
54         _filenames ~= fn;
55     }
56     this(FT_Library library, ref FontDef def) {
57         _library = library;
58         _def = def;
59     }
60 
61     private FontRef _nullFontRef;
62     ref FontRef get(int size) {
63         int index = _activeFonts.find(size);
64         if (index >= 0)
65             return _activeFonts.get(index);
66         FreeTypeFont font = new FreeTypeFont(this, size);
67         if (!font.create()) {
68             destroy(font);
69             return _nullFontRef;
70         }
71         return _activeFonts.add(font);
72     }
73 
74 }
75 
76 private class FreeTypeFontFile {
77     private string _filename;
78     private string _faceName;
79     private FT_Library    _library;
80     private FT_Face       _face;
81     private FT_GlyphSlot  _slot;
82     private FT_Matrix     _matrix;                 /* transformation matrix */
83 
84     @property FT_Library library() { return _library; }
85 
86     private int _height;
87     private int _size;
88     private int _baseline;
89     private int _weight;
90     private bool _italic;
91 
92     /// filename
93     @property string filename() { return _filename; }
94     // properties as detected after opening of file
95     @property string face() { return _faceName; }
96     @property int height() { return _height; }
97     @property int size() { return _size; }
98     @property int baseline() { return _baseline; }
99     @property int weight() { return _weight; }
100     @property bool italic() { return _italic; }
101 
102 	//private static int _instanceCount;
103     this(FT_Library library, string filename) {
104         _library = library;
105         _filename = filename;
106         _matrix.xx = 0x10000;
107         _matrix.yy = 0x10000;
108         _matrix.xy = 0;
109         _matrix.yx = 0;
110 		//Log.d("Created FreeTypeFontFile, count=", ++_instanceCount);
111     }
112 
113 	~this() {
114         clear();
115 		//Log.d("Destroyed FreeTypeFontFile, count=", --_instanceCount);
116     }
117 
118     private static string familyName(FT_Face face)
119     {
120         string faceName = fromStringz(face.family_name);
121         string styleName = fromStringz(face.style_name);
122         if (faceName.equal("Arial") && styleName.equal("Narrow"))
123             faceName ~= " Narrow";
124         else if (styleName.equal("Condensed"))
125             faceName ~= " Condensed";
126         return faceName;
127     }
128 
129     /// open face with specified size
130     bool open(int size, int index = 0) {
131         int error = FT_New_Face( _library, _filename.toStringz, index, &_face); /* create face object */
132         if (error)
133             return false;
134         if ( _filename.endsWith(".pfb") || _filename.endsWith(".pfa") ) {
135         	string kernFile = _filename[0 .. $ - 4];
136             if (exists(kernFile ~ ".afm")) {
137         		kernFile ~= ".afm";
138             } else if (exists(kernFile ~ ".pfm" )) {
139         		kernFile ~= ".pfm";
140         	} else {
141         		kernFile.clear();
142         	}
143         	if (kernFile.length > 0)
144         		error = FT_Attach_File(_face, kernFile.toStringz);
145         }
146         Log.d("Font file opened successfully");
147         _slot = _face.glyph;
148         _faceName = familyName(_face);
149         error = FT_Set_Pixel_Sizes(
150                 _face,    /* handle to face object */
151                 0,        /* pixel_width           */
152                 size );  /* pixel_height          */
153         if (error) {
154             clear();
155             return false;
156         }
157         _height = cast(int)(_face.size.metrics.height >> 6);
158         _size = size;
159         _baseline = _height + cast(int)(_face.size.metrics.descender >> 6);
160         _weight = _face.style_flags & FT_STYLE_FLAG_BOLD ? FontWeight.Bold : FontWeight.Normal;
161         _italic = _face.style_flags & FT_STYLE_FLAG_ITALIC ? true : false;
162         Log.d("Opened font face=", _faceName, " height=", _height, " size=", size, " weight=", weight, " italic=", italic);
163         return true; // successfully opened
164     }
165 
166 	/// find some suitable replacement for important characters missing in font
167     static dchar getReplacementChar(dchar code) {
168         switch (code) {
169             case UNICODE_SOFT_HYPHEN_CODE:
170                 return '-';
171             case 0x0401: // CYRILLIC CAPITAL LETTER IO
172                 return 0x0415; //CYRILLIC CAPITAL LETTER IE
173             case 0x0451: // CYRILLIC SMALL LETTER IO
174                 return 0x0435; // CYRILLIC SMALL LETTER IE
175             case UNICODE_NO_BREAK_SPACE:
176                 return ' ';
177             case 0x2010:
178             case 0x2011:
179             case 0x2012:
180             case 0x2013:
181             case 0x2014:
182             case 0x2015:
183                 return '-';
184             case 0x2018:
185             case 0x2019:
186             case 0x201a:
187             case 0x201b:
188                 return '\'';
189             case 0x201c:
190             case 0x201d:
191             case 0x201e:
192             case 0x201f:
193             case 0x00ab:
194             case 0x00bb:
195                 return '\"';
196             case 0x2039:
197                 return '<';
198             case 0x203A:
199                 return '>';
200             case 0x2044:
201                 return '/';
202             case 0x2022: // css_lst_disc:
203                 return '*';
204             case 0x26AA: // css_lst_disc:
205             case 0x25E6: // css_lst_disc:
206             case 0x25CF: // css_lst_disc:
207                 return 'o';
208             case 0x25CB: // css_lst_circle:
209                 return '*';
210             case 0x25A0: // css_lst_square:
211                 return '-';
212             default:
213                 return 0;
214         }
215     }
216 
217     /// find glyph index for character
218     FT_UInt getCharIndex(dchar code, dchar def_char = 0) {
219         if ( code=='\t' )
220             code = ' ';
221         FT_UInt ch_glyph_index = FT_Get_Char_Index(_face, code);
222         if (ch_glyph_index == 0) {
223             dchar replacement = getReplacementChar(code);
224             if (replacement)
225                 ch_glyph_index = FT_Get_Char_Index(_face, replacement);
226             if (ch_glyph_index == 0 && def_char)
227                 ch_glyph_index = FT_Get_Char_Index( _face, def_char );
228         }
229         return ch_glyph_index;
230     }
231 
232     /// retrieve glyph information, filling glyph struct; returns false if glyph not found
233     bool getGlyphInfo(dchar code, ref Glyph glyph, dchar def_char, bool withImage = true)
234     {
235         //FONT_GUARD
236         int glyph_index = getCharIndex(code, def_char);
237         int flags = FT_LOAD_DEFAULT;
238         const bool _drawMonochrome = false;
239         flags |= (!_drawMonochrome ? FT_LOAD_TARGET_NORMAL : FT_LOAD_TARGET_MONO);
240         if (withImage)
241             flags |= FT_LOAD_RENDER;
242         //if (_hintingMode == HINTING_MODE_AUTOHINT)
243         //    flags |= FT_LOAD_FORCE_AUTOHINT;
244         //else if (_hintingMode == HINTING_MODE_DISABLED)
245         //    flags |= FT_LOAD_NO_AUTOHINT | FT_LOAD_NO_HINTING;
246         int error = FT_Load_Glyph(
247                                   _face,          /* handle to face object */
248                                   glyph_index,   /* glyph index           */
249                                   flags );  /* load flags, see below */
250         if ( error )
251             return false;
252         glyph.lastUsage = 1;
253         glyph.blackBoxX = cast(ubyte)(_slot.metrics.width >> 6);
254         glyph.blackBoxY = cast(ubyte)(_slot.metrics.height >> 6);
255         glyph.originX =   cast(byte)(_slot.metrics.horiBearingX >> 6);
256         glyph.originY =   cast(byte)(_slot.metrics.horiBearingY >> 6);
257         glyph.width =     cast(ubyte)(myabs(cast(int)(_slot.metrics.horiAdvance)) >> 6);
258 		glyph.glyphIndex = cast(ushort)code;
259         if (withImage) {
260             FT_Bitmap*  bitmap = &_slot.bitmap;
261             ubyte w = cast(ubyte)(bitmap.width);
262             ubyte h = cast(ubyte)(bitmap.rows);
263             glyph.blackBoxX = w;
264             glyph.blackBoxY = h;
265             int sz = w * cast(int)h;
266             if (sz > 0) {
267                 glyph.glyph = new ubyte[sz];
268                 for (int i = 0; i < sz; i++)
269                     glyph.glyph[i] = bitmap.buffer[i];
270             }
271             version (USE_OPENGL) {
272                 glyph.id = nextGlyphId();
273             }
274         }
275         return true;
276     }
277 
278     @property bool isNull() {
279         return (_face is null);
280     }
281 
282     void clear() {
283         if (_face !is null)
284             FT_Done_Face(_face);
285         _face = null;
286     }
287 
288 }
289 
290 /**
291 * Font implementation based on Win32 API system fonts.
292 */
293 class FreeTypeFont : Font {
294     private FontFileItem _fontItem;
295     private FreeTypeFontFile[] _files;
296 
297 	static int _instanceCount;
298 	/// need to call create() after construction to initialize font
299     this(FontFileItem item, int size) {
300         _fontItem = item;
301         _size = size;
302         _height = size;
303 		Log.d("Created font, count=", ++_instanceCount);
304     }
305 
306 	/// do cleanup
307 	~this() {
308 		clear();
309 		Log.d("Destroyed font, count=", --_instanceCount);
310 	}
311 	
312     private int _size;
313     private int _height;
314 
315 	private GlyphCache _glyphCache;
316 
317 
318 	/// cleanup resources
319     override void clear() {
320         foreach(ref FreeTypeFontFile file; _files) {
321             destroy(file);
322             file = null;
323         }
324         _files.clear();
325     }
326 
327 	uint getGlyphIndex(dchar code)
328 	{
329         return 0;
330 	}
331 
332     /// find glyph index for character
333     bool findGlyph(dchar code, dchar def_char, ref FT_UInt index, ref FreeTypeFontFile file) {
334         foreach(FreeTypeFontFile f; _files) {
335             index = f.getCharIndex(code, def_char);
336             if (index != 0) {
337                 file = f;
338                 return true;
339             }
340         }
341         return false;
342     }
343 
344     private Glyph tmpGlyphInfo;
345 	override Glyph * getCharGlyph(dchar ch, bool withImage = true) {
346         if (ch > 0xFFFF) // do not support unicode chars above 0xFFFF - due to cache limitations
347             return null;
348         long measureStart = std.datetime.Clock.currStdTime;
349 		Glyph * found = _glyphCache.find(cast(ushort)ch);
350         long measureEnd = std.datetime.Clock.currStdTime;
351         long duration = measureEnd - measureStart;
352         //if (duration > 10000)
353         if (duration > 10000)
354             Log.d("ft _glyphCache.find took ", duration / 10, " ns");
355 		if (found !is null)
356 			return found;
357         Log.v("Glyph ", ch, " is not found in cache, getting from font");
358         FT_UInt index;
359         FreeTypeFontFile file;
360         if (!findGlyph(ch, 0, index, file)) {
361             if (!findGlyph(ch, '?', index, file))
362                 return null;
363         }
364         if (!file.getGlyphInfo(ch, tmpGlyphInfo, 0, withImage))
365             return null;
366         if (withImage)
367 		    return _glyphCache.put(cast(ushort)ch, &tmpGlyphInfo);
368         return &tmpGlyphInfo;
369 	}
370 
371 	// draw text string to buffer
372 	override void drawText(DrawBuf buf, int x, int y, const dchar[] text, uint color) {
373 		int[] widths;
374         int bl = baseline;
375         int xx = 0;
376 		for (int i = 0; i < text.length; i++) {
377 			Glyph * glyph = getCharGlyph(text[i], true);
378 			if (glyph is null)
379 				continue;
380 			if ( glyph.blackBoxX && glyph.blackBoxY ) {
381                 int x0 = x + xx + glyph.originX;
382                 int y0 = y + bl - glyph.originY;
383                 if (x0 > buf.width)
384                     break; // outside right bound
385                 Rect rc = Rect(x0, y0, x0 + glyph.blackBoxX, y0 + glyph.blackBoxY);
386                 if (buf.applyClipping(rc))
387 				    buf.drawGlyph( x0,
388                                y0,
389                               glyph,
390                               color);
391 			}
392             xx += glyph.width;
393 		}
394 	}
395 
396 	override int measureText(const dchar[] text, ref int[] widths, int maxWidth) {
397 		if (text.length == 0)
398 			return 0;
399 		const dchar * pstr = text.ptr;
400 		uint len = cast(uint)text.length;
401         int x = 0;
402         int charsMeasured = 0;
403         int * pwidths = widths.ptr;
404 		for (int i = 0; i < len; i++) {
405             //auto measureStart = std.datetime.Clock.currAppTick;
406 			Glyph * glyph = getCharGlyph(pstr[i], true); // TODO: what is better
407             //auto measureEnd = std.datetime.Clock.currAppTick;
408             //auto duration = measureEnd - measureStart;
409             //if (duration.length > 10)
410             //    Log.d("ft measureText took ", duration.length, " ticks");
411 			if (glyph is null) {
412                 // if no glyph, use previous width - treat as zero width
413                 pwidths[i] = i > 0 ? pwidths[i-1] : 0;
414 				continue;
415             }
416             int w = x + glyph.width; // using advance
417             int w2 = x + glyph.originX + glyph.blackBoxX; // using black box
418             if (w < w2) // choose bigger value
419                 w = w2;
420             pwidths[i] = w;
421             x += glyph.width;
422             charsMeasured = i + 1;
423             if (x > maxWidth)
424                 break;
425         }
426 		return charsMeasured;
427 	}
428 
429 	bool create() {
430         if (!isNull())
431             clear();
432         foreach (string filename; _fontItem.filenames) {
433             FreeTypeFontFile file = new FreeTypeFontFile(_fontItem.library, filename);
434             if (file.open(_size, 0)) {
435                 _files ~= file;
436             }
437         }
438 		return _files.length > 0;
439 	}
440 
441 	// clear usage flags for all entries
442 	override void checkpoint() {
443 		_glyphCache.checkpoint();
444 	}
445 
446 	// removes entries not used after last call of checkpoint() or cleanup()
447 	override void cleanup() {
448 		_glyphCache.cleanup();
449 	}
450 
451     @property override int size() { return _size; }
452     @property override int height() { return _files.length > 0 ? _files[0].height : _size; }
453     @property override int weight() { return _fontItem.def.weight; }
454     @property override int baseline() { return _files.length > 0 ? _files[0].baseline : 0; }
455     @property override bool italic() { return _fontItem.def.italic; }
456     @property override string face() { return _fontItem.def.face; }
457     @property override FontFamily family() { return _fontItem.def.family; }
458     @property override bool isNull() { return _files.length == 0; }
459 }
460 
461 
462 /// FreeType based font manager.
463 class FreeTypeFontManager : FontManager {
464 
465     private FT_Library    _library;
466     private FontFileItem[] _fontFiles;
467 
468     private FontFileItem findFileItem(ref FontDef def) {
469         foreach(FontFileItem item; _fontFiles)
470             if (item.def == def)
471                 return item;
472         return null;
473     }
474 
475     private FontFileItem findBestMatch(int weight, bool italic, FontFamily family, string face) {
476         FontFileItem best = null;
477         int bestScore = 0;
478         foreach(FontFileItem item; _fontFiles) {
479             int score = 0;
480             if (face is null || face.equal(item.def.face))
481                 score += 200; // face match
482             if (family == item.def.family)
483                 score += 100; // family match
484             if (italic == item.def.italic)
485                 score += 50; // italic match
486             int weightDiff = myabs(weight - item.def.weight);
487             score += 30 - weightDiff / 30; // weight match
488             if (score > bestScore) {
489                 bestScore = score;
490                 best = item;
491             }
492         }
493         return best;
494     }
495 
496 	//private FontList _activeFonts;
497 
498     private static FontRef _nullFontRef;
499 
500     this() {
501         // load dynaic library
502         DerelictFT.load();
503         // init library
504         int error = FT_Init_FreeType(&_library);
505         if (error) {
506             Log.e("Cannot init freetype library, error=", error);
507             throw new Exception("Cannot init freetype library");
508         }
509     }
510     ~this() {
511 		Log.d("FreeTypeFontManager ~this()");
512 		//_activeFonts.clear();
513 		foreach(ref FontFileItem item; _fontFiles) {
514 			destroy(item);
515 			item = null;
516 		}
517 		_fontFiles.length = 0;
518 		Log.d("Destroyed all fonts. Freeing library.");
519         // uninit library
520         if (_library)
521             FT_Done_FreeType(_library);
522     }
523 
524     /// get font instance with specified parameters
525     override ref FontRef getFont(int size, int weight, bool italic, FontFamily family, string face) {
526         FontFileItem f = findBestMatch(weight, italic, family, face);
527         if (f is null)
528             return _nullFontRef;
529         return f.get(size);
530     }
531 
532 	/// clear usage flags for all entries
533 	override void checkpoint() {
534     }
535 
536 	/// removes entries not used after last call of checkpoint() or cleanup()
537 	override void cleanup() {
538     }
539 
540     /// register freetype font by filename - optinally font properties can be passed if known (e.g. from libfontconfig).
541     bool registerFont(string filename, FontFamily family = FontFamily.SansSerif, string face = null, bool italic = false, int weight = 0) {
542         if (_library is null)
543             return false;
544         Log.d("FreeTypeFontManager.registerFont ", filename, " ", family, " ", face, " italic=", italic, " weight=", weight);
545         if (!exists(filename) || !isFile(filename))
546             return false;
547 
548         FreeTypeFontFile font = new FreeTypeFontFile(_library, filename);
549         if (!font.open(24)) {
550             Log.e("Failed to open font ", filename);
551             destroy(font);
552             return false;
553         }
554         
555         if (face == null || weight == 0) {
556             // properties are not set by caller
557             // get properties from loaded font
558             face = font.face;
559             italic = font.italic;
560             weight = font.weight;
561             Log.d("Using properties from font file: face=", face, " weight=", weight, " italic=", italic);
562         }
563 
564         FontDef def = FontDef(family, face, italic, weight);
565         FontFileItem item = findFileItem(def);
566         if (item is null) {
567             item = new FontFileItem(_library, def);
568             _fontFiles ~= item;
569         }
570         item.addFile(filename);
571 
572         // registered
573         return true;
574     }
575 
576 }
577 
578 private int myabs(int n) { return n >= 0 ? n : -n; }