File: System\Windows\Media\GlyphsSerializer.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
//
//
 
using System.Globalization;
using System.Text;
 
namespace System.Windows.Media
{
    /// <summary>
    ///
    /// </summary>
    internal class GlyphsSerializer
    {
        #region public methods
        /// <summary>
        ///
        /// </summary>
        /// <param name="glyphRun"></param>
        public GlyphsSerializer(GlyphRun glyphRun)
        {
            ArgumentNullException.ThrowIfNull(glyphRun);
 
            _glyphTypeface = glyphRun.GlyphTypeface;
            _milToEm = EmScaleFactor / glyphRun.FontRenderingEmSize;
 
            _sideways = glyphRun.IsSideways;
 
            _characters = glyphRun.Characters;
            _caretStops = glyphRun.CaretStops;
 
            // the first value in the cluster map can be non-zero, in which case it's applied as an offset to all
            // subsequent entries in the cluster map
            _clusters = glyphRun.ClusterMap;
            if (_clusters != null)
                _glyphClusterInitialOffset = _clusters[0];
 
            _indices = glyphRun.GlyphIndices;
            _advances = glyphRun.AdvanceWidths;
            _offsets = glyphRun.GlyphOffsets;
 
            _currentAdvanceTotal = 0.0;
            _idealAdvanceTotal = 0.0;
 
            // "100,50,,0;".Length is a capacity estimate for an individual glyph
            _glyphStringBuider = new StringBuilder(10);
 
            // string length * _glyphStringBuider.Capacity is an estimate for the whole string
            _indicesStringBuider = new StringBuilder(
                Math.Max( 
                    (_characters == null ? 0 : _characters.Count), 
                    _indices.Count
                ) 
                * _glyphStringBuider.Capacity
            );
        }
 
        /// <summary>
        /// Encode glyph run glyph information into Indices, UnicodeString and CaretStops string.
        /// </summary>
        public void ComputeContentStrings(out string characters, out string indices, out string caretStops)
        {
            _currentAdvanceTotal = 0.0;
            _idealAdvanceTotal = 0.0;
 
            if (_clusters != null)
            {
                // the algorithm works by finding (n:m) clusters and appending m glyphs for each cluster
                int characterIndex;
                int glyphClusterStart = 0;
                int charClusterStart = 0;
                bool forceNewCluster = true;
 
                for (characterIndex = 0; characterIndex < _clusters.Count; ++characterIndex)
                {
                    if (forceNewCluster)
                    {
                        glyphClusterStart = _clusters[characterIndex];
                        charClusterStart = characterIndex;
                        forceNewCluster = false;
                        continue;
                    }
 
                    if (_clusters[characterIndex] != glyphClusterStart)
                    {
                        // end of cluster, flush it
                        Debug.Assert(_clusters[characterIndex] > glyphClusterStart);
                        AddCluster(glyphClusterStart - _glyphClusterInitialOffset, _clusters[characterIndex] - _glyphClusterInitialOffset, charClusterStart, characterIndex);
 
                        // start a new cluster
                        glyphClusterStart = _clusters[characterIndex];
                        charClusterStart = characterIndex;
                    }
                    // otherwise, we are still within a cluster
                }
 
                // flush the last cluster
                Debug.Assert(_indices.Count > glyphClusterStart - _glyphClusterInitialOffset);
                AddCluster(glyphClusterStart - _glyphClusterInitialOffset, _indices.Count, charClusterStart, characterIndex);
            }
            else
            {
                // zero cluster map means 1:1 mapping
                Debug.Assert(_characters == null || _characters.Count == 0 || _indices.Count == _characters.Count);
                for (int i = 0; i < _indices.Count; ++i)
                    AddCluster(i, i + 1, i, i + 1);
            }
 
            // remove trailing semicolons
            RemoveTrailingCharacters(_indicesStringBuider, GlyphSeparator);
            indices = _indicesStringBuider.ToString();
 
            if (_characters == null || _characters.Count == 0)
            {
                characters = string.Empty;
            }
            else
            {
                StringBuilder builder = new StringBuilder(_characters.Count);
                foreach(char ch in _characters)
                {
                    builder.Append(ch);
                }
 
                characters = builder.ToString();
            }
 
            caretStops = CreateCaretStopsString();
        }
 
        #endregion public methods
 
        #region private methods
        private void RemoveTrailingCharacters(StringBuilder sb, char trailingCharacter)
        {
            int length = sb.Length;
            int trailingCharIndex = length - 1;
 
            while (trailingCharIndex >= 0)
            {
                if (sb[trailingCharIndex] != trailingCharacter)
                    break;
 
                --trailingCharIndex;
            }
 
            sb.Length = trailingCharIndex + 1;
        }
 
        private void AddGlyph(int glyph, int sourceCharacter)
        {
            Debug.Assert(_glyphStringBuider.Length == 0);
 
            // glyph index
            ushort fontIndex = _indices[glyph];
            ushort glyphIndexFromCmap;
 
            if (sourceCharacter == -1 ||
                !_glyphTypeface.CharacterToGlyphMap.TryGetValue(sourceCharacter, out glyphIndexFromCmap) ||
                fontIndex != glyphIndexFromCmap)
            {
                _glyphStringBuider.Append(fontIndex.ToString(CultureInfo.InvariantCulture));
            }
 
            _glyphStringBuider.Append(GlyphSubEntrySeparator);
 
            // advance width
            // #7499 Advance width needs to be specified if it differs from what is in the font tables. [ECMA-388 O5.5]
            // Most commonly it differs after shaping, e.g. when kerning is applied. (Ex. 12-15)
            // XPS supports floating point values, but in the interest of file size, we want to specify integers.
 
            double shapingAdvance = _advances[glyph] * _milToEm;
            double fontAdvance = _sideways ? _glyphTypeface.AdvanceHeights[fontIndex] : _glyphTypeface.AdvanceWidths[fontIndex];
 
            // To minimize rounding errors, we keep track of the unrounded advance total as required by [M5.6].
            int roundedShapingAdvance = (int)Math.Round(_idealAdvanceTotal + shapingAdvance - _currentAdvanceTotal);
            int roundedFontAdvance = (int)Math.Round(fontAdvance);
 
            if (roundedShapingAdvance != roundedFontAdvance)
            {
                _glyphStringBuider.Append(roundedShapingAdvance.ToString(CultureInfo.InvariantCulture));
                _currentAdvanceTotal += roundedShapingAdvance;
                _idealAdvanceTotal += shapingAdvance;
            }
            else
            {
                // when the value comes from the font tables, the specification does not mandate clients to do any rounding
                _currentAdvanceTotal += fontAdvance;
                _idealAdvanceTotal += fontAdvance;
            }
 
            _glyphStringBuider.Append(GlyphSubEntrySeparator);
 
            // u,v offset
            if (_offsets != null)
            {
                // u offset
                int offset = (int)Math.Round(_offsets[glyph].X * _milToEm);
 
                if (offset != 0)
                    _glyphStringBuider.Append(offset.ToString(CultureInfo.InvariantCulture));
 
                _glyphStringBuider.Append(GlyphSubEntrySeparator);
 
                // v offset
                offset = (int)Math.Round(_offsets[glyph].Y * _milToEm);
                if (offset != 0)
                    _glyphStringBuider.Append(offset.ToString(CultureInfo.InvariantCulture));
 
                _glyphStringBuider.Append(GlyphSubEntrySeparator);
            }
 
            // flags are not implemented yet
            // remove trailing commas
            RemoveTrailingCharacters(_glyphStringBuider, GlyphSubEntrySeparator);
            _glyphStringBuider.Append(GlyphSeparator);
            _indicesStringBuider.Append(_glyphStringBuider);
 
            // reset for next glyph
            _glyphStringBuider.Length = 0;
        }
 
        private void AddCluster(int glyphClusterStart, int glyphClusterEnd, int charClusterStart, int charClusterEnd)
        {
            int charactersInCluster = charClusterEnd - charClusterStart;
            int glyphsInCluster = glyphClusterEnd - glyphClusterStart;
 
            // no source character to deduce glyph properties from
            int sourceCharacter = -1;
 
            // the format is ... [(CharacterClusterSize[:GlyphClusterSize])] GlyphIndex ...
            if (glyphsInCluster != 1)
            {
                _indicesStringBuider.AppendFormat(CultureInfo.InvariantCulture, "({0}:{1})", charactersInCluster, glyphsInCluster);
            }
            else
            {
                if (charactersInCluster != 1)
                    _indicesStringBuider.AppendFormat(CultureInfo.InvariantCulture, "({0})", charactersInCluster);
                else
                {
                    // 1:1 cluster, we can omit (n:m) specification and possibly deduce some
                    // glyph properties from character
                    if (_characters != null && _characters.Count != 0)
                        sourceCharacter = _characters[charClusterStart];
                }
            }
 
            for (int glyph = glyphClusterStart; glyph < glyphClusterEnd; ++glyph)
            {
                AddGlyph(glyph, sourceCharacter);
            }
        }
 
        private string CreateCaretStopsString()
        {
            if (_caretStops == null)
                return String.Empty;
 
            // Since the trailing 0xF (i.e. all true) entries in the caret stop specifications can be omitted,
            // we can limit the caret stop list walk until the last nibble that contains 'false'.
            
            int caretStopStringLength = 0;
            int lastCaretStop = 0;
            for (int i = _caretStops.Count - 1; i >= 0; --i)
            {
                if (!_caretStops[i])
                {
                    caretStopStringLength = (i + 4) / 4;
 
                    // lastCaretStop to consider when building, the rest will correpond to 0xF entries
                    lastCaretStop = Math.Min(i | 3, _caretStops.Count - 1);
 
                    break;
                }
            }
 
            // All values are set to true, so we don't have to include caret stop string at all.
            if (caretStopStringLength == 0)
                return String.Empty;
 
            StringBuilder sb = new StringBuilder(caretStopStringLength);
 
            byte mask = 0x8;
            byte accumulatedValue = 0;
 
            for (int i = 0; i <= lastCaretStop; ++i)
            {
                if (_caretStops[i])
                    accumulatedValue |= mask;
 
                if (mask != 1)
                    mask >>= 1;
                else
                {
                    sb.AppendFormat("{0:x1}", accumulatedValue);
                    accumulatedValue = 0;
                    mask = 0x8;
                }
            }
            if (mask != 0x8)
                sb.AppendFormat("{0:x1}", accumulatedValue);
 
            Debug.Assert(caretStopStringLength == sb.ToString().Length);
 
            return sb.ToString();
        }
 
        #endregion private methods
        #region private data
        private GlyphTypeface _glyphTypeface;
 
        private IList<char> _characters;
 
        private double _milToEm;
 
        private bool _sideways;
 
        private int _glyphClusterInitialOffset;
 
        private double _currentAdvanceTotal;
 
        private double _idealAdvanceTotal;
 
        private IList<ushort> _clusters;
 
        private IList<ushort> _indices;
 
        private IList<double> _advances;
 
        private IList<Point> _offsets;
 
        private IList<bool>     _caretStops;
 
        private StringBuilder _indicesStringBuider;
 
        private StringBuilder _glyphStringBuider;
 
        private const char GlyphSubEntrySeparator = ',';
 
        private const char GlyphSeparator = ';';
 
        private const double EmScaleFactor = 100.0;
        #endregion region private data
   }
}