Files
font-preview/FontPreview/FontCmap.cs
T

172 lines
4.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace FontPreview
{
/// <summary>
/// 轻量 cmap 表解析器:纯字节读取字体文件,判断某 Unicode 码点是否有字形。
/// 支持 TTF/OTF/TTC,覆盖 cmap 格式 4(BMP)与格式 12(全平面)。
/// 不依赖 GDI+/DirectWrite,跨平台可编译。
/// </summary>
public class FontCmap
{
private List<int[]> ranges = new List<int[]>(); // 排序的 [start, end] 闭区间
public static FontCmap Load(string path)
{
try
{
byte[] d = File.ReadAllBytes(path);
FontCmap c = new FontCmap();
c.Parse(d);
return c.ranges.Count > 0 ? c : null;
}
catch
{
return null;
}
}
private static ushort RU16(byte[] d, uint o)
{
return (ushort)(d[o] * 256u + d[o + 1]);
}
private static uint RU32(byte[] d, uint o)
{
return (uint)(d[o] * 16777216u + d[o + 1] * 65536u + d[o + 2] * 256u + d[o + 3]);
}
private static string Tag(byte[] d, uint o)
{
try
{
return Encoding.ASCII.GetString(d, (int)o, 4);
}
catch { return ""; }
}
private void Parse(byte[] d)
{
uint sfntOff = 0;
if (Tag(d, 0) == "ttcf")
{
sfntOff = RU32(d, 8); // TTC:第一个字体的偏移
}
uint numTables = RU16(d, sfntOff + 4);
uint cmapOff = 0;
for (uint i = 0; i < numTables; i++)
{
uint rec = sfntOff + 12 + i * 16;
if (Tag(d, rec) == "cmap")
{
cmapOff = RU32(d, rec + 8);
break;
}
}
if (cmapOff == 0) return;
uint numSub = RU16(d, cmapOff + 2);
uint best = 0;
bool gotFull = false;
for (uint i = 0; i < numSub; i++)
{
uint rec = cmapOff + 4 + i * 8;
ushort platform = RU16(d, rec);
ushort enc = RU16(d, rec + 2);
uint off = cmapOff + RU32(d, rec + 4);
// Windows: platformID=3;enc=10 为全 Unicode 平面,enc=1 为 BMP
if (platform == 3 && (enc == 10 || enc == 1))
{
if (enc == 10 && !gotFull) { best = off; gotFull = true; }
else if (best == 0) { best = off; }
}
}
if (best == 0)
{
// 退而求其次:任意子表
if (numSub > 0) best = cmapOff + RU32(d, cmapOff + 8);
}
if (best == 0) return;
ushort fmt = RU16(d, best);
if (fmt == 4) ParseFormat4(d, best);
else if (fmt == 12) ParseFormat12(d, best);
else if (fmt == 0) ParseFormat0(d, best);
ranges.Sort((a, b) => a[0].CompareTo(b[0]));
Merge();
}
private void ParseFormat4(byte[] d, uint o)
{
int segCount = RU16(d, o + 6) / 2;
uint endOff = o + 14;
uint startOff = endOff + (uint)segCount * 2 + 2; // 跳过 endCode 与 reservedPad
for (int i = 0; i < segCount; i++)
{
int s = RU16(d, startOff + (uint)i * 2);
int e = RU16(d, endOff + (uint)i * 2);
if (s != 0xFFFF && e >= s && s != 0)
{
ranges.Add(new int[] { s, e });
}
}
}
private void ParseFormat12(byte[] d, uint o)
{
uint numGroups = RU32(d, o + 12);
uint g = o + 16;
for (uint i = 0; i < numGroups; i++)
{
int s = (int)RU32(d, g + i * 12);
int e = (int)RU32(d, g + i * 12 + 4);
if (e >= s) ranges.Add(new int[] { s, e });
}
}
private void ParseFormat0(byte[] d, uint o)
{
// 256 字节 glyph index 表,非 0 即有字形
int start = -1;
for (int i = 0; i < 256; i++)
{
bool has = d[o + 6 + i] != 0;
if (has && start < 0) start = i;
if (!has && start >= 0) { ranges.Add(new int[] { start, i - 1 }); start = -1; }
}
if (start >= 0) ranges.Add(new int[] { start, 255 });
}
private void Merge()
{
List<int[]> merged = new List<int[]>();
foreach (int[] r in ranges)
{
if (merged.Count > 0 && r[0] <= merged[merged.Count - 1][1] + 1)
{
if (r[1] > merged[merged.Count - 1][1]) merged[merged.Count - 1][1] = r[1];
}
else merged.Add(new int[] { r[0], r[1] });
}
ranges = merged;
}
public bool HasGlyph(int codepoint)
{
int lo = 0, hi = ranges.Count - 1;
while (lo <= hi)
{
int mid = (lo + hi) / 2;
if (codepoint < ranges[mid][0]) hi = mid - 1;
else if (codepoint > ranges[mid][1]) lo = mid + 1;
else return true;
}
return false;
}
}
}