Merge pull request #2643 from ag2s20150909/pdf

支持PDF格式
pull/2649/head
kunfei 2 years ago committed by GitHub
commit a045c4f1e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      app/build.gradle
  2. 2
      app/src/main/java/io/legado/app/constant/AppPattern.kt
  3. 4
      app/src/main/java/io/legado/app/help/book/BookExtensions.kt
  4. 51
      app/src/main/java/io/legado/app/help/http/cronet/BodyUploadProvider.kt
  5. 6
      app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt
  6. 7
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  7. 225
      app/src/main/java/io/legado/app/model/localBook/PdfFile.kt
  8. 1
      app/src/main/java/io/legado/app/model/localBook/README.md
  9. 5
      app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt
  10. 12
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt
  11. 16
      app/src/main/java/io/legado/app/utils/BitmapUtils.kt
  12. 1
      app/src/main/java/io/legado/app/utils/ConvertExtensions.kt
  13. 15
      app/src/main/java/io/legado/app/utils/SystemUtils.kt
  14. 2
      build.gradle
  15. 7
      epublib/build.gradle

@ -16,7 +16,8 @@ def version = "3." + releaseTime()
def gitCommits = Integer.parseInt('git rev-list HEAD --count'.execute().text.trim()) def gitCommits = Integer.parseInt('git rev-list HEAD --count'.execute().text.trim())
android { android {
compileSdk 33 compileSdk = compile_sdk_version
buildToolsVersion = build_tool_version
namespace 'io.legado.app' namespace 'io.legado.app'
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"

@ -23,7 +23,7 @@ object AppPattern {
val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]") val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]")
//本地书籍支持类型 //本地书籍支持类型
val bookFileRegex = Regex(".*\\.(txt|epub|umd)", RegexOption.IGNORE_CASE) val bookFileRegex = Regex(".*\\.(txt|epub|umd|pdf)", RegexOption.IGNORE_CASE)
/** /**
* 所有标点 * 所有标点

@ -49,6 +49,10 @@ val Book.isUmd: Boolean
get() { get() {
return isLocal && originName.endsWith(".umd", true) return isLocal && originName.endsWith(".umd", true)
} }
val Book.isPdf: Boolean
get() {
return isLocal && originName.endsWith(".pdf", true)
}
val Book.isOnLineTxt: Boolean val Book.isOnLineTxt: Boolean
get() { get() {

@ -0,0 +1,51 @@
package io.legado.app.help.http.cronet
import okhttp3.RequestBody
import okio.Buffer
import org.chromium.net.UploadDataProvider
import org.chromium.net.UploadDataSink
import java.io.IOException
import java.nio.ByteBuffer
class BodyUploadProvider(body: RequestBody) : UploadDataProvider(), AutoCloseable {
private val body: RequestBody
private val buffer: Buffer?
init {
buffer = Buffer()
this.body = body
try {
body.writeTo(buffer)
} catch (e: IOException) {
e.printStackTrace()
}
}
@Throws(IOException::class)
override fun getLength(): Long {
return body.contentLength()
}
@Throws(IOException::class)
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
check(byteBuffer.hasRemaining()) { "Cronet passed a buffer with no bytes remaining" }
var read: Int
var bytesRead = 0
while (bytesRead == 0) {
read = buffer!!.read(byteBuffer)
bytesRead += read
}
uploadDataSink.onReadSucceeded(false)
}
@Throws(IOException::class)
override fun rewind(uploadDataSink: UploadDataSink) {
uploadDataSink.onRewindSucceeded()
}
@Throws(IOException::class)
override fun close() {
buffer?.close()
super.close()
}
}

@ -7,10 +7,8 @@ import io.legado.app.utils.DebugLog
import okhttp3.Headers import okhttp3.Headers
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.Request import okhttp3.Request
import okio.Buffer
import org.chromium.net.CronetEngine.Builder.HTTP_CACHE_DISK import org.chromium.net.CronetEngine.Builder.HTTP_CACHE_DISK
import org.chromium.net.ExperimentalCronetEngine import org.chromium.net.ExperimentalCronetEngine
import org.chromium.net.UploadDataProviders
import org.chromium.net.UrlRequest import org.chromium.net.UrlRequest
import org.json.JSONObject import org.json.JSONObject
import splitties.init.appCtx import splitties.init.appCtx
@ -85,10 +83,8 @@ fun buildRequest(request: Request, callback: UrlRequest.Callback): UrlRequest? {
} else { } else {
addHeader("Content-Type", "text/plain") addHeader("Content-Type", "text/plain")
} }
val buffer = Buffer()
requestBody.writeTo(buffer)
setUploadDataProvider( setUploadDataProvider(
UploadDataProviders.create(buffer.readByteArray()), BodyUploadProvider(requestBody),
okHttpClient.dispatcher.executorService okHttpClient.dispatcher.executorService
) )

@ -76,6 +76,9 @@ object LocalBook {
book.isUmd -> { book.isUmd -> {
UmdFile.getChapterList(book) UmdFile.getChapterList(book)
} }
book.isPdf -> {
PdfFile.getChapterList(book)
}
else -> { else -> {
TextFile.getChapterList(book) TextFile.getChapterList(book)
} }
@ -100,6 +103,9 @@ object LocalBook {
book.isUmd -> { book.isUmd -> {
UmdFile.getContent(book, chapter) UmdFile.getContent(book, chapter)
} }
book.isPdf -> {
PdfFile.getContent(book, chapter)
}
else -> { else -> {
TextFile.getContent(book, chapter) TextFile.getContent(book, chapter)
} }
@ -166,6 +172,7 @@ object LocalBook {
) )
if (book.isEpub) EpubFile.upBookInfo(book) if (book.isEpub) EpubFile.upBookInfo(book)
if (book.isUmd) UmdFile.upBookInfo(book) if (book.isUmd) UmdFile.upBookInfo(book)
if (book.isPdf) PdfFile.upBookInfo(book)
appDb.bookDao.insert(book) appDb.bookDao.insert(book)
} else { } else {
//已有书籍说明是更新,删除原有目录 //已有书籍说明是更新,删除原有目录

@ -0,0 +1,225 @@
package io.legado.app.model.localBook
import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor
import io.legado.app.constant.AppLog
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.book.getLocalUri
import io.legado.app.utils.*
import splitties.init.appCtx
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import kotlin.math.ceil
class PdfFile(var book: Book) {
companion object : BaseLocalBookParse {
private var pFile: PdfFile? = null
/**
* pdf分页尺寸
*/
const val PAGE_SIZE = 10
@Synchronized
private fun getPFile(book: Book): PdfFile {
if (pFile == null || pFile?.book?.bookUrl != book.bookUrl) {
pFile = PdfFile(book)
return pFile!!
}
pFile?.book = book
return pFile!!
}
@Synchronized
override fun upBookInfo(book: Book) {
getPFile(book).upBookInfo()
}
@Synchronized
override fun getChapterList(book: Book): ArrayList<BookChapter> {
return getPFile(book).getChapterList()
}
@Synchronized
override fun getContent(book: Book, chapter: BookChapter): String? {
return getPFile(book).getContent(chapter)
}
@Synchronized
override fun getImage(book: Book, href: String): InputStream? {
return getPFile(book).getImage(href)
}
}
private var fileDescriptor: ParcelFileDescriptor? = null
private var pdfRenderer: PdfRenderer? = null
get() {
if (field != null) {
return field
}
field = readPdf()
return field
}
init {
try {
pdfRenderer?.let { renderer ->
if (book.coverUrl.isNullOrEmpty()) {
book.coverUrl = FileUtils.getPath(
appCtx.externalFiles,
"covers",
"${MD5Utils.md5Encode16(book.bookUrl)}.jpg"
)
}
if (!File(book.coverUrl!!).exists()) {
FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)).use { out ->
openPdfPage(renderer, 0)?.let { cover ->
cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
}
out.flush()
}
}
}
} catch (e: Exception) {
AppLog.put("加载书籍封面失败\n${e.localizedMessage}", e)
e.printOnDebug()
}
}
/**
* 读取PDF文件
*
* @return
*/
private fun readPdf(): PdfRenderer? {
val uri = book.getLocalUri()
if (uri.isContentScheme()) {
fileDescriptor = appCtx.contentResolver.openFileDescriptor(uri, "r")?.also {
pdfRenderer = PdfRenderer(it)
}
} else {
fileDescriptor =
ParcelFileDescriptor.open(File(uri.path), ParcelFileDescriptor.MODE_READ_ONLY)
?.also {
pdfRenderer = PdfRenderer(it)
}
}
return pdfRenderer
}
/**
* 关闭pdf文件
*
*/
private fun closePdf() {
pdfRenderer?.close()
fileDescriptor?.close()
}
/**
* 渲染PDF页面
* 根据index打开pdf页面,并渲染到Bitmap
*
* @param renderer
* @param index
* @return
*/
private fun openPdfPage(renderer: PdfRenderer, index: Int): Bitmap? {
if (index >= renderer.pageCount) {
return null
}
return renderer.openPage(index)?.use { page ->
Bitmap.createBitmap(
SystemUtils.screenWidthPx,
(SystemUtils.screenWidthPx.toDouble() * page.height / page.width).toInt(),
Bitmap.Config.ARGB_8888
)
.apply {
page.render(this, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
}
}
}
private fun getContent(chapter: BookChapter): String? =
if (pdfRenderer == null) {
null
} else {
pdfRenderer?.let { renderer ->
buildString {
val start = chapter.index * PAGE_SIZE
val end = Math.min((chapter.index + 1) * PAGE_SIZE, renderer.pageCount)
(start until end).forEach {
append("<img src=").append('"').append(it).append('"').append(" >")
.append('\n')
}
}
}
}
private fun getImage(href: String): InputStream? {
if (pdfRenderer == null) {
return null
}
return try {
val index = href.toInt()
val bitmap = openPdfPage(pdfRenderer!!, index)
if (bitmap != null) {
BitmapUtils.toInputStream(bitmap).also { bitmap.recycle() }
} else {
null
}
} catch (e: Exception) {
return null
}
}
private fun getChapterList(): ArrayList<BookChapter> {
val chapterList = ArrayList<BookChapter>()
pdfRenderer?.let { renderer ->
if (renderer.pageCount > 0) {
val chapterCount = ceil((renderer.pageCount.toDouble() / PAGE_SIZE)).toInt()
(0 until chapterCount).forEach {
val chapter = BookChapter()
chapter.index = it
chapter.bookUrl = book.bookUrl
chapter.title = "分段_${it}"
chapter.url = "pdf_${it}"
chapterList.add(chapter)
}
}
}
return chapterList
}
private fun upBookInfo() {
if (pdfRenderer == null) {
pFile = null
book.intro = "书籍导入异常"
} else {
if (book.name.isEmpty()) {
book.name = book.originName.replace(".pdf", "")
}
}
}
protected fun finalize() {
closePdf()
}
}

@ -4,4 +4,5 @@
* LocalBook.kt 导入解析总入口 * LocalBook.kt 导入解析总入口
* TextFile.kt 解析txt * TextFile.kt 解析txt
* EpubFile.kt 解析epub * EpubFile.kt 解析epub
* PdfFile.kt 解析pdf 纯图片形式
* UmdFile.kt 解析umd * UmdFile.kt 解析umd

@ -140,7 +140,10 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
if (docItem.isDir) { if (docItem.isDir) {
scanDoc(docItem, false, scope) scanDoc(docItem, false, scope)
} else if (docItem.name.endsWith(".txt", true) } else if (docItem.name.endsWith(".txt", true)
|| docItem.name.endsWith(".epub", true) || docItem.name.endsWith(".epub", true) || docItem.name.endsWith(
".pdf",
true
) || docItem.name.endsWith(".umd", true)
) { ) {
list.add(docItem) list.add(docItem)
} }

@ -12,13 +12,15 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.isEpub import io.legado.app.help.book.isEpub
import io.legado.app.help.book.isPdf
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.model.localBook.EpubFile import io.legado.app.model.localBook.EpubFile
import io.legado.app.model.localBook.PdfFile
import io.legado.app.utils.BitmapUtils import io.legado.app.utils.BitmapUtils
import io.legado.app.utils.SvgUtils
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.SvgUtils
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -81,6 +83,14 @@ object ImageProvider {
input.copyTo(output) input.copyTo(output)
} }
} }
} else if (book.isPdf) {
PdfFile.getImage(book, src)?.use { input ->
val newFile = FileUtils.createFileIfNotExist(vFile.absolutePath)
@Suppress("BlockingMethodInNonBlockingContext")
FileOutputStream(newFile).use { output ->
input.copyTo(output)
}
}
} else { } else {
BookHelp.saveImage(bookSource, book, src) BookHelp.saveImage(bookSource, book, src)
} }

@ -8,8 +8,7 @@ import android.graphics.Bitmap.Config
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Color import android.graphics.Color
import com.google.android.renderscript.Toolkit import com.google.android.renderscript.Toolkit
import java.io.FileInputStream import java.io.*
import java.io.IOException
import kotlin.math.* import kotlin.math.*
@ -207,6 +206,18 @@ object BitmapUtils {
} }
} }
/**
* 将Bitmap转换成InputStream
*
* @param bitmap
* @return
*/
fun toInputStream(bitmap: Bitmap): InputStream {
val bos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 0 /*ignored for PNG*/, bos)
return ByteArrayInputStream(bos.toByteArray()).also { bos.close() }
}
} }
/** /**
@ -255,4 +266,5 @@ fun Bitmap.getMeanColor(): Int {
averagePixelGreen + 3, averagePixelGreen + 3,
averagePixelBlue + 3 averagePixelBlue + 3
) )
} }

@ -112,6 +112,7 @@ object ConvertUtils {
return sb.toString() return sb.toString()
} }
} }
val Int.hexString: String val Int.hexString: String

@ -6,6 +6,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import android.view.Display import android.view.Display
import splitties.init.appCtx
import splitties.systemservices.displayManager import splitties.systemservices.displayManager
import splitties.systemservices.powerManager import splitties.systemservices.powerManager
@ -35,4 +36,18 @@ object SystemUtils {
it.state != Display.STATE_OFF it.state != Display.STATE_OFF
} }
} }
/**
* 屏幕像素宽度
*/
val screenWidthPx by lazy {
appCtx.resources.displayMetrics.widthPixels
}
/**
* 屏幕像素高度
*/
val screenHeightPx by lazy {
appCtx.resources.displayMetrics.heightPixels
}
} }

@ -9,6 +9,8 @@ buildscript {
exoplayer_version = '2.18.2' exoplayer_version = '2.18.2'
splitties_version = '3.0.0' splitties_version = '3.0.0'
room_version = '2.4.3' room_version = '2.4.3'
compile_sdk_version = 33
build_tool_version = '33.0.1'
} }
} }

@ -3,7 +3,8 @@ plugins {
} }
android { android {
compileSdk 33 compileSdk = compile_sdk_version
buildToolsVersion = build_tool_version
namespace 'me.ag2s.epublib' namespace 'me.ag2s.epublib'
defaultConfig { defaultConfig {
minSdk 21 minSdk 21
@ -21,8 +22,8 @@ android {
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_11
} }
lint { lint {
checkDependencies true checkDependencies true

Loading…
Cancel
Save