Интерпретация метрик шрифтов в WinRT

55

Под термином «метрики шрифтов» понимаются размеры символов и строк конкретного шрифта. В большинстве случаев при работе с текстом в программах Windows 8 информация метрик шрифтов не нужна разработчику. Элемент TextBlock определяет размер для конкретного текста и шрифта, который он должен вывести, и обычно этого оказывается достаточно. Но если вы собираетесь выполнять сколько-нибудь нетривиальный вывод текста, без метрик шрифтов не обойтись; они также играют важную роль в некоторых нестандартных операциях.

В последующем описании я намерен ограничиться вертикальными метриками, то есть высотами вместо ширин. Вертикальные метрики зависят от шрифта, начертания (курсива), насыщенности (жирный шрифт) и размера, но не от конкретных символов или строк.

Программа LookAtFontMetrics наглядно демонстрирует связь между размером текстовой строки, вычисляемой элементом TextBlock, и метриками шрифта, предоставляемыми DirectWrite. В проект включаются такие же ссылки на DirectXWrapper, как и в EnumerateFonts. Файл XAML содержит похожий элемент ListBox, но в нем также присутствует элемент TextBlock, заключенный в Border, с частично определенными элементами Line:

<Page ... >

    <Grid Background="#FF1D1D1D">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <ListBox Name="lbx" Width="300"
                 SelectionChanged="lbx_SelectionChanged">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding}" FontFamily="{Binding}" FontSize="24" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <Grid Grid.Column="1"
              HorizontalAlignment="Center"
              VerticalAlignment="Center">
            <Border BorderBrush="#DEFFFFFF" BorderThickness="1">
                <Grid>
                    <Grid.Resources>
                        <Style TargetType="Line">
                            <Setter Property="Stroke" Value="Red" />
                            <Setter Property="StrokeThickness" Value="2" />
                            <Setter Property="X1" Value="0" />
                        </Style>
                    </Grid.Resources>

                    <TextBlock Name="txb" Text="Texting"
                               FontSize="180"
                               SizeChanged="txb_SizeChanged" />

                    <Line x:Name="ascenderLine" Y1="0" Y2="0" />
                    <Line x:Name="descenderLine" />
                    <Line x:Name="lineGapLine" />
                    <Line x:Name="xHeightLine" />
                    <Line x:Name="capsHeightLine" />
                    <Line x:Name="baselineLine" Stroke="Blue" />
                </Grid>
            </Border>
        </Grid>
    </Grid>
</Page>

Конструктор имеет много общего с конструктором из предыдущей программы — он тоже заполняет ListBox перечнем доступных шрифтов. Программа также обрабатывает событие SelectionChanged из ListBox, задавая свойство FontFamily элемента TextBlock и получая значение WriteFontMetrics из DirectXWrapper. Метрики используются для задания свойств Y1 и Y2 различных элементов Line:

using DirectXWrapper;
using Windows.UI.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        WriteFactory writeFactory;
        WriteFontCollection writeFontCollection;

        public MainPage()
        {
            this.InitializeComponent();

            writeFactory = new WriteFactory();
            writeFontCollection = writeFactory.GetSystemFontCollection();
            int count = writeFontCollection.GetFontFamilyCount();
            string[] fonts = new string[count];

            for (int i = 0; i < count; i++)
            {
                WriteFontFamily writeFontFamily = writeFontCollection.GetFontFamily(i);

                WriteLocalizedStrings writeLocalizedStrings = writeFontFamily.GetFamilyNames();

                int nameCount = writeLocalizedStrings.GetCount();
                int index;

                if (writeLocalizedStrings.FindLocaleName("en-us", out index))
                {
                    fonts[i] = writeLocalizedStrings.GetString(index);
                }
            }

            lbx.ItemsSource = fonts;

            Loaded += (sender, e) =>
            {
                lbx.SelectedIndex = 0;
            };
        }

        private void txb_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            double width = txb.ActualWidth;
            ascenderLine.X2 = width;
            capsHeightLine.X2 = width;
            xHeightLine.X2 = width;
            baselineLine.X2 = width;
            descenderLine.X2 = width;
            lineGapLine.X2 = width;
        }

        private void lbx_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            string fontFamily = (sender as ListBox).SelectedItem as string;

            if (fontFamily == null)
                return;

            txb.FontFamily = new FontFamily(fontFamily);

            int index;
            if (writeFontCollection.FindFamilyName(fontFamily, out index))
            {
                WriteFontFamily writeFontFamily = writeFontCollection.GetFontFamily(index);
                WriteFont writeFont = writeFontFamily.GetFirstMatchingFont(FontWeights.Normal,
                                                                           FontStretch.Normal,
                                                                           FontStyle.Normal);
                WriteFontMetrics fontMetrics = writeFont.GetMetrics();
                double fontSize = txb.FontSize;
                double ascent = fontSize * fontMetrics.Ascent / fontMetrics.DesignUnitsPerEm;
                double capsHeight = fontSize * fontMetrics.CapHeight / fontMetrics.DesignUnitsPerEm;
                double xHeight = fontSize * fontMetrics.XHeight / fontMetrics.DesignUnitsPerEm;
                double descent = fontSize * fontMetrics.Descent / fontMetrics.DesignUnitsPerEm;
                double lineGap = fontSize * fontMetrics.LineGap / fontMetrics.DesignUnitsPerEm;

                baselineLine.Y1 = baselineLine.Y2 = ascent;
                capsHeightLine.Y1 = capsHeightLine.Y2 = ascent - capsHeight;
                xHeightLine.Y1 = xHeightLine.Y2 = ascent - xHeight;
                descenderLine.Y1 = descenderLine.Y2 = ascent + descent;
                lineGapLine.Y1 = lineGapLine.Y2 = ascent + descent + lineGap;
            }
        }
    }
}

Структура DWRITE_FONT_METRICS в DirectWrite содержит поле designUnitsPerEm, которое для большинства шрифтов содержит приятное круглое число: 256, 1024, 2048 или 4096, и только в отдельных случаях попадаются аномальные значения типа 1000. Как следует из имени поля, это высота сетки, которая используется дизайнером для определения характеристик шрифта. Все остальные высоты в структуре задаются относительно этой расчетной высоты. Благодаря этому все поля структуры могут быть целыми числами. Для получения высотных характеристик в пикселах для конкретного шрифта и размера поля структуры необходимо умножить на FontSize и разделить на designUnitsPerEm.

Именно это делает программа LookAtFontMetrics при задании свойств Y1 и Y2 всех элементов Line. Для некоторых шрифтов результат выглядит не совсем корректно, потому что эти шрифты проектировались для нелатинских алфавитов. Но для стандартных шрифтов, используемых для языков на основе латинского алфавита, линии, вычисленные по метрикам шрифтов, находятся точно на своем месте:

Вычисляем метрику шрифта

Линия, на которой «стоят» символы (синяя линия), называется базовой линией (baseline). Во многих шрифтах округлые символы (например, «e») опускаются чуть ниже базовой линии. Следующая линия над базовой называется x-высотой (x-height); она определяет высоту строчных букв. Некоторые округлые символы также слегка поднимаются над этой линией. Следующая линия определяет высоту прописных букв (caps height). Еще выше — в самом верху прямоугольника, который TextBlock вычисляет для себя, — находится линия верхних выносных элементов (ascent line); она используется для диакритических знаков, присутствующих в некоторых буквах (например, U). Под базовой линией находится область нижних выносных элементов (descenders), спускающихся ниже базовой линии.

В самом низу располагается подстрочный интервал (line gap); для многих шрифтов он равен нулю. На следующем рисунке используются имена полей, определенные в исходной структуре DWRITE_FONT_METRICS:

Поля структуры DWRITE_FONT_METRICS, описывающие метрику шрифтов

Общая высота, которую TextBlock вычисляет для своих целей, определяется суммой полей ascent, descent и lineGap.

В статье "Составные преобразования в WinRT" была представлена программа, которая выводит наклонную тень текстовой строки:

Наклонная тень для текста

Тогда я упомянул о том, что сделать нечто подобное для произвольного шрифта с наклоном тени от базовой линии невозможно — для этого необходимо знать метрики шрифта. Теперь вы знаете, как получить эту информацию; давайте попробуем решить эту задачу.

Ниже приведен файл XAML для программы BaselineTiltedShadow. В нем присутствует еще один список ListBox для системных шрифтов, а также разметка XAML для текста и тени:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <ListBox Name="lbx" Width="300"
                 SelectionChanged="lbx_SelectionChanged">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding}" FontFamily="{Binding}" FontSize="24" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <Grid Grid.Column="1"
              HorizontalAlignment="Center"
              VerticalAlignment="Center">
            <TextBlock Name="shadowTextBlock"
                       Text="shadow"
                       FontSize="180"
                       Foreground="Gray">
                <TextBlock.RenderTransform>
                    <CompositeTransform ScaleY="1.5" SkewX="-60" />
                </TextBlock.RenderTransform>
            </TextBlock>

            <TextBlock Name="foregroundTextBlock"
                       Text="shadow"
                       FontSize="180" />
        </Grid>
    </Grid>
</Page>

У этих двух элементов TextBlock нет свойств FontFamily, которым в файле фонового кода задается шрифт, выбранный в ListBox. У элемента TextBlock для тени также не задано свойство RenderTransformOrigin. Конструктор файла фонового кода инициализирует список ListBox так же, как у предыдущей программы; свойство SelectionChanged вычисляет преобразование RenderTransformOrigin для тени на основании процентной доли высоты шрифта над базовой линией:

using DirectXWrapper;
using Windows.Foundation;
using Windows.UI.Text;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        WriteFactory writeFactory;
        WriteFontCollection writeFontCollection;

        public MainPage()
        {
            this.InitializeComponent();

            writeFactory = new WriteFactory();
            writeFontCollection = writeFactory.GetSystemFontCollection();

            int count = writeFontCollection.GetFontFamilyCount();
            string[] fonts = new string[count];

            for (int i = 0; i < count; i++)
            {
                WriteFontFamily writeFontFamily = writeFontCollection.GetFontFamily(i);

                WriteLocalizedStrings writeLocalizedStrings = writeFontFamily.GetFamilyNames();

                int nameCount = writeLocalizedStrings.GetCount();
                int index;

                if (writeLocalizedStrings.FindLocaleName("en-us", out index))
                {
                    fonts[i] = writeLocalizedStrings.GetString(index);
                }
            }

            lbx.ItemsSource = fonts;

            Loaded += (sender, e) =>
            {
                lbx.SelectedIndex = 0;
            };
        }

        private void lbx_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            string fontFamily = (sender as ListBox).SelectedItem as string;

            if (fontFamily == null)
                return;

            foregroundTextBlock.FontFamily = new FontFamily(fontFamily);
            shadowTextBlock.FontFamily = foregroundTextBlock.FontFamily;

            int index;
            if (writeFontCollection.FindFamilyName(fontFamily, out index))
            {
                WriteFontFamily writeFontFamily = writeFontCollection.GetFontFamily(index);
                WriteFont writeFont = writeFontFamily.GetFirstMatchingFont(FontWeights.Normal,
                                                                           FontStretch.Normal,
                                                                           FontStyle.Normal);
                WriteFontMetrics fontMetrics = writeFont.GetMetrics();

                double fractionAboveBaseline = (double)fontMetrics.Ascent /
                            (fontMetrics.Ascent + fontMetrics.Descent + fontMetrics.LineGap);

                shadowTextBlock.RenderTransformOrigin = new Point(0, fractionAboveBaseline);
            }
        }
    }
}

Результат:

Вычисление метрик шрифтов для создания тени
Пройди тесты
Лучший чат для C# программистов