/// <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); // 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; return(r); }
/// <summary> /// Splits a sequence of code points into a series of runs with font fallback applied /// </summary> /// <param name="codePoints">The code points</param> /// <param name="typeface">The preferred typeface</param> /// <returns>A sequence of runs with unsupported code points replaced by a selected font fallback</returns> public static IEnumerable <Run> GetFontRuns(Slice <int> codePoints, SKTypeface typeface) { // Get glyphs using the top-level typeface var glyphs = new ushort[codePoints.Length]; var font = new SKFont(typeface); font.GetGlyphs(codePoints.AsSpan(), glyphs); // Look for subspans that need font fallback (where glyphs are zero) int runStart = 0; for (int i = 0; i < codePoints.Length; i++) { // Do we need fallback for this character? if (glyphs[i] == 0) { // Check if there's a fallback available, if not, might as well continue with the current top-level typeface var subSpanTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[i]); if (subSpanTypeface == null) { continue; } // Must be a cluster boundary if (!GraphemeClusterAlgorithm.IsBoundary(codePoints, i)) { continue; } // We can do font fallback... // Flush the current top-level run if (i > runStart) { yield return(new Run() { Start = runStart, Length = i - runStart, Typeface = typeface, }); } // Count how many unmatched characters var unmatchedStart = i; var unmatchedEnd = i + 1; while (unmatchedEnd < codePoints.Length && (glyphs[unmatchedEnd] == 0 || !GraphemeClusterAlgorithm.IsBoundary(codePoints, unmatchedEnd))) { unmatchedEnd++; } var unmatchedLength = unmatchedEnd - unmatchedStart; // Match the missing characters while (unmatchedLength > 0) { // Find the font fallback using the first character subSpanTypeface = CharacterMatcher.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[unmatchedStart]); if (subSpanTypeface == null) { unmatchedEnd = unmatchedStart; break; } var subSpanFont = new SKFont(subSpanTypeface); // Get the glyphs over the current unmatched range subSpanFont.GetGlyphs(codePoints.SubSlice(unmatchedStart, unmatchedLength).AsSpan(), new Span <ushort>(glyphs, unmatchedStart, unmatchedLength)); // Count how many characters were matched var fallbackStart = unmatchedStart; var fallbackEnd = unmatchedStart + 1; while (fallbackEnd < unmatchedEnd && glyphs[fallbackEnd] != 0) { fallbackEnd++; } var fallbackLength = fallbackEnd - fallbackStart; // Yield this font fallback run yield return(new Run() { Start = fallbackStart, Length = fallbackLength, Typeface = subSpanTypeface, }); // Continue selecting font fallbacks until the entire unmatched ranges has been matched unmatchedStart += fallbackLength; unmatchedLength -= fallbackLength; } // Move onto the next top level span i = unmatchedEnd - 1; // account for i++ on for loop runStart = unmatchedEnd; } } // Flush find run if (codePoints.Length > runStart) { yield return(new Run() { Start = runStart, Length = codePoints.Length - runStart, Typeface = typeface, }); } }