diff --git a/Quik.Media.Defaults/Linux/FontConfig.cs b/Quik.Media.Defaults/Linux/FontConfig.cs new file mode 100644 index 0000000..7c25c92 --- /dev/null +++ b/Quik.Media.Defaults/Linux/FontConfig.cs @@ -0,0 +1,230 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Text; +using Quik; + +namespace Quik.Media.Defaults +{ + public static unsafe class FontConfig + { + private const string fontconfig = "fontconfig"; + + public static bool Exists { get; } + + public static IntPtr FAMILY { get; } = Marshal.StringToHGlobalAnsi("family"); + public static IntPtr STYLE { get; } = Marshal.StringToHGlobalAnsi("style"); + public static IntPtr FILE { get; } = Marshal.StringToHGlobalAnsi("file"); + public static IntPtr WEIGHT { get; } = Marshal.StringToHGlobalAnsi("weight"); + public static IntPtr SLANT { get; } = Marshal.StringToHGlobalAnsi("slant"); + + + static FontConfig() + { + try + { + if (FcInitLoadConfigAndFonts() == null) + { + Exists = false; + } + Exists = true; + } + catch + { + Exists = false; + } + } + + [DllImport(fontconfig, EntryPoint = "FcInitLoadConfigAndFonts")] + public static extern FcConfig* FcInitLoadConfigAndFonts(); + + [DllImport(fontconfig, EntryPoint = "FcConfigGetCurrent")] + public static extern FcConfig ConfigGetCurrent(); + + [DllImport(fontconfig, EntryPoint = "FcPatternCreate")] + public static extern FcPattern PatternCreate(); + + [DllImport(fontconfig, EntryPoint = "FcPatternCreate")] + public static extern bool FcPatternAdd(FcPattern pattern, IntPtr what, FcValue value, bool append); + + [DllImport(fontconfig, EntryPoint = "FcObjectSetBuild", CallingConvention = CallingConvention.Cdecl)] + public static extern FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3, IntPtr i4, IntPtr i5, IntPtr i6); + + public static FcObjectSet ObjectSetBuild(IntPtr i1) + { + return ObjectSetBuild(i1, IntPtr.Zero); + } + + public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2) + { + return ObjectSetBuild(i1, i2, IntPtr.Zero); + } + public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3) + { + return ObjectSetBuild(i1, i2, i3, IntPtr.Zero); + } + public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3, IntPtr i4) + { + return ObjectSetBuild(i1, i2, i3, i4, IntPtr.Zero); + } + public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3, IntPtr i4, IntPtr i5) + { + return ObjectSetBuild(i1, i2, i3, i4, i5, IntPtr.Zero); + } + + [DllImport(fontconfig, EntryPoint = "FcFontList")] + public static extern FcFontSet FontList(FcConfig config, FcPattern pattern, FcObjectSet objectSet); + + [DllImport(fontconfig, EntryPoint = "FcNameUnparse")] + public static extern IntPtr NameUnparse(FcPattern pat); + + public static string NameUnparseStr(FcPattern pat) => Marshal.PtrToStringAnsi(NameUnparse(pat)); + + [DllImport(fontconfig, EntryPoint = "FcPatternGetString")] + public static extern FcResult PatternGetString(FcPattern p, IntPtr what, int n, out IntPtr val); + + public static FcResult PatternGetString(FcPattern p, IntPtr what, out string str) + { + FcResult i = PatternGetString(p, what, 0, out IntPtr ptr); + + if (i == FcResult.Match) + { + str = Marshal.PtrToStringAnsi(ptr); + } + else + { + str = null; + } + + Marshal.FreeHGlobal(ptr); + + return i; + } + + [DllImport(fontconfig, EntryPoint = "FcPatternGet")] + public static extern FcResult PatternGet(FcPattern p, IntPtr what, int id, out FcValue value); + + [DllImport(fontconfig, EntryPoint = "FcFontSetDestroy")] + public static extern void FontSetDestroy(FcFontSet fs); + + [DllImport(fontconfig, EntryPoint = "FcObjectSetDestroy")] + public static extern void ObjectSetDestroy (FcObjectSet os); + + [DllImport(fontconfig, EntryPoint = "FcConfigDestroy")] + public static extern void ConfigDestroy (FcConfig cfg); + + [DllImport(fontconfig, EntryPoint = "FcPatternDestroy")] + public static extern void PatternDestroy (FcPattern os); + + #region Range + + [DllImport(fontconfig, EntryPoint = "FcRangeCreateDouble")] + public static extern IntPtr RangeCreateDouble(double begin, double end); + + [DllImport(fontconfig, EntryPoint = "FcRangeCreateInteger")] + public static extern IntPtr RangeCreateInteger (int begin, int end); + + [DllImport(fontconfig, EntryPoint = "FcRangeDestroy")] + public static extern void RangeDestroy(IntPtr range); + + [DllImport(fontconfig, EntryPoint = "FcRangeCopy")] + public static extern IntPtr RangeCopy (IntPtr range); + + [DllImport(fontconfig, EntryPoint = "FcRangeGetDouble")] + public static extern bool RangeGetDouble(IntPtr range, out double start, out double end); + + #endregion + } + + public enum FcResult + { + Match, + NoMatch, + TypeMismatch, + NoId, + OutOfMemory + } + + public struct FcConfig + { + public readonly IntPtr Handle; + } + + public struct FcPattern + { + public readonly IntPtr Handle; + } + + public unsafe struct FcObjectSet + { + public readonly IntPtr Handle; + + private Accessor* AsPtr => (Accessor*)Handle; + + public int NObject => AsPtr->nobject; + + public int SObject => AsPtr->sobject; + + #pragma warning disable CS0649 // Will always have default value. + private struct Accessor + { + public int nobject; + public int sobject; + public byte** objects; + } + #pragma warning restore CS0649 + } + + public unsafe struct FcFontSet + { + public readonly IntPtr Handle; + private Accessor* AsPtr => (Accessor*)Handle; + + public int NFont => AsPtr->nfont; + public int SFont => AsPtr->sfont; + + public FcPattern this[int i] + { + get + { + if (i < 0 || i >= NFont) + throw new IndexOutOfRangeException(); + + return AsPtr->fonts[i]; + } + } + + #pragma warning disable CS0649 // Will always have default value. + private struct Accessor + { + public int nfont; + public int sfont; + public FcPattern* fonts; + } + #pragma warning restore CS0649 + } + + public enum FcType + { + Unknown = -1, + Void, + Integer, + Double, + String, + Bool, + Matrix, + CharSet, + FTFace, + LangSet, + Range + } + + [StructLayout(LayoutKind.Explicit)] + public readonly struct FcValue + { + [FieldOffset(0)] public readonly FcType Type; + [FieldOffset(sizeof(FcType))] public readonly IntPtr Pointer; + [FieldOffset(sizeof(FcType))] public readonly int Int; + [FieldOffset(sizeof(FcType))] public readonly double Double; + } +} \ No newline at end of file diff --git a/Quik.Media.Defaults/Linux/FontConfigFontDatabase.cs b/Quik.Media.Defaults/Linux/FontConfigFontDatabase.cs new file mode 100644 index 0000000..aa91d5e --- /dev/null +++ b/Quik.Media.Defaults/Linux/FontConfigFontDatabase.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Quik.Media.Font; +using Quik.PAL; + +namespace Quik.Media.Defaults.Linux +{ + /// + /// Font database for Linux libfontconfig systems. + /// + public class FontConfigFontDatabase : IFontDataBase + { + private IReadOnlyDictionary FilesMap { get; } + private IReadOnlyDictionary> ByFamily { get; } + + public IEnumerable All { get; } + + public FontConfigFontDatabase() + { + if (!FontConfig.Exists) + { + throw new NotSupportedException("This host doesn't have fontconfig installed."); + } + + List faces = new List(); + Dictionary files = new Dictionary(); + Dictionary> byFamily = new Dictionary>(); + + FcConfig config = FontConfig.ConfigGetCurrent(); + FcPattern pattern = FontConfig.PatternCreate(); + FcObjectSet os = FontConfig.ObjectSetBuild(FontConfig.FAMILY, FontConfig.STYLE, FontConfig.FILE); + FcFontSet fs = FontConfig.FontList(config, pattern, os); + + for (int i = 0; i < fs.NFont; i++) + { + FcPattern current = fs[i]; + + if ( + FontConfig.PatternGetString(current, FontConfig.FAMILY, 0, out IntPtr pFamily) != FcResult.Match || + FontConfig.PatternGetString(current, FontConfig.STYLE, 0, out IntPtr pStyle) != FcResult.Match) + { + continue; + } + + string family = Marshal.PtrToStringUTF8(pFamily); + string style = Marshal.PtrToStringUTF8(pStyle); + + FontFace face = FontFace.Parse(family, style); + faces.Add(face); + + if (!byFamily.TryGetValue(face.Family, out List siblings)) + { + siblings = new List(); + byFamily.Add(face.Family, siblings); + } + siblings.Add(face); + + FontConfig.PatternGetString(current, FontConfig.FILE, 0, out IntPtr pFile); + string file = Marshal.PtrToStringAnsi(pFile); + + files.TryAdd(face, new FileInfo(file)); + } + + FontConfig.FontSetDestroy(fs); + FontConfig.ObjectSetDestroy(os); + FontConfig.PatternDestroy(pattern); + + All = faces.ToArray(); + FilesMap = files; + ByFamily = new Dictionary>( + byFamily.Select(x => + new KeyValuePair>(x.Key, x.Value) + ) + ); + } + + public IEnumerable Search(FontFace prototype, FontMatchCriteria criteria = FontMatchCriteria.All) + { + // A bit scuffed and LINQ heavy but it should work. + IEnumerable candidates; + + if (criteria.HasFlag(FontMatchCriteria.Family)) + { + IReadOnlyList siblings; + + if (!ByFamily.TryGetValue(prototype.Family, out siblings)) + { + return Enumerable.Empty(); + } + + 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 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(); + } + } +} \ No newline at end of file