You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#实现TrueType阅读器时cmap子表定位错误问题排查

问题描述

我正在编写一个TrueType阅读器,作为学习TrueType/OpenType和C#的练习,但读取cmap表时遇到了问题。已经成功读取文件头和cmap表的编码记录表头(能获取平台ID、编码ID和偏移量),但定位子表时位置错误,读取到Format: 18244, Length: 17734, Language: 1822这类明显异常的值。确定是偏移量起始位置设置错误,但查文档没解决,相关代码如下:

internal class EncodingTable
{
    public ushort Format { get; }
    public ushort Length { get; }
    public ushort Language { get; }
    public byte[] GlyphIdArray { get; }

    public EncodingTable(ushort format, ushort length, ushort language, byte[] glyphIdArrays)
    {
        Format = format;
        Length = length;
        Language = language;
        GlyphIdArray = glyphIdArrays;
    }
}

internal class EncodingRecord
{
    public ushort PlatformId { get; }
    public ushort EncodingId { get; }
    public uint Offset { get; }
    public EncodingTable Table; 

    public EncodingRecord(ushort platformId, ushort encodingId, uint recordOffset, BinaryReader reader, long tableStart)
    {
        PlatformId = platformId;
        EncodingId = encodingId;
        Offset = recordOffset;
        _reader = reader;

        Console.WriteLine("Platform: {0}, Encoding: {1}, Offset: {2}", PlatformId, EncodingId, Offset);

        long start = reader.BaseStream.Position;

        reader.BaseStream.Position = tableStart;
        reader.BaseStream.Seek(Offset, SeekOrigin.Begin);
        ushort format = reader.ReadUInt16();
        ushort length = reader.ReadUInt16();
        ushort language = reader.ReadUInt16();
        Console.WriteLine("Format: {0}, Length: {1}, Language: {2}", format, length, language);

        reader.BaseStream.Position = start;
    }
}

internal class CmapTable : FontDataTable
{
    public ushort Version { get; }
    public ushort NumTables { get; }
    public List<EncodingRecord> EncodingRecords { get; } = new List<EncodingRecord>();

    public CmapTable(string tag, uint checksum, uint offset, uint length, BinaryReader reader, string name) 
        : base(tag, checksum, offset, length, reader, name)
    {
        long start = reader.BaseStream.Position;

        _reader.BaseStream.Seek(_offset, SeekOrigin.Begin);
        Version = reader.ReadUInt16();
        NumTables = reader.ReadUInt16();
        long pos = reader.BaseStream.Position;
        long tableStart = pos + (NumTables * 8);
        for (int i = 0; i < NumTables; i++)
        {
            EncodingRecords.Add(new EncodingRecord(reader.ReadUInt16(), reader.ReadUInt16(), reader.ReadUInt32(), reader, tableStart));
        }
        reader.BaseStream.Position = start;
    }
}

FontDataTable是所有表的基类,存储_reader等公共字段,便于统一管理。


解决方法

问题核心是偏移量的基准位置计算错误。根据OpenType规范,cmap表中编码记录的Offset是相对于cmap表本身的起始位置(而非编码记录区域结束后的位置),你的代码错误地将编码记录结束后的位置作为偏移基准,导致定位到了无效区域。

修改步骤

  1. 修正CmapTable的基准位置传递:
    去掉tableStart的计算,直接将cmap表在文件中的起始偏移_offset传给EncodingRecord。

  2. 修正EncodingRecord的流定位逻辑:
    子表的绝对位置 = cmap表在文件中的起始位置 + 编码记录里的Offset(该偏移是相对于cmap表起始位置的)。

修改后的关键代码

CmapTable类

internal class CmapTable : FontDataTable
{
    public ushort Version { get; }
    public ushort NumTables { get; }
    public List<EncodingRecord> EncodingRecords { get; } = new List<EncodingRecord>();

    public CmapTable(string tag, uint checksum, uint offset, uint length, BinaryReader reader, string name) 
        : base(tag, checksum, offset, length, reader, name)
    {
        long start = reader.BaseStream.Position;

        _reader.BaseStream.Seek(_offset, SeekOrigin.Begin);
        Version = reader.ReadUInt16();
        NumTables = reader.ReadUInt16();
        // 直接传入cmap表的起始偏移作为基准
        for (int i = 0; i < NumTables; i++)
        {
            EncodingRecords.Add(new EncodingRecord(reader.ReadUInt16(), reader.ReadUInt16(), reader.ReadUInt32(), reader, _offset));
        }
        reader.BaseStream.Position = start;
    }
}

EncodingRecord类

internal class EncodingRecord
{
    public ushort PlatformId { get; }
    public ushort EncodingId { get; }
    public uint Offset { get; }
    public EncodingTable Table; 
    private BinaryReader _reader; // 补全缺失的字段定义

    public EncodingRecord(ushort platformId, ushort encodingId, uint recordOffset, BinaryReader reader, long cmapTableStart)
    {
        PlatformId = platformId;
        EncodingId = encodingId;
        Offset = recordOffset;
        _reader = reader;

        Console.WriteLine("Platform: {0}, Encoding: {1}, Offset: {2}", PlatformId, EncodingId, Offset);

        long start = reader.BaseStream.Position;

        // 正确定位子表位置:cmap表起始位置 + 相对偏移
        reader.BaseStream.Position = cmapTableStart + Offset;
        ushort format = reader.ReadUInt16();
        ushort length = reader.ReadUInt16();
        ushort language = reader.ReadUInt16();
        Console.WriteLine("Format: {0}, Length: {1}, Language: {2}", format, length, language);

        reader.BaseStream.Position = start;
    }
}

额外注意事项

TrueType/OpenType文件使用大端字节序(Big-Endian),但.NET的BinaryReader默认是小端字节序。如果读取的数值仍然异常,需要对读取的ushort/uint等类型进行字节反转,比如使用IPAddress.HostToNetworkOrder方法转换,或者自定义一个支持大端的BinaryReader。


内容的提问来源于stack exchange,提问作者Toblexson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 05:15:00