Files
readest/apps/readest-app/src/utils/font.ts
T

179 lines
6.0 KiB
TypeScript

import { getUserLang } from './misc';
function parseUnicodeString(dataView: DataView, offset: number, length: number): string {
const chars: string[] = [];
for (let i = 0; i < length; i += 2) {
const charCode = dataView.getUint16(offset + i, false);
if (charCode !== 0) {
chars.push(String.fromCharCode(charCode));
}
}
return chars.join('');
}
function parseMacintoshString(dataView: DataView, offset: number, length: number): string {
const chars: string[] = [];
for (let i = 0; i < length; i++) {
const charCode = dataView.getUint8(offset + i);
chars.push(String.fromCharCode(charCode));
}
return chars.join('');
}
function getLanguagePriority(platformID: number, languageID: number, userLanguage: string): number {
let priority = 0;
// Base priority by platform (Unicode/Microsoft preferred)
if (platformID === 0)
priority += 100; // Unicode
else if (platformID === 3)
priority += 90; // Microsoft
else if (platformID === 1) priority += 50; // Macintosh
// Language-specific priorities
const userLang = userLanguage.toLowerCase();
if (platformID === 0 || platformID === 3) {
if (userLang.startsWith('zh')) {
if (languageID === 0x0804)
priority += 50; // Simplified Chinese
else if (languageID === 0x0404)
priority += 45; // Traditional Chinese
else if (languageID === 0x0c04)
priority += 40; // Traditional Chinese
else if (languageID === 0x1004) priority += 35; // Simplified Chinese
} else if (userLang.startsWith('ja')) {
if (languageID === 0x0411) priority += 50; // Japanese
} else if (userLang.startsWith('ko')) {
if (languageID === 0x0412) priority += 50; // Korean
} else if (userLang.startsWith('en')) {
if (languageID === 0x0409)
priority += 50; // English (US)
else if (languageID === 0x0809) priority += 45; // English (UK)
}
// Fallback: English
if (languageID === 0x0409) priority += 10; // English fallback
} else if (platformID === 1) {
// Macintosh platform language codes
if (userLang.startsWith('zh')) {
if (languageID === 33)
priority += 50; // Chinese (Simplified)
else if (languageID === 19) priority += 45; // Chinese (Traditional)
} else if (userLang.startsWith('ja')) {
if (languageID === 11) priority += 50; // Japanese
} else if (userLang.startsWith('ko')) {
if (languageID === 23) priority += 50; // Korean
} else if (userLang.startsWith('en')) {
if (languageID === 0) priority += 50; // English
}
// Fallback: English
if (languageID === 0) priority += 10; // English fallback
}
return priority;
}
type FontNameType = {
name: string;
platformID: number;
languageID: number;
priority: number;
};
export const parseFontName = (fontData: ArrayBuffer, filename: string) => {
const fallbackName = filename.replace(/\.[^/.]+$/, '');
try {
const dataView = new DataView(fontData);
const signature = dataView.getUint32(0, false);
if (signature !== 0x00010000 && signature !== 0x74727565 && signature !== 0x4f54544f) {
throw new Error('Unsupported font format');
}
const numTables = dataView.getUint16(4, false);
let nameTableOffset = 0;
for (let i = 0; i < numTables; i++) {
const tableOffset = 12 + i * 16;
const tag = String.fromCharCode(
dataView.getUint8(tableOffset),
dataView.getUint8(tableOffset + 1),
dataView.getUint8(tableOffset + 2),
dataView.getUint8(tableOffset + 3),
);
if (tag === 'name') {
nameTableOffset = dataView.getUint32(tableOffset + 8, false);
break;
}
}
if (nameTableOffset === 0) {
throw new Error('Name table not found');
}
const count = dataView.getUint16(nameTableOffset + 2, false);
const stringOffset = dataView.getUint16(nameTableOffset + 4, false);
const userLanguage = getUserLang();
const fontFamilyNames: Array<FontNameType> = [];
const fontStyleNames: Array<FontNameType> = [];
for (let i = 0; i < count; i++) {
const recordOffset = nameTableOffset + 6 + i * 12;
const platformID = dataView.getUint16(recordOffset, false);
const languageID = dataView.getUint16(recordOffset + 4, false);
const nameID = dataView.getUint16(recordOffset + 6, false);
const nameLength = dataView.getUint16(recordOffset + 8, false);
const nameOffsetInTable = dataView.getUint16(recordOffset + 10, false);
// nameID 1 = Font Family name, nameID 2 = Font Subfamily name (style)
if (nameID === 1 || nameID === 2) {
const stringStart = nameTableOffset + stringOffset + nameOffsetInTable;
let fontName = '';
if (platformID === 0 || platformID === 3) {
// Unicode/Microsoft platform
fontName = parseUnicodeString(dataView, stringStart, nameLength);
} else if (platformID === 1) {
// Macintosh platform
fontName = parseMacintoshString(dataView, stringStart, nameLength);
}
if (fontName && fontName.trim()) {
const priority = getLanguagePriority(platformID, languageID, userLanguage);
const nameEntry = {
name: fontName.trim(),
platformID,
languageID,
priority,
};
if (nameID === 1) {
fontFamilyNames.push(nameEntry);
} else if (nameID === 2) {
fontStyleNames.push(nameEntry);
}
}
}
}
if (fontFamilyNames.length === 0) {
throw new Error('Font family name not found');
}
fontFamilyNames.sort((a, b) => b.priority - a.priority);
fontStyleNames.sort((a, b) => b.priority - a.priority);
const familyName = fontFamilyNames[0]!.name;
const styleName = fontStyleNames.length > 0 ? fontStyleNames[0]!.name : '';
return {
name: styleName ? `${familyName} ${styleName}` : familyName,
family: familyName,
style: styleName,
};
} catch (error) {
console.warn(`Failed to parse font: ${error}`);
return {
name: fallbackName,
family: fallbackName,
style: '',
};
}
};