Files
timi-icon-font/src/test/resources/index.html
T
2026-07-17 15:17:08 +08:00

331 lines
8.8 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TTF 字体预览</title>
<style>
:root {
color-scheme: light;
font-family: "Microsoft YaHei UI", sans-serif;
}
body {
margin: 0;
padding: 16px;
font-size: 14px;
line-height: 1.5;
color: #222;
background: #f7f7f7;
}
h1 {
margin: 0 0 12px;
font-size: 18px;
}
.tip {
margin: 0 0 16px;
color: #666;
}
#status {
margin-bottom: 12px;
color: #666;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 8px;
}
.item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
}
.icon {
width: 32px;
height: 32px;
flex: 0 0 32px;
display: flex;
align-items: center;
text-align: center;
justify-content: center;
font-family: "TimiIconPreview", sans-serif;
font-size: 32px;
line-height: 1;
color: #111;
}
.meta {
min-width: 0;
}
.code {
font-family: Consolas, monospace;
font-size: 13px;
color: #111;
}
.decimal {
font-size: 12px;
color: #666;
}
.error {
color: #c62828;
}
</style>
</head>
<body>
<h1>TTF 字体预览</h1>
<p class="tip">读取同目录的 <code>timi-icon.ttf</code>,解析字体映射并展示可用字形</p>
<div id="status">正在加载...</div>
<div id="grid" class="grid"></div>
<script>
const TTF_FILE = "./timi-icon.ttf";
const statusElement = document.getElementById("status");
const gridElement = document.getElementById("grid");
function createReader(buffer) {
const view = new DataView(buffer);
return {
u16(offset) {
return view.getUint16(offset, false);
},
i16(offset) {
return view.getInt16(offset, false);
},
u32(offset) {
return view.getUint32(offset, false);
},
tag(offset) {
return String.fromCharCode(
view.getUint8(offset),
view.getUint8(offset + 1),
view.getUint8(offset + 2),
view.getUint8(offset + 3)
);
},
text(offset, length) {
let text = "";
for (let index = 0; index < length; index++) {
text += String.fromCharCode(view.getUint8(offset + index));
}
return text;
}
};
}
function readTables(reader) {
const numTables = reader.u16(4);
const tables = new Map();
for (let index = 0; index < numTables; index++) {
const offset = 12 + index * 16;
tables.set(reader.tag(offset), {
offset: reader.u32(offset + 8),
length: reader.u32(offset + 12)
});
}
return tables;
}
function readName(reader, table) {
if (!table) {
return "";
}
const tableOffset = table.offset;
const count = reader.u16(tableOffset + 2);
const storageOffset = reader.u16(tableOffset + 4);
for (let index = 0; index < count; index++) {
const recordOffset = tableOffset + 6 + index * 12;
const platformId = reader.u16(recordOffset);
const nameId = reader.u16(recordOffset + 6);
const length = reader.u16(recordOffset + 8);
const stringOffset = reader.u16(recordOffset + 10);
if (nameId !== 1) {
continue;
}
const valueOffset = tableOffset + storageOffset + stringOffset;
if (platformId === 3) {
let text = "";
for (let cursor = 0; cursor < length; cursor += 2) {
const code = reader.u16(valueOffset + cursor);
if (code !== 0) {
text += String.fromCharCode(code);
}
}
if (text) {
return text;
}
}
if (platformId === 1) {
const text = reader.text(valueOffset, length).trim();
if (text) {
return text;
}
}
}
return "";
}
function parseFormat4(reader, subtableOffset) {
const segmentCount = reader.u16(subtableOffset + 6) / 2;
const endCodesOffset = subtableOffset + 14;
const startCodesOffset = endCodesOffset + segmentCount * 2 + 2;
const idDeltasOffset = startCodesOffset + segmentCount * 2;
const idRangeOffsetsOffset = idDeltasOffset + segmentCount * 2;
const codePoints = new Set();
for (let index = 0; index < segmentCount; index++) {
const endCode = reader.u16(endCodesOffset + index * 2);
const startCode = reader.u16(startCodesOffset + index * 2);
const delta = reader.i16(idDeltasOffset + index * 2);
const rangeOffset = reader.u16(idRangeOffsetsOffset + index * 2);
if (endCode === 0xFFFF && startCode === 0xFFFF) {
continue;
}
for (let codePoint = startCode; codePoint <= endCode; codePoint++) {
let glyphIndex;
if (rangeOffset === 0) {
glyphIndex = (codePoint + delta) & 0xFFFF;
} else {
const glyphOffset = idRangeOffsetsOffset + index * 2 + rangeOffset + (codePoint - startCode) * 2;
glyphIndex = reader.u16(glyphOffset);
if (glyphIndex !== 0) {
glyphIndex = (glyphIndex + delta) & 0xFFFF;
}
}
if (glyphIndex !== 0) {
codePoints.add(codePoint);
}
}
}
return codePoints;
}
function parseFormat12(reader, subtableOffset) {
const groups = reader.u32(subtableOffset + 12);
const codePoints = new Set();
for (let index = 0; index < groups; index++) {
const groupOffset = subtableOffset + 16 + index * 12;
const startCharCode = reader.u32(groupOffset);
const endCharCode = reader.u32(groupOffset + 4);
for (let codePoint = startCharCode; codePoint <= endCharCode; codePoint++) {
codePoints.add(codePoint);
}
}
return codePoints;
}
function parseCmap(reader, table) {
if (!table) {
throw new Error("字体缺少 cmap 表");
}
const tableOffset = table.offset;
const numTables = reader.u16(tableOffset + 2);
let bestUnicodeOffset = 0;
let fallbackOffset = 0;
for (let index = 0; index < numTables; index++) {
const recordOffset = tableOffset + 4 + index * 8;
const platformId = reader.u16(recordOffset);
const encodingId = reader.u16(recordOffset + 2);
const subtableOffset = tableOffset + reader.u32(recordOffset + 4);
const format = reader.u16(subtableOffset);
if (platformId === 3 && encodingId === 10 && format === 12) {
bestUnicodeOffset = subtableOffset;
break;
}
if ((platformId === 3 || platformId === 0) && (format === 4 || format === 12) && fallbackOffset === 0) {
fallbackOffset = subtableOffset;
}
}
const selectedOffset = bestUnicodeOffset || fallbackOffset;
if (selectedOffset === 0) {
throw new Error("未找到可解析的 cmap 子表");
}
const format = reader.u16(selectedOffset);
const codePoints = format === 12
? parseFormat12(reader, selectedOffset)
: parseFormat4(reader, selectedOffset);
return Array.from(codePoints).sort((left, right) => left - right);
}
function createItem(codePoint) {
const item = document.createElement("div");
item.className = "item";
const icon = document.createElement("div");
icon.className = "icon";
icon.textContent = String.fromCodePoint(codePoint);
const meta = document.createElement("div");
meta.className = "meta";
const code = document.createElement("div");
code.className = "code";
code.textContent = "U+%s".replace("%s", codePoint.toString(16).toUpperCase().padStart(4, "0"));
const decimal = document.createElement("div");
decimal.className = "decimal";
decimal.textContent = "十进制 %d".replace("%d", codePoint);
meta.appendChild(code);
meta.appendChild(decimal);
item.appendChild(icon);
item.appendChild(meta);
return item;
}
async function loadFontFace() {
const font = new FontFace("TimiIconPreview", "url(%s)".replace("%s", TTF_FILE));
await font.load();
document.fonts.add(font);
return font;
}
async function loadFont() {
try {
const response = await fetch(TTF_FILE);
if (!response.ok) {
throw new Error("读取 timi-icon.ttf 失败");
}
const buffer = await response.arrayBuffer();
const reader = createReader(buffer);
const tables = readTables(reader);
const family = readName(reader, tables.get("name")) || "未命名字体";
const codePoints = parseCmap(reader, tables.get("cmap"));
await loadFontFace();
statusElement.textContent = "字体 %s,共 %d 个字形".replace("%s", family).replace("%d", codePoints.length);
for (const codePoint of codePoints) {
gridElement.appendChild(createItem(codePoint));
}
if (codePoints.length < 1) {
statusElement.className = "error";
statusElement.textContent = "字体已加载,但没有可展示的字符映射";
}
} catch (error) {
statusElement.className = "error";
statusElement.textContent = "加载失败,请先运行 BuildTTF.java,并通过本地静态服务打开 target/ttf-test/index.html";
console.error(error);
}
}
loadFont();
</script>
</body>
</html>