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表本身的起始位置(而非编码记录区域结束后的位置),你的代码错误地将编码记录结束后的位置作为偏移基准,导致定位到了无效区域。
修改步骤
修正
CmapTable的基准位置传递:
去掉tableStart的计算,直接将cmap表在文件中的起始偏移_offset传给EncodingRecord。修正
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

