From 6421e7d059246dedd72071c7ccd885338e1ba7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=AE=E7=94=9F=EF=BC=88=E5=AD=90=E8=99=9A=EF=BC=89?= <2234839456@qq.com> Date: Thu, 30 Jul 2026 23:50:27 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20extractCodePoints=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=20glyph=20=E8=BD=AE=E5=BB=93=E9=AA=8C=E8=AF=81=20+=20=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=86=97=E4=BD=99=E6=AD=A3=E6=96=87=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - font_meta.ts: extractCodePoints 新增 createGlyphValidator,对 TrueType 字体 检查 loca 表确认 glyph 有真实轮廓数据(非空),CFF 字体保持原逻辑 - lint 同步去掉 bodyTitle/bodyText(FontDetail 已移除正文预览卡片) - FontDetail.vue: 删除正文预览区块(与大标题预览功能重叠),移除对应 computed - allPreviewText 只合并 previewText + charsetPreview --- backend/font_util/font_meta.ts | 56 ++++++++++++++++++++++++++++------ scripts/lint-font-config.ts | 6 +--- src/pages/FontDetail.vue | 44 +------------------------- 3 files changed, 49 insertions(+), 57 deletions(-) diff --git a/backend/font_util/font_meta.ts b/backend/font_util/font_meta.ts index 58186a5..facf898 100644 --- a/backend/font_util/font_meta.ts +++ b/backend/font_util/font_meta.ts @@ -37,7 +37,39 @@ function readTableEntry(dv: DataView, tag: string): TableEntry | null { * 从 cmap 的 format4 subtable 遍历所有 segment,收集每个 segment 中 gid≠0 的 codepoint。 * format4 表布局见 gsub-probe.ts 的 lookupFormat4 注释。 */ -function collectFormat4(dv: DataView, subOff: number, cps: Set): 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; + } + + /** indexToLocFormat:0=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, 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): void { if (idRangeOffset === 0) { /** 线性映射:gid = (cp + idDelta) & 0xFFFF,gid=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): 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): void { * 从 cmap 的 format12 subtable 遍历所有 group,收集 gid 在范围内的 codepoint。 * format12 表布局见 gsub-probe.ts 的 lookupFormat12 注释。 */ -function collectFormat12(dv: DataView, subOff: number, cps: Set): void { +function collectFormat12(dv: DataView, subOff: number, cps: Set, 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): 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= 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; } diff --git a/scripts/lint-font-config.ts b/scripts/lint-font-config.ts index 6995d6c..a65894e 100644 --- a/scripts/lint-font-config.ts +++ b/scripts/lint-font-config.ts @@ -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" }, ]; diff --git a/src/pages/FontDetail.vue b/src/pages/FontDetail.vue index 0102175..eaeb867 100644 --- a/src/pages/FontDetail.vue +++ b/src/pages/FontDetail.vue @@ -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(() =>

- -
-

- {{ bodyTitle }} -

-

- {{ bodyText }} -

-
-