feat: 完成 DOCX 视觉门禁与字体兼容映射
This commit is contained in:
@@ -10,6 +10,7 @@ export const MAXIMUM_DOCX_FONT_SFNT_BYTES = 16 * 1024 * 1024;
|
||||
export const MAXIMUM_DOCX_FONT_TOTAL_SFNT_BYTES =
|
||||
48 * 1024 * 1024;
|
||||
const MAXIMUM_FONT_CONVERSION_CACHE_ENTRIES = 8;
|
||||
const SFNT_CHECKSUM_MAGIC = 0xb1b0afba;
|
||||
const fontConversionCache = new Map<
|
||||
string,
|
||||
Promise<Uint8Array>
|
||||
@@ -42,6 +43,21 @@ export interface SfntFontMetadata {
|
||||
subfamily: string;
|
||||
postscriptName?: string;
|
||||
weightClass: number;
|
||||
panose: readonly [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number
|
||||
];
|
||||
unicodeRanges: readonly [number, number, number, number];
|
||||
codePageRanges: readonly [number, number];
|
||||
fixedPitch: boolean;
|
||||
fsType: number;
|
||||
permission: DocxFontEmbeddingPermission;
|
||||
noSubsetting: boolean;
|
||||
@@ -64,6 +80,7 @@ export interface ObfuscatedDocxFont {
|
||||
interface SfntTable {
|
||||
offset: number;
|
||||
length: number;
|
||||
recordOffset: number;
|
||||
}
|
||||
|
||||
function readTag(content: Uint8Array, offset: number) {
|
||||
@@ -160,7 +177,7 @@ function readSfntTables(content: Uint8Array) {
|
||||
if (tables.has(tag)) {
|
||||
throw new Error(`SFNT 字体包含重复表:${tag}`);
|
||||
}
|
||||
tables.set(tag, { offset, length });
|
||||
tables.set(tag, { offset, length, recordOffset });
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -170,6 +187,80 @@ function readSfntTables(content: Uint8Array) {
|
||||
};
|
||||
}
|
||||
|
||||
function calculateSfntChecksum(
|
||||
content: Uint8Array,
|
||||
offset = 0,
|
||||
length = content.byteLength
|
||||
) {
|
||||
assertRange(content, offset, length, "校验和");
|
||||
let checksum = 0;
|
||||
const paddedLength = Math.ceil(length / 4) * 4;
|
||||
for (
|
||||
let relativeOffset = 0;
|
||||
relativeOffset < paddedLength;
|
||||
relativeOffset += 4
|
||||
) {
|
||||
let word = 0;
|
||||
for (let byteIndex = 0; byteIndex < 4; byteIndex += 1) {
|
||||
const index = relativeOffset + byteIndex;
|
||||
word =
|
||||
(word << 8) |
|
||||
(index < length ? (content[offset + index] ?? 0) : 0);
|
||||
}
|
||||
checksum = (checksum + (word >>> 0)) >>> 0;
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
function normalizeSfntWeightClass(
|
||||
content: Uint8Array,
|
||||
weightClass: number
|
||||
) {
|
||||
if (
|
||||
!Number.isInteger(weightClass) ||
|
||||
weightClass < 1 ||
|
||||
weightClass > 1000
|
||||
) {
|
||||
throw new Error("字体声明权重必须位于 1 到 1000 之间");
|
||||
}
|
||||
const { tables, view } = readSfntTables(content);
|
||||
const os2 = tables.get("OS/2");
|
||||
const head = tables.get("head");
|
||||
if (!os2 || os2.length < 6) {
|
||||
throw new Error("SFNT 字体缺少可写入权重的 OS/2 表");
|
||||
}
|
||||
if (!head || head.length < 12) {
|
||||
throw new Error("SFNT 字体缺少完整 head 表");
|
||||
}
|
||||
if (view.getUint16(os2.offset + 4, false) === weightClass) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.setUint16(os2.offset + 4, weightClass, false);
|
||||
view.setUint32(
|
||||
os2.recordOffset + 4,
|
||||
calculateSfntChecksum(content, os2.offset, os2.length),
|
||||
false
|
||||
);
|
||||
|
||||
const adjustmentOffset = head.offset + 8;
|
||||
view.setUint32(adjustmentOffset, 0, false);
|
||||
view.setUint32(
|
||||
head.recordOffset + 4,
|
||||
calculateSfntChecksum(content, head.offset, head.length),
|
||||
false
|
||||
);
|
||||
const checksum = calculateSfntChecksum(content);
|
||||
view.setUint32(
|
||||
adjustmentOffset,
|
||||
(SFNT_CHECKSUM_MAGIC - checksum) >>> 0,
|
||||
false
|
||||
);
|
||||
if (calculateSfntChecksum(content) !== SFNT_CHECKSUM_MAGIC) {
|
||||
throw new Error("SFNT 字体元数据规范化后校验和无效");
|
||||
}
|
||||
}
|
||||
|
||||
function decodeUtf16Be(content: Uint8Array) {
|
||||
if (content.byteLength % 2 !== 0) {
|
||||
throw new Error("字体名称 UTF-16BE 长度无效");
|
||||
@@ -334,12 +425,16 @@ export function readSfntFontMetadata(
|
||||
const { signature, tables, view } = readSfntTables(content);
|
||||
const os2 = tables.get("OS/2");
|
||||
const name = tables.get("name");
|
||||
if (!os2 || os2.length < 10) {
|
||||
const post = tables.get("post");
|
||||
if (!os2 || os2.length < 68) {
|
||||
throw new Error("SFNT 字体缺少完整 OS/2 表");
|
||||
}
|
||||
if (!name) {
|
||||
throw new Error("SFNT 字体缺少 name 表");
|
||||
}
|
||||
if (post && post.length < 16) {
|
||||
throw new Error("SFNT 字体 post 表不完整");
|
||||
}
|
||||
|
||||
const family = readFontName(content, name, [16, 1]);
|
||||
const subfamily = readFontName(content, name, [17, 2]);
|
||||
@@ -354,6 +449,52 @@ export function readSfntFontMetadata(
|
||||
);
|
||||
const permission = resolveFontEmbeddingPermission(fsType);
|
||||
const postscriptName = readFontName(content, name, [6]);
|
||||
const panose = Array.from(
|
||||
content.subarray(os2.offset + 32, os2.offset + 42)
|
||||
) as unknown as SfntFontMetadata["panose"];
|
||||
const unicodeRanges = [
|
||||
readUint32(
|
||||
view,
|
||||
content,
|
||||
os2.offset + 42,
|
||||
"OS/2 ulUnicodeRange1"
|
||||
),
|
||||
readUint32(
|
||||
view,
|
||||
content,
|
||||
os2.offset + 46,
|
||||
"OS/2 ulUnicodeRange2"
|
||||
),
|
||||
readUint32(
|
||||
view,
|
||||
content,
|
||||
os2.offset + 50,
|
||||
"OS/2 ulUnicodeRange3"
|
||||
),
|
||||
readUint32(
|
||||
view,
|
||||
content,
|
||||
os2.offset + 54,
|
||||
"OS/2 ulUnicodeRange4"
|
||||
)
|
||||
] as const;
|
||||
const codePageRanges =
|
||||
os2.length >= 86
|
||||
? ([
|
||||
readUint32(
|
||||
view,
|
||||
content,
|
||||
os2.offset + 78,
|
||||
"OS/2 ulCodePageRange1"
|
||||
),
|
||||
readUint32(
|
||||
view,
|
||||
content,
|
||||
os2.offset + 82,
|
||||
"OS/2 ulCodePageRange2"
|
||||
)
|
||||
] as const)
|
||||
: ([0, 0] as const);
|
||||
|
||||
return {
|
||||
family,
|
||||
@@ -365,6 +506,18 @@ export function readSfntFontMetadata(
|
||||
os2.offset + 4,
|
||||
"OS/2 usWeightClass"
|
||||
),
|
||||
panose,
|
||||
unicodeRanges,
|
||||
codePageRanges,
|
||||
fixedPitch:
|
||||
post === undefined
|
||||
? false
|
||||
: readUint32(
|
||||
view,
|
||||
content,
|
||||
post.offset + 12,
|
||||
"post isFixedPitch"
|
||||
) !== 0,
|
||||
fsType,
|
||||
permission,
|
||||
noSubsetting: (fsType & 0x0100) !== 0,
|
||||
@@ -423,7 +576,13 @@ export async function prepareDocxFont(
|
||||
});
|
||||
}
|
||||
const sfnt = (await conversion).slice();
|
||||
normalizeSfntWeightClass(sfnt, source.weight);
|
||||
const metadata = readSfntFontMetadata(sfnt);
|
||||
if (metadata.weightClass !== source.weight) {
|
||||
throw new Error(
|
||||
`字体内部权重 ${metadata.weightClass} 与声明权重 ${source.weight} 不一致`
|
||||
);
|
||||
}
|
||||
const fingerprint = createHash("sha256")
|
||||
.update(sfnt)
|
||||
.digest("hex");
|
||||
|
||||
Reference in New Issue
Block a user