using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json.Serialization; using System.Text.Json; using Quik.FreeType; using Quik.Media.Font; using Quik.PAL; using Quik.Media.Defaults.Linux; namespace Quik.Media.Defaults.Fallback { public class FallbackFontDatabase : IFontDataBase { private readonly string DbPath = Environment.GetEnvironmentVariable(EnvironmentVariables.FallbackFontDatabase) ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "QUIK/fontdb.json"); private Dictionary<FontFace, FileInfo> FilesMap { get; } = new Dictionary<FontFace, FileInfo>(); private Dictionary<string, List<FontFace>> ByFamily { get; } = new Dictionary<string, List<FontFace>>(); private Dictionary<SystemFontFamily, FontFace> SystemFonts { get; } = new Dictionary<SystemFontFamily, FontFace>(); private List<FontFace> All { get; } = new List<FontFace>(); IEnumerable<FontFace> IFontDataBase.All => this.All; public FallbackFontDatabase(bool rebuild = false) { // Load existing database if desired. List<DbEntry> database; if(!rebuild) { database = LoadDatabase(); } else { database = new List<DbEntry>(); } VerifyDatabase(database); FlushDatabase(database); (FontFace, FileInfo) serif = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SerifFont, LinuxFonts.DefaultSerifFamilies, this); SystemFonts[SystemFontFamily.Serif] = serif.Item1; (FontFace, FileInfo) sans = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SansFont, LinuxFonts.DefaultSansFamilies, this); SystemFonts[SystemFontFamily.Sans] = sans.Item1; (FontFace, FileInfo) mono = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.MonospaceFont, LinuxFonts.DefaultMonospaceFamilies, this); SystemFonts[SystemFontFamily.Monospace] = mono.Item1; (FontFace, FileInfo) cursive = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.CursiveFont, LinuxFonts.DefaultCursiveFamilies, this); SystemFonts[SystemFontFamily.Cursive] = cursive.Item1; (FontFace, FileInfo) fantasy = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.FantasyFont, LinuxFonts.DefaultFantasyFamilies, this); SystemFonts[SystemFontFamily.Fantasy] = fantasy.Item1; AddFont(serif.Item1, serif.Item2); AddFont(sans.Item1, sans.Item2); AddFont(mono.Item1, mono.Item2); AddFont(cursive.Item1, cursive.Item2); AddFont(fantasy.Item1, fantasy.Item2); database.ForEach(x => AddFont(x.Face, new FileInfo(x.FilePath))); } public FileInfo FontFileInfo(FontFace face) { if (FilesMap.TryGetValue(face, out FileInfo info)) return info; else return null; } public Stream Open(FontFace face) { return FontFileInfo(face)?.OpenRead() ?? throw new FileNotFoundException(); } public IEnumerable<FontFace> Search(FontFace prototype, FontMatchCriteria criteria = FontMatchCriteria.All) { // A bit scuffed and LINQ heavy but it should work. IEnumerable<FontFace> candidates; if (criteria.HasFlag(FontMatchCriteria.Family)) { List<FontFace> siblings; if (!ByFamily.TryGetValue(prototype.Family, out siblings)) { return Enumerable.Empty<FontFace>(); } candidates = siblings; } else { candidates = All; } return candidates .Where(x => implies(criteria.HasFlag(FontMatchCriteria.Slant), prototype.Slant == x.Slant) || implies(criteria.HasFlag(FontMatchCriteria.Weight), prototype.Weight == x.Weight) || implies(criteria.HasFlag(FontMatchCriteria.Stretch), prototype.Stretch == x.Stretch) ) .OrderByDescending(x => (prototype.Slant == x.Slant ? 1 : 0) + (prototype.Weight == x.Weight ? 1 : 0) + (prototype.Stretch == x.Stretch ? 1 : 0) + confidence(prototype.Family, x.Family) * 3 ); // a => b = a'+b static bool implies(bool a, bool b) { return !a || b; } static int confidence(string target, string testee) { int i; for (i = 0; i < target.Length && i < testee.Length && target[i] == testee[i]; i++); return i; } } public FontFace GetSystemFontFace(SystemFontFamily family) { return SystemFonts[family]; } private void AddFont(FontFace face, FileInfo file) { if (!All.Contains(face)) All.Add(face); FilesMap.TryAdd(face, file); if (!ByFamily.TryGetValue(face.Family, out List<FontFace> siblings)) { siblings = new List<FontFace>(); ByFamily.Add(face.Family, siblings); } if (!siblings.Contains(face)) siblings.Add(face); } private List<DbEntry> LoadDatabase() { FileInfo info = new FileInfo(DbPath); if (!info.Exists) return new List<DbEntry>(); using Stream str = info.OpenRead(); try { return JsonSerializer.Deserialize<List<DbEntry>>(str); } catch { return new List<DbEntry>(); } } private void VerifyDatabase(List<DbEntry> db) { // Very slow way to do this but how many fonts could a system have on average? Dictionary<string, DbEntry> entires = new Dictionary<string, DbEntry>(); foreach (DbEntry entry in db) { FileInfo info = new FileInfo(entry.FilePath); // Reprocess fonts that appear like this. if (!info.Exists) continue; else if (info.LastWriteTime > entry.AccessTime) continue; } string fontpath = null; try { fontpath = Environment.GetFolderPath(Environment.SpecialFolder.Fonts); if (string.IsNullOrEmpty(fontpath)) throw new Exception(); } catch { foreach (string path in FontPaths) { if (Directory.Exists(path)) { fontpath = path; break; } } // rip if (string.IsNullOrEmpty(fontpath)) return; } SearchPathForFonts(entires, fontpath); db.Clear(); db.AddRange(entires.Values); } private static void SearchPathForFonts(Dictionary<string, DbEntry> entries, string path) { DirectoryInfo dir = new DirectoryInfo(path); foreach (FileInfo file in dir.EnumerateFiles()) { SearchFileForFonts(entries, file); } foreach (DirectoryInfo directory in dir.EnumerateDirectories()) { SearchPathForFonts(entries, directory.FullName); } } private static void SearchFileForFonts(Dictionary<string, DbEntry> entries, FileInfo file) { if (entries.ContainsKey(file.FullName)) return; if (FT.NewFace(FTProvider.Ft, file.FullName, 0, out FTFace face) != FTError.None) return; FontFace facename = FontFace.Parse(face.FamilyName, face.StyleName); DbEntry entry = new DbEntry(facename, file.FullName); entries.Add(file.FullName, entry); FT.DoneFace(face); } private void FlushDatabase(List<DbEntry> db) { FileInfo info = new FileInfo(DbPath); Directory.CreateDirectory(Path.GetDirectoryName(DbPath)); using Stream str = info.OpenWrite(); JsonSerializer.Serialize(str, db); } private static readonly string[] FontPaths = new string[] { "/usr/share/fonts", }; [JsonSerializable(typeof(DbEntry))] private class DbEntry { [JsonIgnore] public FontFace Face => new FontFace(Family, Slant, Weight, Stretch); public string Family { get; set; } public FontSlant Slant { get; set; } public FontWeight Weight { get; set; } public FontStretch Stretch { get; set; } public string FilePath { get; set; } public DateTime AccessTime { get; set; } public DbEntry() {} public DbEntry(FontFace face, string path) { Family = face.Family; Slant = face.Slant; Weight = face.Weight; Stretch = face.Stretch; FilePath = path; AccessTime = DateTime.Now; } } } }