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