File: MS\Internal\FontFace\PhysicalFontFamily.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.
 
//
//
//
// Description: The PhysicalFontFamily class
//
//
 
using System.Globalization;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using MS.Internal.TextFormatting;
 
namespace MS.Internal.FontFace
{
    /// <summary>
    /// PhysicalFontFamily class represents a font family obtained from a collection of OpenType files.
    /// </summary>
    internal sealed class PhysicalFontFamily : IFontFamily
    {
        private Text.TextInterface.FontFamily    _family;
        private IDictionary<XmlLanguage, string> _familyNames;
 
        // _family.FamilyNames is of type LocalizedStrings which does not support editing (Adding, Replacing, and Clearing)
        // IFontFamily.Names is passed to a LanguageSpecificStringDictionary which is exposed publicly and allows editing
        // operations. Thus to convert from IDictionary<CultureInfo, string> to IDictionary<XmlLanguage, string> we had
        // 2 approaches:
        //          - Copying the elements into a new IDictionary<XmlLanguage, string>
        //          - Implement a new class that wraps the IDictionary<CultureInfo, string> and allow
        //            editing operations
        // The second approach will eventually copy elements into a new structure that allows editing when
        // an editing operation is performed. Since this dictionary is not expected to hold a huge number of elements
        // we chose to do the copying upfront and not lazily and hence use the first approach.
        //
        private static IDictionary<XmlLanguage, string> ConvertDictionary(IDictionary<CultureInfo, string> dictionary)
        {
            Dictionary<XmlLanguage, string> convertedDictionary = new Dictionary<XmlLanguage, string>();
            foreach (KeyValuePair<CultureInfo, string> pair in dictionary)
            {
                // DevDiv.1153238 : In Windows 10, the dictionary argument to this method may contain two different entries
                // for the same language if two fonts in the same font family report two different localized family names.
                // We check for this case, and only add the first one we encounter into convertedDictionary.
                XmlLanguage language = XmlLanguage.GetLanguage(pair.Key.Name);
                if (!convertedDictionary.ContainsKey(language))
                {
                    convertedDictionary.Add(language, pair.Value);
                }
            }
 
            return convertedDictionary;
        }
 
        internal PhysicalFontFamily(Text.TextInterface.FontFamily family)
        {
            Invariant.Assert(family != null);
            _family = family;
        }
 
 
        /// <summary>
        /// Get typeface metrics of the specified typeface
        /// </summary>
        ITypefaceMetrics IFontFamily.GetTypefaceMetrics(
            FontStyle       style,
            FontWeight      weight,
            FontStretch     stretch
            )
        {
            return GetGlyphTypeface(style, weight, stretch);
        }
 
 
        /// <summary>
        /// Look up device font for the typeface.
        /// </summary>
        IDeviceFont IFontFamily.GetDeviceFont(FontStyle style, FontWeight weight, FontStretch stretch)
        {
            return null;
        }
 
 
        /// <summary>
        /// Indexer that indexes the underlying family name table via CultureInfo
        /// </summary>
        /// <value></value>
        IDictionary<XmlLanguage,string> IFontFamily.Names
        {
            get
            {
                if (_familyNames == null)
                {
                    _familyNames = ConvertDictionary(_family.FamilyNames);
                }
                return _familyNames;
            }
        }
 
 
        /// <summary>
        /// Get the matching glyph typeface of a specified style
        /// </summary>
        /// <param name="style">font style</param>
        /// <param name="weight">font weight</param>
        /// <param name="stretch">font stretch</param>
        /// <returns>matching font face</returns>
        internal GlyphTypeface GetGlyphTypeface(
            FontStyle       style,
            FontWeight      weight,
            FontStretch     stretch
            )
        {
            Text.TextInterface.Font bestMatch = _family.GetFirstMatchingFont((Text.TextInterface.FontWeight)weight.ToOpenTypeWeight(),
                                                                             (Text.TextInterface.FontStretch)stretch.ToOpenTypeStretch(),
                                                                             (Text.TextInterface.FontStyle)   style.GetStyleForInternalConstruction());
            Debug.Assert(bestMatch != null);
            return new GlyphTypeface(bestMatch);
        }
 
        /// <summary>
        /// Get the matching typeface for the specified target style that also supports
        /// glyph mapping of the specified character string.
        /// </summary>
        /// <param name="style">font style</param>
        /// <param name="weight">font weight</param>
        /// <param name="stretch">font stretch</param>
        /// <param name="charString">character string</param>
        /// <param name="digitCulture">culture used for digit substitution or null</param>
        /// <param name="advance">number of characters with valid glyph mapped</param>
        /// <param name="nextValid">offset to the character mapping to a valid glyph</param>
        /// <returns>matching typeface</returns>
        internal GlyphTypeface MapGlyphTypeface(
            FontStyle               style,
            FontWeight              weight,
            FontStretch             stretch,
            CharacterBufferRange    charString,
            CultureInfo             digitCulture,
            ref int                 advance,
            ref int                 nextValid
            )
        {
            int smallestInvalid = charString.Length;
 
            // Add all the cached font faces to a priority queue.
            MatchingStyle targetStyle = new MatchingStyle(style, weight, stretch);
 
            LegacyPriorityQueue<MatchingFace> queue = new LegacyPriorityQueue<MatchingFace>(
                checked((int)_family.Count),
                new MatchingFaceComparer(targetStyle));
 
            foreach (Text.TextInterface.Font face in _family)
            {
                queue.Push(new MatchingFace(face));
            }
 
            // Remember the best style match.
            MS.Internal.Text.TextInterface.Font bestStyleTypeface = null;
 
            // Iterate in priority order.
            for (; queue.Count != 0; queue.Pop())
            {
                int invalid = 0;
                MS.Internal.Text.TextInterface.Font font = queue.Top.FontFace;
                int valid = MapCharacters(font, charString, digitCulture, ref invalid);
                if (valid > 0)
                {
                    if (smallestInvalid > 0 && smallestInvalid < valid)
                    {
                        // advance only to smallestInvalid because there's a better match after that
                        advance = smallestInvalid;
                        nextValid = 0;
                    }
                    else
                    {
                        advance = valid;
                        nextValid = invalid;
                    }
 
                    return new GlyphTypeface(font);
                }
                else
                {
                    if (invalid < smallestInvalid)
                    {
                        // keep track of the current shortest length of invalid characters,
                        smallestInvalid = invalid;
                    }
 
                    if (bestStyleTypeface == null)
                    {
                        bestStyleTypeface = font;
                    }
                }
            }
 
            // no face can map the specified character string,
            // fall back to the closest style match
            advance = 0;
            nextValid = smallestInvalid;
            Debug.Assert(bestStyleTypeface != null);
            return new GlyphTypeface(bestStyleTypeface);
        }
 
        /// <summary>
        /// Element type for priority queue used by MapGlyphTypeface.
        /// </summary>
        private struct MatchingFace
        {
            internal MatchingFace(Text.TextInterface.Font face)
            {
                _face = face;
                _style = new MatchingStyle(new FontStyle((int)face.Style), new FontWeight((int)face.Weight), new FontStretch((int)face.Stretch));
            }
 
            internal Text.TextInterface.Font FontFace
            {
                get { return _face; }
            }
 
            internal MatchingStyle MatchingStyle
            {
                get { return _style; }
            }
 
            private Text.TextInterface.Font _face;
            private MatchingStyle           _style;
        }
 
        /// <summary>
        /// Comparer for priority queue used by MapGlyphTypeface.
        /// </summary>
        private class MatchingFaceComparer : IComparer<MatchingFace>
        {
            internal MatchingFaceComparer(MatchingStyle targetStyle)
            {
                _targetStyle = targetStyle;
            }
 
            int IComparer<MatchingFace>.Compare(MatchingFace a, MatchingFace b)
            {
                return a.MatchingStyle.IsBetterMatch(_targetStyle, b.MatchingStyle) ? -1 : 1;
            }
 
            private MatchingStyle _targetStyle;
        }
 
 
        /// <summary>
        /// Map character supported by the typeface
        /// </summary>
        /// <remarks>
        /// Combining mark is considered part of the character that may be supported
        /// thru precomposed form or OpenType glyph substitution table.
        /// </remarks>
        private int MapCharacters(
            MS.Internal.Text.TextInterface.Font font,
            CharacterBufferRange                unicodeString,
            CultureInfo                         digitCulture,
            ref int                             nextValid
            )
        {
            DigitMap digitMap = new DigitMap(digitCulture);
 
            int sizeofChar = 0;
            int advance;
 
            // skip all the leading joiner characters. They need to be shaped with the
            // surrounding strong characters.
            advance = Classification.AdvanceWhile(unicodeString, ItemClass.JoinerClass);
            if (advance >= unicodeString.Length)
            {
                // It is rare that the run only contains joiner characters.
                // If it really happens, just return.
                return advance;
            }
 
            //
            // If the run starts with combining marks, we will not be able to find base characters for them
            // within the run. These combining marks will be mapped to their best fonts as normal characters.
            //
            const int NOBASE = -1; // null char is a valid base
            int baseChar = NOBASE;
 
            // Determine how many characters we can advance, i.e., find the first invalid character.
            for (; advance < unicodeString.Length; advance += sizeofChar)
            {
                // Get the character and apply digit substitution, if any.
                int originalChar = Classification.UnicodeScalar(
                    new CharacterBufferRange(unicodeString, advance, unicodeString.Length - advance),
                    out sizeofChar
                    );
 
                if (Classification.IsJoiner(originalChar))
                    continue;
 
                if (!Classification.IsCombining(originalChar))
                {
                    baseChar = originalChar;
                }
                else if (baseChar != NOBASE)
                {
                    // continue to advance for combining mark with base char (can be precomposed by shaping engine)
                    // except if it is a different script (#6801)
                    if (Classification.GetScript(baseChar) == Classification.GetScript(originalChar))
                    {
                        continue;
                    }
                }
 
                int ch = digitMap[originalChar];
 
                if (font.HasCharacter(checked((uint)ch)))
                    continue;
 
                // If ch is a substituted character, can we substitute a different character instead?
                if (ch != originalChar)
                {
                    ch = DigitMap.GetFallbackCharacter(ch);
                    if (ch != 0 && font.HasCharacter(checked((uint)ch)))
                        continue;
                }
 
                // If we fall through to here it's invalid.
                break;
            }
 
            // UnicodeScalar won't return a sizeofChar that exceeds the string length.
            Debug.Assert(advance <= unicodeString.Length);
 
            // Find the next valid character.
            if (advance < unicodeString.Length)
            {
                // UnicodeScalar won't return a sizeofChar that exceeds the string length.
                Debug.Assert(advance + sizeofChar <= unicodeString.Length);
 
                for (nextValid = advance + sizeofChar; nextValid < unicodeString.Length; nextValid += sizeofChar)
                {
                    // Get the character.
                    int originalChar = Classification.UnicodeScalar(
                        new CharacterBufferRange(unicodeString, nextValid, unicodeString.Length - nextValid),
                        out sizeofChar
                        );
 
                    // Apply digit substitution, if any.
                    int ch = digitMap[originalChar];
 
                    //
                    // Combining mark should always be shaped by the same font as the base char.
                    // If the physical font is invalid for the base char, it should also be invalid for the
                    // following combining mark so that both characters will go onto the same fallback font.
                    // - When the fallback font is physical font, the font will be valid for both characters
                    //   if and only if it is valid for the base char. Otherwise, it will be invalid for both.
                    // - When the fallback font is composite font, it maps the combining mark to the same font
                    //   as the base char such that they will eventually be resolved to the same physical font.
                    //   That means FamilyMap for the combining mark is not used when it follows a base char.
                    //
                    // The same goes for joiner. Note that "hasBaseChar" here indicates if there is an invalid base
                    // char in front.
                    if (Classification.IsJoiner(ch)
                       || (baseChar != NOBASE && Classification.IsCombining(ch) && Classification.GetScript(ch) == Classification.GetScript(baseChar))
                       )
                       continue;
 
                    // If we have a glyph it's valid.
                    if (font.HasCharacter(checked((uint)ch)))
                        break;
 
                    // If ch is a substituted character, can we substitute a different character instead?
                    if (ch != originalChar)
                    {
                        ch = DigitMap.GetFallbackCharacter(ch);
                        if (ch != 0 && font.HasCharacter(checked((uint)ch)))
                            break;
                    }
                }
            }
 
            return advance;
        }
 
 
        /// <summary>
        /// Distance from character cell top to English baseline relative to em size.
        /// </summary>
        double IFontFamily.Baseline(double emSize, double toReal, double pixelsPerDip, TextFormattingMode textFormattingMode)
        {
            if (textFormattingMode == TextFormattingMode.Ideal)
            {
                return emSize * _family.Metrics.Baseline;
            }
            else
            {
                double realEmSize = emSize * toReal;
                return TextFormatterImp.RoundDipForDisplayMode(_family.DisplayMetrics((float)(realEmSize), checked((float)pixelsPerDip)).Baseline * realEmSize, pixelsPerDip) / toReal;
            }
        }
 
        double IFontFamily.BaselineDesign
        {
            get
            {
                return ((IFontFamily)this).Baseline(1, 1, 1, TextFormattingMode.Ideal);
            }
        }
 
 
        double IFontFamily.LineSpacingDesign
        {
            get
            {
                return ((IFontFamily)this).LineSpacing(1, 1, 1, TextFormattingMode.Ideal);
            }
        }
 
 
        /// <summary>
        /// Recommended baseline-to-baseline distance for text in this font
        /// </summary>
        double IFontFamily.LineSpacing(double emSize, double toReal, double pixelsPerDip, TextFormattingMode textFormattingMode)
        {
            if (textFormattingMode == TextFormattingMode.Ideal)
            {
                return emSize * _family.Metrics.LineSpacing;
            }
            else
            {
                double realEmSize = emSize * toReal;
                return TextFormatterImp.RoundDipForDisplayMode(_family.DisplayMetrics((float)(realEmSize), checked((float)pixelsPerDip)).LineSpacing * realEmSize, pixelsPerDip) / toReal;
            }
        }
 
        ICollection<Typeface> IFontFamily.GetTypefaces(FontFamilyIdentifier familyIdentifier)
        {
            return new TypefaceCollection(new FontFamily(familyIdentifier), _family);
        }
 
 
        /// <summary>
        /// Get family name correspondent to the first n-characters of the specified character string
        /// </summary>
        bool IFontFamily.GetMapTargetFamilyNameAndScale(
            CharacterBufferRange    unicodeString,
            CultureInfo             culture,
            CultureInfo             digitCulture,
            double                  defaultSizeInEm,
            out int                 cchAdvance,
            out string              targetFamilyName,
            out double              scaleInEm
            )
        {
            cchAdvance = unicodeString.Length;
            targetFamilyName = null;
            scaleInEm = defaultSizeInEm;
            return false;
        }
    }
}