123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- // 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.
- using HarfBuzzSharp;
- using SkiaSharp;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Runtime.CompilerServices;
- using System.Runtime.InteropServices;
- using Topten.RichTextKit.Utils;
- namespace Topten.RichTextKit
- {
- /// <summary>
- /// Helper class for shaping text
- /// </summary>
- internal class TextShaper : IDisposable
- {
- /// <summary>
- /// Cache of shapers for typefaces
- /// </summary>
- static Dictionary<SKTypeface, TextShaper> _shapers = new Dictionary<SKTypeface, TextShaper>();
- /// <summary>
- /// Get the text shaper for a particular type face
- /// </summary>
- /// <param name="typeface">The typeface being queried for</param>
- /// <returns>A TextShaper</returns>
- public static TextShaper ForTypeface(SKTypeface typeface)
- {
- lock (_shapers)
- {
- if (!_shapers.TryGetValue(typeface, out var shaper))
- {
- shaper = new TextShaper(typeface);
- _shapers.Add(typeface, shaper);
- }
- return shaper;
- }
- }
- /// <summary>
- /// Constructs a new TextShaper
- /// </summary>
- /// <param name="typeface">The typeface of this shaper</param>
- private TextShaper(SKTypeface typeface)
- {
- // Store typeface
- _typeface = typeface;
- // Load the typeface stream to a HarfBuzz font
- int index;
- using (var blob = GetHarfBuzzBlob(typeface.OpenStream(out index)))
- using (var face = new Face(blob, (uint)index))
- {
- face.UnitsPerEm = typeface.UnitsPerEm;
- _font = new HarfBuzzSharp.Font(face);
- _font.SetScale(overScale, overScale);
- _font.SetFunctionsOpenType();
- }
- // Get font metrics for this typeface
- using (var paint = new SKPaint())
- {
- paint.Typeface = typeface;
- paint.TextSize = overScale;
- _fontMetrics = paint.FontMetrics;
- // This is a temporary hack until SkiaSharp exposes
- // a way to check if a font is fixed pitch. For now
- // we just measure and `i` and a `w` and see if they're
- // the same width.
- float[] widths = paint.GetGlyphWidths("iw", out var rects);
- _isFixedPitch = widths != null && widths.Length > 1 && widths[0] == widths[1];
- if (_isFixedPitch)
- _fixedCharacterWidth = widths[0];
- }
- }
- /// <summary>
- /// Dispose this text shaper
- /// </summary>
- public void Dispose()
- {
- if (_font != null)
- {
- _font.Dispose();
- _font = null;
- }
- }
- /// <summary>
- /// The HarfBuzz font for this shaper
- /// </summary>
- HarfBuzzSharp.Font _font;
- /// <summary>
- /// The typeface for this shaper
- /// </summary>
- SKTypeface _typeface;
- /// <summary>
- /// Font metrics for the font
- /// </summary>
- SKFontMetrics _fontMetrics;
- /// <summary>
- /// True if this font face is fixed pitch
- /// </summary>
- bool _isFixedPitch;
- /// <summary>
- /// Fixed pitch character width
- /// </summary>
- float _fixedCharacterWidth;
- /// <summary>
- /// A set of re-usable result buffers to store the result of text shaping operation
- /// </summary>
- public class ResultBufferSet
- {
- public void Clear()
- {
- GlyphIndicies.Clear();
- GlyphPositions.Clear();
- Clusters.Clear();
- CodePointXCoords.Clear();
- }
- public Buffer<ushort> GlyphIndicies = new Buffer<ushort>();
- public Buffer<SKPoint> GlyphPositions = new Buffer<SKPoint>();
- public Buffer<int> Clusters = new Buffer<int>();
- public Buffer<float> CodePointXCoords = new Buffer<float>();
- }
- /// <summary>
- /// Returned as the result of a text shaping operation
- /// </summary>
- public struct Result
- {
- /// <summary>
- /// The glyph indicies of all glyphs required to render the shaped text
- /// </summary>
- public Slice<ushort> GlyphIndicies;
- /// <summary>
- /// The position of each glyph
- /// </summary>
- public Slice<SKPoint> GlyphPositions;
- /// <summary>
- /// One entry for each glyph, showing the code point index
- /// of the characters it was derived from
- /// </summary>
- public Slice<int> Clusters;
- /// <summary>
- /// The end position of the rendered text
- /// </summary>
- public SKPoint EndXCoord;
- /// <summary>
- /// The X-Position of each passed code point
- /// </summary>
- public Slice<float> CodePointXCoords;
- /// <summary>
- /// The ascent of the font
- /// </summary>
- public float Ascent;
- /// <summary>
- /// The descent of the font
- /// </summary>
- public float Descent;
- /// <summary>
- /// The leading of the font
- /// </summary>
- public float Leading;
- /// <summary>
- /// The XMin for the font
- /// </summary>
- public float XMin;
- }
- /// <summary>
- /// Over scale used for all font operations
- /// </summary>
- const int overScale = 512;
- /// <summary>
- /// Shape an array of utf-32 code points replacing each grapheme cluster with a replacement character
- /// </summary>
- /// <param name="bufferSet">A re-usable text shaping buffer set that results will be allocated from</param>
- /// <param name="codePoints">The utf-32 code points to be shaped</param>
- /// <param name="style">The user style for the text</param>
- /// <param name="clusterAdjustment">A value to add to all reported cluster numbers</param>
- /// <returns>A TextShaper.Result representing the shaped text</returns>
- public Result ShapeReplacement(ResultBufferSet bufferSet, Slice<int> codePoints, IStyle style, int clusterAdjustment)
- {
- var clusters = GraphemeClusterAlgorithm.GetBoundaries(codePoints).ToArray();
- var glyph = _typeface.GetGlyph(style.ReplacementCharacter);
- var font = new SKFont(_typeface, overScale);
- float glyphScale = style.FontSize / overScale;
- float[] widths = new float[1];
- SKRect[] bounds = new SKRect[1];
- font.GetGlyphWidths((new ushort[] { glyph }).AsSpan(), widths.AsSpan(), bounds.AsSpan());
- var r = new Result();
- r.GlyphIndicies = bufferSet.GlyphIndicies.Add((int)clusters.Length-1, false);
- r.GlyphPositions = bufferSet.GlyphPositions.Add((int)clusters.Length-1, false);
- r.Clusters = bufferSet.Clusters.Add((int)clusters.Length-1, false);
- r.CodePointXCoords = bufferSet.CodePointXCoords.Add(codePoints.Length, false);
- r.CodePointXCoords.Fill(0);
- float xCoord = 0;
- for (int i = 0; i < clusters.Length-1; i++)
- {
- r.GlyphPositions[i].X = xCoord * glyphScale;
- r.GlyphPositions[i].Y = 0;
- r.GlyphIndicies[i] = codePoints[clusters[i]] == 0x2029 ? (ushort)0 : glyph;
- r.Clusters[i] = clusters[i] + clusterAdjustment;
- for (int j = clusters[i]; j < clusters[i + 1]; j++)
- {
- r.CodePointXCoords[j] = r.GlyphPositions[i].X;
- }
- xCoord += widths[0] + style.LetterSpacing / glyphScale;
- }
- // Also return the end cursor position
- r.EndXCoord = new SKPoint(xCoord * glyphScale, 0);
-
- ApplyFontMetrics(ref r, style.FontSize);
- return r;
- }
- /// <summary>
- /// Shape an array of utf-32 code points
- /// </summary>
- /// <param name="bufferSet">A re-usable text shaping buffer set that results will be allocated from</param>
- /// <param name="codePoints">The utf-32 code points to be shaped</param>
- /// <param name="style">The user style for the text</param>
- /// <param name="direction">LTR or RTL direction</param>
- /// <param name="clusterAdjustment">A value to add to all reported cluster numbers</param>
- /// <param name="asFallbackFor">The type face this font is a fallback for</param>
- /// <param name="textAlignment">The text alignment of the paragraph, used to control placement of glyphs within character cell when letter spacing used</param>
- /// <returns>A TextShaper.Result representing the shaped text</returns>
- public Result Shape(ResultBufferSet bufferSet, Slice<int> codePoints, IStyle style, TextDirection direction, int clusterAdjustment, SKTypeface asFallbackFor, TextAlignment textAlignment)
- {
- // Work out if we need to force this to a fixed pitch and if
- // so the unscale character width we need to use
- float forceFixedPitchWidth = 0;
- // ATZ: keep original shaper to set metrics on exit
- TextShaper originalTypefaceShaper = null;
- if (asFallbackFor != _typeface && asFallbackFor != null)
- {
- originalTypefaceShaper = ForTypeface(asFallbackFor);
- if (originalTypefaceShaper._isFixedPitch)
- {
- forceFixedPitchWidth = originalTypefaceShaper._fixedCharacterWidth;
- }
- }
- // Work out how much to shift glyphs in the character cell when using letter spacing
- // The idea here is to align the glyphs within the character cell the same way as the
- // text block alignment so that left/right aligned text still aligns with the margin
- // and centered text is still centered (and not shifted slightly due to the extra
- // space that would be at the right with normal letter spacing).
- float glyphLetterSpacingAdjustment = 0;
- switch (textAlignment)
- {
- case TextAlignment.Right:
- glyphLetterSpacingAdjustment = style.LetterSpacing;
- break;
- case TextAlignment.Center:
- glyphLetterSpacingAdjustment = style.LetterSpacing / 2;
- break;
- }
- using (var buffer = new HarfBuzzSharp.Buffer())
- {
- // Setup buffer
- buffer.AddUtf32(codePoints.AsSpan(), 0, -1);
- // Setup directionality (if supplied)
- switch (direction)
- {
- case TextDirection.LTR:
- buffer.Direction = Direction.LeftToRight;
- break;
- case TextDirection.RTL:
- buffer.Direction = Direction.RightToLeft;
- break;
- default:
- throw new ArgumentException(nameof(direction));
- }
- // Guess other attributes
- buffer.GuessSegmentProperties();
- // Shape it
- _font.Shape(buffer);
- // RTL?
- bool rtl = buffer.Direction == Direction.RightToLeft;
- // Work out glyph scaling and offsetting for super/subscript
- float glyphScale = style.FontSize / overScale;
- float glyphVOffset = 0;
- if (style.FontVariant == FontVariant.SuperScript)
- {
- glyphScale *= 0.65f;
- glyphVOffset -= style.FontSize * 0.35f;
- }
- if (style.FontVariant == FontVariant.SubScript)
- {
- glyphScale *= 0.65f;
- glyphVOffset += style.FontSize * 0.1f;
- }
- // Create results and get buffes
- var r = new Result();
- r.GlyphIndicies = bufferSet.GlyphIndicies.Add((int)buffer.Length, false);
- r.GlyphPositions = bufferSet.GlyphPositions.Add((int)buffer.Length, false);
- r.Clusters = bufferSet.Clusters.Add((int)buffer.Length, false);
- r.CodePointXCoords = bufferSet.CodePointXCoords.Add(codePoints.Length, false);
- r.CodePointXCoords.Fill(0);
-
- // Convert points
- var gp = buffer.GlyphPositions;
- var gi = buffer.GlyphInfos;
- float cursorX = 0;
- float cursorY = 0;
- float cursorXCluster = 0;
- for (int i = 0; i < buffer.Length; i++)
- {
- r.GlyphIndicies[i] = (ushort)gi[i].Codepoint;
- r.Clusters[i] = (int)gi[i].Cluster + clusterAdjustment;
-
- // Update code point positions
- if (!rtl)
- {
- // First cluster, different cluster, or same cluster with lower x-coord
- if ( i == 0 ||
- (r.Clusters[i] != r.Clusters[i - 1]) ||
- (cursorX < r.CodePointXCoords[r.Clusters[i] - clusterAdjustment]))
- {
- r.CodePointXCoords[r.Clusters[i] - clusterAdjustment] = cursorX;
- }
- }
-
- // Get the position
- var pos = gp[i];
- // Update glyph position
- r.GlyphPositions[i] = new SKPoint(
- cursorX + pos.XOffset * glyphScale + glyphLetterSpacingAdjustment,
- cursorY - pos.YOffset * glyphScale + glyphVOffset
- );
- // Update cursor position
- cursorX += pos.XAdvance * glyphScale;
- cursorY += pos.YAdvance * glyphScale;
- // Ensure paragraph separator character (0x2029) has some
- // width so it can be seen as part of the selection in the editor.
- if (pos.XAdvance == 0 && codePoints[(int)gi[i].Cluster] == 0x2029)
- {
- cursorX += style.FontSize * 2 / 3;
- }
- if (i+1 == gi.Length || gi[i].Cluster != gi[i+1].Cluster)
- {
- cursorX += style.LetterSpacing;
- }
- // Are we falling back for a fixed pitch font and is the next character a
- // new cluster? If so advance by the width of the original font, not this
- // fallback font
- if (forceFixedPitchWidth != 0)
- {
- // New cluster?
- if (i + 1 >= buffer.Length || gi[i].Cluster != gi[i + 1].Cluster)
- {
- // Work out fixed pitch position of next cluster
- cursorXCluster += forceFixedPitchWidth * glyphScale;
- if (cursorXCluster > cursorX)
- {
- // Nudge characters to center them in the fixed pitch width
- if (i == 0 || gi[i - 1].Cluster != gi[i].Cluster)
- {
- r.GlyphPositions[i].X += (cursorXCluster - cursorX)/ 2;
- }
- // Use fixed width character position
- cursorX = cursorXCluster;
- }
- else
- {
- // Character is wider (probably an emoji) so we
- // allow it to exceed the fixed pitch character width
- cursorXCluster = cursorX;
- }
- }
- }
- // Store RTL cursor position
- if (rtl)
- {
- // First cluster, different cluster, or same cluster with lower x-coord
- if (i == 0 ||
- (r.Clusters[i] != r.Clusters[i - 1]) ||
- (cursorX > r.CodePointXCoords[r.Clusters[i] - clusterAdjustment]))
- {
- r.CodePointXCoords[r.Clusters[i] - clusterAdjustment] = cursorX;
- }
- }
- }
- // Finalize cursor positions by filling in any that weren't
- // referenced by a cluster
- if (rtl)
- {
- r.CodePointXCoords[0] = cursorX;
- for (int i = codePoints.Length - 2; i >= 0; i--)
- {
- if (r.CodePointXCoords[i] == 0)
- r.CodePointXCoords[i] = r.CodePointXCoords[i + 1];
- }
- }
- else
- {
- for (int i = 1; i < codePoints.Length; i++)
- {
- if (r.CodePointXCoords[i] == 0)
- r.CodePointXCoords[i] = r.CodePointXCoords[i - 1];
- }
- }
- // Also return the end cursor position
- r.EndXCoord = new SKPoint(cursorX, cursorY);
- // And some other useful metrics
- // ATZ: we have to match original typeface metrics in case of font fallback (mimic gdi+ behavior)
- (originalTypefaceShaper ?? this).ApplyFontMetrics(ref r, style.FontSize);
- // Done
- return r;
- }
- }
- private void ApplyFontMetrics(ref Result result, float fontSize)
- {
- // And some other useful metrics
- result.Ascent = _fontMetrics.Ascent * fontSize / overScale;
- result.Descent = _fontMetrics.Descent * fontSize / overScale;
- result.Leading = _fontMetrics.Leading * fontSize / overScale;
- result.XMin = _fontMetrics.XMin * fontSize / overScale;
- }
- private static Blob GetHarfBuzzBlob(SKStreamAsset asset)
- {
- if (asset == null)
- throw new ArgumentNullException(nameof(asset));
- Blob blob;
- var size = asset.Length;
- var memoryBase = asset.GetMemoryBase();
- if (memoryBase != IntPtr.Zero)
- {
- // the underlying stream is really a mamory block
- // so save on copying and just use that directly
- blob = new Blob(memoryBase, size, MemoryMode.ReadOnly, () => asset.Dispose());
- }
- else
- {
- // this could be a forward-only stream, so we must copy
- var ptr = Marshal.AllocCoTaskMem(size);
- asset.Read(ptr, size);
- blob = new Blob(ptr, size, MemoryMode.ReadOnly, () => Marshal.FreeCoTaskMem(ptr));
- }
- // make immutable for performance?
- blob.MakeImmutable();
- return blob;
- }
- }
- }
|