diff --git a/app/src/main/java/io/legado/app/help/JsExtensions.kt b/app/src/main/java/io/legado/app/help/JsExtensions.kt index 493ede9c3..a58fd7ce6 100644 --- a/app/src/main/java/io/legado/app/help/JsExtensions.kt +++ b/app/src/main/java/io/legado/app/help/JsExtensions.kt @@ -41,11 +41,11 @@ interface JsExtensions { return EncoderUtils.base64Encode(str, flags) } - fun md5Encode(str: String): String? { + fun md5Encode(str: String): String { return MD5Utils.md5Encode(str) } - fun md5Encode16(str: String): String? { + fun md5Encode16(str: String): String { return MD5Utils.md5Encode16(str) } diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt index a65423e78..26c948575 100644 --- a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt +++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt @@ -49,6 +49,17 @@ object HttpHelper { 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? { NetworkUtils.getBaseUrl(url)?.let { baseUrl -> val response = getApiService(baseUrl, encode) @@ -58,7 +69,7 @@ object HttpHelper { return null } - suspend fun simpleGetByteAsync(url: String): ByteArray? { + suspend fun simpleGetBytesAsync(url: String): ByteArray? { NetworkUtils.getBaseUrl(url)?.let { baseUrl -> return getByteRetrofit(baseUrl) .create(HttpGetApi::class.java) diff --git a/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt b/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt index 315dc881f..924361489 100644 --- a/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt +++ b/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt @@ -39,6 +39,13 @@ interface HttpGetApi { @HeaderMap headers: Map ): Call + @GET + fun getMapByte( + @Url url: String, + @QueryMap(encoded = true) queryMap: Map, + @HeaderMap headers: Map + ): Call + @GET suspend fun getMapByteAsync( @Url url: String, diff --git a/app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt b/app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt index c9685729a..2aafc1bc8 100644 --- a/app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt +++ b/app/src/main/java/io/legado/app/model/localBook/EPUBFile.kt @@ -6,7 +6,10 @@ import android.net.Uri import android.text.TextUtils import io.legado.app.App 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.TOCReference import nl.siegmann.epublib.epub.EpubReader @@ -14,6 +17,7 @@ import org.jsoup.Jsoup import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.io.InputStream import java.nio.charset.Charset import java.util.* @@ -21,7 +25,6 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) { companion object { private var eFile: EPUBFile? = null - private val coverDir = App.INSTANCE.externalFilesDir @Synchronized 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? { 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 @@ -64,13 +75,12 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) { ) } if (!File(book.coverUrl!!).exists()) { - epubBook!!.coverImage?.inputStream?.let { + epubBook!!.coverImage?.inputStream?.use { val cover = BitmapFactory.decodeStream(it) val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)) cover.compress(Bitmap.CompressFormat.JPEG, 90, out) out.flush() out.close() - it.close() } } } catch (e: Exception) { @@ -81,30 +91,20 @@ class EPUBFile(val book: io.legado.app.data.entities.Book) { private fun getContent(chapter: BookChapter): String? { epubBook?.let { eBook -> val resource = eBook.resources.getByHref(chapter.url) - val content = StringBuilder() val doc = Jsoup.parse(String(resource.data, mCharset)) - val elements = doc.body().allElements - for (element in elements) { - val contentEs = element.textNodes() - for (i in contentEs.indices) { - 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() + val elements = doc.body().children() + elements.select("script, style").remove() + return elements.outerHtml() + .replace("]*>".toRegex(), "\n") } return null } + private fun getImage(href: String): InputStream? { + val abHref = href.replace("../", "") + return epubBook?.resources?.getByHref(abHref)?.inputStream + } + private fun getChapterList(): ArrayList { val chapterList = ArrayList() epubBook?.let { eBook -> diff --git a/app/src/main/java/io/legado/app/service/help/ReadBook.kt b/app/src/main/java/io/legado/app/service/help/ReadBook.kt index ef15098b0..12549fe3f 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadBook.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadBook.kt @@ -211,7 +211,7 @@ object ReadBook { Coroutine.async { App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter -> BookHelp.getContent(book, chapter)?.let { - contentLoadFinish(chapter, it, upContent, resetPageOffset) + contentLoadFinish(book, chapter, it, upContent, resetPageOffset) removeLoading(chapter.index) } ?: download(chapter, resetPageOffset = resetPageOffset) } ?: removeLoading(index) @@ -247,6 +247,7 @@ object ReadBook { ?.onSuccess(Dispatchers.IO) { content -> if (content.isEmpty()) { contentLoadFinish( + book, chapter, App.INSTANCE.getString(R.string.content_empty), resetPageOffset = resetPageOffset @@ -254,11 +255,12 @@ object ReadBook { removeLoading(chapter.index) } else { BookHelp.saveContent(book, chapter, content) - contentLoadFinish(chapter, content, resetPageOffset = resetPageOffset) + contentLoadFinish(book, chapter, content, resetPageOffset = resetPageOffset) removeLoading(chapter.index) } }?.onError { contentLoadFinish( + book, chapter, it.localizedMessage ?: "未知错误", resetPageOffset = resetPageOffset @@ -286,6 +288,7 @@ object ReadBook { * 内容加载完成 */ private fun contentLoadFinish( + book: Book, chapter: BookChapter, content: String, upContent: Boolean = true, @@ -300,15 +303,15 @@ object ReadBook { } val contents = BookHelp.disposeContent( chapter.title, - book!!.name, + book.name, webBook?.bookSource?.bookSourceUrl, content, - book!!.useReplaceRule + book.useReplaceRule ) when (chapter.index) { durChapterIndex -> { curTextChapter = - ChapterProvider.getTextChapter(chapter, contents, chapterSize) + ChapterProvider.getTextChapter(book, chapter, contents, chapterSize) if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset) callBack?.upView() curPageChanged() @@ -316,12 +319,12 @@ object ReadBook { } durChapterIndex - 1 -> { prevTextChapter = - ChapterProvider.getTextChapter(chapter, contents, chapterSize) + ChapterProvider.getTextChapter(book, chapter, contents, chapterSize) if (upContent) callBack?.upContent(-1, resetPageOffset) } durChapterIndex + 1 -> { nextTextChapter = - ChapterProvider.getTextChapter(chapter, contents, chapterSize) + ChapterProvider.getTextChapter(book, chapter, contents, chapterSize) if (upContent) callBack?.upContent(1, resetPageOffset) } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt index 486b0b1d2..9d6c9b0ff 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt @@ -8,6 +8,7 @@ import android.text.TextPaint import android.text.TextUtils import io.legado.app.App import io.legado.app.constant.PreferKey +import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.help.AppConfig 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.TextPage import io.legado.app.utils.* +import java.util.regex.Pattern @Suppress("DEPRECATION") @@ -34,6 +36,7 @@ object ChapterProvider { var typeface: Typeface = Typeface.SANS_SERIF lateinit var titlePaint: TextPaint lateinit var contentPaint: TextPaint + private val srcPattern = Pattern.compile("", Pattern.CASE_INSENSITIVE) init { upStyle() @@ -43,6 +46,7 @@ object ChapterProvider { * 获取拆分完的章节数据 */ fun getTextChapter( + book: Book, bookChapter: BookChapter, contents: List, chapterSize: Int @@ -54,12 +58,27 @@ object ChapterProvider { var durY = 0f textPages.add(TextPage()) contents.forEachIndexed { index, text -> - val isTitle = index == 0 - if (!(isTitle && ReadBookConfig.titleMode == 2)) { - durY = setTypeText( - text, durY, textPages, pageLines, - pageLengths, stringBuilder, isTitle - ) + 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 + if (!(isTitle && ReadBookConfig.titleMode == 2)) { + durY = setTypeText( + text, durY, textPages, pageLines, + pageLengths, stringBuilder, isTitle + ) + } } } textPages.last().height = durY + 20.dp @@ -90,6 +109,26 @@ object ChapterProvider { ) } + private fun setTypeImage( + book: Book, + src: String, + y: Float, + textPages: ArrayList, + pageLines: ArrayList, + pageLengths: ArrayList, + 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 + } + /** * 排版文字 */ diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt index fb996cc29..75482c2a5 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt @@ -10,7 +10,9 @@ import io.legado.app.R import io.legado.app.constant.PreferKey import io.legado.app.help.ReadBookConfig 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.TextLine import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.utils.activity import io.legado.app.utils.getCompatColor @@ -78,18 +80,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at private fun drawPage(canvas: Canvas) { var relativeOffset = relativeOffset(0) textPage.textLines.forEach { textLine -> - val lineTop = textLine.lineTop + relativeOffset - val lineBase = textLine.lineBase + relativeOffset - val lineBottom = textLine.lineBottom + relativeOffset - drawChars( - canvas, - textLine.textChars, - lineTop, - lineBase, - lineBottom, - textLine.isTitle, - textLine.isReadAloud - ) + draw(canvas, textLine, relativeOffset) } if (!ReadBookConfig.isScroll) return //滚动翻页 @@ -97,37 +88,38 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at val nextPage = relativePage(1) relativeOffset = relativeOffset(1) nextPage.textLines.forEach { textLine -> - val lineTop = textLine.lineTop + relativeOffset - val lineBase = textLine.lineBase + relativeOffset - val lineBottom = textLine.lineBottom + relativeOffset + draw(canvas, textLine, relativeOffset) + } + if (!pageFactory.hasNextPlus()) return + relativeOffset = relativeOffset(2) + if (relativeOffset < ChapterProvider.visibleHeight) { + relativePage(2).textLines.forEach { textLine -> + draw(canvas, textLine, relativeOffset) + } + } + } + + private fun draw( + canvas: Canvas, + textLine: TextLine, + relativeOffset: Float + ) { + val lineTop = textLine.lineTop + relativeOffset + val lineBase = textLine.lineBase + relativeOffset + val lineBottom = textLine.lineBottom + relativeOffset + if (textLine.isImage) { + drawImage(canvas, textLine, lineTop, lineBottom) + } else { drawChars( canvas, textLine.textChars, lineTop, lineBase, lineBottom, - textLine.isTitle, - textLine.isReadAloud + isTitle = textLine.isTitle, + isReadAloud = textLine.isReadAloud ) } - if (!pageFactory.hasNextPlus()) return - relativeOffset = relativeOffset(2) - if (relativeOffset < ChapterProvider.visibleHeight) { - relativePage(2).textLines.forEach { textLine -> - val lineTop = textLine.lineTop + relativeOffset - val lineBase = textLine.lineBase + relativeOffset - val lineBottom = textLine.lineBottom + relativeOffset - drawChars( - canvas, - textLine.textChars, - lineTop, - lineBase, - lineBottom, - textLine.isTitle, - 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) + } + } + /** * 滚动事件 */ diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ImageProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ImageProvider.kt new file mode 100644 index 000000000..6b74d5228 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ImageProvider.kt @@ -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 + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt index c8a9c9ca0..9e6755441 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt @@ -11,6 +11,7 @@ data class TextLine( var lineBase: Float = 0f, var lineBottom: Float = 0f, val isTitle: Boolean = false, + val isImage: Boolean = false, var isReadAloud: Boolean = false ) { diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt index 72af31aca..f5fa3f97b 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt @@ -131,7 +131,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), private suspend fun webData2bitmap(data: String): ByteArray? { return if (URLUtil.isValidUrl(data)) { - HttpHelper.simpleGetByteAsync(data) + HttpHelper.simpleGetBytesAsync(data) } else { Base64.decode(data.split(",").toTypedArray()[1], Base64.DEFAULT) } diff --git a/app/src/main/java/io/legado/app/utils/MD5Utils.kt b/app/src/main/java/io/legado/app/utils/MD5Utils.kt index f0044a999..c0ce9febe 100644 --- a/app/src/main/java/io/legado/app/utils/MD5Utils.kt +++ b/app/src/main/java/io/legado/app/utils/MD5Utils.kt @@ -9,15 +9,15 @@ import java.security.NoSuchAlgorithmException @Suppress("unused") object MD5Utils { - fun md5Encode(str: String?): String? { - if (str == null) return null - var reStr: String? = null + fun md5Encode(str: String?): String { + if (str == null) return "" + var reStr = "" try { - val md5:MessageDigest = MessageDigest.getInstance("MD5") - val bytes:ByteArray = md5.digest(str.toByteArray()) - val stringBuffer:StringBuilder = StringBuilder() + val md5: MessageDigest = MessageDigest.getInstance("MD5") + val bytes: ByteArray = md5.digest(str.toByteArray()) + val stringBuffer: StringBuilder = StringBuilder() for (b in bytes) { - val bt:Int = b.toInt() and 0xff + val bt: Int = b.toInt() and 0xff if (bt < 16) { stringBuffer.append(0) } @@ -31,11 +31,9 @@ object MD5Utils { return reStr } - fun md5Encode16(str: String): String? { + fun md5Encode16(str: String): String { var reStr = md5Encode(str) - if (reStr != null) { - reStr = reStr.substring(8, 24) - } + reStr = reStr.substring(8, 24) return reStr } }