From 3b69b89819710c4e5e67cf8571065d2f628a7aca Mon Sep 17 00:00:00 2001 From: Acclorite <140836141+Acclorite@users.noreply.github.com> Date: Mon, 12 Aug 2024 18:18:23 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Improve=20EPUB=20parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/build.gradle.kts | 3 - .../data/parser/epub/EpubFileParser.kt | 111 ++++++++++++------ .../data/parser/epub/EpubTextParser.kt | 87 +++++--------- 3 files changed, 103 insertions(+), 98 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5f53aad4..c72bc2f5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt index 1c051501..8d2d5201 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt @@ -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? = 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 } \ No newline at end of file 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 3930ddd4..3f8290d2 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 @@ -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() - 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() - - 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()) + 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(