FontFallback.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. // RichTextKit
  2. // Copyright © 2019-2020 Topten Software. All Rights Reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. // not use this product except in compliance with the License. You may obtain
  6. // a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. // License for the specific language governing permissions and limitations
  14. // under the License.
  15. // Modifications by Alexander Tsyganeko: marked by ATZ
  16. using SkiaSharp;
  17. using System;
  18. using System.Collections.Generic;
  19. using Topten.RichTextKit.Utils;
  20. namespace Topten.RichTextKit
  21. {
  22. /// <summary>
  23. /// Helper to split a run of code points based on a particular typeface
  24. /// into a series of runs where unsupported code points are mapped to a
  25. /// fallback font.
  26. /// </summary>
  27. public class FontFallback
  28. {
  29. /// <summary>
  30. /// Specified details about a font fallback run
  31. /// </summary>
  32. public struct Run
  33. {
  34. /// <summary>
  35. /// The starting code point index of this run
  36. /// </summary>
  37. public int Start;
  38. /// <summary>
  39. /// The length of this run in code points
  40. /// </summary>
  41. public int Length;
  42. /// <summary>
  43. /// The typeface to be used for this run
  44. /// </summary>
  45. public SKTypeface Typeface;
  46. }
  47. /// <summary>
  48. /// Specifies the instance of the character matcher to be used for font fallback
  49. /// </summary>
  50. /// <remarks>
  51. /// This instance is shared by all TextBlock instances and should be thread safe
  52. /// if used in a multi-threaded environment.
  53. /// </remarks>
  54. public static ICharacterMatcher CharacterMatcher = new DefaultCharacterMatcher();
  55. /// <summary>
  56. /// Splits a sequence of code points into a series of runs with font fallback applied
  57. /// </summary>
  58. /// <param name="codePoints">The code points</param>
  59. /// <param name="typeface">The preferred typeface</param>
  60. /// <param name="replacementCharacter">The replacement character to be used for the run</param>
  61. /// <returns>A sequence of runs with unsupported code points replaced by a selected font fallback</returns>
  62. // ATZ
  63. /*public static IEnumerable<Run> GetFontRuns(Slice<int> codePoints, SKTypeface typeface, char replacementCharacter = '\0')
  64. {
  65. var font = new SKFont(typeface);
  66. if (replacementCharacter != '\0')
  67. {
  68. var glyph = font.GetGlyph(replacementCharacter);
  69. if (glyph == 0)
  70. {
  71. var fallbackTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, replacementCharacter);
  72. if (fallbackTypeface != null)
  73. typeface = fallbackTypeface;
  74. }
  75. yield return new Run()
  76. {
  77. Start = 0,
  78. Length = codePoints.Length,
  79. Typeface = typeface,
  80. };
  81. yield break;
  82. }
  83. // Get glyphs using the top-level typeface
  84. var glyphs = new ushort[codePoints.Length];
  85. font.GetGlyphs(codePoints.AsSpan(), glyphs);
  86. // Look for subspans that need font fallback (where glyphs are zero)
  87. int runStart = 0;
  88. for (int i = 0; i < codePoints.Length; i++)
  89. {
  90. // Do we need fallback for this character?
  91. if (glyphs[i] == 0)
  92. {
  93. // Check if there's a fallback available, if not, might as well continue with the current top-level typeface
  94. var subSpanTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[i]);
  95. if (subSpanTypeface == null)
  96. continue;
  97. // Don't fallback for whitespace characters
  98. if (UnicodeClasses.BoundaryGroup(codePoints[i]) == WordBoundaryClass.Space)
  99. continue;
  100. // Must be a cluster boundary
  101. if (!GraphemeClusterAlgorithm.IsBoundary(codePoints, i))
  102. continue;
  103. // We can do font fallback...
  104. // Flush the current top-level run
  105. if (i > runStart)
  106. {
  107. yield return new Run()
  108. {
  109. Start = runStart,
  110. Length = i - runStart,
  111. Typeface = typeface,
  112. };
  113. }
  114. // Count how many unmatched characters
  115. var unmatchedStart = i;
  116. var unmatchedEnd = i + 1;
  117. while (unmatchedEnd < codePoints.Length &&
  118. (glyphs[unmatchedEnd] == 0 || !GraphemeClusterAlgorithm.IsBoundary(codePoints, unmatchedEnd)))
  119. {
  120. unmatchedEnd++;
  121. }
  122. var unmatchedLength = unmatchedEnd - unmatchedStart;
  123. // Match the missing characters
  124. while (unmatchedLength > 0)
  125. {
  126. // Find the font fallback using the first character
  127. subSpanTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[unmatchedStart]);
  128. if (subSpanTypeface == null)
  129. {
  130. unmatchedEnd = unmatchedStart;
  131. break;
  132. }
  133. var subSpanFont = new SKFont(subSpanTypeface);
  134. // Get the glyphs over the current unmatched range
  135. subSpanFont.GetGlyphs(codePoints.SubSlice(unmatchedStart, unmatchedLength).AsSpan(), new Span<ushort>(glyphs, unmatchedStart, unmatchedLength));
  136. // Count how many characters were matched
  137. var fallbackStart = unmatchedStart;
  138. var fallbackEnd = unmatchedStart + 1;
  139. while (fallbackEnd < unmatchedEnd && glyphs[fallbackEnd] != 0)
  140. fallbackEnd++;
  141. var fallbackLength = fallbackEnd - fallbackStart;
  142. // Yield this font fallback run
  143. yield return new Run()
  144. {
  145. Start = fallbackStart,
  146. Length = fallbackLength,
  147. Typeface = subSpanTypeface,
  148. };
  149. // Continue selecting font fallbacks until the entire unmatched ranges has been matched
  150. unmatchedStart += fallbackLength;
  151. unmatchedLength -= fallbackLength;
  152. }
  153. // Move onto the next top level span
  154. i = unmatchedEnd - 1; // account for i++ on for loop
  155. runStart = unmatchedEnd;
  156. }
  157. }
  158. // Flush find run
  159. if (codePoints.Length > runStart)
  160. {
  161. yield return new Run()
  162. {
  163. Start = runStart,
  164. Length = codePoints.Length - runStart,
  165. Typeface = typeface,
  166. };
  167. }
  168. }*/
  169. public static IEnumerable<Run> GetFontRuns(Slice<int> codePoints, SKTypeface typeface, char replacementCharacter = '\0')
  170. {
  171. if (replacementCharacter != '\0')
  172. {
  173. var font = new SKFont(typeface);
  174. var glyph = font.GetGlyph(replacementCharacter);
  175. if (glyph == 0)
  176. {
  177. var fallbackTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, replacementCharacter);
  178. if (fallbackTypeface != null)
  179. typeface = fallbackTypeface;
  180. }
  181. yield return new Run()
  182. {
  183. Start = 0,
  184. Length = codePoints.Length,
  185. Typeface = typeface,
  186. };
  187. yield break;
  188. }
  189. var glyphs = new ushort[codePoints.Length];
  190. foreach (var r in GetUnresolvedFontRuns(codePoints, new Slice<ushort>(glyphs), 0, codePoints.Length, typeface, typeface))
  191. {
  192. yield return r;
  193. }
  194. }
  195. // this implementation produces more natural looking result. It replaces empty glyphs only whereas the original implementaion fills glyphs
  196. // from unmatchedStart to unmatchedLength. This may result in oddly looking glyphs if two or more fallbacks are used.
  197. // test case: 漢語,又称中文、汉文、國文
  198. // this text when being drawn with Arial font will require two fallbacks on Win11
  199. private static IEnumerable<Run> GetUnresolvedFontRuns(Slice<int> codePoints, Slice<ushort> glyphs, int start, int length, SKTypeface originalTypeface, SKTypeface typeface)
  200. {
  201. var font = new SKFont(typeface);
  202. font.GetGlyphs(codePoints.SubSlice(start, length).AsSpan(), glyphs.SubSlice(start, length).AsSpan());
  203. // Look for subspans that need font fallback (where glyphs are zero)
  204. int runStart = start;
  205. for (int i = start; i < start + length; i++)
  206. {
  207. if (glyphs[i] == 0)
  208. {
  209. // Don't fallback for whitespace characters
  210. if (UnicodeClasses.BoundaryGroup(codePoints[i]) == WordBoundaryClass.Space)
  211. continue;
  212. // Must be a cluster boundary
  213. if (!GraphemeClusterAlgorithm.IsBoundary(codePoints, i))
  214. continue;
  215. // We can do font fallback...
  216. // Check if there's a fallback available, if not, might as well continue with the current top-level typeface
  217. var subSpanTypeface = CharacterMatcher.MatchCharacter(originalTypeface.FamilyName, originalTypeface.FontWeight, originalTypeface.FontWidth, originalTypeface.FontSlant, null, codePoints[i]);
  218. if (subSpanTypeface == null)
  219. continue;
  220. // Flush the current run
  221. if (i > runStart)
  222. {
  223. yield return new Run()
  224. {
  225. Start = runStart,
  226. Length = i - runStart,
  227. Typeface = typeface,
  228. };
  229. }
  230. // Count how many unmatched characters
  231. var unmatchedStart = i;
  232. var unmatchedEnd = i + 1;
  233. while (unmatchedEnd < codePoints.Length &&
  234. (glyphs[unmatchedEnd] == 0 || !GraphemeClusterAlgorithm.IsBoundary(codePoints, unmatchedEnd)))
  235. {
  236. unmatchedEnd++;
  237. }
  238. var unmatchedLength = unmatchedEnd - unmatchedStart;
  239. foreach (var r in GetUnresolvedFontRuns(codePoints, glyphs, unmatchedStart, unmatchedLength, originalTypeface, subSpanTypeface))
  240. {
  241. yield return r;
  242. }
  243. // Move onto the next top level span
  244. i = unmatchedEnd - 1; // account for i++ on for loop
  245. runStart = unmatchedEnd;
  246. }
  247. }
  248. // Flush find run
  249. if (start + length > runStart)
  250. {
  251. yield return new Run()
  252. {
  253. Start = runStart,
  254. Length = start + length - runStart,
  255. Typeface = typeface,
  256. };
  257. }
  258. }
  259. }
  260. }