🚀 Improve EPUB parser

Replace EpubLib parser with custom that uses ZipFile. The previous parser had multiple problems with reading and parsing books. This should resolve most errors, improve compatibility and provide better user experience.

Resolves: #42
This commit is contained in:
Acclorite 2024-08-12 18:18:23 +03:00
parent 01173a2b72
commit 3b69b89819
3 changed files with 103 additions and 98 deletions

View file

@ -137,9 +137,6 @@ dependencies {
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
// Epub parser
implementation("com.positiondev.epublib:epublib-core:3.1") {
exclude("xmlpull")
}
implementation("org.jsoup:jsoup:1.18.1")
// Fb2 parser

View file

@ -1,16 +1,17 @@
package ua.acclorite.book_story.data.parser.epub
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import nl.siegmann.epublib.epub.EpubReader
import org.jsoup.Jsoup
import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText
import java.io.File
import java.io.FileInputStream
import java.util.zip.ZipFile
import javax.inject.Inject
class EpubFileParser @Inject constructor() : FileParser {
@ -21,45 +22,83 @@ class EpubFileParser @Inject constructor() : FileParser {
}
try {
val epubReader = EpubReader()
val book = withContext(Dispatchers.IO) {
FileInputStream(file).use {
epubReader.readEpub(it)
var book: Pair<Book, CoverImage?>? = null
withContext(Dispatchers.IO) {
ZipFile(file).use { zip ->
val opfEntry = zip.entries().asSequence().find { entry ->
entry.name.endsWith(".opf")
} ?: return@withContext
val opfContent = zip
.getInputStream(opfEntry)
.bufferedReader()
.use { it.readText() }
val document = Jsoup.parse(opfContent)
val title = document.select("metadata > dc|title").text().trim()
val author = UIText.StringValue(
document.select("metadata > dc|creator").text().trim()
)
val description = Jsoup.parse(
document.select("metadata > dc|description").text()
).text()
var coverImagePath: String? = null
val coverId = document
.select("metadata > meta[name=cover]")
.attr("content")
if (coverId.isNotBlank()) {
coverImagePath = document
.select("manifest > item[id=$coverId]")
.attr("href")
}
if (coverImagePath.isNullOrBlank()) {
coverImagePath = document
.select("manifest > item[media-type*=image]")
.firstOrNull()?.attr("href")
}
if (coverImagePath == null) {
return@withContext
}
val coverImage = extractCoverImageBitmap(file, coverImagePath)
book = Book(
title = title,
author = author,
description = description,
textPath = "",
scrollIndex = 0,
scrollOffset = 0,
progress = 0f,
filePath = file.path,
lastOpened = null,
category = Category.entries[0],
coverImage = null
) to coverImage
}
}
val metadata = book.metadata
val author = UIText.StringValue(metadata.authors.joinToString(", "))
val title = metadata.titles.joinToString(", ")
val coverImage = book.coverImage?.let { BitmapFactory.decodeStream(it.inputStream) }
var description: StringBuilder? = StringBuilder()
metadata.descriptions.forEach {
description?.append(it)?.append(" ")
}
if (description != null) {
if (description.isBlank()) {
description = null
}
}
return Book(
title = title,
author = author,
description = description?.toString(),
textPath = "",
scrollIndex = 0,
scrollOffset = 0,
progress = 0f,
filePath = file.path,
lastOpened = null,
category = Category.entries[0],
coverImage = null
) to coverImage
return book
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}
private fun extractCoverImageBitmap(file: File, coverImagePath: String): Bitmap? {
ZipFile(file).use { zip ->
zip.entries().asSequence().forEach { entry ->
if (entry.name.endsWith(coverImagePath)) {
val imageBytes = zip.getInputStream(entry).readBytes()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
}
}
return null
}

View file

@ -2,19 +2,13 @@ package ua.acclorite.book_story.data.parser.epub
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import nl.siegmann.epublib.epub.EpubReader
import org.jsoup.Jsoup
import org.jsoup.nodes.Document.OutputSettings
import org.jsoup.safety.Safelist
import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
import java.io.BufferedReader
import java.io.File
import java.io.FileInputStream
import java.io.InputStreamReader
import java.nio.charset.Charset
import java.util.zip.ZipFile
import javax.inject.Inject
@ -26,66 +20,41 @@ class EpubTextParser @Inject constructor() : TextParser {
}
try {
val book = withContext(Dispatchers.IO) {
EpubReader().readEpub(FileInputStream(file))
}
val unformattedText = StringBuilder()
val lines = mutableListOf<String>()
for (spineReference in book.spine.spineReferences) {
val resource = spineReference.resource
val inputStream = resource.inputStream
val reader = BufferedReader(
InputStreamReader(
inputStream,
Charset.forName("UTF-8")
)
)
var line: String?
withContext(Dispatchers.IO) {
ZipFile(file).use { zip ->
zip.entries().asSequence().forEach { entry ->
if (
entry.name.endsWith(".xhtml")
|| entry.name.endsWith(".html")
|| entry.name.endsWith(".xml")
|| entry.name.endsWith(".htm")
) {
val content = zip.getInputStream(entry).bufferedReader()
.use {
it.readText()
}
withContext(Dispatchers.IO) {
inputStream.close()
}
while (withContext(Dispatchers.IO) { reader.readLine() }
.also { line = it } != null) {
unformattedText.append(line).append("\n")
}
withContext(Dispatchers.IO) {
reader.close()
}
}
val strings = mutableListOf<String>()
val parsedText = Jsoup.parse(unformattedText.toString())
parsedText.outputSettings(OutputSettings().prettyPrint(false))
parsedText.select("br").append("\n")
parsedText.select("p").prepend("\n")
parsedText.select("em").append(" ").prepend("")
val formattedText = Jsoup.clean(
parsedText.html(),
"",
Safelist.none(),
OutputSettings().prettyPrint(false)
)
formattedText
.replace("&nbsp;", "")
.replace("\u00a0", "")
.split("\n")
.forEach {
if (it.isNotBlank()) {
strings.add(it.trim())
val document = Jsoup.parse(content)
document
.wholeText()
.lines()
.forEach { element ->
if (element.isNotBlank()) {
lines.add(element.trim())
}
}
}
}
}
}
if (strings.isEmpty()) {
if (lines.isEmpty()) {
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
}
return Resource.Success(strings)
return Resource.Success(lines)
} catch (e: Exception) {
e.printStackTrace()
return Resource.Error(