1 /**
2 Font matching, font selection.
3 
4 Copyright: Guillaume Piolat 2018.
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 module printed.font.fontregistry;
8 
9 import std.algorithm;
10 import std.file;
11 import std.array;
12 import std.uni;
13 import std..string: format;
14 import std.math: abs;
15 import standardpaths;
16 
17 import printed.font.opentype;
18 
19 //debug = displayParsedFonts;
20 
21 /// FontRegistry register partial font information for all fonts
22 /// from the system directories, plus the ones added by the user.
23 /// Aggregates all fonts by family, a bit like a browser or Word does.
24 /// This allows to get one particular physical font with just a family
25 /// name, an approximate weight etc. Without such font-matching, it's
26 /// impractical and you need to give explicit ttf files manually.
27 class FontRegistry
28 {
29     /// Create a font registry, parsing every available fonts.
30     this()
31     {
32         // Extract all known fonts from system directories
33         foreach(fontFile; listAllFontFiles())
34             registerFontFile(fontFile);
35     }
36 
37     /// Add a font file to parse.
38     /// Registers every font within that file.
39     /// Important: the file must outlive the `FontRegistry` itself.
40     void registerFontFile(string pathToTrueTypeFontFile)
41     {
42         ubyte[] fileContents = cast(ubyte[]) std.file.read(pathToTrueTypeFontFile);
43 
44         try
45         {
46             auto fontFile = new OpenTypeFile(fileContents);
47             scope(exit) fontFile.destroy();
48 
49             foreach(fontIndex; 0..fontFile.numberOfFonts)
50             {
51                 auto font = new OpenTypeFont(fontFile, fontIndex);
52                 scope(exit) font.destroy;
53 
54                 KnownFont kf;
55                 kf.filePath = pathToTrueTypeFontFile;
56                 kf.fontIndex = fontIndex;
57                 kf.familyName = font.familyName;
58                 kf.style = font.style;
59                 kf.weight = font.weight;
60                 _knownFonts ~= kf;
61 
62                 debug(displayParsedFonts)
63                 {
64                     import std.stdio;
65                     writefln("Family name: %s", font.familyName);
66                     writefln("SubFamily name: %s", font.subFamilyName);
67                     writefln("Weight extracted: %s", font.weight);
68                     writefln("Style: %s", font.style());
69                     writefln("Monospace: %s\n", font.isMonospaced());
70                 }
71             }
72         }
73         catch(Exception e)
74         {
75             // For now we consider we shouldn't have unparseable fonts
76         }
77     }
78 
79     /// Returns: a font which best follows the requested characteristics given.
80     OpenTypeFont findBestMatchingFont(string familyName, 
81                                       OpenTypeFontWeight weight, 
82                                       OpenTypeFontStyle style)
83     {
84         KnownFont* best = null;
85         float bestScore = float.infinity;
86 
87         familyName = toLower(familyName);
88 
89         foreach(ref kf; _knownFonts)
90         {
91             // FONT MATCHING HEURISTIC HERE
92             // unlike CSS we don't consider the "current char"
93             // the lower, the better
94             float score = 0;
95 
96             if (familyName != toLower(kf.familyName))
97                 score += 100000; // no matching family name
98 
99             score += abs(weight - kf.weight); // weight difference
100 
101             if (style != kf.style)
102             {
103                 // not a big problem to choose oblique and italic interchangeably
104                 if (style == OpenTypeFontStyle.oblique && kf.style == OpenTypeFontStyle.italic)
105                     score += 1;
106                 else if (style == OpenTypeFontStyle.italic && kf.style == OpenTypeFontStyle.oblique)
107                     score += 1;
108                 else
109                     score += 10000;
110             }
111 
112             if (score < bestScore)
113             {
114                 best = &kf;
115                 bestScore = score;
116             }
117         }
118 
119         if (best is null)
120             throw new Exception(format("No matching font found for '%s'.", familyName));
121 
122         return best.getParsedFont();
123     }
124 
125     auto knownFonts() @property const { return _knownFonts; }
126 
127 private:
128 
129     // Describe a single font registered somewhere, with the information needed
130     // to parse it back.
131     // This is all what we keep before the font is requested,
132     // to avoid keeping unparsed fonts in memory.
133     struct KnownFont
134     {
135         string filePath; // path to the font file
136         int fontIndex;   // index into that font file, which could contain multiple fonts
137         string familyName;
138         OpenTypeFontStyle style;
139         OpenTypeFontWeight weight;
140         OpenTypeFont instance;
141 
142         OpenTypeFont getParsedFont() // opens and parses that font, lazily
143         {
144             if (instance is null)
145             {
146                 ubyte[] fileContents = cast(ubyte[]) std.file.read(filePath);
147                 auto file = new OpenTypeFile(fileContents); // TODO: cache those files too
148                 instance = new OpenTypeFont(file, fontIndex);
149             }
150             return instance;
151         }
152 
153         void releaseParsedFont()
154         {
155             instance = null;
156         }
157     }
158 
159     /// A list of descriptor for all known fonts to the registry.
160     KnownFont[] _knownFonts;
161 
162     /// Get a list of system font directories
163     static private string[] getFontDirectories()
164     {
165         return standardPaths(StandardPath.fonts);
166     }
167 
168     /// Gives back a list of absolute pathes of .ttf files we know about
169     static string[] listAllFontFiles()
170     {
171         string[] listAllLocalFontFiles()
172         {
173             string[] fontAbsolutepathes;
174 
175             foreach(fontDir; getFontDirectories())
176             {
177                 if (!fontDir.exists) continue;
178                 auto files = dirEntries(fontDir, SpanMode.breadth);
179                 foreach(f; files)
180                     if (hasFontExt(f.name))
181                         fontAbsolutepathes ~= f.name;
182             }
183             return fontAbsolutepathes;
184         }
185 
186         string[] listAllSystemFontFiles()
187         {
188             string[] fontAbsolutepathes;
189 
190             foreach(fontDir; getFontDirectories())
191             {
192                 if (!fontDir.exists) continue;
193                 auto files = dirEntries(fontDir, SpanMode.breadth);
194                 foreach(f; files)
195                     if (hasFontExt(f.name))
196                         fontAbsolutepathes ~= f.name;
197             }
198             return fontAbsolutepathes;
199         }
200 
201         return listAllLocalFontFiles() ~ listAllSystemFontFiles();
202     }
203 }
204 
205 unittest
206 {
207     auto registry = new FontRegistry();
208 
209     foreach(FontRegistry.KnownFont font; registry._knownFonts)
210     {
211         font.getParsedFont().ascent(); // This will parse all fonts metrics on the system, thus validating CMAP parsing etc.
212         font.releaseParsedFont(); // else it takes so much memory one could crash
213     }
214 
215     registry.destroy();
216 }
217 
218 /// Returns: A global, lazily constructed font registry.
219 // TODO: synchronization
220 FontRegistry theFontRegistry()
221 {
222     __gshared FontRegistry globalFontRegistry;
223     if (globalFontRegistry is null)
224         globalFontRegistry = new FontRegistry();
225 
226     return globalFontRegistry;
227 }
228 
229 private:
230 
231 
232 static bool hasFontExt(string path)
233 {
234     if (path.length < 4)
235         return false;
236 
237     string ext = path[$-4..$];
238 
239     if (ext == ".ttf" || ext == ".ttc"
240      || ext == ".otf" || ext == ".otc")
241         return true; // This is very likely a font
242 
243     return false;
244 }
245