source: appstream-generator/src/asgen/font.d @ 4841

Last change on this file since 4841 was 4841, checked in by Juanma, 2 years ago

Initial release

File size: 13.4 KB
Line 
1/*
2 * Copyright (C) 2016 Matthias Klumpp <matthias@tenstral.net>
3 *
4 * Licensed under the GNU Lesser General Public License Version 3
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation, either version 3 of the license, or
9 * (at your option) any later version.
10 *
11 * This software is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
18 */
19
20module asgen.font;
21
22import std.string : format, fromStringz, toStringz, toLower, strip;
23import std.conv : to;
24import std.path : buildPath, baseName;
25import std.array : empty, appender, replace;
26import std.algorithm : countUntil, remove;
27static import std.file;
28
29import asgen.bindings.freetype;
30import asgen.bindings.fontconfig;
31import asgen.bindings.pango;
32
33import asgen.logging;
34import asgen.config : Config;
35
36
37// NOTE: The font's full-name (and the family-style combo we use if the full name is unavailable), can be
38// determined on the command-line via:
39// fc-query --format='FN: %{fullname}\nFS: %{family[0]} %{style[0]}\n' <fontfile>
40
41private static __gshared string[string] iconTexts;
42
43// initialize module static data
44shared static this ()
45{
46    if (iconTexts.length != 0)
47        return;
48    synchronized
49        iconTexts = ["en": "Aa",
50                     "ar": "أب",
51                     "as": "অআই",
52                     "bn": "অআই",
53                     "be": "Аа",
54                     "bg": "Аа",
55                     "cs": "Aa",
56                     "da": "Aa",
57                     "de": "Aa",
58                     "es": "Aa",
59                     "fr": "Aa",
60                     "gu": "અબક",
61                     "hi": "अआइ",
62                     "he": "אב",
63                     "it": "Aa",
64                     "kn": "ಅಆಇ",
65                     "ml": "ആഇ",
66                     "ne": "अआइ",
67                     "nl": "Aa",
68                     "or": "ଅଆଇ",
69                     "pa": "ਅਆਇ",
70                     "pl": "ĄĘ",
71                     "pt": "Aa",
72                     "ru": "Аа",
73                     "sv": "Åäö",
74                     "ta": "அஆஇ",
75                     "te": "అఆఇ",
76                     "ua": "Аа",
77                     "zh-tw": "漢"];
78}
79
80final class Font
81{
82
83private:
84
85    FT_Library library;
86    FT_Face fface;
87
88    string[] languages_;
89    string sampleText_;
90    string sampleIconText_;
91
92    string style_;
93    string fullname_;
94
95    immutable string fileBaseName;
96
97public:
98
99    this (string fname)
100    {
101        // NOTE: Freetype is completely non-threadsafe, but we only use it in the constructor.
102        // So mark this section of code as synchronized to never run it in parallel (even having
103        // two Font objects constructed in parallel may lead to errors)
104        synchronized {
105            initFreeType ();
106
107            FT_Error err;
108            err = FT_New_Face (library, fname.toStringz (), 0, &fface);
109            if (err != 0)
110                throw new Exception ("Unable to load font face from file. Error code: %s".format (err));
111
112                loadFontConfigData (fname);
113                fileBaseName = fname.baseName;
114        }
115    }
116
117    this (const(ubyte)[] data, string fileBaseName)
118    {
119        import std.stdio : File;
120
121        // we unfortunately need to create a stupid temporary file here, otherwise Fontconfig
122        // does not work and we can not determine the right demo strings for this font.
123        // (FreeType itself could load from memory)
124        immutable tmpRoot = Config.get ().getTmpDir;
125        std.file.mkdirRecurse (tmpRoot);
126        immutable fname = buildPath (tmpRoot, fileBaseName);
127        auto f = File (fname, "w");
128        f.rawWrite (data);
129        f.close ();
130
131        this (fname);
132    }
133
134    ~this ()
135    {
136        release ();
137    }
138
139    void release ()
140    {
141        if (fface !is null)
142            FT_Done_Face (fface);
143        if (library !is null)
144            FT_Done_Library (library);
145
146        fface = null;
147        library = null;
148    }
149
150    private bool ready () const
151    {
152        return fface !is null && library !is null;
153    }
154
155    private void initFreeType ()
156    {
157        library = null;
158        fface = null;
159        FT_Error err;
160
161        err = FT_Init_FreeType (&library);
162        if (err != 0)
163            throw new Exception ("Unable to load FreeType. Error code: %s".format (err));
164    }
165
166    private void loadFontConfigData (string fname)
167    {
168        // open FC font patter
169        // the count pointer has to be valid, otherwise FcFreeTypeQuery() crashes.
170        int c;
171        auto fpattern = FcFreeTypeQuery (fname.toStringz, 0, null, &c);
172        scope (exit) FcPatternDestroy (fpattern);
173
174        // load information about the font
175        auto res = appender!(string[]);
176
177        auto anyLangAdded = false;
178        auto match = true;
179        for (uint i = 0; match == true; i++) {
180            FcLangSet *ls;
181
182            match = false;
183            if (FcPatternGetLangSet (fpattern, FC_LANG, i, &ls) == FcResult.Match) {
184                match = true;
185                auto langs = FcLangSetGetLangs (ls);
186                auto list = FcStrListCreate (langs);
187                scope (exit) {
188                    FcStrListDone (list);
189                    FcStrSetDestroy (langs);
190                }
191
192                char *tmp;
193                FcStrListFirst (list);
194                while ((tmp = FcStrListNext (list)) !is null) {
195                    res ~= to!string (tmp.fromStringz);
196                    anyLangAdded = true;
197                }
198            }
199        }
200
201        char *fullNameVal;
202        if (FcPatternGetString (fpattern, FC_FULLNAME, 0, &fullNameVal) == FcResult.Match) {
203            fullname_ = fullNameVal.fromStringz.dup;
204        }
205
206        char *styleVal;
207        if (FcPatternGetString (fpattern, FC_STYLE, 0, &styleVal) == FcResult.Match) {
208            style_ = styleVal.fromStringz.dup;
209        }
210
211        // assume 'en' is available
212        if (!anyLangAdded)
213            res ~= "en";
214        languages_ = res.data;
215
216        // prefer the English language if possible
217        // this is a hack since some people don't set their
218        // <languages> tag properly.
219        immutable enIndex = languages_.countUntil ("en");
220        if (anyLangAdded && enIndex > 0) {
221            languages_ = languages_.remove (enIndex);
222            languages_ = "en" ~ languages_;
223        }
224    }
225
226    @property
227    string family ()
228    {
229        assert (ready ());
230        return to!string (fface.family_name.fromStringz);
231    }
232
233    @property
234    string style ()
235    {
236        return style_;
237    }
238
239    @property
240    string fullName ()
241    {
242        if (fullname_.empty)
243            return "%s %s".format (family, style);
244        else
245            return fullname_;
246    }
247
248    @property
249    string id ()
250    {
251        import std.string;
252
253        if (this.family is null)
254            return fileBaseName;
255        if (this.style is null)
256            return fileBaseName;
257        return "%s-%s".format (this.family.strip.toLower.replace (" ", ""),
258                               this.style.strip.toLower.replace (" ", ""));
259    }
260
261    @property
262    FT_Encoding charset ()
263    {
264        assert (ready ());
265        if (fface.num_charmaps == 0)
266            return FT_ENCODING_NONE;
267
268        return fface.charmaps[0].encoding;
269    }
270
271    @property
272    const(FT_Face) fontFace () const
273    {
274        assert (ready ());
275        return fface;
276    }
277
278    @property
279    const(string[]) languages () const
280    {
281        return languages_;
282    }
283
284    private void findSampleTexts ()
285    {
286        assert (ready ());
287        import std.uni : byGrapheme, isGraphical, byCodePoint, Grapheme;
288        import std.range;
289
290        void setFallbackSampleTextIfRequired ()
291        {
292            if (sampleText_.empty)
293                sampleText_ = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.";
294
295            if (sampleIconText_.empty) {
296                import std.conv : text;
297
298                auto graphemes = sampleText_.byGrapheme;
299                if (graphemes.walkLength > 3)
300                    sampleIconText_ = graphemes.array[0..3].byCodePoint.text;
301                else
302                    sampleIconText_ = "Aa";
303            }
304        }
305
306        dchar getFirstUnichar (string str)
307        {
308            auto g = Grapheme (str);
309            return g[0];
310        }
311
312        // determine our sample texts
313        foreach (ref lang; this.languages) {
314            auto plang = pango_language_from_string (lang.toStringz);
315            auto text = pango_language_get_sample_string (plang).fromStringz;
316
317                        if (text is null)
318                                continue;
319
320            sampleText_ = text.dup;
321            auto itP = lang in iconTexts;
322            if (itP !is null) {
323                sampleIconText_ = *itP;
324                break;
325            }
326                }
327
328        // set some default values if we have been unable to find any texts
329        setFallbackSampleTextIfRequired ();
330
331        // check if we have a font that can actually display the characters we picked - in case
332        // it doesn't, we just select random chars.
333        if (FT_Get_Char_Index (fface, getFirstUnichar (sampleIconText_)) == 0) {
334            sampleText_ = "☃❤✓☀★☂♞☯☢∞❄♫↺";
335            sampleIconText_ = "☃❤";
336        }
337        if (FT_Get_Char_Index (fface, getFirstUnichar (sampleIconText_)) == 0) {
338            import std.uni;
339            import std.utf : toUTF8;
340
341            sampleText_ = "";
342            sampleIconText_ = "";
343
344            auto count = 0;
345            for (uint map = 0; map < fface.num_charmaps; map++) {
346                auto charmap = fface.charmaps[map];
347
348                FT_Set_Charmap (fface, charmap);
349
350                FT_UInt gindex;
351                auto charcode = FT_Get_First_Char (fface, &gindex);
352                while (gindex != 0) {
353                    immutable chc = to!dchar (charcode);
354                    if (chc.isGraphical && !chc.isSpace && !chc.isPunctuation) {
355                        count++;
356                        sampleText_ ~= chc;
357                    }
358
359                    if (count >= 24)
360                        break;
361                    charcode = FT_Get_Next_Char (fface, charcode, &gindex);
362                }
363
364                if (count >= 24)
365                    break;
366            }
367
368            sampleText_ = sampleText_.strip;
369
370            // if we were unsuccessful at adding chars, set fallback again
371            // (and in this case, also set the icon text to something useful again)
372            setFallbackSampleTextIfRequired ();
373        }
374    }
375
376    @property
377    string sampleText ()
378    {
379        if (sampleText_.empty)
380            findSampleTexts ();
381        return sampleText_;
382    }
383
384    @property
385    void sampleText (string val)
386    {
387        if (val.length > 2)
388            sampleText_ = val;
389    }
390
391    @property
392    string sampleIconText ()
393    {
394        if (sampleIconText_.empty)
395            findSampleTexts ();
396        return sampleIconText_;
397    }
398
399    @property
400    void sampleIconText (string val)
401    {
402        if (val.length <= 3)
403            sampleIconText_ = val;
404    }
405}
406
407unittest
408{
409    import std.stdio : writeln, File;
410    import std.path : buildPath;
411    import asgen.utils : getTestSamplesDir;
412    writeln ("TEST: ", "Font");
413
414    immutable fontFile = buildPath (getTestSamplesDir (), "NotoSans-Regular.ttf");
415
416    // test reading from file
417    auto font = new Font (fontFile);
418    assert (font.family == "Noto Sans");
419    assert (font.style == "Regular");
420
421    ubyte[] data;
422    auto f = File (fontFile, "r");
423    while (!f.eof) {
424        char[512] buf;
425        data ~= f.rawRead (buf);
426    }
427
428    // test reading from memory
429    font = new Font (data, "test.ttf");
430    assert (font.family == "Noto Sans");
431    assert (font.style == "Regular");
432    assert (font.charset == FT_ENCODING_UNICODE);
433
434    writeln (font.languages);
435    assert (font.languages == ["en", "aa", "ab", "af", "ak", "an", "ast", "av", "ay", "az-az", "ba", "be", "ber-dz", "bg", "bi", "bin", "bm", "br", "bs",
436                               "bua", "ca", "ce", "ch", "chm", "co", "crh", "cs", "csb", "cu", "cv", "cy", "da", "de", "ee", "el", "eo", "es", "et", "eu",
437                               "fat", "ff", "fi", "fil", "fj", "fo", "fr", "fur", "fy", "ga", "gd", "gl", "gn", "gv", "ha", "haw", "ho", "hr", "hsb", "ht",
438                               "hu", "hz", "ia", "id", "ie", "ig", "ik", "io", "is", "it", "jv", "kaa", "kab", "ki", "kj", "kk", "kl", "kr", "ku-am", "ku-tr",
439                               "kum", "kv", "kw", "kwm", "ky", "la", "lb", "lez", "lg", "li", "ln", "lt", "lv", "mg", "mh", "mi", "mk", "mn-mn", "mo", "ms", "mt",
440                               "na", "nb", "nds", "ng", "nl", "nn", "no", "nr", "nso", "nv", "ny", "oc", "om", "os", "pap-an", "pap-aw", "pl", "pt", "qu", "quz",
441                               "rm", "rn", "ro", "ru", "rw", "sah", "sc", "sco", "se", "sel", "sg", "sh", "shs", "sk", "sl", "sm", "sma", "smj", "smn", "sms", "sn",
442                               "so", "sq", "sr", "ss", "st", "su", "sv", "sw", "tg", "tk", "tl", "tn", "to", "tr", "ts", "tt", "tw", "ty", "tyv", "uk", "uz", "ve",
443                               "vi", "vo", "vot", "wa", "wen", "wo", "xh", "yap", "yo", "za", "zu"]);
444}
Note: See TracBrowser for help on using the repository browser.