图片转pdf,优化拆分功能,拆分加密pdf流程。
This commit is contained in:
parent
85a66a9911
commit
0cded6a58d
@ -63,4 +63,7 @@ dependencies {
|
|||||||
implementation(libs.pdfbox.android)
|
implementation(libs.pdfbox.android)
|
||||||
implementation(libs.jp2forandroid)
|
implementation(libs.jp2forandroid)
|
||||||
implementation(libs.flexbox)
|
implementation(libs.flexbox)
|
||||||
|
implementation(libs.pictureselector)
|
||||||
|
implementation(libs.ucrop)
|
||||||
|
implementation(libs.compress)
|
||||||
}
|
}
|
||||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@ -21,3 +21,8 @@
|
|||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
-keep class com.shockwave.**
|
-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** { *; }
|
||||||
|
|||||||
@ -4,8 +4,15 @@
|
|||||||
android:theme="@style/Theme.PDFReaderPro"
|
android:theme="@style/Theme.PDFReaderPro"
|
||||||
tools:ignore="DiscouragedApi,LockedOrientationActivity,RedundantLabel">
|
tools:ignore="DiscouragedApi,LockedOrientationActivity,RedundantLabel">
|
||||||
|
|
||||||
<!-- Android 11+ 存储权限 -->
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
|
|
||||||
<!-- Android 13+ 媒体权限 -->
|
<!-- Android 13+ 媒体权限 -->
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
@ -107,5 +114,16 @@
|
|||||||
android:resource="@xml/file_paths" />
|
android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
<queries package="${applicationId}">
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.IMAGE_CAPTURE">
|
||||||
|
|
||||||
|
</action>
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.media.action.ACTION_VIDEO_CAPTURE">
|
||||||
|
|
||||||
|
</action>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
@ -10,6 +10,7 @@ import android.view.View
|
|||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.R
|
||||||
import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitResultBinding
|
import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitResultBinding
|
||||||
import com.all.pdfreader.pro.app.model.PdfPickerSource
|
import com.all.pdfreader.pro.app.model.PdfPickerSource
|
||||||
@ -105,7 +106,7 @@ class PdfResultActivity : BaseActivity() {
|
|||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (source == PdfPickerSource.MERGE) {
|
} else if (source == PdfPickerSource.MERGE || source == PdfPickerSource.TO_IMAGES) {
|
||||||
if (inputFile.isEmpty()) {
|
if (inputFile.isEmpty()) {
|
||||||
showToast(getString(R.string.pdf_loading_failed))
|
showToast(getString(R.string.pdf_loading_failed))
|
||||||
finish()
|
finish()
|
||||||
@ -229,6 +230,34 @@ class PdfResultActivity : BaseActivity() {
|
|||||||
)
|
)
|
||||||
resultList.add(result)
|
resultList.add(result)
|
||||||
}
|
}
|
||||||
|
} else if(source == PdfPickerSource.TO_IMAGES){
|
||||||
|
val inputFiles: List<File> = 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) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.processingLayout.visibility = View.GONE
|
binding.processingLayout.visibility = View.GONE
|
||||||
|
|||||||
@ -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.model.RenameType
|
||||||
import com.all.pdfreader.pro.app.ui.adapter.SplitPdfAdapter
|
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.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.PromptDialogFragment
|
||||||
import com.all.pdfreader.pro.app.ui.dialog.RenameDialogFragment
|
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.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.FileUtils.toUnderscoreDateTime
|
||||||
import com.all.pdfreader.pro.app.util.PdfUtils
|
import com.all.pdfreader.pro.app.util.PdfUtils
|
||||||
import com.gyf.immersionbar.ImmersionBar
|
import com.gyf.immersionbar.ImmersionBar
|
||||||
@ -149,6 +151,24 @@ class SplitPdfActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initSplitData(file: File) {
|
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 {
|
lifecycleScope.launch {
|
||||||
splitList.clear()
|
splitList.clear()
|
||||||
// 先切换到 IO 线程执行删除,等待完成
|
// 先切换到 IO 线程执行删除,等待完成
|
||||||
@ -157,7 +177,11 @@ class SplitPdfActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
// 删除完成后,开始收集数据
|
// 删除完成后,开始收集数据
|
||||||
var firstPageLoaded = false
|
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")
|
logDebug("splitPdfToPageItemsFlow pageItem->$pageItem")
|
||||||
if (splitList.size <= pageItem.pageIndex) {
|
if (splitList.size <= pageItem.pageIndex) {
|
||||||
splitList.add(pageItem)
|
splitList.add(pageItem)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment
|
|||||||
import com.all.pdfreader.pro.app.databinding.FragmentFavoriteBinding
|
import com.all.pdfreader.pro.app.databinding.FragmentFavoriteBinding
|
||||||
import com.all.pdfreader.pro.app.room.repository.PdfRepository
|
import com.all.pdfreader.pro.app.room.repository.PdfRepository
|
||||||
import com.all.pdfreader.pro.app.sp.AppStore
|
import com.all.pdfreader.pro.app.sp.AppStore
|
||||||
|
import com.all.pdfreader.pro.app.util.ToastUtils
|
||||||
|
|
||||||
abstract class BaseFrag : Fragment() {
|
abstract class BaseFrag : Fragment() {
|
||||||
protected abstract val TAG: String
|
protected abstract val TAG: String
|
||||||
@ -59,6 +60,10 @@ abstract class BaseFrag : Fragment() {
|
|||||||
Log.w("ocean", "$TAG: $message")
|
Log.w("ocean", "$TAG: $message")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun showToast(message: String) {
|
||||||
|
ToastUtils.show(requireActivity(), message)
|
||||||
|
}
|
||||||
|
|
||||||
//获取数据库实例
|
//获取数据库实例
|
||||||
protected fun getRepository(): PdfRepository {
|
protected fun getRepository(): PdfRepository {
|
||||||
return PdfRepository.getInstance()
|
return PdfRepository.getInstance()
|
||||||
|
|||||||
@ -1,15 +1,39 @@
|
|||||||
package com.all.pdfreader.pro.app.ui.fragment
|
package com.all.pdfreader.pro.app.ui.fragment
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.fragment.app.Fragment
|
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.databinding.FragmentToolsBinding
|
||||||
import com.all.pdfreader.pro.app.model.PdfPickerSource
|
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.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
|
private lateinit var binding: FragmentToolsBinding
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
@ -41,5 +65,110 @@ class ToolsFrag : Fragment() {
|
|||||||
val intent = PdfPickerActivity.createIntent(requireActivity(), PdfPickerSource.UNLOCK)
|
val intent = PdfPickerActivity.createIntent(requireActivity(), PdfPickerSource.UNLOCK)
|
||||||
startActivity(intent)
|
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<Uri?>?,
|
||||||
|
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<Any>(inputUri, destinationUri)
|
||||||
|
val buildOptions = UCrop.Options().apply {
|
||||||
|
isDarkStatusBarBlack(true)
|
||||||
|
}
|
||||||
|
uCrop.withOptions(buildOptions)
|
||||||
|
uCrop.start(requireActivity(), fragment, requestCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
}).forResult(object : OnResultCallbackListener<LocalMedia> {
|
||||||
|
override fun onResult(result: ArrayList<LocalMedia>) {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,6 +21,7 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import com.all.pdfreader.pro.app.model.RenameResult
|
import com.all.pdfreader.pro.app.model.RenameResult
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
object FileUtils {
|
object FileUtils {
|
||||||
|
|
||||||
@ -330,6 +331,7 @@ object FileUtils {
|
|||||||
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH)
|
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH)
|
||||||
return sdf.format(Date(this))
|
return sdf.format(Date(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FileInfo(
|
data class FileInfo(
|
||||||
val name: String, val size: Long, val uri: Uri
|
val name: String, val size: Long, val uri: Uri
|
||||||
)
|
)
|
||||||
@ -368,11 +370,12 @@ object FileUtils {
|
|||||||
*/
|
*/
|
||||||
fun isPdfPasswordCorrect(context: Context, file: File, password: String): Boolean {
|
fun isPdfPasswordCorrect(context: Context, file: File, password: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { descriptor ->
|
||||||
val pdfiumCore = PdfiumCore(context)
|
val pdfiumCore = PdfiumCore(context)
|
||||||
val doc = pdfiumCore.newDocument(descriptor, password)
|
val doc = pdfiumCore.newDocument(descriptor, password)
|
||||||
pdfiumCore.closeDocument(doc)
|
pdfiumCore.closeDocument(doc)
|
||||||
true // 没抛异常说明密码正确
|
}
|
||||||
|
true// 没抛异常说明密码正确
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
false // 抛异常说明密码错误
|
false // 抛异常说明密码错误
|
||||||
}
|
}
|
||||||
@ -557,4 +560,28 @@ object FileUtils {
|
|||||||
return newFile
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package com.all.pdfreader.pro.app.util
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.graphics.createBitmap
|
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.io.MemoryUsageSetting
|
||||||
import com.tom_roush.pdfbox.multipdf.PDFMergerUtility
|
import com.tom_roush.pdfbox.multipdf.PDFMergerUtility
|
||||||
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -44,11 +50,17 @@ object PdfUtils {
|
|||||||
inputFile: File,
|
inputFile: File,
|
||||||
dpi: Float = 72f,
|
dpi: Float = 72f,
|
||||||
chunkSize: Int = 5,
|
chunkSize: Int = 5,
|
||||||
thumbWidth: Int = 200
|
thumbWidth: Int = 200,
|
||||||
|
password: String? = null
|
||||||
): Flow<PdfPageItem> = flow {
|
): Flow<PdfPageItem> = flow {
|
||||||
val pdfiumCore = PdfiumCore(context)
|
val pdfiumCore = PdfiumCore(context)
|
||||||
ParcelFileDescriptor.open(inputFile, ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
|
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 pageCount = pdfiumCore.getPageCount(pdfDocument)
|
||||||
|
|
||||||
val pages = List(pageCount) {
|
val pages = List(pageCount) {
|
||||||
@ -208,4 +220,83 @@ object PdfUtils {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片转 PDF(优化版,安全、快速、低内存)
|
||||||
|
*/
|
||||||
|
suspend fun imgToPdfFilesSafe(
|
||||||
|
inputFiles: List<File>,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
app/src/main/res/drawable/engine_image_placeholder.xml
Normal file
16
app/src/main/res/drawable/engine_image_placeholder.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<size
|
||||||
|
android:width="100dp"
|
||||||
|
android:height="100dp" />
|
||||||
|
<solid android:color="@color/ps_color_light_grey" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@drawable/ps_ic_placeholder" />
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
5
app/src/main/res/drawable/num_checkbox_selector.xml
Normal file
5
app/src/main/res/drawable/num_checkbox_selector.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/num_oval_normal" android:state_selected="false" />
|
||||||
|
<item android:drawable="@drawable/num_oval_selected" android:state_selected="true" />
|
||||||
|
</selector>
|
||||||
13
app/src/main/res/drawable/num_oval_normal.xml
Normal file
13
app/src/main/res/drawable/num_oval_normal.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval"
|
||||||
|
android:useLevel="false">
|
||||||
|
|
||||||
|
<solid android:color="@color/ps_color_transparent" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#E0DBDBDB" />
|
||||||
|
<size
|
||||||
|
android:width="18dp"
|
||||||
|
android:height="18dp" />
|
||||||
|
</shape>
|
||||||
9
app/src/main/res/drawable/num_oval_selected.xml
Normal file
9
app/src/main/res/drawable/num_oval_selected.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval"
|
||||||
|
android:useLevel="false">
|
||||||
|
<solid android:color="@color/btn_sel_on_color" />
|
||||||
|
<size
|
||||||
|
android:width="18dp"
|
||||||
|
android:height="18dp" />
|
||||||
|
</shape>
|
||||||
@ -35,4 +35,23 @@
|
|||||||
android:text="@string/unlock_pdf" />
|
android:text="@string/unlock_pdf" />
|
||||||
|
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/imgToPdfBtn"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/img_to_pdf" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/fragment_fl"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@ -113,6 +113,7 @@
|
|||||||
<string name="duplicate_created_successfully">Duplicate file created successfully</string>
|
<string name="duplicate_created_successfully">Duplicate file created successfully</string>
|
||||||
<string name="duplicate_created_failed">Duplicate file created failed</string>
|
<string name="duplicate_created_failed">Duplicate file created failed</string>
|
||||||
<string name="processing">Processing…</string>
|
<string name="processing">Processing…</string>
|
||||||
|
<string name="image_processing">Image Processing…</string>
|
||||||
<string name="view_model">View Model</string>
|
<string name="view_model">View Model</string>
|
||||||
<string name="page_by_page">Page by page</string>
|
<string name="page_by_page">Page by page</string>
|
||||||
<string name="color_inversion">color inversion</string>
|
<string name="color_inversion">color inversion</string>
|
||||||
@ -154,4 +155,6 @@
|
|||||||
<string name="clear">Clear</string>
|
<string name="clear">Clear</string>
|
||||||
<string name="please_select_a_file">Please select a file</string>
|
<string name="please_select_a_file">Please select a file</string>
|
||||||
<string name="unknown_source">Unknown Source</string>
|
<string name="unknown_source">Unknown Source</string>
|
||||||
|
<string name="img_to_pdf">Image to PDF</string>
|
||||||
|
<string name="pdf_to_img">PDF to image</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -16,6 +16,7 @@ lifecycleRuntimeKtx = "2.9.2"
|
|||||||
immersionbar = "3.2.2"
|
immersionbar = "3.2.2"
|
||||||
immersionbarKtx = "3.2.2"
|
immersionbarKtx = "3.2.2"
|
||||||
pdfboxAndroid = "2.0.27.0"
|
pdfboxAndroid = "2.0.27.0"
|
||||||
|
pictureselector = "v3.11.2"
|
||||||
room_version = "2.7.2"
|
room_version = "2.7.2"
|
||||||
swiperefreshlayout = "1.1.0"
|
swiperefreshlayout = "1.1.0"
|
||||||
recyclerview = "1.4.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-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-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room_version" }
|
||||||
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", 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" }
|
flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" }
|
||||||
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
||||||
jp2forandroid = { module = "com.github.Tgo1014:JP2ForAndroid", version.ref = "jp2forandroid" }
|
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-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
|
||||||
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
|
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
|
||||||
pdfbox-android = { module = "com.tom-roush:pdfbox-android", version.ref = "pdfboxAndroid" }
|
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" }
|
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" }
|
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||||
androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" }
|
androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" }
|
||||||
|
ucrop = { module = "io.github.lucksiege:ucrop", version.ref = "pictureselector" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user