TextShaper.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  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. using HarfBuzzSharp;
  16. using SkiaSharp;
  17. using System;
  18. using System.Collections.Generic;
  19. using System.Linq;
  20. using System.Runtime.CompilerServices;
  21. using System.Runtime.InteropServices;
  22. using Topten.RichTextKit.Utils;
  23. namespace Topten.RichTextKit
  24. {
  25. /// <summary>
  26. /// Helper class for shaping text
  27. /// </summary>
  28. internal class TextShaper : IDisposable
  29. {
  30. /// <summary>
  31. /// Cache of shapers for typefaces
  32. /// </summary>
  33. static Dictionary<SKTypeface, TextShaper> _shapers = new Dictionary<SKTypeface, TextShaper>();
  34. /// <summary>
  35. /// Get the text shaper for a particular type face
  36. /// </summary>
  37. /// <param name="typeface">The typeface being queried for</param>
  38. /// <returns>A TextShaper</returns>
  39. public static TextShaper ForTypeface(SKTypeface typeface)
  40. {
  41. lock (_shapers)
  42. {
  43. if (!_shapers.TryGetValue(typeface, out var shaper))
  44. {
  45. shaper = new TextShaper(typeface);
  46. _shapers.Add(typeface, shaper);
  47. }
  48. return shaper;
  49. }
  50. }
  51. /// <summary>
  52. /// Constructs a new TextShaper
  53. /// </summary>
  54. /// <param name="typeface">The typeface of this shaper</param>
  55. private TextShaper(SKTypeface typeface)
  56. {
  57. // Store typeface
  58. _typeface = typeface;
  59. // Load the typeface stream to a HarfBuzz font
  60. int index;
  61. using (var blob = GetHarfBuzzBlob(typeface.OpenStream(out index)))
  62. using (var face = new Face(blob, (uint)index))
  63. {
  64. face.UnitsPerEm = typeface.UnitsPerEm;
  65. _font = new HarfBuzzSharp.Font(face);
  66. _font.SetScale(overScale, overScale);
  67. _font.SetFunctionsOpenType();
  68. }
  69. // Get font metrics for this typeface
  70. using (var paint = new SKPaint())
  71. {
  72. paint.Typeface = typeface;
  73. paint.TextSize = overScale;
  74. _fontMetrics = paint.FontMetrics;
  75. // This is a temporary hack until SkiaSharp exposes
  76. // a way to check if a font is fixed pitch. For now
  77. // we just measure and `i` and a `w` and see if they're
  78. // the same width.
  79. float[] widths = paint.GetGlyphWidths("iw", out var rects);
  80. _isFixedPitch = widths != null && widths.Length > 1 && widths[0] == widths[1];
  81. if (_isFixedPitch)
  82. _fixedCharacterWidth = widths[0];
  83. }
  84. }
  85. /// <summary>
  86. /// Dispose this text shaper
  87. /// </summary>
  88. public void Dispose()
  89. {
  90. if (_font != null)
  91. {
  92. _font.Dispose();
  93. _font = null;
  94. }
  95. }
  96. /// <summary>
  97. /// The HarfBuzz font for this shaper
  98. /// </summary>
  99. HarfBuzzSharp.Font _font;
  100. /// <summary>
  101. /// The typeface for this shaper
  102. /// </summary>
  103. SKTypeface _typeface;
  104. /// <summary>
  105. /// Font metrics for the font
  106. /// </summary>
  107. SKFontMetrics _fontMetrics;
  108. /// <summary>
  109. /// True if this font face is fixed pitch
  110. /// </summary>
  111. bool _isFixedPitch;
  112. /// <summary>
  113. /// Fixed pitch character width
  114. /// </summary>
  115. float _fixedCharacterWidth;
  116. /// <summary>
  117. /// A set of re-usable result buffers to store the result of text shaping operation
  118. /// </summary>
  119. public class ResultBufferSet
  120. {
  121. public void Clear()
  122. {
  123. GlyphIndicies.Clear();
  124. GlyphPositions.Clear();
  125. Clusters.Clear();
  126. CodePointXCoords.Clear();
  127. }
  128. public Buffer<ushort> GlyphIndicies = new Buffer<ushort>();
  129. public Buffer<SKPoint> GlyphPositions = new Buffer<SKPoint>();
  130. public Buffer<int> Clusters = new Buffer<int>();
  131. public Buffer<float> CodePointXCoords = new Buffer<float>();
  132. }
  133. /// <summary>
  134. /// Returned as the result of a text shaping operation
  135. /// </summary>
  136. public struct Result
  137. {
  138. /// <summary>
  139. /// The glyph indicies of all glyphs required to render the shaped text
  140. /// </summary>
  141. public Slice<ushort> GlyphIndicies;
  142. /// <summary>
  143. /// The position of each glyph
  144. /// </summary>
  145. public Slice<SKPoint> GlyphPositions;
  146. /// <summary>
  147. /// One entry for each glyph, showing the code point index
  148. /// of the characters it was derived from
  149. /// </summary>
  150. public Slice<int> Clusters;
  151. /// <summary>
  152. /// The end position of the rendered text
  153. /// </summary>
  154. public SKPoint EndXCoord;
  155. /// <summary>
  156. /// The X-Position of each passed code point
  157. /// </summary>
  158. public Slice<float> CodePointXCoords;
  159. /// <summary>
  160. /// The ascent of the font
  161. /// </summary>
  162. public float Ascent;
  163. /// <summary>
  164. /// The descent of the font
  165. /// </summary>
  166. public float Descent;
  167. /// <summary>
  168. /// The leading of the font
  169. /// </summary>
  170. public float Leading;
  171. /// <summary>
  172. /// The XMin for the font
  173. /// </summary>
  174. public float XMin;
  175. }
  176. /// <summary>
  177. /// Over scale used for all font operations
  178. /// </summary>
  179. const int overScale = 512;
  180. /// <summary>
  181. /// Shape an array of utf-32 code points replacing each grapheme cluster with a replacement character
  182. /// </summary>
  183. /// <param name="bufferSet">A re-usable text shaping buffer set that results will be allocated from</param>
  184. /// <param name="codePoints">The utf-32 code points to be shaped</param>
  185. /// <param name="style">The user style for the text</param>
  186. /// <param name="clusterAdjustment">A value to add to all reported cluster numbers</param>
  187. /// <returns>A TextShaper.Result representing the shaped text</returns>
  188. public Result ShapeReplacement(ResultBufferSet bufferSet, Slice<int> codePoints, IStyle style, int clusterAdjustment)
  189. {
  190. var clusters = GraphemeClusterAlgorithm.GetBoundaries(codePoints).ToArray();
  191. var glyph = _typeface.GetGlyph(style.ReplacementCharacter);
  192. var font = new SKFont(_typeface, overScale);
  193. float glyphScale = style.FontSize / overScale;
  194. float[] widths = new float[1];
  195. SKRect[] bounds = new SKRect[1];
  196. font.GetGlyphWidths((new ushort[] { glyph }).AsSpan(), widths.AsSpan(), bounds.AsSpan());
  197. var r = new Result();
  198. r.GlyphIndicies = bufferSet.GlyphIndicies.Add((int)clusters.Length-1, false);
  199. r.GlyphPositions = bufferSet.GlyphPositions.Add((int)clusters.Length-1, false);
  200. r.Clusters = bufferSet.Clusters.Add((int)clusters.Length-1, false);
  201. r.CodePointXCoords = bufferSet.CodePointXCoords.Add(codePoints.Length, false);
  202. r.CodePointXCoords.Fill(0);
  203. float xCoord = 0;
  204. for (int i = 0; i < clusters.Length-1; i++)
  205. {
  206. r.GlyphPositions[i].X = xCoord * glyphScale;
  207. r.GlyphPositions[i].Y = 0;
  208. r.GlyphIndicies[i] = codePoints[clusters[i]] == 0x2029 ? (ushort)0 : glyph;
  209. r.Clusters[i] = clusters[i] + clusterAdjustment;
  210. for (int j = clusters[i]; j < clusters[i + 1]; j++)
  211. {
  212. r.CodePointXCoords[j] = r.GlyphPositions[i].X;
  213. }
  214. xCoord += widths[0] + style.LetterSpacing / glyphScale;
  215. }
  216. // Also return the end cursor position
  217. r.EndXCoord = new SKPoint(xCoord * glyphScale, 0);
  218. ApplyFontMetrics(ref r, style.FontSize);
  219. return r;
  220. }
  221. /// <summary>
  222. /// Shape an array of utf-32 code points
  223. /// </summary>
  224. /// <param name="bufferSet">A re-usable text shaping buffer set that results will be allocated from</param>
  225. /// <param name="codePoints">The utf-32 code points to be shaped</param>
  226. /// <param name="style">The user style for the text</param>
  227. /// <param name="direction">LTR or RTL direction</param>
  228. /// <param name="clusterAdjustment">A value to add to all reported cluster numbers</param>
  229. /// <param name="asFallbackFor">The type face this font is a fallback for</param>
  230. /// <param name="textAlignment">The text alignment of the paragraph, used to control placement of glyphs within character cell when letter spacing used</param>
  231. /// <returns>A TextShaper.Result representing the shaped text</returns>
  232. public Result Shape(ResultBufferSet bufferSet, Slice<int> codePoints, IStyle style, TextDirection direction, int clusterAdjustment, SKTypeface asFallbackFor, TextAlignment textAlignment)
  233. {
  234. // Work out if we need to force this to a fixed pitch and if
  235. // so the unscale character width we need to use
  236. float forceFixedPitchWidth = 0;
  237. // ATZ: keep original shaper to set metrics on exit
  238. TextShaper originalTypefaceShaper = null;
  239. if (asFallbackFor != _typeface && asFallbackFor != null)
  240. {
  241. originalTypefaceShaper = ForTypeface(asFallbackFor);
  242. if (originalTypefaceShaper._isFixedPitch)
  243. {
  244. forceFixedPitchWidth = originalTypefaceShaper._fixedCharacterWidth;
  245. }
  246. }
  247. // Work out how much to shift glyphs in the character cell when using letter spacing
  248. // The idea here is to align the glyphs within the character cell the same way as the
  249. // text block alignment so that left/right aligned text still aligns with the margin
  250. // and centered text is still centered (and not shifted slightly due to the extra
  251. // space that would be at the right with normal letter spacing).
  252. float glyphLetterSpacingAdjustment = 0;
  253. switch (textAlignment)
  254. {
  255. case TextAlignment.Right:
  256. glyphLetterSpacingAdjustment = style.LetterSpacing;
  257. break;
  258. case TextAlignment.Center:
  259. glyphLetterSpacingAdjustment = style.LetterSpacing / 2;
  260. break;
  261. }
  262. using (var buffer = new HarfBuzzSharp.Buffer())
  263. {
  264. // Setup buffer
  265. buffer.AddUtf32(codePoints.AsSpan(), 0, -1);
  266. // Setup directionality (if supplied)
  267. switch (direction)
  268. {
  269. case TextDirection.LTR:
  270. buffer.Direction = Direction.LeftToRight;
  271. break;
  272. case TextDirection.RTL:
  273. buffer.Direction = Direction.RightToLeft;
  274. break;
  275. default:
  276. throw new ArgumentException(nameof(direction));
  277. }
  278. // Guess other attributes
  279. buffer.GuessSegmentProperties();
  280. // Shape it
  281. _font.Shape(buffer);
  282. // RTL?
  283. bool rtl = buffer.Direction == Direction.RightToLeft;
  284. // Work out glyph scaling and offsetting for super/subscript
  285. float glyphScale = style.FontSize / overScale;
  286. float glyphVOffset = 0;
  287. if (style.FontVariant == FontVariant.SuperScript)
  288. {
  289. glyphScale *= 0.65f;
  290. glyphVOffset -= style.FontSize * 0.35f;
  291. }
  292. if (style.FontVariant == FontVariant.SubScript)
  293. {
  294. glyphScale *= 0.65f;
  295. glyphVOffset += style.FontSize * 0.1f;
  296. }
  297. // Create results and get buffes
  298. var r = new Result();
  299. r.GlyphIndicies = bufferSet.GlyphIndicies.Add((int)buffer.Length, false);
  300. r.GlyphPositions = bufferSet.GlyphPositions.Add((int)buffer.Length, false);
  301. r.Clusters = bufferSet.Clusters.Add((int)buffer.Length, false);
  302. r.CodePointXCoords = bufferSet.CodePointXCoords.Add(codePoints.Length, false);
  303. r.CodePointXCoords.Fill(0);
  304. // Convert points
  305. var gp = buffer.GlyphPositions;
  306. var gi = buffer.GlyphInfos;
  307. float cursorX = 0;
  308. float cursorY = 0;
  309. float cursorXCluster = 0;
  310. for (int i = 0; i < buffer.Length; i++)
  311. {
  312. r.GlyphIndicies[i] = (ushort)gi[i].Codepoint;
  313. r.Clusters[i] = (int)gi[i].Cluster + clusterAdjustment;
  314. // Update code point positions
  315. if (!rtl)
  316. {
  317. // First cluster, different cluster, or same cluster with lower x-coord
  318. if ( i == 0 ||
  319. (r.Clusters[i] != r.Clusters[i - 1]) ||
  320. (cursorX < r.CodePointXCoords[r.Clusters[i] - clusterAdjustment]))
  321. {
  322. r.CodePointXCoords[r.Clusters[i] - clusterAdjustment] = cursorX;
  323. }
  324. }
  325. // Get the position
  326. var pos = gp[i];
  327. // Update glyph position
  328. r.GlyphPositions[i] = new SKPoint(
  329. cursorX + pos.XOffset * glyphScale + glyphLetterSpacingAdjustment,
  330. cursorY - pos.YOffset * glyphScale + glyphVOffset
  331. );
  332. // Update cursor position
  333. cursorX += pos.XAdvance * glyphScale;
  334. cursorY += pos.YAdvance * glyphScale;
  335. // Ensure paragraph separator character (0x2029) has some
  336. // width so it can be seen as part of the selection in the editor.
  337. if (pos.XAdvance == 0 && codePoints[(int)gi[i].Cluster] == 0x2029)
  338. {
  339. cursorX += style.FontSize * 2 / 3;
  340. }
  341. if (i+1 == gi.Length || gi[i].Cluster != gi[i+1].Cluster)
  342. {
  343. cursorX += style.LetterSpacing;
  344. }
  345. // Are we falling back for a fixed pitch font and is the next character a
  346. // new cluster? If so advance by the width of the original font, not this
  347. // fallback font
  348. if (forceFixedPitchWidth != 0)
  349. {
  350. // New cluster?
  351. if (i + 1 >= buffer.Length || gi[i].Cluster != gi[i + 1].Cluster)
  352. {
  353. // Work out fixed pitch position of next cluster
  354. cursorXCluster += forceFixedPitchWidth * glyphScale;
  355. if (cursorXCluster > cursorX)
  356. {
  357. // Nudge characters to center them in the fixed pitch width
  358. if (i == 0 || gi[i - 1].Cluster != gi[i].Cluster)
  359. {
  360. r.GlyphPositions[i].X += (cursorXCluster - cursorX)/ 2;
  361. }
  362. // Use fixed width character position
  363. cursorX = cursorXCluster;
  364. }
  365. else
  366. {
  367. // Character is wider (probably an emoji) so we
  368. // allow it to exceed the fixed pitch character width
  369. cursorXCluster = cursorX;
  370. }
  371. }
  372. }
  373. // Store RTL cursor position
  374. if (rtl)
  375. {
  376. // First cluster, different cluster, or same cluster with lower x-coord
  377. if (i == 0 ||
  378. (r.Clusters[i] != r.Clusters[i - 1]) ||
  379. (cursorX > r.CodePointXCoords[r.Clusters[i] - clusterAdjustment]))
  380. {
  381. r.CodePointXCoords[r.Clusters[i] - clusterAdjustment] = cursorX;
  382. }
  383. }
  384. }
  385. // Finalize cursor positions by filling in any that weren't
  386. // referenced by a cluster
  387. if (rtl)
  388. {
  389. r.CodePointXCoords[0] = cursorX;
  390. for (int i = codePoints.Length - 2; i >= 0; i--)
  391. {
  392. if (r.CodePointXCoords[i] == 0)
  393. r.CodePointXCoords[i] = r.CodePointXCoords[i + 1];
  394. }
  395. }
  396. else
  397. {
  398. for (int i = 1; i < codePoints.Length; i++)
  399. {
  400. if (r.CodePointXCoords[i] == 0)
  401. r.CodePointXCoords[i] = r.CodePointXCoords[i - 1];
  402. }
  403. }
  404. // Also return the end cursor position
  405. r.EndXCoord = new SKPoint(cursorX, cursorY);
  406. // And some other useful metrics
  407. // ATZ: we have to match original typeface metrics in case of font fallback (mimic gdi+ behavior)
  408. (originalTypefaceShaper ?? this).ApplyFontMetrics(ref r, style.FontSize);
  409. // Done
  410. return r;
  411. }
  412. }
  413. private void ApplyFontMetrics(ref Result result, float fontSize)
  414. {
  415. // And some other useful metrics
  416. result.Ascent = _fontMetrics.Ascent * fontSize / overScale;
  417. result.Descent = _fontMetrics.Descent * fontSize / overScale;
  418. result.Leading = _fontMetrics.Leading * fontSize / overScale;
  419. result.XMin = _fontMetrics.XMin * fontSize / overScale;
  420. }
  421. private static Blob GetHarfBuzzBlob(SKStreamAsset asset)
  422. {
  423. if (asset == null)
  424. throw new ArgumentNullException(nameof(asset));
  425. Blob blob;
  426. var size = asset.Length;
  427. var memoryBase = asset.GetMemoryBase();
  428. if (memoryBase != IntPtr.Zero)
  429. {
  430. // the underlying stream is really a mamory block
  431. // so save on copying and just use that directly
  432. blob = new Blob(memoryBase, size, MemoryMode.ReadOnly, () => asset.Dispose());
  433. }
  434. else
  435. {
  436. // this could be a forward-only stream, so we must copy
  437. var ptr = Marshal.AllocCoTaskMem(size);
  438. asset.Read(ptr, size);
  439. blob = new Blob(ptr, size, MemoryMode.ReadOnly, () => Marshal.FreeCoTaskMem(ptr));
  440. }
  441. // make immutable for performance?
  442. blob.MakeImmutable();
  443. return blob;
  444. }
  445. }
  446. }