File: System\Windows\Media\CharacterMetrics.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 StringBuilder = System.Text.StringBuilder;
using CompositeFontParser = MS.Internal.FontFace.CompositeFontParser;
using Constants = MS.Internal.TextFormatting.Constants;
 
#pragma warning disable 1634, 1691  // suppressing PreSharp warnings
 
namespace System.Windows.Media
{
    /// <summary>
    /// Metrics used to lay out a character in a device font.
    /// </summary>
    public class CharacterMetrics
    {
        private double _blackBoxWidth;
        private double _blackBoxHeight;
        private double _baseline;
        private double _leftSideBearing;
        private double _rightSideBearing;
        private double _topSideBearing;
        private double _bottomSideBearing;
 
        private enum FieldIndex
        {
            BlackBoxWidth,
            BlackBoxHeight,
            Baseline,
            LeftSideBearing,
            RightSideBearing,
            TopSideBearing,
            BottomSideBearing
        }
 
        private const int NumFields = (int)FieldIndex.BottomSideBearing + 1;
        private const int NumRequiredFields = (int)FieldIndex.BlackBoxHeight + 1;
 
        /// <summary>
        /// Constructs a CharacterMetrics object with default values.
        /// </summary>
        public CharacterMetrics()
        {
        }
 
        /// <summary>
        /// Constructs a CharacterMetrics with the specified values.
        /// </summary>
        /// <param name="metrics">Value of the Metrics property.</param>
        public CharacterMetrics(string metrics)
        {
            ArgumentNullException.ThrowIfNull(metrics);
            Metrics = metrics;
        }
 
        /// <summary>
        /// String specifying the following properties in the following order: BlackBoxWidth, BlackBoxHeight,
        /// Baseline, LeftSideBearing, RightSideBearing, TopSideBearing, BottomSideBearing. Property values 
        /// are delimited by commas, and the first two properties are required. The remaining properties may 
        /// be omitted and default to zero. For example, "0.75,0.75,,0.1" sets the first, second, and fourth
        /// properties to the specified values and the rest to zero.
        /// </summary>
        public string Metrics
        {
            get
            {
                StringBuilder s = new StringBuilder();
 
                // The following fields are required.
                s.Append(_blackBoxWidth.ToString(System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS));
                s.Append(',');
                s.Append(_blackBoxHeight.ToString(System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS));
 
                // Index of the last field we added to the string; this tells us how many commas to
                // insert before the next optional field we add.
                int lastIndex = (int)FieldIndex.BlackBoxHeight;
 
                // The following fields are optional, but must be in ascending order of field index.
                AppendField(_baseline, FieldIndex.Baseline, ref lastIndex, s);
                AppendField(_leftSideBearing, FieldIndex.LeftSideBearing, ref lastIndex, s);
                AppendField(_rightSideBearing, FieldIndex.RightSideBearing, ref lastIndex, s);
                AppendField(_topSideBearing, FieldIndex.TopSideBearing, ref lastIndex, s);
                AppendField(_bottomSideBearing, FieldIndex.BottomSideBearing, ref lastIndex, s);
 
                return s.ToString();
            }
 
            set
            {
                double[] metrics = ParseMetrics(value);
 
                // Validate all the values before we assign to any field.
                CompositeFontParser.VerifyNonNegativeMultiplierOfEm("BlackBoxWidth", ref metrics[(int)FieldIndex.BlackBoxWidth]);
                CompositeFontParser.VerifyNonNegativeMultiplierOfEm("BlackBoxHeight", ref metrics[(int)FieldIndex.BlackBoxHeight]);
                CompositeFontParser.VerifyMultiplierOfEm("Baseline", ref metrics[(int)FieldIndex.Baseline]);
                CompositeFontParser.VerifyMultiplierOfEm("LeftSideBearing", ref metrics[(int)FieldIndex.LeftSideBearing]);
                CompositeFontParser.VerifyMultiplierOfEm("RightSideBearing", ref metrics[(int)FieldIndex.RightSideBearing]);
                CompositeFontParser.VerifyMultiplierOfEm("TopSideBearing", ref metrics[(int)FieldIndex.TopSideBearing]);
                CompositeFontParser.VerifyMultiplierOfEm("BottomSideBearing", ref metrics[(int)FieldIndex.BottomSideBearing]);
 
                double horizontalAdvance = metrics[(int)FieldIndex.BlackBoxWidth]
                    + metrics[(int)FieldIndex.LeftSideBearing]
                    + metrics[(int)FieldIndex.RightSideBearing];
                if (horizontalAdvance < 0)
                    throw new ArgumentException(SR.CharacterMetrics_NegativeHorizontalAdvance);
 
                double verticalAdvance = metrics[(int)FieldIndex.BlackBoxHeight]
                    + metrics[(int)FieldIndex.TopSideBearing]
                    + metrics[(int)FieldIndex.BottomSideBearing];
                if (verticalAdvance < 0)
                    throw new ArgumentException(SR.CharacterMetrics_NegativeVerticalAdvance);
 
                // Set all the properties.
                _blackBoxWidth = metrics[(int)FieldIndex.BlackBoxWidth];
                _blackBoxHeight = metrics[(int)FieldIndex.BlackBoxHeight];
                _baseline = metrics[(int)FieldIndex.Baseline];
                _leftSideBearing = metrics[(int)FieldIndex.LeftSideBearing];
                _rightSideBearing = metrics[(int)FieldIndex.RightSideBearing];
                _topSideBearing = metrics[(int)FieldIndex.TopSideBearing];
                _bottomSideBearing = metrics[(int)FieldIndex.BottomSideBearing];
            }
        }
 
        private static void AppendField(double value, FieldIndex fieldIndex, ref int lastIndex, StringBuilder s)
        {
            if (value != 0)
            {
                s.Append(',', (int)fieldIndex - lastIndex);
                lastIndex = (int)fieldIndex;
                s.Append(value.ToString(System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS));
            }
        }
 
        private static double[] ParseMetrics(string s)
        {
            double[] metrics = new double[NumFields];
 
            int i = 0, fieldIndex = 0;
            for (; ; )
            {
                // Let i be first non-whitespace character or end-of-string.
                while (i < s.Length && s[i] == ' ')
                    ++i;
 
                // Let j be delimiter or end-of-string.
                int j = i;
                while (j < s.Length && s[j] != ',')
                    ++j;
 
                // Let k be end-of-field without trailing whitespace.
                int k = j;
                while (k > i && s[k - 1] == ' ')
                    --k;
 
                if (k > i)
                {
                    // Non-empty field; convert it to double.
                    ReadOnlySpan<char> field = s.AsSpan(i, k - i);
                    if (!double.TryParse(
                        field,
                        NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign,
                        System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS,
                        out metrics[fieldIndex]
                        ))
                    {
                        throw new ArgumentException(SR.Format(SR.CannotConvertStringToType, field.ToString(), "double"));
                    }
                }
                else if (fieldIndex < NumRequiredFields)
                {
                    // Empty field; make sure it's an optional one.
                    throw new ArgumentException(SR.CharacterMetrics_MissingRequiredField);
                }
 
                ++fieldIndex;
 
                if (j < s.Length)
                {
                    // There's a comma so check if we've exceeded the number of fields.
                    if (fieldIndex == NumFields)
                        throw new ArgumentException(SR.CharacterMetrics_TooManyFields);
 
                    // Initialize character index for next iteration.
                    i = j + 1;
                }
                else
                {
                    // No more fields; check if we have all required fields.
                    if (fieldIndex < NumRequiredFields)
                    {
                        throw new ArgumentException(SR.CharacterMetrics_MissingRequiredField);
                    }
 
                    break;
                }
            }
 
            return metrics;
        }
 
 
        /// <summary>
        /// Width of the black box for the character.
        /// </summary>
        public double BlackBoxWidth
        {
            get { return _blackBoxWidth; }
        }
        
        /// <summary>
        /// Height of the black box for the character.
        /// </summary>
        public double BlackBoxHeight
        {
            get { return _blackBoxHeight; }
        }
 
        /// <summary>
        /// Vertical offset from the bottom of the black box to the baseline. A positive
        /// value indicates the baseline is above the bottom of the black box, and a negative
        /// value indicates the baseline is below the bottom of the black box.
        /// </summary>
        public double Baseline
        {
            get { return _baseline; }
        }
 
        /// <summary>
        /// Recommended white space to the left of the black box. A negative value results in
        /// an overhang. The horizontal advance for the character is LeftSideBearing +
        /// BlackBoxWidth + RightSideBearing, and cannot be less than zero.
        /// </summary>
        public double LeftSideBearing
        {
            get { return _leftSideBearing; }
        }
 
        /// <summary>
        /// Recommended white space to the right of the black box. A negative value results in
        /// an overhang. The horizontal advance for the character is LeftSideBearing +
        /// BlackBoxWidth + RightSideBearing, and cannot be less than zero.
        /// </summary>
        public double RightSideBearing
        {
            get { return _rightSideBearing; }
        }
 
        /// <summary>
        /// Recommended white space above the black box. A negative value results in
        /// an overhang. The vertical advance for the character is TopSideBearing +
        /// BlackBoxHeight + BottomSideBearing, and cannot be less than zero.
        /// </summary>
        public double TopSideBearing
        {
            get { return _topSideBearing; }
        }
 
        /// <summary>
        /// Recommended white space below the black box. A negative value results in
        /// an overhang. The vertical advance for the character is TopSideBearing +
        /// BlackBoxHeight + BottomSideBearing, and cannot be less than zero.
        /// </summary>
        public double BottomSideBearing
        {
            get { return _bottomSideBearing; }
        }
 
 
        /// <summary>
        /// Compares two CharacterMetrics for equality.
        /// </summary>
        public override bool Equals(object obj)
        {
            CharacterMetrics other = obj as CharacterMetrics;
 
            // Suppress PRESharp warning that other can be null; apparently PRESharp
            // doesn't understand short circuit evaluation of operator &&.
            #pragma warning disable 6506
            return other != null &&
                other._blackBoxWidth == _blackBoxWidth &&
                other._blackBoxHeight == _blackBoxHeight &&
                other._leftSideBearing == _leftSideBearing &&
                other._rightSideBearing == _rightSideBearing &&
                other._topSideBearing == _topSideBearing &&
                other._bottomSideBearing == _bottomSideBearing;
            #pragma warning restore 6506
        }
 
        /// <summary>
        /// Computes a hash value for a CharacterMetrics.
        /// </summary>
        public override int GetHashCode()
        {
            int hash = (int)(_blackBoxWidth * Constants.DefaultRealToIdeal);
            hash = (hash * HashMultiplier) + (int)(_blackBoxHeight * Constants.DefaultRealToIdeal);
            hash = (hash * HashMultiplier) + (int)(_baseline * Constants.DefaultRealToIdeal);
            hash = (hash * HashMultiplier) + (int)(_leftSideBearing * Constants.DefaultRealToIdeal);
            hash = (hash * HashMultiplier) + (int)(_rightSideBearing * Constants.DefaultRealToIdeal);
            hash = (hash * HashMultiplier) + (int)(_topSideBearing * Constants.DefaultRealToIdeal);
            hash = (hash * HashMultiplier) + (int)(_bottomSideBearing * Constants.DefaultRealToIdeal);
            return hash;
        }
 
        private const int HashMultiplier = 101;
    }
}