public ShapedBuffer ShapeText(ReadOnlySlice <char> text, TextShaperOptions options)
        {
            var typeface            = options.Typeface;
            var fontRenderingEmSize = options.FontRenderingEmSize;
            var bidiLevel           = options.BidLevel;
            var culture             = options.Culture;

            using (var buffer = new Buffer())
            {
                buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);

                MergeBreakPair(buffer);

                buffer.GuessSegmentProperties();

                buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft;

                buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);

                var font = ((HarfBuzzGlyphTypefaceImpl)typeface.PlatformImpl).Font;

                font.Shape(buffer);

                if (buffer.Direction == Direction.RightToLeft)
                {
                    buffer.Reverse();
                }

                font.GetScale(out var scaleX, out _);

                var textScale = fontRenderingEmSize / scaleX;

                var bufferLength = buffer.Length;

                var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);

                var glyphInfos = buffer.GetGlyphInfoSpan();

                var glyphPositions = buffer.GetGlyphPositionSpan();

                for (var i = 0; i < bufferLength; i++)
                {
                    var sourceInfo = glyphInfos[i];

                    var glyphIndex = (ushort)sourceInfo.Codepoint;

                    var glyphCluster = (int)sourceInfo.Cluster;

                    var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);

                    var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);

                    var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);

                    shapedBuffer[i] = targetInfo;
                }

                return(shapedBuffer);
            }
        }
Example #2
0
        public new Result Shape(string text, float xOffset, float yOffset, SKPaint paint)
        {
            if (string.IsNullOrEmpty(text))
            {
                return(new Result());
            }

            using var buffer = new Buffer();
            switch (paint.TextEncoding)
            {
            case SKTextEncoding.Utf8:
                buffer.AddUtf8(text);
                break;

            case SKTextEncoding.Utf16:
                buffer.AddUtf16(text);
                break;

            case SKTextEncoding.Utf32:
                buffer.AddUtf32(text);
                break;

            default:
                throw new NotSupportedException("TextEncoding of type GlyphId is not supported.");
            }

            buffer.GuessSegmentProperties();
            return(Shape(buffer, xOffset, yOffset, paint));
        }
Example #3
0
        public void CanCreateFaceShaperFromTypeface()
        {
            var skiaTypeface = SKTypeface.FromFile(Path.Combine(PathToFonts, "content-font.ttf"));

            var clusters   = new uint[] { 4, 2, 0 };
            var codepoints = new uint[] { 629, 668, 891 };

            using (var face = new Face(GetFaceBlob, () => skiaTypeface.Dispose()))
                using (var font = new Font(face))
                    using (var buffer = new HarfBuzzSharp.Buffer())
                    {
                        buffer.AddUtf8("متن");
                        buffer.GuessSegmentProperties();

                        font.Shape(buffer);

                        Assert.Equal(clusters, buffer.GlyphInfos.Select(i => i.Cluster));
                        Assert.Equal(codepoints, buffer.GlyphInfos.Select(i => i.Codepoint));
                    }

            Blob GetFaceBlob(Face face, Tag tag)
            {
                var size = skiaTypeface.GetTableSize(tag);
                var data = Marshal.AllocCoTaskMem(size);

                skiaTypeface.TryGetTableData(tag, 0, size, data);
                return(new Blob(data, size, MemoryMode.Writeable, () => Marshal.FreeCoTaskMem(data)));
            }
        }
Example #4
0
        public GlyphRun ShapeText(ReadOnlySlice <char> text, Typeface typeface, double fontRenderingEmSize,
                                  CultureInfo culture)
        {
            using (var buffer = new Buffer())
            {
                FillBuffer(buffer, text);

                buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);

                buffer.GuessSegmentProperties();

                var glyphTypeface = typeface.GlyphTypeface;

                var font = ((HarfBuzzGlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font;

                font.Shape(buffer);

                font.GetScale(out var scaleX, out _);

                var textScale = fontRenderingEmSize / scaleX;

                var bufferLength = buffer.Length;

                var glyphInfos = buffer.GetGlyphInfoSpan();

                var glyphPositions = buffer.GetGlyphPositionSpan();

                var glyphIndices = new ushort[bufferLength];

                var clusters = new ushort[bufferLength];

                double[] glyphAdvances = null;

                Vector[] glyphOffsets = null;

                for (var i = 0; i < bufferLength; i++)
                {
                    glyphIndices[i] = (ushort)glyphInfos[i].Codepoint;

                    clusters[i] = (ushort)glyphInfos[i].Cluster;

                    if (!glyphTypeface.IsFixedPitch)
                    {
                        SetAdvance(glyphPositions, i, textScale, ref glyphAdvances);
                    }

                    SetOffset(glyphPositions, i, textScale, ref glyphOffsets);
                }

                return(new GlyphRun(glyphTypeface, fontRenderingEmSize,
                                    new ReadOnlySlice <ushort>(glyphIndices),
                                    new ReadOnlySlice <double>(glyphAdvances),
                                    new ReadOnlySlice <Vector>(glyphOffsets),
                                    text,
                                    new ReadOnlySlice <ushort>(clusters),
                                    buffer.Direction == Direction.LeftToRight ? 0 : 1));
            }
        }
Example #5
0
        public void ShouldSerializeGlyphs()
        {
            using (var typeface = SKTypeface.FromFile(Path.Combine(PathToFonts, "content-font.ttf")))
                using (var blob = typeface.OpenStream(out var index).ToHarfBuzzBlob())
                    using (var face = new Face(blob, index))
                        using (var font = new Font(face))
                            using (var buffer = new Buffer())
                            {
                                buffer.AddUtf16(SimpleText);

                                buffer.GuessSegmentProperties();

                                font.Shape(buffer);

                                var serializedGlyphs = buffer.SerializeGlyphs();

                                Assert.Equal(SerializedSimpleText, serializedGlyphs);
                            }
        }
Example #6
0
        public GlyphRun ShapeText(ReadOnlySlice <char> text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture)
        {
            using (var buffer = new Buffer())
            {
                buffer.ContentType = ContentType.Unicode;

                var breakCharPosition = text.Length - 1;

                var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count);

                if (codepoint.IsBreakChar)
                {
                    var breakCharCount = 1;

                    if (text.Length > 1)
                    {
                        var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _);

                        if (codepoint == '\r' && previousCodepoint == '\n' ||
                            codepoint == '\n' && previousCodepoint == '\r')
                        {
                            breakCharCount = 2;
                        }
                    }

                    if (breakCharPosition != text.Start)
                    {
                        buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount));
                    }

                    var cluster = buffer.GlyphInfos.Length > 0 ?
                                  buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 :
                                  (uint)text.Start;

                    switch (breakCharCount)
                    {
                    case 1:
                        buffer.Add('\u200C', cluster);
                        break;

                    case 2:
                        buffer.Add('\u200C', cluster);
                        buffer.Add('\u200D', cluster);
                        break;
                    }
                }
                else
                {
                    buffer.AddUtf16(text.Buffer.Span);
                }

                buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);

                buffer.GuessSegmentProperties();

                var glyphTypeface = typeface.GlyphTypeface;

                var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font;

                font.Shape(buffer);

                font.GetScale(out var scaleX, out _);

                var textScale = fontRenderingEmSize / scaleX;

                var bufferLength = buffer.Length;

                var glyphInfos = buffer.GetGlyphInfoSpan();

                var glyphPositions = buffer.GetGlyphPositionSpan();

                var glyphIndices = new ushort[bufferLength];

                var clusters = new ushort[bufferLength];

                double[] glyphAdvances = null;

                Vector[] glyphOffsets = null;

                for (var i = 0; i < bufferLength; i++)
                {
                    glyphIndices[i] = (ushort)glyphInfos[i].Codepoint;

                    clusters[i] = (ushort)(text.Start + glyphInfos[i].Cluster);

                    if (!glyphTypeface.IsFixedPitch)
                    {
                        SetAdvance(glyphPositions, i, textScale, ref glyphAdvances);
                    }

                    SetOffset(glyphPositions, i, textScale, ref glyphOffsets);
                }

                return(new GlyphRun(glyphTypeface, fontRenderingEmSize,
                                    new ReadOnlySlice <ushort>(glyphIndices),
                                    new ReadOnlySlice <double>(glyphAdvances),
                                    new ReadOnlySlice <Vector>(glyphOffsets),
                                    text,
                                    new ReadOnlySlice <ushort>(clusters)));
            }
        }
Example #7
0
        public ShapedBuffer ShapeText(ReadOnlySlice <char> text, TextShaperOptions options)
        {
            var typeface            = options.Typeface;
            var fontRenderingEmSize = options.FontRenderingEmSize;
            var bidiLevel           = options.BidLevel;
            var culture             = options.Culture;

            using (var buffer = new Buffer())
            {
                buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length);

                MergeBreakPair(buffer);

                buffer.GuessSegmentProperties();

                buffer.Direction = Direction.LeftToRight; //Always shape LeftToRight

                buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);

                var font = ((GlyphTypefaceImpl)typeface.PlatformImpl).Font;

                font.Shape(buffer);

                font.GetScale(out var scaleX, out _);

                var textScale = fontRenderingEmSize / scaleX;

                var bufferLength = buffer.Length;

                var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel);

                var glyphInfos = buffer.GetGlyphInfoSpan();

                var glyphPositions = buffer.GetGlyphPositionSpan();

                for (var i = 0; i < bufferLength; i++)
                {
                    var sourceInfo = glyphInfos[i];

                    var glyphIndex = (ushort)sourceInfo.Codepoint;

                    var glyphCluster = (int)(sourceInfo.Cluster);

                    var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);

                    var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);

                    if (glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
                    {
                        glyphIndex = typeface.GetGlyph(' ');

                        glyphAdvance = options.IncrementalTabWidth > 0 ?
                                       options.IncrementalTabWidth :
                                       4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
                    }

                    var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);

                    shapedBuffer[i] = targetInfo;
                }

                return(shapedBuffer);
            }
        }
Example #8
0
        public GlyphRun ShapeText(ReadOnlySlice <char> text, TextFormat textFormat)
        {
            using (var buffer = new Buffer())
            {
                buffer.ContentType = ContentType.Unicode;

                var breakCharPosition = text.Length - 1;

                var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count);

                if (codepoint.IsBreakChar)
                {
                    var breakCharCount = 1;

                    if (text.Length > 1)
                    {
                        var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _);

                        if (codepoint == '\r' && previousCodepoint == '\n' ||
                            codepoint == '\n' && previousCodepoint == '\r')
                        {
                            breakCharCount = 2;
                        }
                    }

                    if (breakCharPosition != text.Start)
                    {
                        buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount));
                    }

                    var cluster = buffer.GlyphInfos.Length > 0 ?
                                  buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 :
                                  (uint)text.Start;

                    switch (breakCharCount)
                    {
                    case 1:
                        buffer.Add('\u200C', cluster);
                        break;

                    case 2:
                        buffer.Add('\u200C', cluster);
                        buffer.Add('\u200D', cluster);
                        break;
                    }
                }
                else
                {
                    buffer.AddUtf16(text.Buffer.Span);
                }

                buffer.GuessSegmentProperties();

                var glyphTypeface = textFormat.Typeface.GlyphTypeface;

                var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font;

                font.Shape(buffer);

                font.GetScale(out var scaleX, out _);

                var textScale = textFormat.FontRenderingEmSize / scaleX;

                var len = buffer.Length;

                var info = buffer.GetGlyphInfoSpan();

                var pos = buffer.GetGlyphPositionSpan();

                var glyphIndices = new ushort[len];

                var clusters = new ushort[len];

                var glyphAdvances = new double[len];

                var glyphOffsets = new Vector[len];

                for (var i = 0; i < len; i++)
                {
                    glyphIndices[i] = (ushort)info[i].Codepoint;

                    clusters[i] = (ushort)(text.Start + info[i].Cluster);

                    var advanceX = pos[i].XAdvance * textScale;
                    // Depends on direction of layout
                    //var advanceY = pos[i].YAdvance * textScale;

                    glyphAdvances[i] = advanceX;

                    var offsetX = pos[i].XOffset * textScale;
                    var offsetY = pos[i].YOffset * textScale;

                    glyphOffsets[i] = new Vector(offsetX, offsetY);
                }

                return(new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize,
                                    new ReadOnlySlice <ushort>(glyphIndices),
                                    new ReadOnlySlice <double>(glyphAdvances),
                                    new ReadOnlySlice <Vector>(glyphOffsets),
                                    text,
                                    new ReadOnlySlice <ushort>(clusters)));
            }
        }
Example #9
0
        /// <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;

            if (asFallbackFor != _typeface && asFallbackFor != null)
            {
                var 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(new ReadOnlySpan <int>(codePoints.Underlying, codePoints.Start, codePoints.Length), 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;

                    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
                r.Ascent  = _fontMetrics.Ascent * style.FontSize / overScale;
                r.Descent = _fontMetrics.Descent * style.FontSize / overScale;
                r.XMin    = _fontMetrics.XMin * style.FontSize / overScale;

                // Done
                return(r);
            }
        }
Example #10
0
        /// <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>
        /// <returns>A TextShaper.Result representing the shaped text</returns>
        public Result Shape(ResultBufferSet bufferSet, Slice <int> codePoints, IStyle style, TextDirection direction, int clusterAdjustment = 0)
        {
            using (var buffer = new HarfBuzzSharp.Buffer())
            {
                // Setup buffer
                unsafe
                {
                    fixed(int *pCodePoints = codePoints.Underlying)
                    {
                        hb_buffer_add_utf32(buffer.Handle, (IntPtr)(pCodePoints + codePoints.Start), codePoints.Length, 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);

                // Convert points
                var   gp      = buffer.GlyphPositions;
                var   gi      = buffer.GlyphInfos;
                float cursorX = 0;
                float cursorY = 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,
                        cursorY - pos.YOffset * glyphScale + glyphVOffset
                        );

                    // Update cursor position
                    cursorX += pos.XAdvance * glyphScale;
                    cursorY += pos.YAdvance * glyphScale;

                    // 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
                r.Ascent  = _fontMetrics.Ascent * style.FontSize / overScale;
                r.Descent = _fontMetrics.Descent * style.FontSize / overScale;
                r.XMin    = _fontMetrics.XMin * style.FontSize / overScale;

                // Done
                return(r);
            }
        }