示例#1
0
        /// <summary>
        /// Client to notify change in part of the cache when text or
        /// properties of the run is being added, removed or replaced.
        /// </summary> 
        /// <remarks>
        /// The client's expectation of this notification is that TextFormatter 
        /// retains valid state for run cache in response to this change. It's 
        /// expected that at least TextFormatter will refetch the runs affected
        /// by the change. Subsequent runs may or may not be refected depending 
        /// on the state of the cache after the change.
        /// </remarks>
        public void Change(
            int     textSourceCharacterIndex, 
            int     addition,
            int     removal 
            ) 
        {
            if (textSourceCharacterIndex < 0) 
                return;

            int cchActive = 0;
            for (int i = 0; i < _textRunVector.Count; i++) 
                cchActive += _textRunVector[i].length;
 
            if (textSourceCharacterIndex >= cchActive) 
                return;
 
            SpanRider textRunSpanRider = new SpanRider(_textRunVector, _latestPosition, textSourceCharacterIndex);
            _latestPosition = textRunSpanRider.SpanPosition;

            // we remove runs from the cache starting from the one containing the change 
            // to the end of the active range. We do not try to interpret the scope of
            // the change and try to minimize the range in which the cache is invalidated, 
            // because that would require an in-depth understanding of how our client 
            // implement their formatting change mechanism and how they respond to future
            // refetch after the change which could vary among different clients. That 
            // kind of work is beyond the purpose of this notification.

            _latestPosition = _textRunVector.SetValue(
                textRunSpanRider.CurrentSpanStart, 
                cchActive - textRunSpanRider.CurrentSpanStart,
                _textRunVector.Default, 
                _latestPosition 
                );
        } 
示例#2
0
        private unsafe void ValidateMapResult(
            int ichRange,
            int cchRange,
            ref SpanVector <int> cachedScaledTypefaceIndexSpans
            )
        {
            int ich = 0;

            SpanRider <int> typefaceIndexSpanRider = new SpanRider <int>(cachedScaledTypefaceIndexSpans);

            while (ich < cchRange)
            {
                typefaceIndexSpanRider.At(ichRange + ich);
                if ((int)typefaceIndexSpanRider.CurrentValue < 0)
                {
                    Debug.Assert(false, "Invalid font face spans");
                    return;
                }

                int cch = Math.Min(cchRange - ich, typefaceIndexSpanRider.Length);
                ich += cch;
            }
        }
示例#3
0
        /// <summary>
        /// Get text immediately preceding cpLimit.
        /// </summary>
        internal TextSpan <CultureSpecificCharacterBufferRange> GetPrecedingText(TextSource textSource, int cpLimit)
        {
            if (cpLimit > 0)
            {
                SpanRider textRunSpanRider = new SpanRider(_textRunVector, _latestPosition);
                if (textRunSpanRider.At(cpLimit - 1))
                {
                    CharacterBufferRange charString = CharacterBufferRange.Empty;
                    CultureInfo          culture    = null;

                    TextRun run = textRunSpanRider.CurrentElement as TextRun;

                    if (run != null)
                    {
                        // Only TextRun containing text would have non-empty Character buffer range.
                        if (TextRunInfo.GetRunType(run) == Plsrun.Text &&
                            run.CharacterBufferReference.CharacterBuffer != null)
                        {
                            charString = new CharacterBufferRange(
                                run.CharacterBufferReference,
                                cpLimit - textRunSpanRider.CurrentSpanStart);

                            culture = CultureMapper.GetSpecificCulture(run.Properties.CultureInfo);
                        }

                        return(new TextSpan <CultureSpecificCharacterBufferRange>(
                                   cpLimit - textRunSpanRider.CurrentSpanStart, // cp length
                                   new CultureSpecificCharacterBufferRange(culture, charString)
                                   ));
                    }
                }
            }

            // not in cache so call back to client
            return(textSource.GetPrecedingText(cpLimit));
        }
示例#4
0
        /// <summary>
        /// Get text immediately preceding cpLimit.
        /// </summary>
        internal TextSpan<CultureSpecificCharacterBufferRange> GetPrecedingText(TextSource textSource, int cpLimit) 
        {
            if (cpLimit > 0) 
            { 
                SpanRider textRunSpanRider = new SpanRider(_textRunVector, _latestPosition);
                if (textRunSpanRider.At(cpLimit - 1)) 
                {
                    CharacterBufferRange charString = CharacterBufferRange.Empty;
                    CultureInfo culture = null;
 
                    TextRun run = textRunSpanRider.CurrentElement as TextRun;
 
                    if (run != null) 
                    {
                        // Only TextRun containing text would have non-empty Character buffer range. 
                        if ( TextRunInfo.GetRunType(run) == Plsrun.Text
                          && run.CharacterBufferReference.CharacterBuffer != null)
                        {
                            charString = new CharacterBufferRange( 
                                run.CharacterBufferReference,
                                cpLimit - textRunSpanRider.CurrentSpanStart); 
 
                            culture = CultureMapper.GetSpecificCulture(run.Properties.CultureInfo);
                        } 

                        return new TextSpan<CultureSpecificCharacterBufferRange>(
                            cpLimit - textRunSpanRider.CurrentSpanStart, // cp length
                            new CultureSpecificCharacterBufferRange(culture, charString) 
                         );
                    } 
                } 
            }
 
            // not in cache so call back to client
            return textSource.GetPrecedingText(cpLimit);
        }
示例#5
0
        /// <summary>
        /// Fetch cached textrun 
        /// </summary>
        internal TextRun FetchTextRun( 
            FormatSettings          settings, 
            int                     cpFetch,
            int                     cpFirst, 
            out int                 offsetToFirstCp,
            out int                 runLength
            )
        { 
            SpanRider textRunSpanRider = new SpanRider(_textRunVector, _latestPosition, cpFetch);
            _latestPosition = textRunSpanRider.SpanPosition; 
            TextRun textRun = (TextRun)textRunSpanRider.CurrentElement; 

            if(textRun == null) 
            {
                // run not already cached, fetch new run and cache it

                textRun = settings.TextSource.GetTextRun(cpFetch); 

                if (textRun.Length < 1) 
                { 
                    throw new ArgumentOutOfRangeException("textRun.Length", SR.Get(SRID.ParameterMustBeGreaterThanZero));
                } 

                Plsrun plsrun = TextRunInfo.GetRunType(textRun);

                if (plsrun == Plsrun.Text || plsrun == Plsrun.InlineObject) 
                {
                    TextRunProperties properties = textRun.Properties; 
 
                    if (properties == null)
                        throw new ArgumentException(SR.Get(SRID.TextRunPropertiesCannotBeNull)); 

                    if (properties.FontRenderingEmSize <= 0)
                        throw new ArgumentException(SR.Get(SRID.PropertyOfClassMustBeGreaterThanZero, "FontRenderingEmSize", "TextRunProperties"));
 
                    double realMaxFontRenderingEmSize = Constants.RealInfiniteWidth / Constants.GreatestMutiplierOfEm;
 
                    if (properties.FontRenderingEmSize > realMaxFontRenderingEmSize) 
                        throw new ArgumentException(SR.Get(SRID.PropertyOfClassCannotBeGreaterThan, "FontRenderingEmSize", "TextRunProperties", realMaxFontRenderingEmSize));
 
                    CultureInfo culture = CultureMapper.GetSpecificCulture(properties.CultureInfo);

                    if (culture == null)
                        throw new ArgumentException(SR.Get(SRID.PropertyOfClassCannotBeNull, "CultureInfo", "TextRunProperties")); 

                    if (properties.Typeface == null) 
                        throw new ArgumentException(SR.Get(SRID.PropertyOfClassCannotBeNull, "Typeface", "TextRunProperties")); 
                }
 

                //
                // TextRun is specifial to SpanVector because TextRun also encodes position which needs to be
                // consistent with the positions encoded by SpanVector. In run cache, the begining of a span 
                // should always correspond to the begining of a cached text run. If the end of the currently fetched
                // run overlaps with the begining of an already cached run, the begining of the cached run needs to be 
                // adjusted as well as its span. Because we can't gurantee the correctness of the overlapped range 
                // so we'll simply remove the overlapped runs here.
                // 

                // Move the rider to the end of the current run
                textRunSpanRider.At(cpFetch + textRun.Length - 1);
                _latestPosition = textRunSpanRider.SpanPosition; 

                if (textRunSpanRider.CurrentElement != _textRunVector.Default) 
                { 
                    // The end overlaps with one or more cached runs, clear the range from the
                    // begining of the current fetched run to the end of the last overlapped cached run. 
                    _latestPosition = _textRunVector.SetReference(
                        cpFetch,
                        textRunSpanRider.CurrentPosition + textRunSpanRider.Length - cpFetch,
                        _textRunVector.Default, 
                        _latestPosition
                        ); 
                } 

                _latestPosition = _textRunVector.SetReference(cpFetch, textRun.Length, textRun, _latestPosition); 

                // Refresh the rider's SpanPosition following previous SpanVector.SetReference calls
                textRunSpanRider.At(_latestPosition, cpFetch);
            } 

            offsetToFirstCp = textRunSpanRider.CurrentPosition - textRunSpanRider.CurrentSpanStart; 
            runLength = textRunSpanRider.Length; 
            Debug.Assert(textRun != null && runLength > 0, "Invalid run!");
 
            bool isText = textRun is ITextSymbols;

            if (isText)
            { 
                // Chop text run to optimal length so we dont spend forever analysing
                // them all at once. 
 
                int looseCharLength = TextStore.TypicalCharactersPerLine - cpFetch + cpFirst;
 
                if(looseCharLength <= 0)
                {
                    // this line already exceeds typical line length, incremental fetch goes
                    // about a quarter of the typical length. 

                    looseCharLength = (int)Math.Round(TextStore.TypicalCharactersPerLine * 0.25); 
                } 

                if(runLength > looseCharLength) 
                {
                    if (TextRunInfo.GetRunType(textRun) == Plsrun.Text)
                    {
                        // 
                        // When chopping the run at the typical line length,
                        // - don't chop in between of higher & lower surrogate 
                        // - don't chop combining mark away from its base character 
                        // - don't chop joiner from surrounding characters
                        // 
                        // Starting from the initial chopping point, we look ahead to find a safe position. We stop at
                        // a limit in case the run consists of many combining mark & joiner. That is rare and doesn't make
                        // much sense in shaping already.
                        // 

                        CharacterBufferReference charBufferRef = textRun.CharacterBufferReference; 
 
                        // We look ahead by one more line at most. It is not normal to have
                        // so many combining mark or joiner characters in a row. It doesn't make sense to 
                        // look further if so.
                        int lookAheadLimit = Math.Min(runLength, looseCharLength + TextStore.TypicalCharactersPerLine);

                        int sizeOfChar = 0; 
                        int endOffset  = 0;
                        bool canBreakAfterPrecedingChar = false; 
 
                        for (endOffset = looseCharLength - 1; endOffset < lookAheadLimit; endOffset += sizeOfChar)
                        { 
                            CharacterBufferRange charString = new CharacterBufferRange(
                                charBufferRef.CharacterBuffer,
                                charBufferRef.OffsetToFirstChar + offsetToFirstCp + endOffset,
                                runLength - endOffset 
                                );
 
                            int ch = Classification.UnicodeScalar(charString, out sizeOfChar); 

                            // We can only safely break if the preceding char is not a joiner character (i.e. can-break-after), 
                            // and the current char is not combining or joiner (i.e. can-break-before).
                            if (canBreakAfterPrecedingChar && !Classification.IsCombining(ch) && !Classification.IsJoiner(ch) )
                            {
                                break; 
                            }
 
                            canBreakAfterPrecedingChar = !Classification.IsJoiner(ch); 
                        }
 
                        looseCharLength = Math.Min(runLength, endOffset);
                    }

                    runLength = looseCharLength; 
                }
            } 
 

            Debug.Assert( 

                // valid run found
                runLength > 0
 
                // non-text run always fetched at run start
                &&  (   isText 
                    || textRunSpanRider.CurrentSpanStart - textRunSpanRider.CurrentPosition == 0) 

                // span rider of both text and format point to valid position 
                &&  (textRunSpanRider.Length > 0 && textRunSpanRider.CurrentElement != null),

                "Text run fetching error!"
                ); 

            return textRun; 
        } 
示例#6
0
        /// <summary>
        /// Fetch cached textrun
        /// </summary>
        internal TextRun FetchTextRun(
            FormatSettings settings,
            int cpFetch,
            int cpFirst,
            out int offsetToFirstCp,
            out int runLength
            )
        {
            SpanRider textRunSpanRider = new SpanRider(_textRunVector, _latestPosition, cpFetch);

            _latestPosition = textRunSpanRider.SpanPosition;
            TextRun textRun = (TextRun)textRunSpanRider.CurrentElement;

            if (textRun == null)
            {
                // run not already cached, fetch new run and cache it

                textRun = settings.TextSource.GetTextRun(cpFetch);

                if (textRun.Length < 1)
                {
                    throw new ArgumentOutOfRangeException("textRun.Length", SR.Get(SRID.ParameterMustBeGreaterThanZero));
                }

                Plsrun plsrun = TextRunInfo.GetRunType(textRun);

                if (plsrun == Plsrun.Text || plsrun == Plsrun.InlineObject)
                {
                    TextRunProperties properties = textRun.Properties;

                    if (properties == null)
                    {
                        throw new ArgumentException(SR.Get(SRID.TextRunPropertiesCannotBeNull));
                    }

                    if (properties.FontRenderingEmSize <= 0)
                    {
                        throw new ArgumentException(SR.Get(SRID.PropertyOfClassMustBeGreaterThanZero, "FontRenderingEmSize", "TextRunProperties"));
                    }

                    double realMaxFontRenderingEmSize = Constants.RealInfiniteWidth / Constants.GreatestMutiplierOfEm;

                    if (properties.FontRenderingEmSize > realMaxFontRenderingEmSize)
                    {
                        throw new ArgumentException(SR.Get(SRID.PropertyOfClassCannotBeGreaterThan, "FontRenderingEmSize", "TextRunProperties", realMaxFontRenderingEmSize));
                    }

                    CultureInfo culture = CultureMapper.GetSpecificCulture(properties.CultureInfo);

                    if (culture == null)
                    {
                        throw new ArgumentException(SR.Get(SRID.PropertyOfClassCannotBeNull, "CultureInfo", "TextRunProperties"));
                    }

                    if (properties.Typeface == null)
                    {
                        throw new ArgumentException(SR.Get(SRID.PropertyOfClassCannotBeNull, "Typeface", "TextRunProperties"));
                    }
                }


                //
                // TextRun is specifial to SpanVector because TextRun also encodes position which needs to be
                // consistent with the positions encoded by SpanVector. In run cache, the begining of a span
                // should always correspond to the begining of a cached text run. If the end of the currently fetched
                // run overlaps with the begining of an already cached run, the begining of the cached run needs to be
                // adjusted as well as its span. Because we can't gurantee the correctness of the overlapped range
                // so we'll simply remove the overlapped runs here.
                //

                // Move the rider to the end of the current run
                textRunSpanRider.At(cpFetch + textRun.Length - 1);
                _latestPosition = textRunSpanRider.SpanPosition;

                if (textRunSpanRider.CurrentElement != _textRunVector.Default)
                {
                    // The end overlaps with one or more cached runs, clear the range from the
                    // begining of the current fetched run to the end of the last overlapped cached run.
                    _latestPosition = _textRunVector.SetReference(
                        cpFetch,
                        textRunSpanRider.CurrentPosition + textRunSpanRider.Length - cpFetch,
                        _textRunVector.Default,
                        _latestPosition
                        );
                }

                _latestPosition = _textRunVector.SetReference(cpFetch, textRun.Length, textRun, _latestPosition);

                // Refresh the rider's SpanPosition following previous SpanVector.SetReference calls
                textRunSpanRider.At(_latestPosition, cpFetch);
            }

            // If the TextRun was obtained from the cache, make sure it has the right PixelsPerDip set on its properties.

            if (textRun.Properties != null)
            {
                textRun.Properties.PixelsPerDip = settings.TextSource.PixelsPerDip;
            }

            offsetToFirstCp = textRunSpanRider.CurrentPosition - textRunSpanRider.CurrentSpanStart;
            runLength       = textRunSpanRider.Length;
            Debug.Assert(textRun != null && runLength > 0, "Invalid run!");

            bool isText = textRun is ITextSymbols;

            if (isText)
            {
                // Chop text run to optimal length so we dont spend forever analysing
                // them all at once.

                int looseCharLength = TextStore.TypicalCharactersPerLine - cpFetch + cpFirst;

                if (looseCharLength <= 0)
                {
                    // this line already exceeds typical line length, incremental fetch goes
                    // about a quarter of the typical length.

                    looseCharLength = (int)Math.Round(TextStore.TypicalCharactersPerLine * 0.25);
                }

                if (runLength > looseCharLength)
                {
                    if (TextRunInfo.GetRunType(textRun) == Plsrun.Text)
                    {
                        //
                        // When chopping the run at the typical line length,
                        // - don't chop in between of higher & lower surrogate
                        // - don't chop combining mark away from its base character
                        // - don't chop joiner from surrounding characters
                        //
                        // Starting from the initial chopping point, we look ahead to find a safe position. We stop at
                        // a limit in case the run consists of many combining mark & joiner. That is rare and doesn't make
                        // much sense in shaping already.
                        //

                        CharacterBufferReference charBufferRef = textRun.CharacterBufferReference;

                        // We look ahead by one more line at most. It is not normal to have
                        // so many combining mark or joiner characters in a row. It doesn't make sense to
                        // look further if so.
                        int lookAheadLimit = Math.Min(runLength, looseCharLength + TextStore.TypicalCharactersPerLine);

                        int  sizeOfChar = 0;
                        int  endOffset  = 0;
                        bool canBreakAfterPrecedingChar = false;

                        for (endOffset = looseCharLength - 1; endOffset < lookAheadLimit; endOffset += sizeOfChar)
                        {
                            CharacterBufferRange charString = new CharacterBufferRange(
                                charBufferRef.CharacterBuffer,
                                charBufferRef.OffsetToFirstChar + offsetToFirstCp + endOffset,
                                runLength - endOffset
                                );

                            int ch = Classification.UnicodeScalar(charString, out sizeOfChar);

                            // We can only safely break if the preceding char is not a joiner character (i.e. can-break-after),
                            // and the current char is not combining or joiner (i.e. can-break-before).
                            if (canBreakAfterPrecedingChar && !Classification.IsCombining(ch) && !Classification.IsJoiner(ch))
                            {
                                break;
                            }

                            canBreakAfterPrecedingChar = !Classification.IsJoiner(ch);
                        }

                        looseCharLength = Math.Min(runLength, endOffset);
                    }

                    runLength = looseCharLength;
                }
            }


            Debug.Assert(

                // valid run found
                runLength > 0

                // non-text run always fetched at run start
                && (isText ||
                    textRunSpanRider.CurrentSpanStart - textRunSpanRider.CurrentPosition == 0)

                // span rider of both text and format point to valid position
                && (textRunSpanRider.Length > 0 && textRunSpanRider.CurrentElement != null),

                "Text run fetching error!"
                );

            return(textRun);
        }
        internal void GetShapeableText(
            CharacterBufferReference characterBufferReference,
            int stringLength,
            TextRunProperties textRunProperties,
            CultureInfo digitCulture,
            bool isRightToLeftParagraph,
            IList <TextShapeableSymbols> shapeableList,
            IShapeableTextCollector collector,
            TextFormattingMode textFormattingMode
            )
        {
            SpanVector <int> cachedScaledTypefaceIndexSpans;

            int ichItem = 0;

            CharacterBufferRange unicodeString = new CharacterBufferRange(
                characterBufferReference,
                stringLength
                );

            CultureInfo  culture = textRunProperties.CultureInfo;
            IList <Span> spans;

            GCHandle gcHandle;
            IntPtr   ptext = characterBufferReference.CharacterBuffer.PinAndGetCharacterPointer(characterBufferReference.OffsetToFirstChar, out gcHandle);

            // Contextual number substitution cannot be performed on the run level, since it depends
            // on context - nearest preceding strong character. For this reason, contextual number
            // substitutions has been already done (TextStore.CreateLSRunsUniformBidiLevel) and
            // digitCulture has been updated to reflect culture which is dependent on the context.
            // NumberSubstitutionMethod.AsCulture method can be resolved to Context, hence it also needs to be resolved to appropriate
            // not ambiguous method.
            // Both of those values (Context and AsCulture) are resolved to one of following: European, Traditional or NativeNational,
            // which can be safely handled by DWrite without getting context information.
            bool ignoreUserOverride;
            NumberSubstitutionMethod numberSubstitutionMethod = DigitState.GetResolvedSubstitutionMethod(textRunProperties, digitCulture, out ignoreUserOverride);

            // Itemize the text based on DWrite's text analysis for scripts and number substitution.
            unsafe
            {
                checked
                {
                    spans = MS.Internal.Text.TextInterface.TextAnalyzer.Itemize(
                        (ushort *)ptext.ToPointer(),
                        (uint)stringLength,
                        culture,
                        MS.Internal.FontCache.DWriteFactory.Instance,
                        isRightToLeftParagraph,
                        digitCulture,
                        ignoreUserOverride,
                        (uint)numberSubstitutionMethod,
                        ClassificationUtility.Instance,
                        UnsafeNativeMethods.CreateTextAnalysisSink,
                        UnsafeNativeMethods.GetScriptAnalysisList,
                        UnsafeNativeMethods.GetNumberSubstitutionList,
                        UnsafeNativeMethods.CreateTextAnalysisSource
                        );
                }
            }
            characterBufferReference.CharacterBuffer.UnpinCharacterPointer(gcHandle);

            SpanVector itemSpans = new SpanVector(null, new FrugalStructList <Span>((ICollection <Span>)spans));

            cachedScaledTypefaceIndexSpans = new SpanVector <int>(-1);
            foreach (Span itemSpan in itemSpans)
            {
                MapItem(
                    new CharacterBufferRange(
                        unicodeString,
                        ichItem,
                        itemSpan.length
                        ),
                    culture,
                    itemSpan,
                    ref cachedScaledTypefaceIndexSpans,
                    ichItem
                    );

                #if DEBUG
                ValidateMapResult(
                    ichItem,
                    itemSpan.length,
                    ref cachedScaledTypefaceIndexSpans
                    );
                #endif

                ichItem += itemSpan.length;
            }


            Debug.Assert(ichItem == unicodeString.Length);

            // intersect item spans with shapeable spans to create span of shapeable runs

            int ich = 0;

            SpanRider       itemSpanRider          = new SpanRider(itemSpans);
            SpanRider <int> typefaceIndexSpanRider = new SpanRider <int>(cachedScaledTypefaceIndexSpans);

            while (ich < unicodeString.Length)
            {
                itemSpanRider.At(ich);
                typefaceIndexSpanRider.At(ich);

                int index = typefaceIndexSpanRider.CurrentValue;
                Debug.Assert(index >= 0);

                int cch = unicodeString.Length - ich;
                cch = Math.Min(cch, itemSpanRider.Length);
                cch = Math.Min(cch, typefaceIndexSpanRider.Length);

                ScaledShapeTypeface scaledShapeTypeface = _cachedScaledTypefaces[index];

                collector.Add(
                    shapeableList,
                    new CharacterBufferRange(
                        unicodeString,
                        ich,
                        cch
                        ),
                    textRunProperties,
                    (MS.Internal.Text.TextInterface.ItemProps)itemSpanRider.CurrentElement,
                    scaledShapeTypeface.ShapeTypeface,
                    scaledShapeTypeface.ScaleInEm,
                    scaledShapeTypeface.NullShape,
                    textFormattingMode
                    );

                ich += cch;
            }
        }
        /// <summary>
        /// Cache index to the list of scaled shapeable typeface
        /// </summary>
        private void CacheScaledTypefaceMap(
            CharacterBufferRange unicodeString,
            CultureInfo culture,
            CultureInfo digitCulture,
            SpanVector scaledTypefaceSpans,
            ref SpanVector <int> cachedScaledTypefaceIndexSpans,
            int ichItem
            )
        {
            IntMap map;

            if (!_intMaps.TryGetValue(culture, out map))
            {
                map = new IntMap();
                _intMaps.Add(culture, map);
            }

            DigitMap digitMap = new DigitMap(digitCulture);

            SpanRider typefaceSpanRider = new SpanRider(scaledTypefaceSpans);

            int ich = 0;

            while (ich < unicodeString.Length)
            {
                typefaceSpanRider.At(ich);

                int cch = Math.Min(unicodeString.Length - ich, typefaceSpanRider.Length);

                int index = IndexOfScaledTypeface((ScaledShapeTypeface)typefaceSpanRider.CurrentElement);
                Debug.Assert(index >= 0, "Invalid scaled shapeable typeface index spans");

                cachedScaledTypefaceIndexSpans.Set(ichItem + ich, cch, index);

                // we keep index + 1 in the map, so that we leave map entry zero
                // to indicate uninitialized entry.
                index++;

                int sizeofChar;
                for (int c = 0; c < cch; c += sizeofChar)
                {
                    int ch = digitMap[
                        Classification.UnicodeScalar(
                            new CharacterBufferRange(unicodeString, ich + c, unicodeString.Length - ich - c),
                            out sizeofChar
                            )
                             ];

                    // only cache typeface map index for base characters
                    if (!Classification.IsCombining(ch) && !Classification.IsJoiner(ch))
                    {
                        // Dump values of local variables when the condition fails for better debuggability.
                        // We use "if" to avoid the expensive string.Format() in normal case.
                        if (map[ch] != 0 && map[ch] != index)
                        {
                            Invariant.Assert(
                                false,
                                string.Format(
                                    CultureInfo.InvariantCulture,
                                    "shapeable cache stores conflicting info, ch = {0}, map[ch] = {1}, index = {2}",
                                    ch, map[ch], index
                                    )
                                );
                        }

                        map[ch] = (ushort)index;
                    }
                }

                ich += cch;
            }
        }
        /// <summary>
        /// Convert the specified character index to LSCP
        /// </summary>
        internal int CharacterIndexToLSCP(int ich)
        {
            if (_ichVector.Count > 0)
            {
                SpanRider<int> ichRider = new SpanRider<int>(_ichVector);
                ichRider.At(ich);
                return ichRider.CurrentValue + ich - ichRider.CurrentSpanStart;
            }

            return ich;
        }