🚀 Images in Reader

* Added Images in Reader
* Alternative(caption) text below Image

Resolves: #69
This commit is contained in:
Acclorite 2025-01-07 20:04:26 +02:00
parent a407e54c56
commit c9832c3183
7 changed files with 147 additions and 10 deletions

View file

@ -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<ReaderText> {
suspend fun Document.parseDocument(
zipFile: ZipFile? = null,
imageEntries: List<ZipEntry>? = null,
includeChapter: Boolean = true
): List<ReaderText> {
yield()
val readerText = mutableListOf<ReaderText>()
@ -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 (

View file

@ -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<ZipEntry>,
imageEntries: List<ZipEntry>,
chapterTitleEntries: Map<Title, List<String>>?
): List<ReaderText> {
@ -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<ZipEntry>,
chapterTitleMap: Map<Title, List<String>>?
) {
// 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

View file

@ -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()
}

View file

@ -9,4 +9,10 @@ fun Constants.provideExtensions() = listOf(
".html",
".htm",
".md"
)
fun Constants.provideImageExtensions() = listOf(
".png",
".jpg",
".jpeg"
)

View file

@ -156,7 +156,7 @@ fun ReaderLayout(
ReaderLayoutText(
activity = activity,
showMenu = showMenu,
textEntry = textEntry,
entry = textEntry,
fontFamily = fontFamily,
fontColor = fontColor,
lineHeight = lineHeight,

View file

@ -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,

View file

@ -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
)
}