🚀 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")
|
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
|
||||||
|
|
||||||
// Epub parser
|
// Epub parser
|
||||||
implementation("com.positiondev.epublib:epublib-core:3.1") {
|
|
||||||
exclude("xmlpull")
|
|
||||||
}
|
|
||||||
implementation("org.jsoup:jsoup:1.18.1")
|
implementation("org.jsoup:jsoup:1.18.1")
|
||||||
|
|
||||||
// Fb2 parser
|
// Fb2 parser
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
package ua.acclorite.book_story.data.parser.epub
|
package ua.acclorite.book_story.data.parser.epub
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
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.data.parser.FileParser
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.domain.util.CoverImage
|
import ua.acclorite.book_story.domain.util.CoverImage
|
||||||
import ua.acclorite.book_story.domain.util.UIText
|
import ua.acclorite.book_story.domain.util.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.util.zip.ZipFile
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class EpubFileParser @Inject constructor() : FileParser {
|
class EpubFileParser @Inject constructor() : FileParser {
|
||||||
|
|
@ -21,45 +22,83 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val epubReader = EpubReader()
|
var book: Pair<Book, CoverImage?>? = null
|
||||||
val book = withContext(Dispatchers.IO) {
|
|
||||||
FileInputStream(file).use {
|
withContext(Dispatchers.IO) {
|
||||||
epubReader.readEpub(it)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return book
|
||||||
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
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return null
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nl.siegmann.epublib.epub.EpubReader
|
|
||||||
import org.jsoup.Jsoup
|
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.R
|
||||||
import ua.acclorite.book_story.data.parser.TextParser
|
import ua.acclorite.book_story.data.parser.TextParser
|
||||||
import ua.acclorite.book_story.domain.util.Resource
|
import ua.acclorite.book_story.domain.util.Resource
|
||||||
import ua.acclorite.book_story.domain.util.UIText
|
import ua.acclorite.book_story.domain.util.UIText
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.util.zip.ZipFile
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -26,66 +20,41 @@ class EpubTextParser @Inject constructor() : TextParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val book = withContext(Dispatchers.IO) {
|
val lines = mutableListOf<String>()
|
||||||
EpubReader().readEpub(FileInputStream(file))
|
|
||||||
}
|
|
||||||
val unformattedText = StringBuilder()
|
|
||||||
|
|
||||||
for (spineReference in book.spine.spineReferences) {
|
withContext(Dispatchers.IO) {
|
||||||
val resource = spineReference.resource
|
ZipFile(file).use { zip ->
|
||||||
val inputStream = resource.inputStream
|
zip.entries().asSequence().forEach { entry ->
|
||||||
val reader = BufferedReader(
|
if (
|
||||||
InputStreamReader(
|
entry.name.endsWith(".xhtml")
|
||||||
inputStream,
|
|| entry.name.endsWith(".html")
|
||||||
Charset.forName("UTF-8")
|
|| entry.name.endsWith(".xml")
|
||||||
)
|
|| entry.name.endsWith(".htm")
|
||||||
)
|
) {
|
||||||
var line: String?
|
val content = zip.getInputStream(entry).bufferedReader()
|
||||||
|
.use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
val document = Jsoup.parse(content)
|
||||||
inputStream.close()
|
document
|
||||||
}
|
.wholeText()
|
||||||
|
.lines()
|
||||||
while (withContext(Dispatchers.IO) { reader.readLine() }
|
.forEach { element ->
|
||||||
.also { line = it } != null) {
|
if (element.isNotBlank()) {
|
||||||
unformattedText.append(line).append("\n")
|
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.Error(UIText.StringResource(R.string.error_file_empty))
|
||||||
}
|
}
|
||||||
|
|
||||||
return Resource.Success(strings)
|
return Resource.Success(lines)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return Resource.Error(
|
return Resource.Error(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue