Wednesday, February 19, 2020

Decoding TTF files

Have you ever wondered about the internal structure of ttf (trutype font) files? Of course you have. Here's some code that will peek inside them. For simplicity I hard coded the file name and focused on the name records, which is where the most interesting data is stored.

This blog is based on the ttf definition which I found here

Simply put, the file starts with an offset table which includes the number of table structures that follow it. Each table structure has a tag and we are going to find the table structure with the "name" tag. This table structure points to a TableHeader which contains a pointer to a number of name records. We read those name records and decode them, displaying the results to the user. We're only going to process the name records that are in English.


The XAML is simply a datagrid.



<Window x:Class="TtfDecode.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TtfDecode"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <DataGrid ItemsSource="{Binding FontProperties}" IsReadOnly="True" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="ID" Binding="{Binding ID}" Width="80"/>
                <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*"/>
                <DataGridTextColumn Header="Value" Binding="{Binding Value}" Width="2*"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

The code is mainly a bunch of classes for the different structures in the file and the DecodeTTF method which parses the file. I've cheated a little while reading strings from the file because there doesn't seem to be any values that indicate their encoding.


using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows;

namespace TtfDecode
{
    class cOffsetTable
    {
        public ushort uMajorVersion;
        public ushort uMinorVersion;
        public ushort uNumOfTables;
        public ushort uSearchRange;
        public ushort uEntrySelector;
        public ushort uRangeShift;

        public void Load(cBinaryContent BinaryContent)
        {
            uMajorVersion = BinaryContent.ReadUShort();
            uMinorVersion = BinaryContent.ReadUShort();
            uNumOfTables = BinaryContent.ReadUShort();
            uSearchRange = BinaryContent.ReadUShort();
            uEntrySelector = BinaryContent.ReadUShort();
            uRangeShift = BinaryContent.ReadUShort();
        }
    }

    class cTableDirectory
    {
        public string szTag;
        public ulong uCheckSum;
        public ulong uOffset;
        public ulong uLength;

        public void Load(cBinaryContent BinaryContent)
        {
            szTag = BinaryContent.ReadString(4);
            uCheckSum = BinaryContent.ReadULong();
            uOffset = BinaryContent.ReadULong();
            uLength = BinaryContent.ReadULong();
        }
    }

    class cTableHeader
    {
        public ushort uFSelector;
        public ushort uNRCount;
        public ushort uStorageOffset;

        public void Load(cBinaryContent BinaryContent)
        {
            uFSelector = BinaryContent.ReadUShort();
            uNRCount = BinaryContent.ReadUShort();
            uStorageOffset = BinaryContent.ReadUShort();
        }
    }

    class cNameRecord
    {
        public ushort uPlatformID;
        public ushort uEncodingID;
        public ushort uLanguageID;
        public ushort uNameID;
        public ushort uStringLength;
        public ushort uStringOffset;

        public void Load(cBinaryContent BinaryContent)
        {
            uPlatformID = BinaryContent.ReadUShort();
            uEncodingID = BinaryContent.ReadUShort();
            uLanguageID = BinaryContent.ReadUShort();
            uNameID = BinaryContent.ReadUShort();
            uStringLength = BinaryContent.ReadUShort();
            uStringOffset = BinaryContent.ReadUShort();
        }
    }

    class cBinaryContent
    {
        private byte[] Content;
        private ulong _p = 0;

        public ulong p
        {
            get { return _p; }
            set
            {
                if (value >= 0 && value < (ulong)Content.Length)
                    _p = value;
                else
                   throw new Exception(string.Format("Invalid value for content offset {0}", p));
            }
        }

        public void LoadFromFile(String FileName)
        {
            Content = File.ReadAllBytes(FileName);
            p = 0;
        }

        public byte ReadByte()
        {
            Byte b = Content[p];
            p += 1;
            return b;
        }

        public ushort ReadUShort()
        {
            return (ushort)(ReadByte() * 256 + ReadByte());
        }

        public ulong ReadULong()
        {
            return (ulong)(ReadByte() * 16777216 + ReadByte() * 65536 + ReadByte() * 256 + ReadByte());
        }

        public string ReadString(ulong count)
        {
            String s = "";
            foreach (byte b in Content.Skip((int)p).Take((int)count))
            {
                if (b != 0) s += (char)b;
            }
            p += count;
            return s;
        }
    }

    public class cFontProperty
    {
        public ushort ID { get; set; }
        public String Name { get; set; }
        public string Value { get; set; }
    }

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private ObservableCollection<cFontProperty> _FontProperties = new ObservableCollection<cFontProperty>();
        public ObservableCollection<cFontProperty> FontProperties
        {
            get { return _FontProperties; }
            set
            {
                _FontProperties = value;
                PropChanged("FontProperties");
            }
        }

        public MainWindow()
        {
            InitializeComponent();
            DecodeTTF(@"C:\Windows\Fonts\webdings.ttf");
        }

        public void DecodeTTF(String FileName)
        {
            cBinaryContent BinaryContent = new cBinaryContent();
            cOffsetTable OffsetTable = new cOffsetTable();
            List<cTableDirectory> TableDirectories = new List<cTableDirectory>();
            cTableDirectory NameTable = null;
            cTableHeader NameHeader = new cTableHeader();
            List<cNameRecord> NameRecords = new List<cNameRecord>();

            BinaryContent.LoadFromFile(FileName);
            OffsetTable.Load(BinaryContent);

            for (int i = 0; i < OffsetTable.uNumOfTables; i++)
            {
                cTableDirectory td = new cTableDirectory();
                td.Load(BinaryContent);
                if (td.szTag == "name") NameTable = td;
            }

            if (NameTable != null)
            {
                BinaryContent.p = NameTable.uOffset;
                NameHeader.Load(BinaryContent);
                for (int i = 0; i < NameHeader.uNRCount; i++)
                {
                    cNameRecord nr = new cNameRecord();
                    nr.Load(BinaryContent);
                    NameRecords.Add(nr);
                }
            }

            // decode the english name records
            foreach (cNameRecord nr in NameRecords.Where(n => n.uLanguageID == 0))
            {
                String PropertyName = "";
                String PropertyValue = "";

                BinaryContent.p = NameTable.uOffset + nr.uStringOffset + NameHeader.uStorageOffset;
                switch (nr.uNameID)
                {
                    case 0: PropertyName = "Copyright Notice"; break;
                    case 1: PropertyName = "Font Family"; break;
                    case 2: PropertyName = "Font Subfamily"; break;
                    case 3: PropertyName = "Unique subfamily identification"; break;
                    case 4: PropertyName = "Full name"; break;
                    case 5: PropertyName = "Version"; break;
                    case 6: PropertyName = "Postscript name"; break;
                    case 7: PropertyName = "Trademark"; break;
                    case 8: PropertyName = "Manufacturer"; break;
                    case 9: PropertyName = "Designer"; break;
                    case 10: PropertyName = "Description"; break;
                    case 11: PropertyName = "Vendor URL"; break;
                    case 12: PropertyName = "Designer URL"; break;
                    case 13: PropertyName = "License"; break;
                    case 14: PropertyName = "License URL"; break;
                    case 16: PropertyName = "Preferred Family"; break;
                    case 17: PropertyName = "Preferred Subfamily"; break;
                    case 19: PropertyName = "Sample text"; break;
                }
                PropertyValue = BinaryContent.ReadString(nr.uStringLength);
                if (PropertyName != "" && PropertyValue != "")
                    FontProperties.Add(new cFontProperty() { ID = nr.uNameID, Name = PropertyName, Value = PropertyValue });

            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public void PropChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}

The result is


No comments:

Post a Comment