From 0cded6a58d742498c5afd408118af167ba4fbfac Mon Sep 17 00:00:00 2001
From: ocean <503259349@qq.com>
Date: Thu, 16 Oct 2025 14:32:59 +0800
Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E7=89=87=E8=BD=ACpdf=EF=BC=8C?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8B=86=E5=88=86=E5=8A=9F=E8=83=BD=EF=BC=8C?=
=?UTF-8?q?=E6=8B=86=E5=88=86=E5=8A=A0=E5=AF=86pdf=E6=B5=81=E7=A8=8B?=
=?UTF-8?q?=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/build.gradle.kts | 3 +
app/proguard-rules.pro | 7 +-
app/src/main/AndroidManifest.xml | 20 ++-
.../pro/app/ui/act/PdfResultActivity.kt | 31 ++++-
.../pro/app/ui/act/SplitPdfActivity.kt | 26 +++-
.../pdfreader/pro/app/ui/fragment/BaseFrag.kt | 5 +
.../pro/app/ui/fragment/ToolsFrag.kt | 131 +++++++++++++++++-
.../all/pdfreader/pro/app/util/FileUtils.kt | 39 +++++-
.../all/pdfreader/pro/app/util/GlideEngine.kt | 87 ++++++++++++
.../all/pdfreader/pro/app/util/PdfUtils.kt | 95 ++++++++++++-
.../res/drawable/engine_image_placeholder.xml | 16 +++
.../res/drawable/num_checkbox_selector.xml | 5 +
app/src/main/res/drawable/num_oval_normal.xml | 13 ++
.../main/res/drawable/num_oval_selected.xml | 9 ++
app/src/main/res/layout/fragment_tools.xml | 19 +++
app/src/main/res/values/strings.xml | 3 +
gradle/libs.versions.toml | 4 +
17 files changed, 500 insertions(+), 13 deletions(-)
create mode 100644 app/src/main/java/com/all/pdfreader/pro/app/util/GlideEngine.kt
create mode 100644 app/src/main/res/drawable/engine_image_placeholder.xml
create mode 100644 app/src/main/res/drawable/num_checkbox_selector.xml
create mode 100644 app/src/main/res/drawable/num_oval_normal.xml
create mode 100644 app/src/main/res/drawable/num_oval_selected.xml
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3ff13d9..5e7cb52 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -63,4 +63,7 @@ dependencies {
implementation(libs.pdfbox.android)
implementation(libs.jp2forandroid)
implementation(libs.flexbox)
+ implementation(libs.pictureselector)
+ implementation(libs.ucrop)
+ implementation(libs.compress)
}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index c639003..e43e75c 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -20,4 +20,9 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile
--keep class com.shockwave.**
\ No newline at end of file
+-keep class com.shockwave.**
+-keep class com.luck.picture.lib.** { *; }
+-keep class com.luck.lib.camerax.** { *; }
+-dontwarn com.yalantis.ucrop**
+-keep class com.yalantis.ucrop** { *; }
+-keep interface com.yalantis.ucrop** { *; }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6c79ea4..bd1d9e6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,8 +4,15 @@
android:theme="@style/Theme.PDFReaderPro"
tools:ignore="DiscouragedApi,LockedOrientationActivity,RedundantLabel">
-
+
+
+
+
+
+
@@ -107,5 +114,16 @@
android:resource="@xml/file_paths" />
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt
index ff7963f..6ddcec6 100644
--- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt
@@ -10,6 +10,7 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
+import com.all.pdfreader.pro.app.PRApp
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitResultBinding
import com.all.pdfreader.pro.app.model.PdfPickerSource
@@ -105,7 +106,7 @@ class PdfResultActivity : BaseActivity() {
finish()
return
}
- } else if (source == PdfPickerSource.MERGE) {
+ } else if (source == PdfPickerSource.MERGE || source == PdfPickerSource.TO_IMAGES) {
if (inputFile.isEmpty()) {
showToast(getString(R.string.pdf_loading_failed))
finish()
@@ -229,6 +230,34 @@ class PdfResultActivity : BaseActivity() {
)
resultList.add(result)
}
+ } else if(source == PdfPickerSource.TO_IMAGES){
+ val inputFiles: List = inputFile.map { path -> File(path) }
+ val outputDir = File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
+ "PDFReaderPro/img2Pdf"
+ ).apply { if (!exists()) mkdirs() }
+ val outputFileName =
+ getString(R.string.app_name) + "_" + System.currentTimeMillis()
+ .toUnderscoreDateTime() + ".pdf"
+ PdfUtils.imgToPdfFilesSafe(
+ inputFiles = inputFiles,
+ outputDir = outputDir,
+ outputFileName = outputFileName,
+ onProgress = { current, total ->
+ logDebug("current->$current total->$total")
+ val progressPercent = current * 100 / total
+ logDebug("progressPercent->$progressPercent")
+ runOnUiThread {
+ binding.progressBar.progress = progressPercent
+ binding.progressTv.text = "$progressPercent"
+ }
+ })?.let { resultFile ->
+ val thumbnails = generateFastThumbnail(this@PdfResultActivity, resultFile)
+ val result = PdfSplitResultItem(resultFile.absolutePath, thumbnails, false)
+ pdfScanner.addNewPdfToDatabase(result.filePath, result.thumbnailPath) {
+ resultList.add(result)
+ }
+ }
}
withContext(Dispatchers.Main) {
binding.processingLayout.visibility = View.GONE
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt
index b60fee6..08b0f72 100644
--- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SplitPdfActivity.kt
@@ -16,9 +16,11 @@ import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
import com.all.pdfreader.pro.app.model.RenameType
import com.all.pdfreader.pro.app.ui.adapter.SplitPdfAdapter
import com.all.pdfreader.pro.app.ui.adapter.SplitSelectedPdfAdapter
+import com.all.pdfreader.pro.app.ui.dialog.PdfPasswordProtectionDialogFragment
import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment
import com.all.pdfreader.pro.app.ui.dialog.RenameDialogFragment
import com.all.pdfreader.pro.app.util.AppUtils.setOnSingleClickListener
+import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted
import com.all.pdfreader.pro.app.util.FileUtils.toUnderscoreDateTime
import com.all.pdfreader.pro.app.util.PdfUtils
import com.gyf.immersionbar.ImmersionBar
@@ -149,6 +151,24 @@ class SplitPdfActivity : BaseActivity() {
}
private fun initSplitData(file: File) {
+ lifecycleScope.launch {
+ // 是否存在密码
+ val isEncrypted = withContext(Dispatchers.IO) {
+ isPdfEncrypted(file)
+ }
+ if (isEncrypted) {
+ PdfPasswordProtectionDialogFragment(file, onOkClick = { password ->
+ initSplitDataWithPassword(file, password)
+ }, onCancelClick = {
+ finish()
+ }).show(supportFragmentManager, TAG)
+ } else {
+ initSplitDataWithPassword(file)
+ }
+ }
+ }
+
+ private fun initSplitDataWithPassword(file: File, password: String? = null) {
lifecycleScope.launch {
splitList.clear()
// 先切换到 IO 线程执行删除,等待完成
@@ -157,7 +177,11 @@ class SplitPdfActivity : BaseActivity() {
}
// 删除完成后,开始收集数据
var firstPageLoaded = false
- PdfUtils.splitPdfToPageItemsFlow(this@SplitPdfActivity, file).collect { pageItem ->
+ PdfUtils.splitPdfToPageItemsFlow(
+ context = this@SplitPdfActivity,
+ inputFile = file,
+ password = password
+ ).collect { pageItem ->
logDebug("splitPdfToPageItemsFlow pageItem->$pageItem")
if (splitList.size <= pageItem.pageIndex) {
splitList.add(pageItem)
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt
index e8b57a8..2cd2a42 100644
--- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/BaseFrag.kt
@@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment
import com.all.pdfreader.pro.app.databinding.FragmentFavoriteBinding
import com.all.pdfreader.pro.app.room.repository.PdfRepository
import com.all.pdfreader.pro.app.sp.AppStore
+import com.all.pdfreader.pro.app.util.ToastUtils
abstract class BaseFrag : Fragment() {
protected abstract val TAG: String
@@ -59,6 +60,10 @@ abstract class BaseFrag : Fragment() {
Log.w("ocean", "$TAG: $message")
}
+ protected fun showToast(message: String) {
+ ToastUtils.show(requireActivity(), message)
+ }
+
//获取数据库实例
protected fun getRepository(): PdfRepository {
return PdfRepository.getInstance()
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt
index 36bb404..0eaf49b 100644
--- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt
+++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt
@@ -1,15 +1,39 @@
package com.all.pdfreader.pro.app.ui.fragment
+import android.content.Context
+import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.net.toUri
import androidx.fragment.app.Fragment
+import com.all.pdfreader.pro.app.PRApp
+import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.FragmentToolsBinding
import com.all.pdfreader.pro.app.model.PdfPickerSource
import com.all.pdfreader.pro.app.ui.act.PdfPickerActivity
+import com.all.pdfreader.pro.app.ui.act.PdfResultActivity
+import com.all.pdfreader.pro.app.util.GlideEngine
+import com.luck.picture.lib.basic.PictureSelector
+import com.luck.picture.lib.config.PictureMimeType
+import com.luck.picture.lib.config.SelectMimeType
+import com.luck.picture.lib.config.SelectModeConfig
+import com.luck.picture.lib.engine.CompressFileEngine
+import com.luck.picture.lib.entity.LocalMedia
+import com.luck.picture.lib.interfaces.OnKeyValueResultCallbackListener
+import com.luck.picture.lib.interfaces.OnMediaEditInterceptListener
+import com.luck.picture.lib.interfaces.OnResultCallbackListener
+import com.luck.picture.lib.style.PictureSelectorStyle
+import com.luck.picture.lib.style.SelectMainStyle
+import com.luck.picture.lib.utils.DateUtils
+import com.yalantis.ucrop.UCrop
+import top.zibin.luban.Luban
+import top.zibin.luban.OnNewCompressListener
+import java.io.File
-class ToolsFrag : Fragment() {
+class ToolsFrag : BaseFrag() {
+ override val TAG: String = "ToolsFrag"
private lateinit var binding: FragmentToolsBinding
override fun onCreateView(
@@ -41,5 +65,110 @@ class ToolsFrag : Fragment() {
val intent = PdfPickerActivity.createIntent(requireActivity(), PdfPickerSource.UNLOCK)
startActivity(intent)
}
+ binding.imgToPdfBtn.setOnClickListener {
+ openImagePicker()
+ }
+ }
+
+ private fun openImagePicker() {
+ val selectorStyle = PictureSelectorStyle().apply {
+ selectMainStyle = SelectMainStyle().apply {
+ isSelectNumberStyle = true
+ isPreviewSelectNumberStyle = true
+ selectBackground = R.drawable.num_checkbox_selector
+ }
+ }
+ PictureSelector.create(this)
+ .openGallery(SelectMimeType.ofImage())
+ .setSelectorUIStyle(selectorStyle)
+ .setImageEngine(GlideEngine.createGlideEngine())
+ .setImageSpanCount(3)
+ .isGif(false)
+ .isFastSlidingSelect(true)
+ .setSelectionMode(SelectModeConfig.MULTIPLE)
+ .setMaxSelectNum(50)
+
+ .setCompressEngine(object : CompressFileEngine {
+ override fun onStartCompress(
+ context: Context?,
+ source: ArrayList?,
+ call: OnKeyValueResultCallbackListener?
+ ) {
+ Luban.with(context)
+ .load(source)
+ .ignoreBy(100)
+ .setCompressListener(object : OnNewCompressListener {
+ override fun onStart() {
+
+ }
+
+ override fun onSuccess(
+ source: String?,
+ compressFile: File?
+ ) {
+ call?.onCallback(source, compressFile?.absolutePath)
+ }
+
+ override fun onError(source: String?, e: Throwable?) {
+ call?.onCallback(source, null)
+ }
+ })
+ .launch()
+ }
+ })
+ .setEditMediaInterceptListener(object : OnMediaEditInterceptListener {
+ override fun onStartMediaEdit(
+ fragment: Fragment, currentLocalMedia: LocalMedia, requestCode: Int
+ ) {
+ val currentEditPath = currentLocalMedia.getAvailablePath()
+ val inputUri = if (PictureMimeType.isContent(currentEditPath)) {
+ currentEditPath.toUri()
+ } else {
+ Uri.fromFile(File(currentEditPath))
+ }
+ val destinationUri = Uri.fromFile(
+ File(
+ getSandboxPath(), DateUtils.getCreateFileName("CROP_") + ".jpeg"
+ )
+ )
+ val uCrop = UCrop.of(inputUri, destinationUri)
+ val buildOptions = UCrop.Options().apply {
+ isDarkStatusBarBlack(true)
+ }
+ uCrop.withOptions(buildOptions)
+ uCrop.start(requireActivity(), fragment, requestCode)
+ }
+
+ }).forResult(object : OnResultCallbackListener {
+ override fun onResult(result: ArrayList) {
+ logDebug("forResult ->${result.size}")
+ if (result.isNotEmpty()) {
+ // result 中可能有带裁剪的 Uri 或原图 Uri
+ // 取出路径列表
+ val paths = result.mapNotNull { media ->
+ media.availablePath
+ }
+ if (paths.isEmpty()) {
+ return
+ }
+ val intent = PdfResultActivity.createIntentInputFile(
+ requireActivity(),
+ ArrayList(paths),
+ PdfPickerSource.TO_IMAGES
+ )
+ startActivity(intent)
+ }
+ }
+
+ override fun onCancel() {
+ logDebug("onCancel")
+ }
+ })
+ }
+
+ fun getSandboxPath(): String {
+ val dir = File(PRApp.getContext().externalCacheDir, "SandboxPdfPro")
+ if (!dir.exists()) dir.mkdirs()
+ return dir.absolutePath
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt
index e2434b1..b72eb6c 100644
--- a/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt
+++ b/app/src/main/java/com/all/pdfreader/pro/app/util/FileUtils.kt
@@ -6,7 +6,7 @@ import android.graphics.pdf.PdfRenderer
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
-import android.provider.OpenableColumns
+import android.provider.OpenableColumns
import android.util.Log
import com.all.pdfreader.pro.app.PRApp
import com.all.pdfreader.pro.app.R
@@ -21,6 +21,7 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import com.all.pdfreader.pro.app.model.RenameResult
+import androidx.core.net.toUri
object FileUtils {
@@ -330,6 +331,7 @@ object FileUtils {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH)
return sdf.format(Date(this))
}
+
data class FileInfo(
val name: String, val size: Long, val uri: Uri
)
@@ -368,11 +370,12 @@ object FileUtils {
*/
fun isPdfPasswordCorrect(context: Context, file: File, password: String): Boolean {
return try {
- val descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
- val pdfiumCore = PdfiumCore(context)
- val doc = pdfiumCore.newDocument(descriptor, password)
- pdfiumCore.closeDocument(doc)
- true // 没抛异常说明密码正确
+ ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { descriptor ->
+ val pdfiumCore = PdfiumCore(context)
+ val doc = pdfiumCore.newDocument(descriptor, password)
+ pdfiumCore.closeDocument(doc)
+ }
+ true// 没抛异常说明密码正确
} catch (e: Exception) {
false // 抛异常说明密码错误
}
@@ -557,4 +560,28 @@ object FileUtils {
return newFile
}
+
+ fun uriToFile(context: Context, path: String): File? {
+ return try {
+ if (path.startsWith("content://")) {
+ // 是 content:// Uri,需要拷贝到临时文件
+ val uri = path.toUri()
+ val inputStream = context.contentResolver.openInputStream(uri) ?: return null
+ val tempFile = File(context.cacheDir, "temp_${System.currentTimeMillis()}.jpg")
+ tempFile.outputStream().use { output ->
+ inputStream.copyTo(output)
+ }
+ inputStream.close()
+ tempFile
+ } else {
+ // 是普通文件路径
+ val file = File(path)
+ if (file.exists()) file else null
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/GlideEngine.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/GlideEngine.kt
new file mode 100644
index 0000000..5ecc988
--- /dev/null
+++ b/app/src/main/java/com/all/pdfreader/pro/app/util/GlideEngine.kt
@@ -0,0 +1,87 @@
+package com.all.pdfreader.pro.app.util
+
+import android.content.Context
+import android.widget.ImageView
+import com.all.pdfreader.pro.app.R
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.luck.picture.lib.engine.ImageEngine
+import com.luck.picture.lib.utils.ActivityCompatHelper
+
+class GlideEngine private constructor() : ImageEngine {
+
+ /**
+ * 加载图片
+ */
+ override fun loadImage(context: Context, url: String?, imageView: ImageView) {
+ if (!ActivityCompatHelper.assertValidRequest(context)) return
+ Glide.with(context)
+ .load(url)
+ .into(imageView)
+ }
+
+ /**
+ * 按指定大小加载图片
+ */
+ override fun loadImage(
+ context: Context,
+ imageView: ImageView,
+ url: String?,
+ maxWidth: Int,
+ maxHeight: Int
+ ) {
+ if (!ActivityCompatHelper.assertValidRequest(context)) return
+ Glide.with(context)
+ .load(url)
+ .override(maxWidth, maxHeight)
+ .into(imageView)
+ }
+
+ /**
+ * 加载相册目录封面
+ */
+ override fun loadAlbumCover(context: Context, url: String?, imageView: ImageView) {
+ if (!ActivityCompatHelper.assertValidRequest(context)) return
+ Glide.with(context)
+ .asBitmap()
+ .load(url)
+ .override(180, 180)
+ .sizeMultiplier(0.5f)
+ .transform(CenterCrop(), RoundedCorners(8))
+ .placeholder(R.drawable.engine_image_placeholder)
+ .into(imageView)
+ }
+
+ /**
+ * 加载图片列表图片
+ */
+ override fun loadGridImage(context: Context, url: String?, imageView: ImageView) {
+ if (!ActivityCompatHelper.assertValidRequest(context)) return
+ Glide.with(context)
+ .load(url)
+ .override(200, 200)
+ .centerCrop()
+ .placeholder(R.drawable.engine_image_placeholder)
+ .into(imageView)
+ }
+
+ override fun pauseRequests(context: Context) {
+ if (!ActivityCompatHelper.assertValidRequest(context)) return
+ Glide.with(context).pauseRequests()
+ }
+
+ override fun resumeRequests(context: Context) {
+ if (!ActivityCompatHelper.assertValidRequest(context)) return
+ Glide.with(context).resumeRequests()
+ }
+
+ companion object {
+ @JvmStatic
+ fun createGlideEngine(): GlideEngine = InstanceHolder.instance
+ }
+
+ private object InstanceHolder {
+ val instance = GlideEngine()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt
index b938cf4..b009b59 100644
--- a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt
+++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt
@@ -2,6 +2,7 @@ package com.all.pdfreader.pro.app.util
import android.content.Context
import android.graphics.Bitmap
+import android.graphics.BitmapFactory
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.core.graphics.createBitmap
@@ -10,6 +11,11 @@ import com.shockwave.pdfium.PdfiumCore
import com.tom_roush.pdfbox.io.MemoryUsageSetting
import com.tom_roush.pdfbox.multipdf.PDFMergerUtility
import com.tom_roush.pdfbox.pdmodel.PDDocument
+import com.tom_roush.pdfbox.pdmodel.PDPage
+import com.tom_roush.pdfbox.pdmodel.PDPageContentStream
+import com.tom_roush.pdfbox.pdmodel.common.PDRectangle
+import com.tom_roush.pdfbox.pdmodel.graphics.image.JPEGFactory
+import com.tom_roush.pdfbox.pdmodel.graphics.image.LosslessFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -44,11 +50,17 @@ object PdfUtils {
inputFile: File,
dpi: Float = 72f,
chunkSize: Int = 5,
- thumbWidth: Int = 200
+ thumbWidth: Int = 200,
+ password: String? = null
): Flow = flow {
val pdfiumCore = PdfiumCore(context)
ParcelFileDescriptor.open(inputFile, ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
- val pdfDocument = pdfiumCore.newDocument(fd)
+ val pdfDocument = if (password.isNullOrEmpty()) {
+ pdfiumCore.newDocument(fd)
+ } else {
+ pdfiumCore.newDocument(fd, password)
+ }
+
val pageCount = pdfiumCore.getPageCount(pdfDocument)
val pages = List(pageCount) {
@@ -208,4 +220,83 @@ object PdfUtils {
null
}
}
+
+ /**
+ * 图片转 PDF(优化版,安全、快速、低内存)
+ */
+ suspend fun imgToPdfFilesSafe(
+ inputFiles: List,
+ outputDir: File,
+ outputFileName: String,
+ onProgress: ((current: Int, total: Int) -> Unit)? = null
+ ): File? = withContext(Dispatchers.IO) {
+ if (inputFiles.isEmpty()) return@withContext null
+
+ val outputFile = File(outputDir, outputFileName)
+ if (!outputDir.exists()) outputDir.mkdirs()
+
+ var document: PDDocument? = null
+ try {
+ document = PDDocument()
+ val total = inputFiles.size
+
+ inputFiles.forEachIndexed { index, imageFile ->
+ try {
+ //安全加载压缩后的图片
+ val options = BitmapFactory.Options().apply {
+ inPreferredConfig = Bitmap.Config.RGB_565
+ }
+ val bitmap = BitmapFactory.decodeFile(imageFile.absolutePath, options)
+ ?: return@forEachIndexed
+
+ //按 A4 比例缩放
+ val a4Width = PDRectangle.A4.width
+ val a4Height = PDRectangle.A4.height
+ val imageRatio = bitmap.width.toFloat() / bitmap.height
+ val pageRatio = a4Width / a4Height
+
+ val targetWidth: Float
+ val targetHeight: Float
+ if (imageRatio > pageRatio) {
+ targetWidth = a4Width
+ targetHeight = a4Width / imageRatio
+ } else {
+ targetHeight = a4Height
+ targetWidth = a4Height * imageRatio
+ }
+
+ val offsetX = (a4Width - targetWidth) / 2
+ val offsetY = (a4Height - targetHeight) / 2
+
+ val page = PDPage(PDRectangle.A4)
+ document.addPage(page)
+
+ val pdImage = JPEGFactory.createFromImage(document, bitmap)
+ PDPageContentStream(document, page).use { contentStream ->
+ contentStream.drawImage(pdImage, offsetX, offsetY, targetWidth, targetHeight)
+ }
+
+ bitmap.recycle()
+
+ } catch (e: OutOfMemoryError) {
+ e.printStackTrace()
+ System.gc() // 手动GC一次(极端情况下)
+ return@forEachIndexed
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ onProgress?.invoke(index + 1, total)
+ }
+
+ document.save(outputFile)
+ outputFile
+
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ } finally {
+ document?.close()
+ }
+ }
}
diff --git a/app/src/main/res/drawable/engine_image_placeholder.xml b/app/src/main/res/drawable/engine_image_placeholder.xml
new file mode 100644
index 0000000..8c63cb1
--- /dev/null
+++ b/app/src/main/res/drawable/engine_image_placeholder.xml
@@ -0,0 +1,16 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/num_checkbox_selector.xml b/app/src/main/res/drawable/num_checkbox_selector.xml
new file mode 100644
index 0000000..31c8361
--- /dev/null
+++ b/app/src/main/res/drawable/num_checkbox_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/num_oval_normal.xml b/app/src/main/res/drawable/num_oval_normal.xml
new file mode 100644
index 0000000..8e6c552
--- /dev/null
+++ b/app/src/main/res/drawable/num_oval_normal.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/num_oval_selected.xml b/app/src/main/res/drawable/num_oval_selected.xml
new file mode 100644
index 0000000..89c689c
--- /dev/null
+++ b/app/src/main/res/drawable/num_oval_selected.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_tools.xml b/app/src/main/res/layout/fragment_tools.xml
index 921f28d..9fa1944 100644
--- a/app/src/main/res/layout/fragment_tools.xml
+++ b/app/src/main/res/layout/fragment_tools.xml
@@ -35,4 +35,23 @@
android:text="@string/unlock_pdf" />
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c82a54a..90fa28f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -113,6 +113,7 @@
Duplicate file created successfully
Duplicate file created failed
Processing…
+ Image Processing…
View Model
Page by page
color inversion
@@ -154,4 +155,6 @@
Clear
Please select a file
Unknown Source
+ Image to PDF
+ PDF to image
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e45212e..e9d15ca 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -16,6 +16,7 @@ lifecycleRuntimeKtx = "2.9.2"
immersionbar = "3.2.2"
immersionbarKtx = "3.2.2"
pdfboxAndroid = "2.0.27.0"
+pictureselector = "v3.11.2"
room_version = "2.7.2"
swiperefreshlayout = "1.1.0"
recyclerview = "1.4.0"
@@ -31,6 +32,7 @@ androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room_version" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room_version" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room_version" }
+compress = { module = "io.github.lucksiege:compress", version.ref = "pictureselector" }
flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
jp2forandroid = { module = "com.github.Tgo1014:JP2ForAndroid", version.ref = "jp2forandroid" }
@@ -43,9 +45,11 @@ immersionbar-ktx = { group = "com.geyifeng.immersionbar", name = "immersionbar-k
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
pdfbox-android = { module = "com.tom-roush:pdfbox-android", version.ref = "pdfboxAndroid" }
+pictureselector = { module = "io.github.lucksiege:pictureselector", version.ref = "pictureselector" }
protolite-well-known-types = { group = "com.google.firebase", name = "protolite-well-known-types", version.ref = "protoliteWellKnownTypes" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" }
+ucrop = { module = "io.github.lucksiege:ucrop", version.ref = "pictureselector" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }