mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* System font API This is a new API that allows operating system fonts to be loaded by the engine and used by content. Fonts are provided in a flat list exposing all the relevant metadata. They are loaded from disk with a Load call. Initial implementation is only for Windows DirectWrite. * Load system fonts as memory mapped files if possible. This allows sharing the font file memory with other processes which is always good. * Use ArrayPool to reduce char array allocations * Disable verbose logging * Implement system font support on Linux via Fontconfig * Implement macOS support * Add "FREEDESKTOP" define constant This is basically LINUX || FREEBSD. Though FreeBSD currently gets detected as LINUX too. Oh well. * Compile out Fontconfig and CoreText system font backends when not on those platforms * Don't add Fontconfig package dep on Mac/Windows * Allow disabling system font support via CVar Cuz why not.
236 lines
7.9 KiB
C#
236 lines
7.9 KiB
C#
#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<byte> 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<byte> @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<byte> @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<T>(FcPattern* p, sbyte* o, int i, out T value);
|
|
private static unsafe T[]? PatternGetValues<T>(FcPattern* pattern, ReadOnlySpan<byte> @object, GetValue<T> getValue)
|
|
{
|
|
var list = new List<T>();
|
|
|
|
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<byte> @object, ReadOnlySpan<byte> 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<string, string>();
|
|
|
|
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
|