The OpenD Programming Language

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