🚀 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:
parent
01173a2b72
commit
3b69b89819
3 changed files with 103 additions and 98 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,33 +22,55 @@ 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")
|
||||
}
|
||||
|
||||
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 (coverImagePath.isNullOrBlank()) {
|
||||
coverImagePath = document
|
||||
.select("manifest > item[media-type*=image]")
|
||||
.firstOrNull()?.attr("href")
|
||||
}
|
||||
|
||||
if (description != null) {
|
||||
if (description.isBlank()) {
|
||||
description = null
|
||||
}
|
||||
if (coverImagePath == null) {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
return Book(
|
||||
val coverImage = extractCoverImageBitmap(file, coverImagePath)
|
||||
|
||||
book = Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = description?.toString(),
|
||||
description = description,
|
||||
textPath = "",
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
|
|
@ -57,9 +80,25 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
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?
|
||||
val lines = mutableListOf<String>()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
inputStream.close()
|
||||
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()
|
||||
}
|
||||
|
||||
while (withContext(Dispatchers.IO) { reader.readLine() }
|
||||
.also { line = it } != null) {
|
||||
unformattedText.append(line).append("\n")
|
||||
val document = Jsoup.parse(content)
|
||||
document
|
||||
.wholeText()
|
||||
.lines()
|
||||
.forEach { element ->
|
||||
if (element.isNotBlank()) {
|
||||
lines.add(element.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(" ", "")
|
||||
.replace("\u00a0", "")
|
||||
.split("\n")
|
||||
.forEach {
|
||||
if (it.isNotBlank()) {
|
||||
strings.add(it.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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue