/* * Episteme Reader - A native Android document reader. * Copyright (C) 2026 Episteme * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * mail: epistemereader@gmail.com */ package com.aryan.reader.paginatedreader import timber.log.Timber import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import java.io.File import java.security.MessageDigest private fun getCacheKeyForFont(bookId: String, fontPath: String): String { val identifier = "$bookId:$fontPath" val digest = MessageDigest.getInstance("MD5").digest(identifier.toByteArray()) return digest.joinToString("") { "%02x".format(it) } + ".ttf" } /** * Loads custom font faces defined in the EPUB's CSS into a map of [FontFamily] objects. * It handles WOFF2 fonts by converting them to TTF and storing them in a global, persistent cache. */ fun loadFontFamilies(fontFaces: List, extractionPath: String): Map { if (fontFaces.isEmpty()) { return emptyMap() } Timber.d("Loading ${fontFaces.size} font faces from extraction path: $extractionPath") // 1. Define a stable, global font cache directory. // This assumes the parent of the extraction path is a stable base directory for epubs. val baseCacheDir = File(extractionPath).parentFile ?: return emptyMap() val fontCacheDir = File(baseCacheDir, "font_cache") if (!fontCacheDir.exists()) { fontCacheDir.mkdirs() } // 2. Get a stable identifier for the book from the extraction path. // e.g., "d0e205bf-65cc-4ab4-93cc-cd2d613a7bb3.epub" from a longer temp path. val bookId = File(extractionPath).name.substringBeforeLast("_") val fontsByFamily = fontFaces.groupBy { it.fontFamily.trim().removeSurrounding("'").removeSurrounding("\"").lowercase() } Timber.d("Grouped font faces by normalized family: ${fontsByFamily.keys}") return fontsByFamily.mapValues { (familyName, fontInfos) -> val fontList = fontInfos.mapNotNull { fontInfo -> try { Timber.d("Attempting to load font '$familyName' from resolved src path: '${fontInfo.src}'") var fontFile = File(extractionPath, fontInfo.src) if (!fontFile.exists()) { Timber.w("Font file not found at: ${fontFile.absolutePath}") return@mapNotNull null } // Handle WOFF2 conversion and global caching if (fontFile.extension.equals("woff2", ignoreCase = true)) { // 3. Generate a unique, deterministic cache key for the font. val cacheKey = getCacheKeyForFont(bookId, fontInfo.src) val cachedTtfFile = File(fontCacheDir, cacheKey) if (cachedTtfFile.exists()) { // Use the globally cached TTF file if it exists fontFile = cachedTtfFile Timber.d("Using globally cached TTF for '${fontInfo.src}'") } else { // Convert and save the TTF to the global cache if it doesn't exist Timber.d("Converting woff2 font: ${fontFile.name}") val woff2Data = fontFile.readBytes() val ttfData = Woff2Converter.convertWoff2ToTtf(woff2Data) if (ttfData != null) { cachedTtfFile.writeBytes(ttfData) fontFile = cachedTtfFile // Use the newly created TTF file Timber.d("Successfully converted and globally cached woff2 as '${cachedTtfFile.name}'") } else { Timber.e("Failed to convert woff2 font: ${fontFile.name}") return@mapNotNull null } } } Font( fontFile, fontInfo.fontWeight ?: FontWeight.Normal, fontInfo.fontStyle ?: FontStyle.Normal ) } catch (e: Exception) { Timber.e(e, "Error loading font: ${fontInfo.src}") null } } if (fontList.isNotEmpty()) { Timber.d("Loaded family '$familyName' with ${fontList.size} font styles.") FontFamily(fontList) } else { Timber.w("Could not load any font styles for family '$familyName'.") null } }.filterValues { it != null }.mapValues { it.value!! } }