// 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,
};
}
}
}
}