Files
RobustToolbox/Robust.Client/Graphics/FontManagement/SystemFontManagerFontconfig.cs
Pieter-Jan Briers f3a3f564e1 System font API (#5393)
* 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.
2025-10-28 22:07:55 +01:00

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