epub 显示图片,未完善

pull/274/head
gedoor 4 years ago
parent b2b8d45911
commit 6f936c377c
  1. 4
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  2. 13
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  3. 7
      app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt
  4. 46
      app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt
  5. 17
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  6. 39
      app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt
  7. 60
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  8. 46
      app/src/main/java/io/legado/app/ui/book/read/page/ImageProvider.kt
  9. 1
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  10. 2
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  11. 18
      app/src/main/java/io/legado/app/utils/MD5Utils.kt

@ -41,11 +41,11 @@ interface JsExtensions {
return EncoderUtils.base64Encode(str, flags) return EncoderUtils.base64Encode(str, flags)
} }
fun md5Encode(str: String): String? { fun md5Encode(str: String): String {
return MD5Utils.md5Encode(str) return MD5Utils.md5Encode(str)
} }
fun md5Encode16(str: String): String? { fun md5Encode16(str: String): String {
return MD5Utils.md5Encode16(str) return MD5Utils.md5Encode16(str)
} }

@ -49,6 +49,17 @@ object HttpHelper {
return null return null
} }
fun getBytes(url: String): ByteArray? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
return getByteRetrofit(baseUrl)
.create(HttpGetApi::class.java)
.getMapByte(url, mapOf(), mapOf(Pair(AppConst.UA_NAME, AppConst.userAgent)))
.execute()
.body()
}
return null
}
suspend fun simpleGetAsync(url: String, encode: String? = null): String? { suspend fun simpleGetAsync(url: String, encode: String? = null): String? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl -> NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
val response = getApiService<HttpGetApi>(baseUrl, encode) val response = getApiService<HttpGetApi>(baseUrl, encode)
@ -58,7 +69,7 @@ object HttpHelper {
return null return null
} }
suspend fun simpleGetByteAsync(url: String): ByteArray? { suspend fun simpleGetBytesAsync(url: String): ByteArray? {
NetworkUtils.getBaseUrl(url)?.let { baseUrl -> NetworkUtils.getBaseUrl(url)?.let { baseUrl ->
return getByteRetrofit(baseUrl) return getByteRetrofit(baseUrl)
.create(HttpGetApi::class.java) .create(HttpGetApi::class.java)

@ -39,6 +39,13 @@ interface HttpGetApi {
@HeaderMap headers: Map<String, String> @HeaderMap headers: Map<String, String>
): Call<String> ): Call<String>
@GET
fun getMapByte(
@Url url: String,
@QueryMap(encoded = true) queryMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Call<ByteArray>
@GET @GET
suspend fun getMapByteAsync( suspend fun getMapByteAsync(
@Url url: String, @Url url: String,

@ -6,7 +6,10 @@ import android.net.Uri
import android.text.TextUtils import android.text.TextUtils
import io.legado.app.App import io.legado.app.App
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.utils.* import io.legado.app.utils.FileUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.externalFilesDir
import io.legado.app.utils.isContentPath
import nl.siegmann.epublib.domain.Book import nl.siegmann.epublib.domain.Book
import nl.siegmann.epublib.domain.TOCReference import nl.siegmann.epublib.domain.TOCReference
import nl.siegmann.epublib.epub.EpubReader import nl.siegmann.epublib.epub.EpubReader
@ -14,6 +17,7 @@ import org.jsoup.Jsoup
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream
import java.nio.charset.Charset import java.nio.charset.Charset
import java.util.* import java.util.*
@ -21,7 +25,6 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
companion object { companion object {
private var eFile: EPUBFile? = null private var eFile: EPUBFile? = null
private val coverDir = App.INSTANCE.externalFilesDir
@Synchronized @Synchronized
private fun getEFile(book: io.legado.app.data.entities.Book): EPUBFile { private fun getEFile(book: io.legado.app.data.entities.Book): EPUBFile {
@ -41,6 +44,14 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
fun getContent(book: io.legado.app.data.entities.Book, chapter: BookChapter): String? { fun getContent(book: io.legado.app.data.entities.Book, chapter: BookChapter): String? {
return getEFile(book).getContent(chapter) return getEFile(book).getContent(chapter)
} }
@Synchronized
fun getImage(
book: io.legado.app.data.entities.Book,
href: String
): InputStream? {
return getEFile(book).getImage(href)
}
} }
private var epubBook: Book? = null private var epubBook: Book? = null
@ -64,13 +75,12 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
) )
} }
if (!File(book.coverUrl!!).exists()) { if (!File(book.coverUrl!!).exists()) {
epubBook!!.coverImage?.inputStream?.let { epubBook!!.coverImage?.inputStream?.use {
val cover = BitmapFactory.decodeStream(it) val cover = BitmapFactory.decodeStream(it)
val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)) val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
cover.compress(Bitmap.CompressFormat.JPEG, 90, out) cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
out.flush() out.flush()
out.close() out.close()
it.close()
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -81,30 +91,20 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) {
private fun getContent(chapter: BookChapter): String? { private fun getContent(chapter: BookChapter): String? {
epubBook?.let { eBook -> epubBook?.let { eBook ->
val resource = eBook.resources.getByHref(chapter.url) val resource = eBook.resources.getByHref(chapter.url)
val content = StringBuilder()
val doc = Jsoup.parse(String(resource.data, mCharset)) val doc = Jsoup.parse(String(resource.data, mCharset))
val elements = doc.body().allElements val elements = doc.body().children()
for (element in elements) { elements.select("script, style").remove()
val contentEs = element.textNodes() return elements.outerHtml()
for (i in contentEs.indices) { .replace("</?(?:div|p|b|br|hr|h\\d|article|dd|dl)[^>]*>".toRegex(), "\n")
val text = contentEs[i].text().trim { it <= ' ' }.htmlFormat()
if (elements.size > 1) {
if (text.isNotEmpty()) {
if (content.isNotEmpty()) {
content.append("\r\n")
}
content.append("\u3000\u3000").append(text)
}
} else {
content.append(text)
}
}
}
return content.toString()
} }
return null return null
} }
private fun getImage(href: String): InputStream? {
val abHref = href.replace("../", "")
return epubBook?.resources?.getByHref(abHref)?.inputStream
}
private fun getChapterList(): ArrayList<BookChapter> { private fun getChapterList(): ArrayList<BookChapter> {
val chapterList = ArrayList<BookChapter>() val chapterList = ArrayList<BookChapter>()
epubBook?.let { eBook -> epubBook?.let { eBook ->

@ -211,7 +211,7 @@ object ReadBook {
Coroutine.async { Coroutine.async {
App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter -> App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter ->
BookHelp.getContent(book, chapter)?.let { BookHelp.getContent(book, chapter)?.let {
contentLoadFinish(chapter, it, upContent, resetPageOffset) contentLoadFinish(book, chapter, it, upContent, resetPageOffset)
removeLoading(chapter.index) removeLoading(chapter.index)
} ?: download(chapter, resetPageOffset = resetPageOffset) } ?: download(chapter, resetPageOffset = resetPageOffset)
} ?: removeLoading(index) } ?: removeLoading(index)
@ -247,6 +247,7 @@ object ReadBook {
?.onSuccess(Dispatchers.IO) { content -> ?.onSuccess(Dispatchers.IO) { content ->
if (content.isEmpty()) { if (content.isEmpty()) {
contentLoadFinish( contentLoadFinish(
book,
chapter, chapter,
App.INSTANCE.getString(R.string.content_empty), App.INSTANCE.getString(R.string.content_empty),
resetPageOffset = resetPageOffset resetPageOffset = resetPageOffset
@ -254,11 +255,12 @@ object ReadBook {
removeLoading(chapter.index) removeLoading(chapter.index)
} else { } else {
BookHelp.saveContent(book, chapter, content) BookHelp.saveContent(book, chapter, content)
contentLoadFinish(chapter, content, resetPageOffset = resetPageOffset) contentLoadFinish(book, chapter, content, resetPageOffset = resetPageOffset)
removeLoading(chapter.index) removeLoading(chapter.index)
} }
}?.onError { }?.onError {
contentLoadFinish( contentLoadFinish(
book,
chapter, chapter,
it.localizedMessage ?: "未知错误", it.localizedMessage ?: "未知错误",
resetPageOffset = resetPageOffset resetPageOffset = resetPageOffset
@ -286,6 +288,7 @@ object ReadBook {
* 内容加载完成 * 内容加载完成
*/ */
private fun contentLoadFinish( private fun contentLoadFinish(
book: Book,
chapter: BookChapter, chapter: BookChapter,
content: String, content: String,
upContent: Boolean = true, upContent: Boolean = true,
@ -300,15 +303,15 @@ object ReadBook {
} }
val contents = BookHelp.disposeContent( val contents = BookHelp.disposeContent(
chapter.title, chapter.title,
book!!.name, book.name,
webBook?.bookSource?.bookSourceUrl, webBook?.bookSource?.bookSourceUrl,
content, content,
book!!.useReplaceRule book.useReplaceRule
) )
when (chapter.index) { when (chapter.index) {
durChapterIndex -> { durChapterIndex -> {
curTextChapter = curTextChapter =
ChapterProvider.getTextChapter(chapter, contents, chapterSize) ChapterProvider.getTextChapter(book, chapter, contents, chapterSize)
if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset) if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset)
callBack?.upView() callBack?.upView()
curPageChanged() curPageChanged()
@ -316,12 +319,12 @@ object ReadBook {
} }
durChapterIndex - 1 -> { durChapterIndex - 1 -> {
prevTextChapter = prevTextChapter =
ChapterProvider.getTextChapter(chapter, contents, chapterSize) ChapterProvider.getTextChapter(book, chapter, contents, chapterSize)
if (upContent) callBack?.upContent(-1, resetPageOffset) if (upContent) callBack?.upContent(-1, resetPageOffset)
} }
durChapterIndex + 1 -> { durChapterIndex + 1 -> {
nextTextChapter = nextTextChapter =
ChapterProvider.getTextChapter(chapter, contents, chapterSize) ChapterProvider.getTextChapter(book, chapter, contents, chapterSize)
if (upContent) callBack?.upContent(1, resetPageOffset) if (upContent) callBack?.upContent(1, resetPageOffset)
} }
} }

@ -8,6 +8,7 @@ import android.text.TextPaint
import android.text.TextUtils import android.text.TextUtils
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
@ -15,6 +16,7 @@ import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.* import io.legado.app.utils.*
import java.util.regex.Pattern
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -34,6 +36,7 @@ object ChapterProvider {
var typeface: Typeface = Typeface.SANS_SERIF var typeface: Typeface = Typeface.SANS_SERIF
lateinit var titlePaint: TextPaint lateinit var titlePaint: TextPaint
lateinit var contentPaint: TextPaint lateinit var contentPaint: TextPaint
private val srcPattern = Pattern.compile("<img .*?src=\"(.*?)\".*?>", Pattern.CASE_INSENSITIVE)
init { init {
upStyle() upStyle()
@ -43,6 +46,7 @@ object ChapterProvider {
* 获取拆分完的章节数据 * 获取拆分完的章节数据
*/ */
fun getTextChapter( fun getTextChapter(
book: Book,
bookChapter: BookChapter, bookChapter: BookChapter,
contents: List<String>, contents: List<String>,
chapterSize: Int chapterSize: Int
@ -54,6 +58,20 @@ object ChapterProvider {
var durY = 0f var durY = 0f
textPages.add(TextPage()) textPages.add(TextPage())
contents.forEachIndexed { index, text -> contents.forEachIndexed { index, text ->
val matcher = srcPattern.matcher(text)
if (matcher.find()) {
var src = matcher.group(1)
if (!book.isEpub()) {
src = NetworkUtils.getAbsoluteURL(bookChapter.url, src)
}
src?.let {
durY = setTypeImage(
book, src, durY,
textPages, pageLines,
pageLengths, stringBuilder
)
}
} else {
val isTitle = index == 0 val isTitle = index == 0
if (!(isTitle && ReadBookConfig.titleMode == 2)) { if (!(isTitle && ReadBookConfig.titleMode == 2)) {
durY = setTypeText( durY = setTypeText(
@ -62,6 +80,7 @@ object ChapterProvider {
) )
} }
} }
}
textPages.last().height = durY + 20.dp textPages.last().height = durY + 20.dp
textPages.last().text = stringBuilder.toString() textPages.last().text = stringBuilder.toString()
if (pageLines.size < textPages.size) { if (pageLines.size < textPages.size) {
@ -90,6 +109,26 @@ object ChapterProvider {
) )
} }
private fun setTypeImage(
book: Book,
src: String,
y: Float,
textPages: ArrayList<TextPage>,
pageLines: ArrayList<Int>,
pageLengths: ArrayList<Int>,
stringBuilder: StringBuilder
): Float {
var durY = y
ImageProvider.getImage(book, src)?.let {
val textLine = TextLine(text = src, isImage = true)
textLine.lineTop = durY
durY += it.height
textLine.lineBottom = durY
textPages.last().textLines.add(textLine)
}
return durY + paragraphSpacing / 10f
}
/** /**
* 排版文字 * 排版文字
*/ */

@ -10,7 +10,9 @@ import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.entities.TextChar import io.legado.app.ui.book.read.page.entities.TextChar
import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.getCompatColor import io.legado.app.utils.getCompatColor
@ -78,18 +80,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
private fun drawPage(canvas: Canvas) { private fun drawPage(canvas: Canvas) {
var relativeOffset = relativeOffset(0) var relativeOffset = relativeOffset(0)
textPage.textLines.forEach { textLine -> textPage.textLines.forEach { textLine ->
val lineTop = textLine.lineTop + relativeOffset draw(canvas, textLine, relativeOffset)
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
} }
if (!ReadBookConfig.isScroll) return if (!ReadBookConfig.isScroll) return
//滚动翻页 //滚动翻页
@ -97,38 +88,39 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
val nextPage = relativePage(1) val nextPage = relativePage(1)
relativeOffset = relativeOffset(1) relativeOffset = relativeOffset(1)
nextPage.textLines.forEach { textLine -> nextPage.textLines.forEach { textLine ->
val lineTop = textLine.lineTop + relativeOffset draw(canvas, textLine, relativeOffset)
val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset
drawChars(
canvas,
textLine.textChars,
lineTop,
lineBase,
lineBottom,
textLine.isTitle,
textLine.isReadAloud
)
} }
if (!pageFactory.hasNextPlus()) return if (!pageFactory.hasNextPlus()) return
relativeOffset = relativeOffset(2) relativeOffset = relativeOffset(2)
if (relativeOffset < ChapterProvider.visibleHeight) { if (relativeOffset < ChapterProvider.visibleHeight) {
relativePage(2).textLines.forEach { textLine -> relativePage(2).textLines.forEach { textLine ->
draw(canvas, textLine, relativeOffset)
}
}
}
private fun draw(
canvas: Canvas,
textLine: TextLine,
relativeOffset: Float
) {
val lineTop = textLine.lineTop + relativeOffset val lineTop = textLine.lineTop + relativeOffset
val lineBase = textLine.lineBase + relativeOffset val lineBase = textLine.lineBase + relativeOffset
val lineBottom = textLine.lineBottom + relativeOffset val lineBottom = textLine.lineBottom + relativeOffset
if (textLine.isImage) {
drawImage(canvas, textLine, lineTop, lineBottom)
} else {
drawChars( drawChars(
canvas, canvas,
textLine.textChars, textLine.textChars,
lineTop, lineTop,
lineBase, lineBase,
lineBottom, lineBottom,
textLine.isTitle, isTitle = textLine.isTitle,
textLine.isReadAloud isReadAloud = textLine.isReadAloud
) )
} }
} }
}
/** /**
* 绘制文字 * 绘制文字
@ -153,6 +145,20 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
} }
} }
/**
* 绘制图片
*/
private fun drawImage(
canvas: Canvas,
textLine: TextLine,
lineTop: Float,
lineBottom: Float
) {
ImageProvider.getImage(ReadBook.book!!, textLine.text)?.let {
canvas.drawBitmap(it, ChapterProvider.paddingLeft.toFloat(), lineTop, null)
}
}
/** /**
* 滚动事件 * 滚动事件
*/ */

@ -0,0 +1,46 @@
package io.legado.app.ui.book.read.page
import android.graphics.Bitmap
import io.legado.app.App
import io.legado.app.data.entities.Book
import io.legado.app.help.http.HttpHelper
import io.legado.app.model.localBook.EPUBFile
import io.legado.app.utils.BitmapUtils
import io.legado.app.utils.FileUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.externalFilesDir
import java.io.FileOutputStream
object ImageProvider {
fun getImage(book: Book, src: String): Bitmap? {
val vFile = FileUtils.getFile(
App.INSTANCE.externalFilesDir,
"${MD5Utils.md5Encode16(src)}.jpg",
"images", book.name
)
if (!vFile.exists()) {
if (book.isEpub()) {
EPUBFile.getImage(book, src).use {
val out = FileOutputStream(FileUtils.createFileIfNotExist(vFile.absolutePath))
it?.copyTo(out)
out.flush()
out.close()
}
} else {
HttpHelper.getBytes(src)
}
}
return try {
BitmapUtils.decodeBitmap(
vFile.absolutePath,
ChapterProvider.visibleWidth,
ChapterProvider.visibleHeight
)
} catch (e: Exception) {
null
}
}
}

@ -11,6 +11,7 @@ data class TextLine(
var lineBase: Float = 0f, var lineBase: Float = 0f,
var lineBottom: Float = 0f, var lineBottom: Float = 0f,
val isTitle: Boolean = false, val isTitle: Boolean = false,
val isImage: Boolean = false,
var isReadAloud: Boolean = false var isReadAloud: Boolean = false
) { ) {

@ -131,7 +131,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
private suspend fun webData2bitmap(data: String): ByteArray? { private suspend fun webData2bitmap(data: String): ByteArray? {
return if (URLUtil.isValidUrl(data)) { return if (URLUtil.isValidUrl(data)) {
HttpHelper.simpleGetByteAsync(data) HttpHelper.simpleGetBytesAsync(data)
} else { } else {
Base64.decode(data.split(",").toTypedArray()[1], Base64.DEFAULT) Base64.decode(data.split(",").toTypedArray()[1], Base64.DEFAULT)
} }

@ -9,15 +9,15 @@ import java.security.NoSuchAlgorithmException
@Suppress("unused") @Suppress("unused")
object MD5Utils { object MD5Utils {
fun md5Encode(str: String?): String? { fun md5Encode(str: String?): String {
if (str == null) return null if (str == null) return ""
var reStr: String? = null var reStr = ""
try { try {
val md5:MessageDigest = MessageDigest.getInstance("MD5") val md5: MessageDigest = MessageDigest.getInstance("MD5")
val bytes:ByteArray = md5.digest(str.toByteArray()) val bytes: ByteArray = md5.digest(str.toByteArray())
val stringBuffer:StringBuilder = StringBuilder() val stringBuffer: StringBuilder = StringBuilder()
for (b in bytes) { for (b in bytes) {
val bt:Int = b.toInt() and 0xff val bt: Int = b.toInt() and 0xff
if (bt < 16) { if (bt < 16) {
stringBuffer.append(0) stringBuffer.append(0)
} }
@ -31,11 +31,9 @@ object MD5Utils {
return reStr return reStr
} }
fun md5Encode16(str: String): String? { fun md5Encode16(str: String): String {
var reStr = md5Encode(str) var reStr = md5Encode(str)
if (reStr != null) {
reStr = reStr.substring(8, 24) reStr = reStr.substring(8, 24)
}
return reStr return reStr
} }
} }

Loading…
Cancel
Save