从 TTF 文件中检索字体名称






4.86/5 (23投票s)
解释如何从 TrueType 或 OpenType 文件 (.ttf) 中检索字体名称
引言
每个人都可以获取已安装字体的字体名称。但如果字体尚未安装在系统中,您又想以编程方式知道它是什么呢?当然,您可以暂时将其添加到系统字体中,然后获取其属性(嗯...但您现在将如何找到已安装的字体?)。也许您可以考虑其他方法,但我决定查找 TrueType 和 OpenType 字体文件的规范。幸运的是,微软在这方面有非常好的文章。如果您想了解更多关于它们的信息,请查看本文末尾的链接。
编写代码
由于所有让我感兴趣的(并且在大多数情况下让您感兴趣的)只是字体名称,而不是 TTF 文件中的其他属性,因此我们的代码将很简单(实际上只有一个函数)。该函数将从给定的文件中检索字体名称并将其返回给调用程序。
数据类型定义
由于 Windows 标头文件中没有定义结构(或者我没有找到它们),我们将自己创建。我们需要 4 个结构和 2 个宏(稍后我会解释它们)。
TTF 文件由几个表组成,每个表代表与其类型相关的一些数据。有些表是必需的,有些则不是。我们实际上只需要其中一个,称为“name
”,例如名称表。这是存储字体信息的地方,如字体名称、版权、商标等。
//This is TTF file header
typedef struct _tagTT_OFFSET_TABLE{
USHORT uMajorVersion;
USHORT uMinorVersion;
USHORT uNumOfTables;
USHORT uSearchRange;
USHORT uEntrySelector;
USHORT uRangeShift;
}TT_OFFSET_TABLE;
//Tables in TTF file and there placement and name (tag)
typedef struct _tagTT_TABLE_DIRECTORY{
char szTag[4]; //table name
ULONG uCheckSum; //Check sum
ULONG uOffset; //Offset from beginning of file
ULONG uLength; //length of the table in bytes
}TT_TABLE_DIRECTORY;
//Header of names table
typedef struct _tagTT_NAME_TABLE_HEADER{
USHORT uFSelector; //format selector. Always 0
USHORT uNRCount; //Name Records count
USHORT uStorageOffset; //Offset for strings storage,
//from start of the table
}TT_NAME_TABLE_HEADER;
//Record in names table
typedef struct _tagTT_NAME_RECORD{
USHORT uPlatformID;
USHORT uEncodingID;
USHORT uLanguageID;
USHORT uNameID;
USHORT uStringLength;
USHORT uStringOffset; //from start of storage area
}TT_NAME_RECORD;
宏
现在,剩下的就是我之前一直在谈论的宏。宏定义如下所示
#define SWAPWORD(x) MAKEWORD(HIBYTE(x), LOBYTE(x))
#define SWAPLONG(x) MAKELONG(SWAPWORD(HIWORD(x)), SWAPWORD(LOWORD(x)))
这是什么意思?我们需要这些宏的原因是 TTF 文件以 Big-Endian 格式存储,这与 Windows 系统不同,Windows 系统中的所有文件都采用 Little Endian。是的,我知道这听起来很傻,涉及到所有这些“endians” :)。例如,Motorolla 处理器使用 Big Endian,其中较高的字节首先存储,而在 Little Endian(用于 Intel 处理器)中,较高的字节是最后一个。例如,您有一个整数变量 1
(长度为 4 个字节)。尝试将其保存到文件中并在任何十六进制编辑器中打开,您将看到
01 00 00 00 //Little Endian - Intel
这是 Little Endian 系统 (Intel)。但是对于 Big-Endian (Motorolla),该数字将被反向存储
00 00 00 01 //Big Endian - Motorolla
因此,这些格式是不兼容的。正如我所说,TTF 文件以 Motorolla 风格 (Big Endian) 存储。这就是为什么我们需要这两个宏来重新排列从 TrueType 字体文件中检索的变量中的字节。
读取文件
现在我们已准备好读取 TTF 文件。所以让我们开始吧。
首先,我们需要读取文件头 (TT_OFFSET_TABLE
结构)
CFile f;
CString csRetVal;
//lpszFilePath is the path to our font file
if(f.Open(lpszFilePath, CFile::modeRead|CFile::shareDenyWrite)){
//define and read file header
TT_OFFSET_TABLE ttOffsetTable;
f.Read(&ttOffsetTable, sizeof(TT_OFFSET_TABLE));
//remember to rearrange bytes in the field you gonna use
ttOffsetTable.uNumOfTables = SWAPWORD(ttOffsetTable.uNumOfTables);
ttOffsetTable.uMajorVersion = SWAPWORD(ttOffsetTable.uMajorVersion);
ttOffsetTable.uMinorVersion = SWAPWORD(ttOffsetTable.uMinorVersion);
//check is this is a true type font and the version is 1.0
if(ttOffsetTable.uMajorVersion != 1 || ttOffsetTable.uMinorVersion != 0)
return csRetVal;
文件头之后紧接着是 Offsets
表。您可以在此处找到感兴趣的表偏移量,在本例中为“name
”。
TT_TABLE_DIRECTORY tblDir;
BOOL bFound = FALSE;
CString csTemp;
for(int i=0; i< ttOffsetTable.uNumOfTables; i++){
f.Read(&tblDir, sizeof(TT_TABLE_DIRECTORY));
csTemp.Empty();
//table's tag cannot exceed 4 characters
strncpy(csTemp.GetBuffer(4), tblDir.szTag, 4);
csTemp.ReleaseBuffer();
if(csTemp.CompareNoCase(_T("name")) == 0){
//we found our table. Rearrange order and quit the loop
bFound = TRUE;
tblDir.uLength = SWAPLONG(tblDir.uLength);
tblDir.uOffset = SWAPLONG(tblDir.uOffset);
break;
}
}
我们终于找到了名称表,所以让我们读取它的标头
if(bFound){
//move to offset we got from Offsets Table
f.Seek(tblDir.uOffset, CFile::begin);
TT_NAME_TABLE_HEADER ttNTHeader;
f.Read(&ttNTHeader, sizeof(TT_NAME_TABLE_HEADER));
//again, don't forget to swap bytes!
ttNTHeader.uNRCount = SWAPWORD(ttNTHeader.uNRCount);
ttNTHeader.uStorageOffset = SWAPWORD(ttNTHeader.uStorageOffset);
TT_NAME_RECORD ttRecord;
bFound = FALSE;
在名称表标头之后,表中会插入记录。因此,我们需要遍历所有记录以找到我们感兴趣的信息 - 字体名称。
for(int i=0; i<ttNTHeader.uNRCount; i++){
f.Read(&ttRecord, sizeof(TT_NAME_RECORD));
ttRecord.uNameID = SWAPWORD(ttRecord.uNameID);
//1 says that this is font name. 0 for example determines copyright info
if(ttRecord.uNameID == 1){
ttRecord.uStringLength = SWAPWORD(ttRecord.uStringLength);
ttRecord.uStringOffset = SWAPWORD(ttRecord.uStringOffset);
//save file position, so we can return to continue with search
int nPos = f.GetPosition();
f.Seek(tblDir.uOffset + ttRecord.uStringOffset +
ttNTHeader.uStorageOffset, CFile::begin);
//bug fix: see the post by SimonSays to read more about it
TCHAR lpszNameBuf = csTemp.GetBuffer(ttRecord.uStringLength + 1);
ZeroMemory(lpszNameBuf, ttRecord.uStringLength + 1);
f.Read(lpszNameBuf, ttRecord.uStringLength);
csTemp.ReleaseBuffer();
//yes, still need to check if the font name is not empty
//if it is, continue the search
if(csTemp.GetLength() > 0){
csRetVal = csTemp;
break;
}
f.Seek(nPos, CFile::begin);
}
}
就这样!现在我们可以返回包含字体名称的 csRetVal
。
您可以下载完整的工作函数并在您的代码中使用。我还包括了一个演示项目,其中包含相同的函数,但稍作自定义,因此它还返回了版权和商标信息。
如果您想继续使用 TTF 文件,可以查看微软关于它们的规范。但请记住,您深入了解 TTF 的程度越深,就可能找到 TrueType 和 OpenType 之间的更多差异。无论如何,下面是关于 TTF 的文章的链接。