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