图片转pdf,优化拆分功能,拆分加密pdf流程。
This commit is contained in:
parent
85a66a9911
commit
0cded6a58d
@ -63,4 +63,7 @@ dependencies {
|
||||
implementation(libs.pdfbox.android)
|
||||
implementation(libs.jp2forandroid)
|
||||
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
|
||||
|
||||
-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"
|
||||
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.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
<!-- Android 13+ 媒体权限 -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
@ -107,5 +114,16 @@
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</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>
|
||||
@ -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<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) {
|
||||
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.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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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<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.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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.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<PdfPageItem> = 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<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" />
|
||||
|
||||
|
||||
<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>
|
||||
@ -113,6 +113,7 @@
|
||||
<string name="duplicate_created_successfully">Duplicate file created successfully</string>
|
||||
<string name="duplicate_created_failed">Duplicate file created failed</string>
|
||||
<string name="processing">Processing…</string>
|
||||
<string name="image_processing">Image Processing…</string>
|
||||
<string name="view_model">View Model</string>
|
||||
<string name="page_by_page">Page by page</string>
|
||||
<string name="color_inversion">color inversion</string>
|
||||
@ -154,4 +155,6 @@
|
||||
<string name="clear">Clear</string>
|
||||
<string name="please_select_a_file">Please select a file</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>
|
||||
@ -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" }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user