Initial commit
This commit is contained in:
commit
6072b2ba29
844 changed files with 220532 additions and 0 deletions
143
app/src/main/java/com/aryan/reader/paginatedreader/FontLoader.kt
Normal file
143
app/src/main/java/com/aryan/reader/paginatedreader/FontLoader.kt
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// FontLoader.kt
|
||||
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
|
||||
|
||||
/**
|
||||
* A centralized mapper for handling conversions between generic CSS font family names
|
||||
* and Compose's FontFamily objects.
|
||||
*/
|
||||
object FontFamilyMapper {
|
||||
private val genericFontMap = mapOf(
|
||||
"serif" to FontFamily.Serif,
|
||||
"sans-serif" to FontFamily.SansSerif,
|
||||
"monospace" to FontFamily.Monospace,
|
||||
"cursive" to FontFamily.Cursive,
|
||||
"default" to FontFamily.Default,
|
||||
"system-ui" to FontFamily.Default,
|
||||
"ui-sans-serif" to FontFamily.Default,
|
||||
"ui-serif" to FontFamily.Default,
|
||||
"ui-monospace" to FontFamily.Default,
|
||||
"ui-rounded" to FontFamily.Default
|
||||
)
|
||||
|
||||
/**
|
||||
* Converts a string name (e.g., "serif") to a Compose [FontFamily].
|
||||
*/
|
||||
fun nameToFontFamily(name: String): FontFamily? {
|
||||
return genericFontMap[name.trim().lowercase()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Compose [FontFamily] back to its primary string name for serialization.
|
||||
* Custom fonts are not serialized by name and will return null.
|
||||
*/
|
||||
fun fontFamilyToName(fontFamily: FontFamily): String? {
|
||||
return when (fontFamily) {
|
||||
FontFamily.Serif -> "serif"
|
||||
FontFamily.SansSerif -> "sans-serif"
|
||||
FontFamily.Monospace -> "monospace"
|
||||
FontFamily.Cursive -> "cursive"
|
||||
FontFamily.Default -> "default"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<FontFaceInfo>, extractionPath: String): Map<String, FontFamily> {
|
||||
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!! }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue