HarfBuzzTextShaperImpl.cs 5.8 KB

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