diff --git a/app/build.gradle b/app/build.gradle index 1f7dcb901..4385d8699 100644 --- a/app/build.gradle +++ b/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" diff --git a/app/src/main/java/io/legado/app/constant/AppPattern.kt b/app/src/main/java/io/legado/app/constant/AppPattern.kt index 731a7296a..78eb47d09 100644 --- a/app/src/main/java/io/legado/app/constant/AppPattern.kt +++ b/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) /** * 所有标点 diff --git a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt index 148c63b67..65cdef262 100644 --- a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt +++ b/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() { diff --git a/app/src/main/java/io/legado/app/help/http/cronet/BodyUploadProvider.kt b/app/src/main/java/io/legado/app/help/http/cronet/BodyUploadProvider.kt new file mode 100644 index 000000000..860a24648 --- /dev/null +++ b/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() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt b/app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt index 0d0c37a73..37c34fc8d 100644 --- a/app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt +++ b/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 ) diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt index c4fc91c84..ea7bad72e 100644 --- a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt +++ b/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 { //已有书籍说明是更新,删除原有目录 diff --git a/app/src/main/java/io/legado/app/model/localBook/PdfFile.kt b/app/src/main/java/io/legado/app/model/localBook/PdfFile.kt new file mode 100644 index 000000000..42482db73 --- /dev/null +++ b/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 { + 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("") + .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 { + val chapterList = ArrayList() + + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/README.md b/app/src/main/java/io/legado/app/model/localBook/README.md index 04aaa6c38..e11cb2cde 100644 --- a/app/src/main/java/io/legado/app/model/localBook/README.md +++ b/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 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt index 054c0ae19..5b9818c98 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt +++ b/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) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt index 27a541345..f973d72a9 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt +++ b/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) } diff --git a/app/src/main/java/io/legado/app/utils/BitmapUtils.kt b/app/src/main/java/io/legado/app/utils/BitmapUtils.kt index b6ef49b99..3a08e9702 100644 --- a/app/src/main/java/io/legado/app/utils/BitmapUtils.kt +++ b/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 ) + } diff --git a/app/src/main/java/io/legado/app/utils/ConvertExtensions.kt b/app/src/main/java/io/legado/app/utils/ConvertExtensions.kt index 214cc207a..167664327 100644 --- a/app/src/main/java/io/legado/app/utils/ConvertExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ConvertExtensions.kt @@ -112,6 +112,7 @@ object ConvertUtils { return sb.toString() } + } val Int.hexString: String diff --git a/app/src/main/java/io/legado/app/utils/SystemUtils.kt b/app/src/main/java/io/legado/app/utils/SystemUtils.kt index 4ac96eaae..dec3485ff 100644 --- a/app/src/main/java/io/legado/app/utils/SystemUtils.kt +++ b/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 + } } diff --git a/build.gradle b/build.gradle index e777939dd..9ba00cc4f 100644 --- a/build.gradle +++ b/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' } } diff --git a/epublib/build.gradle b/epublib/build.gradle index 5ac5e28c5..db9ef3981 100644 --- a/epublib/build.gradle +++ b/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