fix: extractCodePoints 增加 glyph 轮廓验证 + 删除冗余正文预览卡片

- font_meta.ts: extractCodePoints 新增 createGlyphValidator,对 TrueType 字体
  检查 loca 表确认 glyph 有真实轮廓数据(非空),CFF 字体保持原逻辑
- lint 同步去掉 bodyTitle/bodyText(FontDetail 已移除正文预览卡片)
- FontDetail.vue: 删除正文预览区块(与大标题预览功能重叠),移除对应 computed
- allPreviewText 只合并 previewText + charsetPreview
This commit is contained in:
崮生(子虚) 2026-07-30 23:50:27 +08:00
parent afbd4db77c
commit 6421e7d059
3 changed files with 49 additions and 57 deletions

View File

@ -37,7 +37,39 @@ function readTableEntry(dv: DataView, tag: string): TableEntry | null {
* cmap format4 subtable segment segment gid0 codepoint
* format4 gsub-probe.ts lookupFormat4
*/
function collectFormat4(dv: DataView, subOff: number, cps: Set<number>): void {
/**
* glyph TrueType gid loca glyf > 0
* CFF glyf/loca
*/
function createGlyphValidator(dv: DataView): (gid: number) => boolean {
const glyfEntry = readTableEntry(dv, "glyf");
const locaEntry = readTableEntry(dv, "loca");
const headEntry = readTableEntry(dv, "head");
if (!glyfEntry || !locaEntry || !headEntry) {
/** CFF 字体cmap 有映射即有效 */
return () => true;
}
/** indexToLocFormat0=short(uint16偏移×2), 1=long(uint32偏移) */
const locaFormat = dv.getUint16(headEntry.offset + 50, false);
const locaBase = locaEntry.offset;
return (gid: number): boolean => {
let start: number;
let end: number;
if (locaFormat === 0) {
start = dv.getUint16(locaBase + gid * 2, false) * 2;
end = dv.getUint16(locaBase + (gid + 1) * 2, false) * 2;
} else {
start = dv.getUint32(locaBase + gid * 4, false);
end = dv.getUint32(locaBase + (gid + 1) * 4, false);
}
return end - start > 0;
};
}
function collectFormat4(dv: DataView, subOff: number, cps: Set<number>, hasGlyph: (gid: number) => boolean): void {
if (subOff + 14 > dv.byteLength) return;
const segCountX2 = dv.getUint16(subOff + 6, false);
const segCount = segCountX2 / 2;
@ -57,7 +89,8 @@ function collectFormat4(dv: DataView, subOff: number, cps: Set<number>): void {
if (idRangeOffset === 0) {
/** 线性映射gid = (cp + idDelta) & 0xFFFFgid=0 表示缺失 */
for (let cp = start; cp <= end; cp++) {
if (((cp + idDelta) & 0xFFFF) !== 0) cps.add(cp);
const gid = (cp + idDelta) & 0xFFFF;
if (gid !== 0 && hasGlyph(gid)) cps.add(cp);
}
} else {
/** 逐个查 glyphIdArray */
@ -65,7 +98,8 @@ function collectFormat4(dv: DataView, subOff: number, cps: Set<number>): void {
const glyphOff = idRangeOffsetBase + i * 2 + idRangeOffset + (cp - start) * 2;
if (glyphOff + 2 > dv.byteLength) break;
const glyphId = dv.getUint16(glyphOff, false);
if (glyphId !== 0 && ((glyphId + idDelta) & 0xFFFF) !== 0) cps.add(cp);
const gid = (glyphId + idDelta) & 0xFFFF;
if (glyphId !== 0 && gid !== 0 && hasGlyph(gid)) cps.add(cp);
}
}
}
@ -75,7 +109,7 @@ function collectFormat4(dv: DataView, subOff: number, cps: Set<number>): void {
* cmap format12 subtable group gid codepoint
* format12 gsub-probe.ts lookupFormat12
*/
function collectFormat12(dv: DataView, subOff: number, cps: Set<number>): void {
function collectFormat12(dv: DataView, subOff: number, cps: Set<number>, hasGlyph: (gid: number) => boolean): void {
if (subOff + 16 > dv.byteLength) return;
const nGroups = dv.getUint32(subOff + 12, false);
const groupsBase = subOff + 16;
@ -84,10 +118,11 @@ function collectFormat12(dv: DataView, subOff: number, cps: Set<number>): void {
if (gOff + 12 > dv.byteLength) break;
const gStart = dv.getUint32(gOff, false);
const gEnd = dv.getUint32(gOff + 4, false);
/** startGlyphID gid startGlyphID 0
* format12 group [gStart, gEnd] */
const gGid = dv.getUint32(gOff + 8, false);
/** 逐个验证 gid 是否有真实轮廓(空轮廓 glyph 会被 loca 表过滤掉) */
for (let cp = gStart; cp <= gEnd; cp++) {
cps.add(cp);
const gid = gGid + (cp - gStart);
if (gid !== 0 && hasGlyph(gid)) cps.add(cp);
}
}
}
@ -125,9 +160,12 @@ export function extractCodePoints(fontBuffer: ArrayBuffer | Uint8Array): Set<num
dirOff += 8;
}
/** 创建 glyph 轮廓验证器TrueType 检查 locaCFF 恒真) */
const hasGlyph = createGlyphValidator(dv);
/** format12 优先含补充平面format4 补充 BMP */
if (fmt12Off >= 0) collectFormat12(dv, fmt12Off, cps);
if (fmt4Off >= 0) collectFormat4(dv, fmt4Off, cps);
if (fmt12Off >= 0) collectFormat12(dv, fmt12Off, cps, hasGlyph);
if (fmt4Off >= 0) collectFormat4(dv, fmt4Off, cps, hasGlyph);
return cps;
}

View File

@ -1,7 +1,7 @@
/**
* lint-font-config.ts
*
* font/font-config.json previewText / bodyTitle / bodyText / charsetPreview
* font/font-config.json previewText / charsetPreview
*
*
* pnpm lint:font-config
@ -15,16 +15,12 @@ import { extractCodePoints } from "../backend/font_util/font_meta.ts";
interface FontUserConfig {
displayName?: string;
previewText?: string;
bodyTitle?: string;
bodyText?: string;
charsetPreview?: string;
}
/** 需要检查的文本字段及显示名 */
const TEXT_FIELDS: Array<{ key: keyof FontUserConfig; label: string }> = [
{ key: "previewText", label: "previewText" },
{ key: "bodyTitle", label: "bodyTitle" },
{ key: "bodyText", label: "bodyText" },
{ key: "charsetPreview", label: "charsetPreview" },
];

View File

@ -90,12 +90,6 @@ const previewLines = computed(() => {
};
});
/** 正文标题:来自 config.bodyTitle */
const bodyTitle = computed(() => meta.value?.config?.bodyTitle ?? "");
/** 正文段落:来自 config.bodyText */
const bodyText = computed(() => meta.value?.config?.bodyText ?? "");
/** 字符预览行:来自 config.charsetPreview */
const charsetPreview = computed(() => meta.value?.config?.charsetPreview ?? "");
@ -103,7 +97,7 @@ const charsetPreview = computed(() => meta.value?.config?.charsetPreview ?? "");
const allPreviewText = computed(() => {
if (!meta.value?.config) return "";
const c = meta.value.config;
return [c.previewText ?? "", c.bodyTitle ?? "", c.bodyText ?? "", c.charsetPreview ?? ""].join("");
return [c.previewText ?? "", c.charsetPreview ?? ""].join("");
});
/** 使用方法示例代码(动态拼接 origin + fontName */
@ -219,42 +213,6 @@ const usageCode = computed(() =>
</p>
</div>
<!-- 正文预览 -->
<div
v-if="bodyTitle || bodyText"
style="
background: #fff;
border-radius: 12px;
padding: 40px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
"
>
<h2
:style="{
fontFamily: `'${fontName}', serif`,
fontSize: '24px',
fontWeight: 600,
margin: '0 0 20px',
color: '#3a3a3a',
}"
>
{{ bodyTitle }}
</h2>
<p
:style="{
fontFamily: `'${fontName}', serif`,
fontSize: '16px',
lineHeight: 1.8,
color: '#4a4a4a',
textIndent: '2em',
margin: 0,
}"
>
{{ bodyText }}
</p>
</div>
<!-- 字符集预览 -->
<div
v-if="charsetPreview"