HarfBuzzTextShaperImpl.cs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. using System;
  2. using System.Buffers;
  3. using System.Collections.Concurrent;
  4. using System.Globalization;
  5. using System.Runtime.InteropServices;
  6. using Avalonia.Media.TextFormatting;
  7. using Avalonia.Media.TextFormatting.Unicode;
  8. using Avalonia.Platform;
  9. using HarfBuzzSharp;
  10. using Buffer = HarfBuzzSharp.Buffer;
  11. using GlyphInfo = HarfBuzzSharp.GlyphInfo;
  12. namespace Avalonia.UnitTests
  13. {
  14. internal class HarfBuzzTextShaperImpl : ITextShaperImpl
  15. {
  16. private static readonly ConcurrentDictionary<int, Language> s_cachedLanguage = new();
  17. public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
  18. {
  19. var textSpan = text.Span;
  20. var typeface = options.Typeface;
  21. var fontRenderingEmSize = options.FontRenderingEmSize;
  22. var bidiLevel = options.BidiLevel;
  23. var culture = options.Culture;
  24. using (var buffer = new Buffer())
  25. {
  26. // HarfBuzz needs the surrounding characters to correctly shape the text
  27. var containingText = GetContainingMemory(text, out var start, out var length).Span;
  28. buffer.AddUtf16(containingText, start, length);
  29. MergeBreakPair(buffer);
  30. buffer.GuessSegmentProperties();
  31. buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft;
  32. var usedCulture = culture ?? CultureInfo.CurrentCulture;
  33. buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture));
  34. var font = ((HarfBuzzGlyphTypefaceImpl)typeface).Font;
  35. font.Shape(buffer);
  36. if (buffer.Direction == Direction.RightToLeft)
  37. {
  38. buffer.Reverse();
  39. }
  40. font.GetScale(out var scaleX, out _);
  41. var textScale = fontRenderingEmSize / scaleX;
  42. var bufferLength = buffer.Length;
  43. var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);
  44. var glyphInfos = buffer.GetGlyphInfoSpan();
  45. var glyphPositions = buffer.GetGlyphPositionSpan();
  46. for (var i = 0; i < bufferLength; i++)
  47. {
  48. var sourceInfo = glyphInfos[i];
  49. var glyphIndex = (ushort)sourceInfo.Codepoint;
  50. var glyphCluster = (int)(sourceInfo.Cluster);
  51. var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing;
  52. var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
  53. if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t')
  54. {
  55. glyphIndex = typeface.GetGlyph(' ');
  56. glyphAdvance = options.IncrementalTabWidth > 0 ?
  57. options.IncrementalTabWidth :
  58. 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
  59. }
  60. shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
  61. }
  62. return shapedBuffer;
  63. }
  64. }
  65. private static void MergeBreakPair(Buffer buffer)
  66. {
  67. var length = buffer.Length;
  68. var glyphInfos = buffer.GetGlyphInfoSpan();
  69. var second = glyphInfos[length - 1];
  70. if (!new Codepoint(second.Codepoint).IsBreakChar)
  71. {
  72. return;
  73. }
  74. if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n')
  75. {
  76. var first = glyphInfos[length - 2];
  77. first.Codepoint = '\u200C';
  78. second.Codepoint = '\u200C';
  79. second.Cluster = first.Cluster;
  80. unsafe
  81. {
  82. fixed (GlyphInfo* p = &glyphInfos[length - 2])
  83. {
  84. *p = first;
  85. }
  86. fixed (GlyphInfo* p = &glyphInfos[length - 1])
  87. {
  88. *p = second;
  89. }
  90. }
  91. }
  92. else
  93. {
  94. second.Codepoint = '\u200C';
  95. unsafe
  96. {
  97. fixed (GlyphInfo* p = &glyphInfos[length - 1])
  98. {
  99. *p = second;
  100. }
  101. }
  102. }
  103. }
  104. private static Vector GetGlyphOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale)
  105. {
  106. var position = glyphPositions[index];
  107. var offsetX = position.XOffset * textScale;
  108. var offsetY = position.YOffset * textScale;
  109. return new Vector(offsetX, offsetY);
  110. }
  111. private static double GetGlyphAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale)
  112. {
  113. // Depends on direction of layout
  114. // glyphPositions[index].YAdvance * textScale;
  115. return glyphPositions[index].XAdvance * textScale;
  116. }
  117. private static ReadOnlyMemory<char> GetContainingMemory(ReadOnlyMemory<char> memory, out int start, out int length)
  118. {
  119. if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length))
  120. {
  121. return containingString.AsMemory();
  122. }
  123. if (MemoryMarshal.TryGetArray(memory, out var segment))
  124. {
  125. start = segment.Offset;
  126. length = segment.Count;
  127. return segment.Array.AsMemory();
  128. }
  129. if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager<char> memoryManager, out start, out length))
  130. {
  131. return memoryManager.Memory;
  132. }
  133. // should never happen
  134. throw new InvalidOperationException("Memory not backed by string, array or manager");
  135. }
  136. }
  137. }