ag2s20150909 3 лет назад
Родитель
Сommit
40c05c093f

+ 2 - 1
app/build.gradle

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

+ 1 - 1
app/src/main/java/io/legado/app/constant/AppPattern.kt

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

+ 4 - 0
app/src/main/java/io/legado/app/help/book/BookExtensions.kt

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

+ 51 - 0
app/src/main/java/io/legado/app/help/http/cronet/BodyUploadProvider.kt

@@ -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()
+    }
+}

+ 1 - 5
app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt

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

+ 7 - 0
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

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

+ 225 - 0
app/src/main/java/io/legado/app/model/localBook/PdfFile.kt

@@ -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()
+    }
+}

+ 1 - 0
app/src/main/java/io/legado/app/model/localBook/README.md

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

+ 4 - 1
app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt

@@ -140,7 +140,10 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
                 if (docItem.isDir) {
                     scanDoc(docItem, false, scope)
                 } 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)
                 }

+ 11 - 1
app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt

@@ -12,13 +12,15 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.book.BookHelp
 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.coroutine.Coroutine
 import io.legado.app.model.ReadBook
 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.SvgUtils
 import io.legado.app.utils.FileUtils
+import io.legado.app.utils.SvgUtils
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.Main
@@ -81,6 +83,14 @@ object ImageProvider {
                             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 {
                     BookHelp.saveImage(bookSource, book, src)
                 }

+ 14 - 2
app/src/main/java/io/legado/app/utils/BitmapUtils.kt

@@ -8,8 +8,7 @@ import android.graphics.Bitmap.Config
 import android.graphics.BitmapFactory
 import android.graphics.Color
 import com.google.android.renderscript.Toolkit
-import java.io.FileInputStream
-import java.io.IOException
+import java.io.*
 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,
         averagePixelBlue + 3
     )
+
 }

+ 1 - 0
app/src/main/java/io/legado/app/utils/ConvertExtensions.kt

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

+ 15 - 0
app/src/main/java/io/legado/app/utils/SystemUtils.kt

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

+ 2 - 0
build.gradle

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

+ 4 - 3
epublib/build.gradle

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