From ed4ee33bb12cfef80974315bf1c264276685f336 Mon Sep 17 00:00:00 2001 From: bbbirder <502100554@qq.com> Date: Fri, 11 Jul 2025 01:23:51 +0800 Subject: [PATCH 1/2] feat: support system fallback fonts --- Assets/Editor/UIConfigEditor.cs | 15 +- Assets/Scripts/Core/Text/FontManager.cs | 32 ++- .../TextMeshPro/FontEngineNameResolver.cs | 28 +++ .../FontEngineNameResolver.cs.meta | 11 + .../LightWeightFontNameResolver.cs | 21 ++ .../LightWeightFontNameResolver.cs.meta | 11 + .../TextMeshPro/LightWeightReader.meta | 8 + .../TextMeshPro/LightWeightReader/BEReader.cs | 107 +++++++++ .../LightWeightReader/BEReader.cs.meta | 11 + .../LightWeightReader/EncodingCodePage.cs | 18 ++ .../EncodingCodePage.cs.meta | 11 + .../LightWeightReader/LanguageID.cs | 25 +++ .../LightWeightReader/LanguageID.cs.meta | 11 + .../LightWeightFontFaceImpl.cs | 207 ++++++++++++++++++ .../LightWeightFontFaceImpl.cs.meta | 11 + .../TextMeshPro/LightWeightReader/NameID.cs | 35 +++ .../LightWeightReader/NameID.cs.meta | 11 + .../LightWeightReader/PlatformID.cs | 13 ++ .../LightWeightReader/PlatformID.cs.meta | 11 + .../TextMeshPro/SystemFontService.cs | 117 ++++++++++ .../TextMeshPro/SystemFontService.cs.meta | 11 + .../Scripts/Extensions/TextMeshPro/TMPFont.cs | 81 ++++++- Assets/Scripts/FairyGUI.asmdef | 2 +- Assets/Scripts/UI/UIConfig.cs | 32 +++ 24 files changed, 828 insertions(+), 12 deletions(-) create mode 100644 Assets/Scripts/Extensions/TextMeshPro/FontEngineNameResolver.cs create mode 100644 Assets/Scripts/Extensions/TextMeshPro/FontEngineNameResolver.cs.meta create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightFontNameResolver.cs create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightFontNameResolver.cs.meta create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader.meta create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/BEReader.cs create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/BEReader.cs.meta create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/EncodingCodePage.cs create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/EncodingCodePage.cs.meta create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LanguageID.cs create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LanguageID.cs.meta create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs.meta create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/NameID.cs create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/NameID.cs.meta create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/PlatformID.cs create mode 100644 Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/PlatformID.cs.meta create mode 100644 Assets/Scripts/Extensions/TextMeshPro/SystemFontService.cs create mode 100644 Assets/Scripts/Extensions/TextMeshPro/SystemFontService.cs.meta diff --git a/Assets/Editor/UIConfigEditor.cs b/Assets/Editor/UIConfigEditor.cs index 279ddb48..ae9a5033 100644 --- a/Assets/Editor/UIConfigEditor.cs +++ b/Assets/Editor/UIConfigEditor.cs @@ -1,6 +1,6 @@ -using UnityEngine; +using FairyGUI; using UnityEditor; -using FairyGUI; +using UnityEngine; namespace FairyGUIEditor { @@ -14,7 +14,7 @@ public class UIConfigEditor : Editor bool itemsFoldout; bool packagesFoldOut; int errorState; - + static GUIStyle s_textAreaStyle; private const float kButtonWidth = 18f; void OnEnable() @@ -31,7 +31,7 @@ public override void OnInspectorGUI() serializedObject.Update(); DrawPropertiesExcluding(serializedObject, propertyToExclude); - + UIConfig config = (UIConfig)target; EditorGUILayout.BeginHorizontal(); @@ -141,6 +141,13 @@ public override void OnInspectorGUI() if (EditorGUI.EndChangeCheck()) modified = true; break; + + case UIConfig.ConfigKey.SystemFontFamily: + value.s = EditorGUILayout.TextArea(value.s, s_textAreaStyle ??= new GUIStyle("TextField") + { + wordWrap = true, + }); + break; } if (GUILayout.Button(new GUIContent("X", "Delete Item"), EditorStyles.miniButtonRight, GUILayout.Width(30))) diff --git a/Assets/Scripts/Core/Text/FontManager.cs b/Assets/Scripts/Core/Text/FontManager.cs index 812d5b1e..97aecc17 100644 --- a/Assets/Scripts/Core/Text/FontManager.cs +++ b/Assets/Scripts/Core/Text/FontManager.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; +using UnityEngine.TextCore.LowLevel; namespace FairyGUI { @@ -61,6 +63,7 @@ static public BaseFont GetFont(string name) return font; object asset = Resources.Load(name); + if (asset == null) asset = Resources.Load("Fonts/" + name); @@ -82,21 +85,28 @@ static public BaseFont GetFont(string name) if (asset == null) return Fallback(name); - if (asset is Font) + if (asset is Font nativeFont) { font = new DynamicFont(); font.name = name; sFontFactory.Add(name, font); - ((DynamicFont)font).nativeFont = (Font)asset; + AppendSystemFontsFromUIConfig(nativeFont); + + ((DynamicFont)font).nativeFont = nativeFont; } #if FAIRYGUI_TMPRO - else if (asset is TMPro.TMP_FontAsset) + else if (asset is TMPro.TMP_FontAsset tmpFontAsset) { font = new TMPFont(); font.name = name; sFontFactory.Add(name, font); - ((TMPFont)font).fontAsset = (TMPro.TMP_FontAsset)asset; + // if (name == UIConfig.defaultFont) // apply to all may be better + { + ; ((TMPFont)font).SetFallbackSystemFontFamily(UIConfig.systemFontFamily); + } + + ; ((TMPFont)font).fontAsset = tmpFontAsset; } #endif else @@ -116,6 +126,16 @@ static public BaseFont GetFont(string name) return font; } + static void AppendSystemFontsFromUIConfig(Font nativeFont) + { + if (UIConfig.systemFontFamily.Length == 0) return; + + nativeFont.fontNames = nativeFont.fontNames + .Concat(UIConfig.systemFontFamily) + .Distinct() + .ToArray(); + } + static BaseFont Fallback(string name) { if (name != UIConfig.defaultFont) @@ -132,6 +152,8 @@ static BaseFont Fallback(string name) if (asset == null) throw new Exception("Failed to load font '" + name + "'"); + AppendSystemFontsFromUIConfig(asset); + BaseFont font = new DynamicFont(); font.name = name; ((DynamicFont)font).nativeFont = asset; @@ -149,6 +171,8 @@ static public void Clear() kv.Value.Dispose(); sFontFactory.Clear(); + + SystemFontService.Clear(); } #if UNITY_2019_3_OR_NEWER diff --git a/Assets/Scripts/Extensions/TextMeshPro/FontEngineNameResolver.cs b/Assets/Scripts/Extensions/TextMeshPro/FontEngineNameResolver.cs new file mode 100644 index 00000000..f6fd6aae --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/FontEngineNameResolver.cs @@ -0,0 +1,28 @@ +#if FAIRYGUI_TMPRO + +using System.Collections.Generic; +using UnityEngine.TextCore.LowLevel; + +namespace FairyGUI +{ + // FontEngine loads all infos besides names and caches permanently. This implement will significantly increase memory unsage. + public class FontEngineNameResolver : IFontNameResolver + { + public void GetFontNames(string filePath, List results) + { + var error = FontEngine.LoadFontFace(filePath); + if (error is not FontEngineError.Success) return; + + var numFaces = FontEngine.GetFontFaces().Length; // a collection when it is ttc format + for (int i = 0; i < numFaces; i++) + { + // Dont worry: unity caches the result as TextCore:FontFaceCache + FontEngine.LoadFontFace(filePath, 0/*pointSize: default is 0*/, i); + var faceInfo = FontEngine.GetFaceInfo(); + results.Add(FontName.Create(faceInfo.familyName, faceInfo.styleName, filePath)); + } + } + } +} + +#endif diff --git a/Assets/Scripts/Extensions/TextMeshPro/FontEngineNameResolver.cs.meta b/Assets/Scripts/Extensions/TextMeshPro/FontEngineNameResolver.cs.meta new file mode 100644 index 00000000..28336195 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/FontEngineNameResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 34e8f877a30e5284bb264e559ad5dcca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightFontNameResolver.cs b/Assets/Scripts/Extensions/TextMeshPro/LightWeightFontNameResolver.cs new file mode 100644 index 00000000..21c2c1e0 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightFontNameResolver.cs @@ -0,0 +1,21 @@ +#if FAIRYGUI_TMPRO +using System; +using System.Collections.Generic; +using System.Linq; +using TMPro; +using UnityEngine; +using UnityEngine.TextCore; +using UnityEngine.TextCore.LowLevel; + +namespace FairyGUI +{ + public class LightWeightFontNameResolver : IFontNameResolver + { + public void GetFontNames(string filePath, List results) + { + LightWeightFontFaceImpl.Default.GetFontFamilyNames(filePath, results); + } + } +} + +#endif diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightFontNameResolver.cs.meta b/Assets/Scripts/Extensions/TextMeshPro/LightWeightFontNameResolver.cs.meta new file mode 100644 index 00000000..9644942f --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightFontNameResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7aa498deef0547e4ebb084314e77f1a4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader.meta b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader.meta new file mode 100644 index 00000000..ad01d0f0 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 36f188d39de5edb46a28bc817236cf96 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/BEReader.cs b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/BEReader.cs new file mode 100644 index 00000000..69815043 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/BEReader.cs @@ -0,0 +1,107 @@ +#if FAIRYGUI_TMPRO +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +namespace FairyGUI +{ + + internal unsafe class BEReader + { + [ThreadStatic] static byte[] tls_buffer = new byte[16]; + + private Stream m_stream; + + public long Position + { + get => m_stream.Position; + set => m_stream.Position = value; + } + + public BEReader(Stream stream) + { + this.m_stream = stream; + } + + public string ReadString(int numBytes, Encoding encoding = null) + { + encoding ??= Encoding.UTF8; + + if (numBytes <= 16) + { + tls_buffer ??= new byte[16]; + m_stream.Read(tls_buffer, 0, numBytes); + return encoding.GetString(tls_buffer, 0, numBytes); + } + else + { + var largeBytes = ArrayPool.Shared.Rent(numBytes); + try + { + m_stream.Read(largeBytes, 0, numBytes); + return encoding.GetString(largeBytes, 0, numBytes); + } + finally + { + ArrayPool.Shared.Return(largeBytes); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public char ReadChar() => ReadPrimite(m_stream); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte ReadByte() => ReadPrimite(m_stream); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadInt32() => ReadPrimite(m_stream); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long ReadInt64() => ReadPrimite(m_stream); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public sbyte ReadSByte() => ReadPrimite(m_stream); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short ReadInt16() => ReadPrimite(m_stream); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort ReadUInt16() => ReadPrimite(m_stream); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ReadUInt32() => ReadPrimite(m_stream); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ReadUInt64() => ReadPrimite(m_stream); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static T ReadPrimite(Stream stream) where T : unmanaged + { + tls_buffer ??= new byte[16]; + stream.Read(tls_buffer, 0, sizeof(T)); + fixed (byte* p = tls_buffer) + { + var raw = *(T*)p; + if (BitConverter.IsLittleEndian) + { + for (int i = 0; i < sizeof(T) >> 1; i++) + { + int j = sizeof(T) - 1 - i; + *(p + i) ^= *(p + j); + *(p + j) ^= *(p + i); + *(p + i) ^= *(p + j); + } + } + + return *(T*)p; + } + } + + } + +} + +#endif diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/BEReader.cs.meta b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/BEReader.cs.meta new file mode 100644 index 00000000..3adce5c5 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/BEReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e3db89a507079204aa3db569c2772094 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/EncodingCodePage.cs b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/EncodingCodePage.cs new file mode 100644 index 00000000..239658a0 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/EncodingCodePage.cs @@ -0,0 +1,18 @@ +#if FAIRYGUI_TMPRO +namespace FairyGUI +{ + // see: https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers + public enum EncodingCodePage : int + { + Unicode = 1200, + UTF8 = 65001, + UTF16LE = 1200, + UTF16BE = 1201, + GB2312 = 936, + KS_C_5601_1987 = 949, + Big5 = 950, + Macintosh = 10000, + } +} + +#endif diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/EncodingCodePage.cs.meta b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/EncodingCodePage.cs.meta new file mode 100644 index 00000000..69471d1f --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/EncodingCodePage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 55023f9c8316b9c4db07bd6c5fca56be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LanguageID.cs b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LanguageID.cs new file mode 100644 index 00000000..6653f9c5 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LanguageID.cs @@ -0,0 +1,25 @@ +#if FAIRYGUI_TMPRO +namespace FairyGUI +{ + /* + FYR: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid + */ + internal enum LanguageID + { + English = 0x0409, + French = 0x040c, + German = 0x0407, + Italian = 0x0410, + Japanese = 0x0411, + Korean = 0x0412, + Spanish = 0x040a, + ChineseSimplified = 0x0804, + ChineseTraditional = 0x0404, + Russian = 0x0419, + Arabic = 0x0401, + Portuguese = 0x0816, + Hindi = 0x0439, + } +} + +#endif diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LanguageID.cs.meta b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LanguageID.cs.meta new file mode 100644 index 00000000..64f528f6 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LanguageID.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f4c42af7ec326ff45aa0c7a1487b11dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs new file mode 100644 index 00000000..ec0eb78c --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs @@ -0,0 +1,207 @@ +#if FAIRYGUI_TMPRO +using System; +using System.Collections.Generic; +#if !UNITY_WEBGL +using System.IO; +#endif +using System.Linq; +using System.Text; +using UnityEngine; + +//see: https://learn.microsoft.com/en-us/typography/opentype/spec/otff + +namespace FairyGUI +{ + public class LightWeightFontFaceImpl + { + const string TTC_TAG = "ttcf"; + public static readonly LightWeightFontFaceImpl Default = new(); + private static HashSet supportedEncodingCodePages = new(); + + static LightWeightFontFaceImpl() + { + foreach (var encoding in Encoding.GetEncodings()) + { + supportedEncodingCodePages.Add((EncodingCodePage)encoding.CodePage); + } + } + + static Encoding GetEncoding(EncodingCodePage codePage) + { + if (supportedEncodingCodePages.Contains(codePage)) + { + return Encoding.GetEncoding((int)codePage); + } + else + { + return null; + } + } + + /// + /// + /// + /// + /// Pass a optional result container to avoid memory alloc + /// if provided, othewise, a new collection + public List GetFontFamilyNames(string path, List resultNames = null) + { + resultNames ??= new(); +#if !UNITY_WEBGL + + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read); + var reader = new BEReader(stream); + var isTTC = reader.ReadString(4) == TTC_TAG; + reader.Position = 0; + + if (isTTC) + { + var context = new TTCContext(); + // Debug.LogWarning("ttc " + path); + LoadTTC(reader, context); + + foreach (var (offset, fontContext) in context.fonts) + { + resultNames.Add(FontName.Create( + fontContext.ExtractString(NameID.FamilyName, reader), + fontContext.ExtractString(NameID.SubfamilyName, reader), + path)); + } + } + else + { + var fontContext = new TTFContext(); + // Debug.LogWarning("ttf " + path); + LoadTTF(reader, fontContext); + + resultNames.Add(FontName.Create( + fontContext.ExtractString(NameID.FamilyName, reader), + fontContext.ExtractString(NameID.SubfamilyName, reader), + path)); + } +#endif // end of Non-WebGL plaform + + return resultNames; + } + + private static void LoadTTC(BEReader reader, TTCContext context) + { + reader.Position = TTC_TAG.Length; + var majorVersion = reader.ReadUInt16(); + var minorVersion = reader.ReadUInt16(); + var numFonts = reader.ReadUInt32(); + for (var i = 0; i < numFonts; i++) + { + var tableDirectoryOffset = reader.ReadUInt32(); + context.fonts[tableDirectoryOffset] = new TTFContext(); + } + + if (majorVersion > 1) + { + var dsigTag = reader.ReadUInt32(); + var dsigLength = reader.ReadUInt32(); + var dsigOffset = reader.ReadUInt32(); + } + + foreach (var (offset, fontContext) in context.fonts) + { + reader.Position = offset; + LoadTTF(reader, fontContext); + } + } + + private static void LoadTTF(BEReader reader, TTFContext context) + { + var sfntVersion = reader.ReadUInt32(); + var numTables = reader.ReadUInt16(); + var searchRange = reader.ReadUInt16(); + var entrySelector = reader.ReadUInt16(); + var rangeShift = reader.ReadUInt16(); + + for (var i = 0; i < numTables; i++) + { + var tag = reader.ReadString(4); + var checkSum = reader.ReadUInt32(); + var offset = reader.ReadUInt32(); + var length = reader.ReadUInt32(); + + if (tag == "name") + { + reader.Position = offset; + var version = reader.ReadUInt16(); + var count = reader.ReadUInt16(); + var storageOffset = reader.ReadUInt16() + offset; + for (int j = 0; j < count; j++) + { + var platformID = (PlatformID)reader.ReadUInt16(); + var encodingID = reader.ReadUInt16(); + var languageID = (LanguageID)reader.ReadUInt16(); + var nameID = (NameID)reader.ReadUInt16(); + var stringLength = reader.ReadUInt16(); + var stringOffset = reader.ReadUInt16(); + if (languageID is LanguageID.English || !context.records.ContainsKey(nameID)) + { + EncodingCodePage codePage = platformID switch + { + PlatformID.Unicode => EncodingCodePage.UTF16BE, + PlatformID.Macintosh => EncodingCodePage.Macintosh, + PlatformID.Windows => encodingID switch + { + 3 => EncodingCodePage.GB2312, + 4 => EncodingCodePage.KS_C_5601_1987, + 5 => EncodingCodePage.Big5, + _ => EncodingCodePage.UTF16BE, + }, + _ => EncodingCodePage.UTF8, + }; + + var encoding = GetEncoding(codePage); + + if (encoding != null) + { + context.records[nameID] = new() + { + offset = storageOffset + stringOffset, + length = stringLength, + encoding = encoding, + }; + } + } + } + + break; + } + } + } + + private struct StringRecord + { + public long offset; + public ushort length; + public Encoding encoding; + } + + private class TTFContext + { + public Dictionary records = new(); + public string ExtractString(NameID nameID, BEReader reader) + { + if (records.TryGetValue(nameID, out var record)) + { + reader.Position = record.offset; + return reader.ReadString(record.length, record.encoding); + } + + return null; + } + } + + private class TTCContext + { + public Dictionary fonts = new(); + } + + } +} + +#endif diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs.meta b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs.meta new file mode 100644 index 00000000..0e927752 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e0dcc644148479e498d525fe7c6b2ee1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/NameID.cs b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/NameID.cs new file mode 100644 index 00000000..4fd36fb7 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/NameID.cs @@ -0,0 +1,35 @@ +#if FAIRYGUI_TMPRO +namespace FairyGUI +{ + internal enum NameID + { + Copyright = 0, + FamilyName = 1, + SubfamilyName = 2, + UniqueID = 3, + FullName = 4, + Version = 5, + PostScriptName = 6, + Trademark = 7, + ManufacturerName = 8, + Designer = 9, + Description = 10, + VendorURL = 11, + DesignerURL = 12, + License = 13, + LicenseURL = 14, + Reserved = 15, + TypographicFamilyName = 16, + TypographicSubfamilyName = 17, + CompatibleFullName = 18, + SampleText = 19, + PostScriptCIDFindfontName = 20, + WWSFamilyName = 21, + WWSSubfamilyName = 22, + LightBackgroundPalette = 23, + DarkBackgroundPalette = 24, + VariationPostScriptNamePrefix = 25, + } +} + +#endif diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/NameID.cs.meta b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/NameID.cs.meta new file mode 100644 index 00000000..ba5c043a --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/NameID.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a6f380c04c7a4284cb695e511e6e01fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/PlatformID.cs b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/PlatformID.cs new file mode 100644 index 00000000..8cfdd1ea --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/PlatformID.cs @@ -0,0 +1,13 @@ +#if FAIRYGUI_TMPRO + +namespace FairyGUI +{ + internal enum PlatformID + { + Unicode = 0, + Macintosh = 1, + Windows = 3, + } +} + +#endif diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/PlatformID.cs.meta b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/PlatformID.cs.meta new file mode 100644 index 00000000..c62f932e --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/PlatformID.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b03bb2ed910c4d479c59583e38204c9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Extensions/TextMeshPro/SystemFontService.cs b/Assets/Scripts/Extensions/TextMeshPro/SystemFontService.cs new file mode 100644 index 00000000..8833e994 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/SystemFontService.cs @@ -0,0 +1,117 @@ +#if FAIRYGUI_TMPRO + +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.TextCore.LowLevel; + +namespace FairyGUI +{ + public struct FontName + { + public string familyName; // NameID 1 + public string subfamilyName; // NameID 2 + public string exactName; // similar to FullName + public string filePath; + + public static FontName Create(string familyName, string subfamilyName, string filePath) + { + return new() + { + familyName = familyName, + subfamilyName = subfamilyName, + exactName = familyName + " " + subfamilyName, + filePath = filePath, + }; + } + } + + public interface IFontNameResolver + { + void GetFontNames(string filePath, List results); + } + + public static class SystemFontService + { + private static Dictionary> s_lutFontNames = new(); + private static Dictionary s_lutFontAssets = new(); + public static IFontNameResolver fontNameResolver = new LightWeightFontNameResolver(); + + public static IReadOnlyList GetFontNames(string filePath) + { + return GetFontNamesInternal(filePath); + } + + private static List GetFontNamesInternal(string filePath) + { + if (!s_lutFontNames.TryGetValue(filePath, out var cachedResults)) + { + s_lutFontNames[filePath] = cachedResults = new(); + fontNameResolver.GetFontNames(filePath, cachedResults); + } + + return cachedResults; + } + + public static List ResolveInstalledFonts(string[] fontFamily, List resultPaths = null) + { + resultPaths ??= new(); + var systemFontPaths = Font.GetPathsToOSFonts(); + + foreach (var queryingName in fontFamily) + { + foreach (var fontPath in Font.GetPathsToOSFonts()) + { + var names = GetFontNamesInternal(fontPath); + if (IsFontMatch(queryingName, names)) + { + resultPaths.Add(fontPath); + } + } + } + + return resultPaths; + } + + private static bool IsFontMatch(string inputName, List fontNames) + { + foreach (var fontName in fontNames) + { + if (string.Equals(fontName.exactName, inputName, System.StringComparison.InvariantCultureIgnoreCase)) return true; + if (string.Equals(fontName.familyName, inputName, System.StringComparison.InvariantCultureIgnoreCase)) return true; + } + + return false; + } + + public static TMP_FontAsset GetSystemFontAsset(string fontPath, TMP_FontAsset originFontAsset = null) + { + if (!s_lutFontAssets.TryGetValue(fontPath, out var fontAsset)) + { + var atlasWidth = originFontAsset?.atlasWidth ?? 2048; + var atlasHeight = originFontAsset?.atlasHeight ?? 2048; + + var nativeFont = new Font(fontPath); + s_lutFontAssets[fontPath] + = fontAsset + = TMP_FontAsset.CreateFontAsset(nativeFont, 60, 9, GlyphRenderMode.SDFAA, atlasWidth, atlasHeight); + } + + return fontAsset; + } + + public static void Clear() + { + s_lutFontNames.Clear(); + foreach (var (path, fontAsset) in s_lutFontAssets) + { + Object.Destroy(fontAsset.sourceFontFile); + Object.Destroy(fontAsset); + } + + s_lutFontAssets.Clear(); + } + } +} + +#endif diff --git a/Assets/Scripts/Extensions/TextMeshPro/SystemFontService.cs.meta b/Assets/Scripts/Extensions/TextMeshPro/SystemFontService.cs.meta new file mode 100644 index 00000000..3895c6b9 --- /dev/null +++ b/Assets/Scripts/Extensions/TextMeshPro/SystemFontService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b24db3b714d70e24eb28dfcc4236588c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Extensions/TextMeshPro/TMPFont.cs b/Assets/Scripts/Extensions/TextMeshPro/TMPFont.cs index 01482ba6..5c7a8931 100644 --- a/Assets/Scripts/Extensions/TextMeshPro/TMPFont.cs +++ b/Assets/Scripts/Extensions/TextMeshPro/TMPFont.cs @@ -2,9 +2,11 @@ using System; using System.Collections.Generic; +using System.IO; +using TMPro; using UnityEngine; using UnityEngine.TextCore; -using TMPro; +using UnityEngine.TextCore.LowLevel; namespace FairyGUI { @@ -40,6 +42,7 @@ public class TMPFont : BaseFont TMPFont _fallbackFont; List _fallbackFonts; VertexBuffer[] _subMeshBuffers; + FallbackSystemFontContext fallbackSystemFontContext; bool _preparing; public TMPFont() @@ -57,6 +60,7 @@ public TMPFont() override public void Dispose() { Release(); + fallbackSystemFontContext?.ClearReferences(); } public TMP_FontAsset fontAsset @@ -94,7 +98,7 @@ void Init() // _lineHeight = _fontAsset.faceInfo.lineHeight; _ascent = _fontAsset.faceInfo.pointSize; _lineHeight = _fontAsset.faceInfo.pointSize * 1.25f; - _gradientScale = fontAsset.atlasPadding + 1; + _gradientScale = _fontAsset.atlasPadding + 1; } void OnCreateNewMaterial(Material mat) @@ -106,6 +110,12 @@ void OnCreateNewMaterial(Material mat) mat.SetFloat(ShaderUtilities.ID_WeightBold, fontAsset.boldStyle); } + public void SetFallbackSystemFontFamily(string[] family) + { + fallbackSystemFontContext?.ClearReferences(); + fallbackSystemFontContext = new(this, family); + } + public override void Prepare(TextFormat format) { _preparing = true; @@ -332,6 +342,7 @@ override public bool GetGlyph(char ch, out float width, out float height, out fl if (!GetCharacterFromFontAsset(ch, _style, _fontWeight)) { width = height = baseline = 0; + Debug.LogWarning($"character {ch} not found"); return false; } @@ -375,6 +386,13 @@ bool GetCharacterFromFontAsset(uint unicode, FontStyles fontStyle, FontWeight fo _char = TMP_FontAssetUtilities.GetCharacterFromFontAsset(unicode, _fontAsset, true, fontStyle, fontWeight, out isAlternativeTypeface ); + + if (_char == null && fallbackSystemFontContext != null) + { + // find in system font and result the relevant fontAsset as _fallbackFont; + _char = fallbackSystemFontContext.Resolve(unicode, fontStyle, fontWeight); + } + if (_char == null) return false; @@ -786,6 +804,63 @@ void UpdateShaderRatios() } } + class FallbackSystemFontContext + { + private TMPFont ownerFont; + private List candidateSystemFontPaths = new(); + public List loadedFontAssets = new(); + + internal FallbackSystemFontContext(TMPFont tmpFont, string[] fontFamily) + { + this.ownerFont = tmpFont; + SystemFontService.ResolveInstalledFonts(fontFamily ?? Array.Empty(), resultPaths: candidateSystemFontPaths); + Debug.Log($"{candidateSystemFontPaths.Count} fallback system fonts resolved:"); + foreach (var path in candidateSystemFontPaths) + { + Debug.Log(path); + } + } + + public TMP_Character Resolve(uint unicode, FontStyles fontStyles, FontWeight fontWeight) + { + // find in the loaded + foreach (var fontAsset in loadedFontAssets) + { + // why dont we check character in nativeFont directly? + var characterInfo = TMP_FontAssetUtilities.GetCharacterFromFontAsset(unicode, fontAsset, false, fontStyles, fontWeight, out _); + if (characterInfo != null) + { + return characterInfo; + } + } + + // load and find + var pathsCount = candidateSystemFontPaths.Count; + for (int i = loadedFontAssets.Count; i < pathsCount; i++) + { + var fontPath = candidateSystemFontPaths[i]; + var nativeFont = new Font(fontPath); + var originFontAsset = ownerFont.fontAsset; + var fontAsset = TMP_FontAsset.CreateFontAsset(nativeFont, 60, 9, GlyphRenderMode.SDFAA, originFontAsset.atlasWidth, originFontAsset.atlasHeight); + loadedFontAssets.Add(fontAsset); + var characterInfo = TMP_FontAssetUtilities.GetCharacterFromFontAsset(unicode, fontAsset, false, fontStyles, fontWeight, out _); + if (characterInfo != null) + { + return characterInfo; + } + } + + return null; + } + + public void ClearReferences() + { + ownerFont = null; + loadedFontAssets?.Clear(); + candidateSystemFontPaths?.Clear(); + } + } + class TMPFont_SubMesh : IMeshFactory { public TMPFont font; @@ -804,4 +879,4 @@ public void OnPopulateMesh(VertexBuffer vb) } } -#endif \ No newline at end of file +#endif diff --git a/Assets/Scripts/FairyGUI.asmdef b/Assets/Scripts/FairyGUI.asmdef index a2a73d76..d222baa0 100644 --- a/Assets/Scripts/FairyGUI.asmdef +++ b/Assets/Scripts/FairyGUI.asmdef @@ -6,7 +6,7 @@ ], "includePlatforms": [], "excludePlatforms": [], - "allowUnsafeCode": false, + "allowUnsafeCode": true, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, diff --git a/Assets/Scripts/UI/UIConfig.cs b/Assets/Scripts/UI/UIConfig.cs index 88195e69..d65cd2ab 100644 --- a/Assets/Scripts/UI/UIConfig.cs +++ b/Assets/Scripts/UI/UIConfig.cs @@ -17,6 +17,17 @@ public class UIConfig : MonoBehaviour /// public static string defaultFont = ""; + /// + /// Fallback system font when charater in the default font is not available.
+ /// + /// You can either type the family name or full name of the font. The full name is compared first to avoid naming conflicts. e.g. valid name can be: `Microsoft YaHei`, `Microsoft YaHei Bold`, `"Microsoft YaHei Regular`
+ /// + /// Fonts referenced here are not included in the build, when a character is failed to resolve, the first who matches with installed font in local machine will be taken to render the text continuously.
+ /// + /// NOTE: this feature is not supported on WebGL (the same as Console platform), as WebGL does not support system font loading for its limited file system. Additionally, the system fonts query API in which has permission limitations, meanwhile, it does not has an ability to query by specific LCID. see: https://wicg.github.io/local-font-access/#issue-05afe850 + ///
+ public static string[] systemFontFamily = Array.Empty(); + [Obsolete("No use anymore")] public static bool renderingTextBrighterOnDesktop = true; @@ -209,6 +220,7 @@ public enum ConfigKey DepthSupportForPaintingMode, RichTextRowVerticalAlign, Branch, + SystemFontFamily, PleaseSelect = 100 } @@ -365,6 +377,21 @@ public void Load() case ConfigKey.Branch: UIPackage.branch = value.s; break; + + case ConfigKey.SystemFontFamily: + if (string.IsNullOrEmpty(value.s)) + { + UIConfig.systemFontFamily = Array.Empty(); + + } + else + { + string[] familyNames = value.s.Split(new char[] { ',', ';', '\n' }, StringSplitOptions.RemoveEmptyEntries); + for (int j = 0; j < familyNames.Length; j++) + familyNames[j] = familyNames[j].Trim(); + UIConfig.systemFontFamily = familyNames; + } + break; } } } @@ -429,7 +456,12 @@ public static void SetDefaultValue(ConfigKey key, ConfigValue value) case ConfigKey.Branch: value.s = ""; break; + + case ConfigKey.SystemFontFamily: + value.s = "Noto Sans CJK SC, Noto Sans SC, Noto Sans Han, PingFang SC, Arial Unicode, SimHei, Hiragino Sans GB, Heiti SC, WenQuanYi Micro Hei"; + break; } + } public static void ClearResourceRefs() From 459703cbebb142bc066371b83a309d1d189b819c Mon Sep 17 00:00:00 2001 From: labbbirder <502100554@qq.com> Date: Fri, 18 Jul 2025 19:37:11 +0800 Subject: [PATCH 2/2] fix: add essential exception handler while retrieving encoding --- .../LightWeightReader/LightWeightFontFaceImpl.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs index ec0eb78c..9d8ddb26 100644 --- a/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs +++ b/Assets/Scripts/Extensions/TextMeshPro/LightWeightReader/LightWeightFontFaceImpl.cs @@ -30,7 +30,15 @@ static Encoding GetEncoding(EncodingCodePage codePage) { if (supportedEncodingCodePages.Contains(codePage)) { - return Encoding.GetEncoding((int)codePage); + try + { + return Encoding.GetEncoding((int)codePage); + } + catch + { + supportedEncodingCodePages.Remove(codePage); + return null; + } } else {