diff --git a/apps/readest-app/src/app/reader/components/settings/CustomFonts.tsx b/apps/readest-app/src/app/reader/components/settings/CustomFonts.tsx index 7a1e30cf..b610eb28 100644 --- a/apps/readest-app/src/app/reader/components/settings/CustomFonts.tsx +++ b/apps/readest-app/src/app/reader/components/settings/CustomFonts.tsx @@ -74,6 +74,7 @@ const CustomFonts: React.FC = ({ bookKey, onBack }) => { family: fontInfo.family, style: fontInfo.style, weight: fontInfo.weight, + variable: fontInfo.variable, }); console.log('Added custom font:', customFont); if (customFont && !customFont.error) { diff --git a/apps/readest-app/src/store/customFontStore.ts b/apps/readest-app/src/store/customFontStore.ts index 49c148a9..171cdb0d 100644 --- a/apps/readest-app/src/store/customFontStore.ts +++ b/apps/readest-app/src/store/customFontStore.ts @@ -8,10 +8,7 @@ interface FontStoreState { loading: boolean; setFonts: (fonts: CustomFont[]) => void; - addFont: ( - path: string, - options?: { name?: string; family?: string; style?: string; weight?: number }, - ) => CustomFont; + addFont: (path: string, options?: Partial>) => CustomFont; removeFont: (id: string) => boolean; updateFont: (id: string, updates: Partial) => boolean; getFont: (id: string) => CustomFont | undefined; diff --git a/apps/readest-app/src/styles/fonts.ts b/apps/readest-app/src/styles/fonts.ts index ac2cee03..9db45041 100644 --- a/apps/readest-app/src/styles/fonts.ts +++ b/apps/readest-app/src/styles/fonts.ts @@ -120,10 +120,12 @@ export type FontStyle = 'normal' | 'italic' | 'oblique'; export interface CustomFont { id: string; name: string; + path: string; family?: string; style?: string; weight?: number; - path: string; + variable?: boolean; + downloadedAt?: number; deletedAt?: number; @@ -177,6 +179,7 @@ export function createFontCSS(font: CustomFont): string { const fontFamily = createFontFamily(font.family || font.name); const fontStyle = font.style || 'normal'; const fontWeight = font.weight || 400; + const variable = font.variable || false; if (!font.blobUrl) { throw new Error(`Blob URL not available for font: ${font.name}`); } @@ -184,8 +187,8 @@ export function createFontCSS(font: CustomFont): string { const css = ` @font-face { font-family: "${fontFamily}"; - font-style: ${fontStyle}; - font-weight: ${fontWeight}; + ${variable ? '' : `font-style: ${fontStyle};`} + ${variable ? '' : `font-weight: ${fontWeight};`} src: url("${font.blobUrl}") format("${cssFormat}"); font-display: swap; } @@ -196,12 +199,7 @@ export function createFontCSS(font: CustomFont): string { export function createCustomFont( path: string, - options?: { - name?: string; - family?: string; - style?: string; - weight?: number; - }, + options?: Partial>, ): CustomFont { const name = options?.name || getFontName(path); return { @@ -210,6 +208,7 @@ export function createCustomFont( family: options?.family, style: options?.style, weight: options?.weight, + variable: options?.variable, path, }; } diff --git a/apps/readest-app/src/utils/font.ts b/apps/readest-app/src/utils/font.ts index 67b16bc0..cb6cd29a 100644 --- a/apps/readest-app/src/utils/font.ts +++ b/apps/readest-app/src/utils/font.ts @@ -88,6 +88,59 @@ function parseOS2Selection(dataView: DataView, os2TableOffset: number): number { return dataView.getUint16(os2TableOffset + 62, false); } +interface VariableFontAxis { + tag: string; + minValue: number; + defaultValue: number; + maxValue: number; + name?: string; +} + +function parseVariableFontAxes(dataView: DataView, fvarTableOffset: number): VariableFontAxis[] { + try { + // fvar table structure: + // version (4 bytes) + axisCount (2 bytes) + axisSize (2 bytes) + instanceCount (2 bytes) + instanceSize (2 bytes) + const axisCount = dataView.getUint16(fvarTableOffset + 4, false); + const axisSize = dataView.getUint16(fvarTableOffset + 6, false); + + const axes: VariableFontAxis[] = []; + + // Each axis record starts at offset 16 from table start + let axisOffset = fvarTableOffset + 16; + + for (let i = 0; i < axisCount; i++) { + // Axis record structure: + // axisTag (4 bytes) + minValue (4 bytes) + defaultValue (4 bytes) + maxValue (4 bytes) + flags (2 bytes) + axisNameID (2 bytes) + + const tag = String.fromCharCode( + dataView.getUint8(axisOffset), + dataView.getUint8(axisOffset + 1), + dataView.getUint8(axisOffset + 2), + dataView.getUint8(axisOffset + 3), + ); + + // Fixed-point values (16.16 format) + const minValue = dataView.getInt32(axisOffset + 4, false) / 65536; + const defaultValue = dataView.getInt32(axisOffset + 8, false) / 65536; + const maxValue = dataView.getInt32(axisOffset + 12, false) / 65536; + + axes.push({ + tag, + minValue, + defaultValue, + maxValue, + }); + + axisOffset += axisSize; + } + + return axes; + } catch (error) { + console.warn('Failed to parse fvar table:', error); + return []; + } +} + function weightClassToCSSWeight(weightClass: number): number { // Map OpenType weight class to CSS weight if (weightClass >= 1 && weightClass <= 100) return 100; @@ -164,6 +217,7 @@ export const parseFontInfo = (fontData: ArrayBuffer, filename: string) => { const numTables = dataView.getUint16(4, false); let nameTableOffset = 0; let os2TableOffset = 0; + let fvarTableOffset = 0; for (let i = 0; i < numTables; i++) { const tableOffset = 12 + i * 16; const tag = String.fromCharCode( @@ -177,6 +231,8 @@ export const parseFontInfo = (fontData: ArrayBuffer, filename: string) => { nameTableOffset = dataView.getUint32(tableOffset + 8, false); } else if (tag === 'OS/2') { os2TableOffset = dataView.getUint32(tableOffset + 8, false); + } else if (tag === 'fvar') { + fvarTableOffset = dataView.getUint32(tableOffset + 8, false); } } @@ -263,6 +319,14 @@ export const parseFontInfo = (fontData: ArrayBuffer, filename: string) => { } } + let isVariable = false; + if (fvarTableOffset > 0) { + const axes = parseVariableFontAxes(dataView, fvarTableOffset); + if (axes && axes.length > 0) { + isVariable = true; + } + } + // If OS/2 table weight is default (400) or unavailable, try to infer from style name if (fontWeight === 400 && styleName) { const inferredWeight = inferWeightFromStyleName(styleName); @@ -281,6 +345,7 @@ export const parseFontInfo = (fontData: ArrayBuffer, filename: string) => { family: familyName, weight: fontWeight, style: fontStyle, + variable: isVariable, }; } catch (error) { console.warn(`Failed to parse font: ${error}`); @@ -289,6 +354,7 @@ export const parseFontInfo = (fontData: ArrayBuffer, filename: string) => { family: fallbackName, weight: 400, style: 'normal' as FontStyle, + variable: false, }; } };