275 lines
9.6 KiB
C#
275 lines
9.6 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
} |