From c9832c31836fb2c8138873ca6d916aa8a96df4c0 Mon Sep 17 00:00:00 2001 From: Acclorite <140836141+Acclorite@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:04:26 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Images=20in=20Reader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added Images in Reader * Alternative(caption) text below Image Resolves: #69 --- .../book_story/data/parser/DocumentParser.kt | 81 ++++++++++++++++++- .../data/parser/epub/EpubTextParser.kt | 17 +++- .../book_story/domain/reader/ReaderText.kt | 6 ++ .../constants/SupportedExtensionConstants.kt | 6 ++ .../presentation/reader/ReaderLayout.kt | 2 +- .../presentation/reader/ReaderLayoutText.kt | 15 +++- .../reader/ReaderLayoutTextImage.kt | 30 +++++++ 7 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutTextImage.kt diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/DocumentParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/DocumentParser.kt index fe1ab414..e2f96160 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/DocumentParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/DocumentParser.kt @@ -1,11 +1,16 @@ package ua.acclorite.book_story.data.parser +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.compose.ui.graphics.asImageBitmap import kotlinx.coroutines.yield import org.jsoup.nodes.Document import ua.acclorite.book_story.domain.reader.ReaderText import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown import ua.acclorite.book_story.presentation.core.util.clearMarkdown import ua.acclorite.book_story.presentation.core.util.containsVisibleText +import java.util.zip.ZipEntry +import java.util.zip.ZipFile import javax.inject.Inject class DocumentParser @Inject constructor( @@ -18,7 +23,11 @@ class DocumentParser @Inject constructor( * * @return Parsed text line by line with Markdown(all lines are not blank). */ - suspend fun Document.parseDocument(includeChapter: Boolean = true): List { + suspend fun Document.parseDocument( + zipFile: ZipFile? = null, + imageEntries: List? = null, + includeChapter: Boolean = true + ): List { yield() val readerText = mutableListOf() @@ -53,7 +62,26 @@ class DocumentParser @Inject constructor( element.prepend("[") element.append("]($link)") } - }.wholeText().lines().forEachIndexed { index, line -> + + // Image + select("img").forEach { element -> + val src = element.attr("src") + .trim() + .substringAfterLast("/") + .lowercase() + .takeIf { + it.containsVisibleText() && imageEntries?.any { image -> + it == image.name.substringAfterLast('/').lowercase() + } == true + } ?: return@forEach + + val alt = element.attr("alt").trim().takeIf { + it.clearMarkdown().containsVisibleText() + } ?: src.substringBeforeLast(".") + + element.append("\n[[$src|$alt]]\n") + } + }.wholeText().lines().forEach { line -> yield() val formattedLine = line.replace( @@ -64,9 +92,54 @@ class DocumentParser @Inject constructor( Regex("""_\s*(.*?)\s*_"""), "_$1_" ).trim() + val imageRegex = Regex("""\[\[(.*?)\|(.*?)]]""") + if (line.containsVisibleText()) { - when (line) { - "***", "---" -> readerText.add(ReaderText.Separator) + when { + imageRegex.matches(line) -> { + val trimmedLine = line.removeSurrounding("[[", "]]") + val src = trimmedLine.substringBefore("|") + val alt = "_${trimmedLine.substringAfter("|")}_" + + val image = try { + val imageEntry = imageEntries?.find { image -> + src == image.name.substringAfterLast('/').lowercase() + } ?: return@forEach + + zipFile?.getInputStream(imageEntry)?.use { inputStream -> + val options = BitmapFactory.Options().apply { + inSampleSize = 2 + inPreferredConfig = Bitmap.Config.RGB_565 + } + BitmapFactory.decodeStream( + /* is = */ inputStream, + /* outPadding = */ null, + /* opts = */ options + )?.asImageBitmap() + } + } catch (e: Exception) { + e.printStackTrace() + return@forEach + } + + if (image == null) return@forEach + + // Optimize + image.prepareToDraw() + + readerText.add( // Adding image + ReaderText.Image( + imageBitmap = image + ) + ) + readerText.add( // Adding alternative text (caption) for image + ReaderText.Text( + markdownParser.parse(alt) + ) + ) + } + + line == "---" || line == "***" -> readerText.add(ReaderText.Separator) else -> { if ( diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt index 1be3dcca..63d3d02f 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt @@ -15,6 +15,8 @@ import org.jsoup.Jsoup import ua.acclorite.book_story.data.parser.DocumentParser import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.domain.reader.ReaderText +import ua.acclorite.book_story.presentation.core.constants.Constants +import ua.acclorite.book_story.presentation.core.constants.provideImageExtensions import ua.acclorite.book_story.presentation.core.util.addAll import ua.acclorite.book_story.presentation.core.util.containsVisibleText import java.io.File @@ -50,6 +52,11 @@ class EpubTextParser @Inject constructor( } val chapterEntries = zip.getChapterEntries(opfEntry) + val imageEntries = zip.entries().toList().filter { + Constants.provideImageExtensions().any { format -> + it.name.endsWith(format, ignoreCase = true) + } + } val chapterTitleEntries = zip.getChapterTitleMapFromToc(tocEntry) Log.i(EPUB_TAG, "TOC Entry: ${tocEntry?.name ?: "no toc.ncx"}") @@ -59,6 +66,7 @@ class EpubTextParser @Inject constructor( readerText = zip.parseEpub( chapterEntries = chapterEntries, + imageEntries = imageEntries, chapterTitleEntries = chapterTitleEntries ) } @@ -94,6 +102,7 @@ class EpubTextParser @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private suspend fun ZipFile.parseEpub( chapterEntries: List, + imageEntries: List, chapterTitleEntries: Map>? ): List { @@ -110,6 +119,7 @@ class EpubTextParser @Inject constructor( zip = this@parseEpub, index = index, entry = entry, + imageEntries = imageEntries, chapterTitleMap = chapterTitleEntries ) @@ -143,12 +153,17 @@ class EpubTextParser @Inject constructor( zip: ZipFile, index: Int, entry: ZipEntry, + imageEntries: List, chapterTitleMap: Map>? ) { // Getting all text val content = zip.getInputStream(entry).bufferedReader().use { it.readText() } var readerText = documentParser.run { - Jsoup.parse(content).parseDocument(includeChapter = false) + Jsoup.parse(content).parseDocument( + zipFile = zip, + imageEntries = imageEntries, + includeChapter = false + ) }.toMutableList() // Adding chapter title from TOC if found diff --git a/app/src/main/java/ua/acclorite/book_story/domain/reader/ReaderText.kt b/app/src/main/java/ua/acclorite/book_story/domain/reader/ReaderText.kt index 69be258b..df905342 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/reader/ReaderText.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/reader/ReaderText.kt @@ -1,6 +1,7 @@ package ua.acclorite.book_story.domain.reader import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.text.AnnotatedString @Immutable @@ -13,4 +14,9 @@ sealed class ReaderText { @Immutable data object Separator : ReaderText() + + @Immutable + data class Image( + val imageBitmap: ImageBitmap + ) : ReaderText() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/SupportedExtensionConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/SupportedExtensionConstants.kt index 5dc1056a..117a76ca 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/SupportedExtensionConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/SupportedExtensionConstants.kt @@ -9,4 +9,10 @@ fun Constants.provideExtensions() = listOf( ".html", ".htm", ".md" +) + +fun Constants.provideImageExtensions() = listOf( + ".png", + ".jpg", + ".jpeg" ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayout.kt index ed54e5fc..085c09e4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayout.kt @@ -156,7 +156,7 @@ fun ReaderLayout( ReaderLayoutText( activity = activity, showMenu = showMenu, - textEntry = textEntry, + entry = textEntry, fontFamily = fontFamily, fontColor = fontColor, lineHeight = lineHeight, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutText.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutText.kt index fbd4124c..3c90430b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutText.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutText.kt @@ -18,7 +18,7 @@ import ua.acclorite.book_story.ui.reader.ReaderEvent fun LazyItemScope.ReaderLayoutText( activity: ComponentActivity, showMenu: Boolean, - textEntry: ReaderText, + entry: ReaderText, fontFamily: FontWithName, fontColor: Color, lineHeight: TextUnit, @@ -38,7 +38,14 @@ fun LazyItemScope.ReaderLayoutText( openTranslator: (ReaderEvent.OnOpenTranslator) -> Unit, menuVisibility: (ReaderEvent.OnMenuVisibility) -> Unit ) { - when (textEntry) { + when (entry) { + is ReaderText.Image -> { + ReaderLayoutTextImage( + entry = entry, + sidePadding = sidePadding + ) + } + is ReaderText.Separator -> { ReaderLayoutTextSeparator( sidePadding = sidePadding, @@ -48,7 +55,7 @@ fun LazyItemScope.ReaderLayoutText( is ReaderText.Chapter -> { ReaderLayoutTextChapter( - chapter = textEntry, + chapter = entry, chapterTitleAlignment = chapterTitleAlignment, fontColor = fontColor, sidePadding = sidePadding, @@ -59,7 +66,7 @@ fun LazyItemScope.ReaderLayoutText( is ReaderText.Text -> { ReaderLayoutTextParagraph( - paragraph = textEntry, + paragraph = entry, activity = activity, showMenu = showMenu, fontFamily = fontFamily, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutTextImage.kt b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutTextImage.kt new file mode 100644 index 00000000..34151147 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/reader/ReaderLayoutTextImage.kt @@ -0,0 +1,30 @@ +package ua.acclorite.book_story.presentation.reader + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import ua.acclorite.book_story.domain.reader.ReaderText + +@Composable +fun LazyItemScope.ReaderLayoutTextImage( + entry: ReaderText.Image, + sidePadding: Dp +) { + Image( + modifier = Modifier + .animateItem( + fadeInSpec = null, + fadeOutSpec = null + ) + .padding(horizontal = sidePadding) + .fillMaxWidth(), + bitmap = entry.imageBitmap, + contentDescription = null, + contentScale = ContentScale.FillWidth + ) +} \ No newline at end of file