From 93f03941b273b36249d909fb20f2ac41f5f18928 Mon Sep 17 00:00:00 2001 From: Aryan Date: Tue, 17 Mar 2026 10:32:47 +0530 Subject: [PATCH] Optimized HTML importing and PDF core management: (#82) - Refactored `SingleFileImporter` to use a streaming approach for HTML imports, improving memory efficiency for large files. - Implemented `writeHtmlChapter` helper to handle segmented HTML processing and chapter generation. - Introduced `PdfiumCoreProvider` to provide a singleton instance of `PdfiumCoreKt`. - Updated `PdfToHtmlGenerator` and `PdfViewerScreen` to use the shared `PdfiumCoreProvider` instance. --- .../aryan/reader/epub/SingleFileImporter.kt | 195 +++++++++++++----- .../aryan/reader/pdf/PdfToHtmlGenerator.kt | 2 +- .../com/aryan/reader/pdf/PdfViewerScreen.kt | 10 +- 3 files changed, 152 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt b/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt index ff29632..bd2ed7c 100644 --- a/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt +++ b/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt @@ -366,68 +366,120 @@ class SingleFileImporter(private val context: Context) { val parseStart = System.currentTimeMillis() Timber.tag("FileOpenPerf").d("[HTML] parseHtml START | file=$originalBookNameHint") - Timber.d("Importing HTML: $originalBookNameHint") + Timber.d("Importing HTML (Streaming): $originalBookNameHint") - val content = inputStream.bufferedReader().use { it.readText() } - val doc = Jsoup.parse(content) - val title = doc.title().takeIf { it.isNotBlank() } ?: originalBookNameHint.substringBeforeLast(".") - Timber.tag("FileOpenPerf").d("[HTML] parseHtml: Read ${content.length} chars | elapsed=${System.currentTimeMillis() - parseStart}ms") + var title = originalBookNameHint.substringBeforeLast(".") + var author = "Unknown" + val cssBuilder = java.lang.StringBuilder() + val chapters = mutableListOf() - val author = doc.select("meta[name=author]").attr("content").takeIf { it.isNotBlank() } - ?: doc.select("meta[property=article:author]").attr("content").takeIf { it.isNotBlank() } + inputStream.bufferedReader().use { reader -> + var inStyle = false + var inBody = false + var pageNum = 1 + val currentChapterBuilder = java.lang.StringBuilder() - val cssStyle = doc.select("style").html() - val bodyHtml = doc.body().html() + var line: String? + while (reader.readLine().also { line = it } != null) { + val trimmed = line!!.trim() - val rawChapters = if (bodyHtml.contains("")) { - bodyHtml.split("") - } else { - listOf(bodyHtml) + if (!inBody) { + if (trimmed.startsWith("").substringBefore("") + if (t.isNotBlank()) title = t + } + val authorMatch = Regex("]+name=\"author\"[^>]+content=\"([^\"]+)\"").find( + line + ) + ?: Regex("]+property=\"article:author\"[^>]+content=\"([^\"]+)\"").find( + line + ) + if (authorMatch != null) { + author = authorMatch.groupValues[1] + } + + if (trimmed.startsWith("").substringBefore("") + if (styleContent.isNotBlank()) cssBuilder.append(styleContent).append("\n") + if (trimmed.contains("")) { + inStyle = false + } + continue + } + if (inStyle) { + if (trimmed.contains("")) { + cssBuilder.append(line.substringBefore("")).append("\n") + inStyle = false + } else { + cssBuilder.append(line).append("\n") + } + continue + } + + if (trimmed.equals("", ignoreCase = true)) { + inBody = true + continue + } + if (trimmed.startsWith("", "") + if (afterBody.isNotBlank()) currentChapterBuilder.append(afterBody).append("\n") + continue + } + + if (trimmed.startsWith("") || + (trimmed.isNotBlank() && !trimmed.startsWith("<") && !trimmed.startsWith("", ignoreCase = true) || trimmed.equals("", ignoreCase = true)) { + continue + } + + if (line.contains("")) { + val parts = line.split("") + for (i in parts.indices) { + currentChapterBuilder.append(parts[i]).append("\n") + if (i < parts.size - 1) { + val chapterHtml = currentChapterBuilder.toString() + if (chapterHtml.isNotBlank()) { + chapters.add(writeHtmlChapter(extractionDir, bookId, pageNum++, title, cssBuilder.toString(), chapterHtml)) + } + currentChapterBuilder.clear() // Clean memory allocation + } + } + continue + } + + currentChapterBuilder.append(line).append("\n") + + if (currentChapterBuilder.length > 2_000_000) { + chapters.add(writeHtmlChapter(extractionDir, bookId, pageNum++, title, cssBuilder.toString(), currentChapterBuilder.toString())) + currentChapterBuilder.clear() + } + } + } + + val finalChapterHtml = currentChapterBuilder.toString() + if (finalChapterHtml.isNotBlank()) { + chapters.add(writeHtmlChapter(extractionDir, bookId, pageNum++, title, cssBuilder.toString(), finalChapterHtml)) + } } - val chapters = rawChapters.mapIndexed { index, rawText -> - async(Dispatchers.Default) { - if (rawText.isBlank()) return@async null + if (chapters.isEmpty()) { + chapters.add(writeHtmlChapter(extractionDir, bookId, 1, title, cssBuilder.toString(), "

(Empty File)

")) + } - val pageNum = index + 1 - val chapterTitle = if (rawChapters.size > 1) "Page $pageNum" else title - val fileName = "page_$pageNum.html" - val file = File(extractionDir, fileName) - - val fullHtml = """ - - - - ${title.replace("\"", """)} - - - - ${rawText.trim()} - - - """.trimIndent() - - file.writeText(fullHtml) - - EpubChapter( - chapterId = "${bookId}_$pageNum", - absPath = fileName, - title = chapterTitle, - htmlFilePath = fileName, - plainTextContent = Jsoup.parse(fullHtml).text(), - htmlContent = "", - depth = 0, - isInToc = true - ) - } - }.awaitAll().filterNotNull() - - Timber.tag("FileOpenPerf").d("[HTML] parseHtml COMPLETE | elapsed=${System.currentTimeMillis() - parseStart}ms") + Timber.tag("FileOpenPerf").d("[HTML] parseHtml COMPLETE | chapters=${chapters.size} | elapsed=${System.currentTimeMillis() - parseStart}ms") val book = EpubBook( fileName = originalBookNameHint, title = title, - author = author ?: "Unknown", + author = author, language = "en", coverImage = null, chapters = chapters, @@ -446,4 +498,45 @@ class SingleFileImporter(private val context: Context) { return@withContext book } + + private fun writeHtmlChapter( + extractionDir: File, + bookId: String, + pageNum: Int, + title: String, + cssStyle: String, + bodyContent: String + ): EpubChapter { + val chapterTitle = if (pageNum > 1 || bodyContent.contains(" + + + ${title.replace("\"", """)} + + + + ${bodyContent.trim()} + + + """.trimIndent() + + file.writeText(fullHtml) + + val plainText = Jsoup.parse(fullHtml).text() + + return EpubChapter( + chapterId = "${bookId}_$pageNum", + absPath = fileName, + title = chapterTitle, + htmlFilePath = fileName, + plainTextContent = plainText, + htmlContent = "", + depth = 0, + isInToc = true + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfToHtmlGenerator.kt b/app/src/main/java/com/aryan/reader/pdf/PdfToHtmlGenerator.kt index 353a678..d556cff 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfToHtmlGenerator.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfToHtmlGenerator.kt @@ -29,7 +29,7 @@ object PdfToHtmlGenerator { val t0 = System.currentTimeMillis() Timber.tag(TAG).d("generateHtmlFile START | uri=$pdfUri | startPage=$startPage") - val pdfiumCore = PdfiumCoreKt(Dispatchers.Default) + val pdfiumCore = PdfiumCoreProvider.core val pfd = context.contentResolver.openFileDescriptor(pdfUri, "r") ?: run { Timber.tag(TAG).e("Failed to open ParcelFileDescriptor") return@withContext false diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt index f177548..3e1e3dc 100644 --- a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt +++ b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt @@ -344,6 +344,12 @@ private const val PREF_EXTERNAL_DICT_PKG = "external_dictionary_package" private const val PREF_EXTERNAL_TRANSLATE_PKG = "external_translate_package" private const val PREF_EXTERNAL_SEARCH_PKG = "external_search_package" +object PdfiumCoreProvider { + val core: PdfiumCoreKt by lazy { + PdfiumCoreKt(Dispatchers.Default) + } +} + private fun loadUseOnlineDict(context: Context): Boolean { @Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss") return false val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE) @@ -1426,7 +1432,7 @@ fun PdfViewerScreen( var selectedTextBoxId by rememberSaveable { mutableStateOf(null) } val userHighlights = remember { mutableStateListOf() } val drawingState = remember { PdfDrawingState() } - val pdfiumCore = remember(context) { PdfiumCoreKt(Dispatchers.Default) } + val pdfiumCore = remember { PdfiumCoreProvider.core } val verticalReaderState = rememberVerticalPdfReaderState() var virtualPages by remember { mutableStateOf>(emptyList()) } val totalDisplayPages by remember(virtualPages, totalPages) { @@ -2331,8 +2337,6 @@ fun PdfViewerScreen( onToggleBookmark(currentPage) } - LaunchedEffect(pdfUri) { debugPdfLinks(context, pdfUri, pdfiumCore, this) } - LaunchedEffect(currentBookId) { if (currentBookId != null) { val loaded = annotationRepository.loadAnnotations(currentBookId!!)