// RichTextKit // Copyright © 2019-2020 Topten Software. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this product except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // Modifications by Alexander Tsyganeko: marked by ATZ using SkiaSharp; using System; using System.Collections.Generic; using Topten.RichTextKit.Utils; namespace Topten.RichTextKit { /// /// Helper to split a run of code points based on a particular typeface /// into a series of runs where unsupported code points are mapped to a /// fallback font. /// public class FontFallback { /// /// Specified details about a font fallback run /// public struct Run { /// /// The starting code point index of this run /// public int Start; /// /// The length of this run in code points /// public int Length; /// /// The typeface to be used for this run /// public SKTypeface Typeface; } /// /// Specifies the instance of the character matcher to be used for font fallback /// /// /// This instance is shared by all TextBlock instances and should be thread safe /// if used in a multi-threaded environment. /// public static ICharacterMatcher CharacterMatcher = new DefaultCharacterMatcher(); /// /// Splits a sequence of code points into a series of runs with font fallback applied /// /// The code points /// The preferred typeface /// The replacement character to be used for the run /// A sequence of runs with unsupported code points replaced by a selected font fallback // ATZ /*public static IEnumerable GetFontRuns(Slice codePoints, SKTypeface typeface, char replacementCharacter = '\0') { var font = new SKFont(typeface); if (replacementCharacter != '\0') { var glyph = font.GetGlyph(replacementCharacter); if (glyph == 0) { var fallbackTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, replacementCharacter); if (fallbackTypeface != null) typeface = fallbackTypeface; } yield return new Run() { Start = 0, Length = codePoints.Length, Typeface = typeface, }; yield break; } // Get glyphs using the top-level typeface var glyphs = new ushort[codePoints.Length]; font.GetGlyphs(codePoints.AsSpan(), glyphs); // Look for subspans that need font fallback (where glyphs are zero) int runStart = 0; for (int i = 0; i < codePoints.Length; i++) { // Do we need fallback for this character? if (glyphs[i] == 0) { // Check if there's a fallback available, if not, might as well continue with the current top-level typeface var subSpanTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[i]); if (subSpanTypeface == null) continue; // Don't fallback for whitespace characters if (UnicodeClasses.BoundaryGroup(codePoints[i]) == WordBoundaryClass.Space) continue; // Must be a cluster boundary if (!GraphemeClusterAlgorithm.IsBoundary(codePoints, i)) continue; // We can do font fallback... // Flush the current top-level run if (i > runStart) { yield return new Run() { Start = runStart, Length = i - runStart, Typeface = typeface, }; } // Count how many unmatched characters var unmatchedStart = i; var unmatchedEnd = i + 1; while (unmatchedEnd < codePoints.Length && (glyphs[unmatchedEnd] == 0 || !GraphemeClusterAlgorithm.IsBoundary(codePoints, unmatchedEnd))) { unmatchedEnd++; } var unmatchedLength = unmatchedEnd - unmatchedStart; // Match the missing characters while (unmatchedLength > 0) { // Find the font fallback using the first character subSpanTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[unmatchedStart]); if (subSpanTypeface == null) { unmatchedEnd = unmatchedStart; break; } var subSpanFont = new SKFont(subSpanTypeface); // Get the glyphs over the current unmatched range subSpanFont.GetGlyphs(codePoints.SubSlice(unmatchedStart, unmatchedLength).AsSpan(), new Span(glyphs, unmatchedStart, unmatchedLength)); // Count how many characters were matched var fallbackStart = unmatchedStart; var fallbackEnd = unmatchedStart + 1; while (fallbackEnd < unmatchedEnd && glyphs[fallbackEnd] != 0) fallbackEnd++; var fallbackLength = fallbackEnd - fallbackStart; // Yield this font fallback run yield return new Run() { Start = fallbackStart, Length = fallbackLength, Typeface = subSpanTypeface, }; // Continue selecting font fallbacks until the entire unmatched ranges has been matched unmatchedStart += fallbackLength; unmatchedLength -= fallbackLength; } // Move onto the next top level span i = unmatchedEnd - 1; // account for i++ on for loop runStart = unmatchedEnd; } } // Flush find run if (codePoints.Length > runStart) { yield return new Run() { Start = runStart, Length = codePoints.Length - runStart, Typeface = typeface, }; } }*/ public static IEnumerable GetFontRuns(Slice codePoints, SKTypeface typeface, char replacementCharacter = '\0') { if (replacementCharacter != '\0') { var font = new SKFont(typeface); var glyph = font.GetGlyph(replacementCharacter); if (glyph == 0) { var fallbackTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, replacementCharacter); if (fallbackTypeface != null) typeface = fallbackTypeface; } yield return new Run() { Start = 0, Length = codePoints.Length, Typeface = typeface, }; yield break; } var glyphs = new ushort[codePoints.Length]; foreach (var r in GetUnresolvedFontRuns(codePoints, new Slice(glyphs), 0, codePoints.Length, typeface, typeface)) { yield return r; } } // this implementation produces more natural looking result. It replaces empty glyphs only whereas the original implementaion fills glyphs // from unmatchedStart to unmatchedLength. This may result in oddly looking glyphs if two or more fallbacks are used. // test case: 漢語,又称中文、汉文、國文 // this text when being drawn with Arial font will require two fallbacks on Win11 private static IEnumerable GetUnresolvedFontRuns(Slice codePoints, Slice glyphs, int start, int length, SKTypeface originalTypeface, SKTypeface typeface) { var font = new SKFont(typeface); font.GetGlyphs(codePoints.SubSlice(start, length).AsSpan(), glyphs.SubSlice(start, length).AsSpan()); // Look for subspans that need font fallback (where glyphs are zero) int runStart = start; for (int i = start; i < start + length; i++) { if (glyphs[i] == 0) { // Don't fallback for whitespace characters if (UnicodeClasses.BoundaryGroup(codePoints[i]) == WordBoundaryClass.Space) continue; // Must be a cluster boundary if (!GraphemeClusterAlgorithm.IsBoundary(codePoints, i)) continue; // We can do font fallback... // Check if there's a fallback available, if not, might as well continue with the current top-level typeface var subSpanTypeface = CharacterMatcher.MatchCharacter(originalTypeface.FamilyName, originalTypeface.FontWeight, originalTypeface.FontWidth, originalTypeface.FontSlant, null, codePoints[i]); if (subSpanTypeface == null) continue; // Flush the current run if (i > runStart) { yield return new Run() { Start = runStart, Length = i - runStart, Typeface = typeface, }; } // Count how many unmatched characters var unmatchedStart = i; var unmatchedEnd = i + 1; while (unmatchedEnd < codePoints.Length && (glyphs[unmatchedEnd] == 0 || !GraphemeClusterAlgorithm.IsBoundary(codePoints, unmatchedEnd))) { unmatchedEnd++; } var unmatchedLength = unmatchedEnd - unmatchedStart; foreach (var r in GetUnresolvedFontRuns(codePoints, glyphs, unmatchedStart, unmatchedLength, originalTypeface, subSpanTypeface)) { yield return r; } // Move onto the next top level span i = unmatchedEnd - 1; // account for i++ on for loop runStart = unmatchedEnd; } } // Flush find run if (start + length > runStart) { yield return new Run() { Start = runStart, Length = start + length - runStart, Typeface = typeface, }; } } } }