#if FREEDESKTOP using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Robust.Shared.Log; using SpaceWizards.Fontconfig.Interop; namespace Robust.Client.Graphics.FontManagement; internal sealed unsafe class SystemFontManagerFontconfig : SystemFontManagerBase, ISystemFontManagerInternal { private static readonly (int Fc, FontWidth Width)[] WidthTable = [ (Fontconfig.FC_WIDTH_ULTRACONDENSED, FontWidth.UltraCondensed), (Fontconfig.FC_WIDTH_EXTRACONDENSED, FontWidth.ExtraCondensed), (Fontconfig.FC_WIDTH_CONDENSED, FontWidth.Condensed), (Fontconfig.FC_WIDTH_SEMICONDENSED, FontWidth.SemiCondensed), (Fontconfig.FC_WIDTH_NORMAL, FontWidth.Normal), (Fontconfig.FC_WIDTH_SEMIEXPANDED, FontWidth.SemiExpanded), (Fontconfig.FC_WIDTH_EXPANDED, FontWidth.Expanded), (Fontconfig.FC_WIDTH_EXTRAEXPANDED, FontWidth.ExtraExpanded), (Fontconfig.FC_WIDTH_ULTRAEXPANDED, FontWidth.UltraExpanded), ]; public bool IsSupported => true; public SystemFontManagerFontconfig(ILogManager logManager, IFontManagerInternal fontManager) : base(logManager, fontManager) { } public void Initialize() { Sawmill.Verbose("Initializing Fontconfig..."); var result = Fontconfig.FcInit(); if (result == Fontconfig.FcFalse) throw new InvalidOperationException("Failed to initialize fontconfig!"); Sawmill.Verbose("Listing fonts..."); var os = Fontconfig.FcObjectSetCreate(); AddToObjectSet(os, Fontconfig.FC_FAMILY); AddToObjectSet(os, Fontconfig.FC_FAMILYLANG); AddToObjectSet(os, Fontconfig.FC_STYLE); AddToObjectSet(os, Fontconfig.FC_STYLELANG); AddToObjectSet(os, Fontconfig.FC_FULLNAME); AddToObjectSet(os, Fontconfig.FC_FULLNAMELANG); AddToObjectSet(os, Fontconfig.FC_POSTSCRIPT_NAME); AddToObjectSet(os, Fontconfig.FC_SLANT); AddToObjectSet(os, Fontconfig.FC_WEIGHT); AddToObjectSet(os, Fontconfig.FC_WIDTH); AddToObjectSet(os, Fontconfig.FC_FILE); AddToObjectSet(os, Fontconfig.FC_INDEX); var allPattern = Fontconfig.FcPatternCreate(); var set = Fontconfig.FcFontList(null, allPattern, os); for (var i = 0; i < set->nfont; i++) { var pattern = set->fonts[i]; try { LoadPattern(pattern); } catch (Exception e) { Sawmill.Error($"Error while loading pattern: {e}"); } } Fontconfig.FcPatternDestroy(allPattern); Fontconfig.FcObjectSetDestroy(os); Fontconfig.FcFontSetDestroy(set); } public void Shutdown() { // Nada. } private void LoadPattern(FcPattern* pattern) { var path = PatternGetStrings(pattern, Fontconfig.FC_FILE)![0]; var idx = PatternGetInts(pattern, Fontconfig.FC_INDEX)![0]; var family = PatternToLocalized(pattern, Fontconfig.FC_FAMILY, Fontconfig.FC_FAMILYLANG); var style = PatternToLocalized(pattern, Fontconfig.FC_STYLE, Fontconfig.FC_STYLELANG); var fullName = PatternToLocalized(pattern, Fontconfig.FC_FULLNAME, Fontconfig.FC_FULLNAMELANG); var psName = PatternGetStrings(pattern, Fontconfig.FC_POSTSCRIPT_NAME); if (psName == null) return; var slant = PatternGetInts(pattern, Fontconfig.FC_SLANT) ?? [Fontconfig.FC_SLANT_ROMAN]; var weight = PatternGetInts(pattern, Fontconfig.FC_WEIGHT) ?? [Fontconfig.FC_WEIGHT_REGULAR]; var width = PatternGetInts(pattern, Fontconfig.FC_WIDTH) ?? [Fontconfig.FC_WIDTH_NORMAL]; Fonts.Add(new Handle(this) { FilePath = path, FileIndex = idx, FaceNames = style ?? LocalizedStringSet.Empty, FullNames = fullName ?? LocalizedStringSet.Empty, FamilyNames = family ?? LocalizedStringSet.Empty, PostscriptName = psName[0], Slant = SlantFromFontconfig(slant[0]), Weight = WeightFromFontconfig(weight[0]), Width = WidthFromFontconfig(width[0]) }); } private static FontWeight WeightFromFontconfig(int value) { return (FontWeight)Fontconfig.FcWeightToOpenType(value); } private static FontSlant SlantFromFontconfig(int value) { return value switch { Fontconfig.FC_SLANT_ITALIC => FontSlant.Italic, Fontconfig.FC_SLANT_OBLIQUE => FontSlant.Italic, _ => FontSlant.Normal, }; } private static FontWidth WidthFromFontconfig(int value) { return WidthTable.MinBy(t => Math.Abs(t.Fc - value)).Width; } private static unsafe void AddToObjectSet(FcObjectSet* os, ReadOnlySpan value) { var result = Fontconfig.FcObjectSetAdd(os, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(value))); if (result == Fontconfig.FcFalse) throw new InvalidOperationException("Failed to add to object set!"); } private static unsafe string[]? PatternGetStrings(FcPattern* pattern, ReadOnlySpan @object) { return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out string value) => { byte* str = null; var res = Fontconfig.FcPatternGetString(p, o, i, &str); value = Marshal.PtrToStringUTF8((nint)str)!; return res; }); } private static unsafe int[]? PatternGetInts(FcPattern* pattern, ReadOnlySpan @object) { return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out int value) => { FcResult res; fixed (int* pValue = &value) { res = Fontconfig.FcPatternGetInteger(p, o, i, pValue); } return res; }); } private delegate FcResult GetValue(FcPattern* p, sbyte* o, int i, out T value); private static unsafe T[]? PatternGetValues(FcPattern* pattern, ReadOnlySpan @object, GetValue getValue) { var list = new List(); var i = 0; while (true) { var result = getValue(pattern, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(@object)), i++, out var value); if (result == FcResult.FcResultMatch) { list.Add(value); } else if (result == FcResult.FcResultNoMatch) { return null; } else if (result == FcResult.FcResultNoId) { break; } else { throw new Exception($"FcPatternGetString gave error: {result}"); } } return list.ToArray(); } private static LocalizedStringSet? PatternToLocalized(FcPattern* pattern, ReadOnlySpan @object, ReadOnlySpan objectLang) { var values = PatternGetStrings(pattern, @object); var languages = PatternGetStrings(pattern, objectLang); if (values == null || languages == null || values.Length == 0 || languages.Length != values.Length) return null; var dict = new Dictionary(); for (var i = 0; i < values.Length; i++) { var val = values[i]; var lang = languages[i]; dict.TryAdd(lang, val); } return new LocalizedStringSet { Primary = languages[0], Values = dict }; } protected override IFontFaceHandle LoadFontFace(BaseHandle handle) { var cast = (Handle)handle; return FontManager.Load(new MemoryMappedFontMemoryHandle(cast.FilePath), cast.FileIndex); } private sealed class Handle(SystemFontManagerFontconfig parent) : BaseHandle(parent) { public required string FilePath; public required int FileIndex; } } #endif