添加pdf转换图片功能
优化一些其余项
This commit is contained in:
parent
2c88b4271d
commit
d9dfa75b9b
@ -7,12 +7,13 @@
|
||||
<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"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- Android 13+ 媒体权限 -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
@ -104,6 +105,13 @@
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".ui.act.PdfToImageActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
@ -6,6 +6,7 @@ enum class PdfPickerSource {
|
||||
SPLIT, // PDF拆分
|
||||
LOCK, // PDF加密
|
||||
UNLOCK, // PDF解密
|
||||
TO_IMAGES, // PDF转图片
|
||||
IMAGES_TO_PDF,// 图片转PDF
|
||||
PDF_TO_IMAGES,// PDF转图片
|
||||
TO_LONG_IMAGE // PDF转长图
|
||||
}
|
||||
@ -3,6 +3,7 @@ package com.all.pdfreader.pro.app.ui.act
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
@ -55,6 +56,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupDoubleBackExit()
|
||||
initObserve()
|
||||
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
|
||||
.navigationBarColor(R.color.white).init()
|
||||
@ -475,4 +477,25 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
||||
binding.multiSelectMergeBtn.isClickable = false
|
||||
}
|
||||
}
|
||||
|
||||
private var lastBackPressedTime: Long = 0
|
||||
private val EXIT_INTERVAL = 2000L // 2 秒内双击退出
|
||||
|
||||
private fun setupDoubleBackExit() {
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastBackPressedTime < EXIT_INTERVAL) {
|
||||
// 双击退出
|
||||
isEnabled = false // 解除拦截
|
||||
onBackPressedDispatcher.onBackPressed() // 调用系统默认返回逻辑
|
||||
} else {
|
||||
lastBackPressedTime = currentTime
|
||||
showToast(getString(R.string.press_again_to_exit))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -105,9 +105,14 @@ class PdfPickerActivity : BaseActivity() {
|
||||
}).show(supportFragmentManager, "PdfRemovePasswordDialog")
|
||||
}
|
||||
|
||||
PdfPickerSource.TO_IMAGES -> {}
|
||||
PdfPickerSource.IMAGES_TO_PDF -> {}
|
||||
PdfPickerSource.TO_LONG_IMAGE -> {}
|
||||
PdfPickerSource.NONE -> {}
|
||||
PdfPickerSource.PDF_TO_IMAGES -> {
|
||||
val intent = PdfToImageActivity.createIntent(this, pdf.filePath)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
},
|
||||
onSelectModelItemClick = {
|
||||
|
||||
@ -13,6 +13,7 @@ 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.PdfPageItem
|
||||
import com.all.pdfreader.pro.app.model.PdfPickerSource
|
||||
import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
|
||||
import com.all.pdfreader.pro.app.model.PdfSplitResultItem
|
||||
@ -43,6 +44,8 @@ class PdfResultActivity : BaseActivity() {
|
||||
private const val EXTRA_LOCK_UNLOCK_PASSWORD = "extra_lock_unlock_password"
|
||||
private const val EXTRA_FILE_PATH = "extra_file_path"
|
||||
private const val EXTRA_SPLIT_PASSWORD = "extra_split_password"
|
||||
private const val EXTRA_PDF_TO_IMAGE_LIST = "extra_pdf_to_image_list"
|
||||
private const val EXTRA_PDF_TO_IMAGE_PASSWORD = "extra_pdf_to_image_password"
|
||||
|
||||
fun createIntentSplitPdfActivityToResult(
|
||||
context: Context,
|
||||
@ -84,6 +87,21 @@ class PdfResultActivity : BaseActivity() {
|
||||
putExtra(EXTRA_SOURCE, source)
|
||||
}
|
||||
}
|
||||
|
||||
fun createIntentPdfToImageActivityToResult(
|
||||
context: Context,
|
||||
filepath: String,
|
||||
list: ArrayList<PdfPageItem>,
|
||||
source: PdfPickerSource,
|
||||
password: String? = null
|
||||
): Intent {
|
||||
return Intent(context, PdfResultActivity::class.java).apply {
|
||||
putExtra(EXTRA_FILE_PATH, filepath)
|
||||
putParcelableArrayListExtra(EXTRA_PDF_TO_IMAGE_LIST, list)
|
||||
putExtra(EXTRA_SOURCE, source)
|
||||
putExtra(EXTRA_PDF_TO_IMAGE_PASSWORD, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityPdfSplitResultBinding
|
||||
@ -248,7 +266,7 @@ class PdfResultActivity : BaseActivity() {
|
||||
)
|
||||
resultList.add(result)
|
||||
}
|
||||
} else if (source == PdfPickerSource.TO_IMAGES) {
|
||||
} else if (source == PdfPickerSource.IMAGES_TO_PDF) {
|
||||
val files = requireStringArrayList(EXTRA_FILE_LIST)
|
||||
if (files.isEmpty()) {
|
||||
showToast(getString(R.string.pdf_loading_failed))
|
||||
@ -267,9 +285,7 @@ class PdfResultActivity : BaseActivity() {
|
||||
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"
|
||||
@ -281,6 +297,41 @@ class PdfResultActivity : BaseActivity() {
|
||||
resultList.add(result)
|
||||
}
|
||||
}
|
||||
} else if (source == PdfPickerSource.PDF_TO_IMAGES) {
|
||||
val filepath = intent.getStringExtra(EXTRA_FILE_PATH) ?: ""
|
||||
if (filepath.isEmpty()) {
|
||||
showToast(getString(R.string.pdf_loading_failed))
|
||||
finish()
|
||||
return@launch
|
||||
}
|
||||
binding.congratulationsDesc.text = getString(R.string.converted_successfully)
|
||||
val selectedPages: ArrayList<PdfPageItem> =
|
||||
requireParcelableArrayList(EXTRA_PDF_TO_IMAGE_LIST)
|
||||
val pdfToImgPassword = intent.getStringExtra(EXTRA_PDF_TO_IMAGE_PASSWORD) ?: ""
|
||||
val outputDir = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
|
||||
"PDFReaderPro/pdf2Img"
|
||||
).apply { if (!exists()) mkdirs() }
|
||||
PdfUtils.exportSelectedPagesToImages(
|
||||
context = this@PdfResultActivity,
|
||||
inputFile = File(filepath),
|
||||
selectedPages = selectedPages,
|
||||
outputDir = outputDir,
|
||||
password = pdfToImgPassword,
|
||||
onProgress = { current, total ->
|
||||
val progressPercent = current * 100 / total
|
||||
runOnUiThread {
|
||||
binding.progressBar.progress = progressPercent
|
||||
binding.progressTv.text = "$progressPercent"
|
||||
}
|
||||
}).let { resultFiles ->
|
||||
if (resultFiles.isNotEmpty()) {
|
||||
resultFiles.forEach {
|
||||
val result = PdfSplitResultItem(it.absolutePath, it.absolutePath, false)
|
||||
resultList.add(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.processingLayout.visibility = View.GONE
|
||||
@ -307,12 +358,23 @@ class PdfResultActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
binding.okBtn.setOnClickListener {
|
||||
val selectedItem = adapter.getSelectedItem()
|
||||
selectedItem?.let {
|
||||
val intent = PdfViewActivity.createIntent(this, selectedItem.filePath)
|
||||
startActivity(intent)
|
||||
if (source == PdfPickerSource.PDF_TO_IMAGES) {
|
||||
val selectedItem = adapter.getSelectedItem()
|
||||
selectedItem?.let {
|
||||
val string = AppUtils.openImageFile(this, File(selectedItem.filePath))
|
||||
if (string.isNotEmpty()) {
|
||||
showToast(string)
|
||||
}
|
||||
}
|
||||
finish()
|
||||
} else {
|
||||
val selectedItem = adapter.getSelectedItem()
|
||||
selectedItem?.let {
|
||||
val intent = PdfViewActivity.createIntent(this, selectedItem.filePath)
|
||||
startActivity(intent)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,175 @@
|
||||
package com.all.pdfreader.pro.app.ui.act
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.all.pdfreader.pro.app.R
|
||||
import com.all.pdfreader.pro.app.databinding.ActivityPdfToImgBinding
|
||||
import com.all.pdfreader.pro.app.model.PdfPageItem
|
||||
import com.all.pdfreader.pro.app.model.PdfPickerSource
|
||||
import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
|
||||
import com.all.pdfreader.pro.app.ui.adapter.SplitPdfAdapter
|
||||
import com.all.pdfreader.pro.app.ui.dialog.PdfPasswordProtectionDialogFragment
|
||||
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
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class PdfToImageActivity : BaseActivity() {
|
||||
override val TAG: String = "PdfToImageActivity"
|
||||
|
||||
|
||||
var currentPassword: String? = null//只会选择一个文件。直接进行密码传递
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_PDF_PATH = "extra_pdf_path"
|
||||
|
||||
fun createIntent(context: Context, filePath: String): Intent {
|
||||
return Intent(context, PdfToImageActivity::class.java).apply {
|
||||
putExtra(EXTRA_PDF_PATH, filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityPdfToImgBinding
|
||||
private lateinit var adapter: SplitPdfAdapter
|
||||
private var pdfPageList: MutableList<PdfPageItem> = mutableListOf()
|
||||
private lateinit var filePath: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPdfToImgBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
|
||||
.navigationBarColor(R.color.bg_color).init()
|
||||
filePath = intent.getStringExtra(EXTRA_PDF_PATH)
|
||||
?: throw IllegalArgumentException("PDF file hash is required")
|
||||
if (filePath.isEmpty()) {
|
||||
showToast(getString(R.string.file_not))
|
||||
finish()
|
||||
}
|
||||
initView()
|
||||
setupClick()
|
||||
initSplitData(File(filePath))
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.continueNowBtn.isEnabled = false
|
||||
binding.title.text = getString(R.string.selected_page, 0)
|
||||
binding.loadingRoot.root.visibility = View.VISIBLE//初始显示loading
|
||||
binding.splitRv.layoutManager = GridLayoutManager(this, 2)
|
||||
adapter = SplitPdfAdapter(pdfPageList) { item, pos ->
|
||||
item.isSelected = !item.isSelected
|
||||
adapter.setItemSelected(pos, item.isSelected)
|
||||
binding.title.text = getString(R.string.selected_page, adapter.getSelPages())
|
||||
|
||||
// 只有存在选中的item则为true
|
||||
val anySelected = pdfPageList.any { it.isSelected }
|
||||
binding.continueNowBtn.isEnabled = anySelected
|
||||
updateContinueNowBtnState(anySelected)
|
||||
}
|
||||
binding.splitRv.adapter = adapter
|
||||
|
||||
}
|
||||
|
||||
private fun setupClick() {
|
||||
binding.backBtn.setOnSingleClickListener { onBackPressedDispatcher.onBackPressed() }
|
||||
binding.selectAllBtn.setOnSingleClickListener {
|
||||
val selectAll = pdfPageList.any { !it.isSelected }//如果列表里有一页没选中 → 返回 true
|
||||
adapter.setAllSelected(selectAll)
|
||||
binding.title.text = getString(R.string.selected_page, adapter.getSelPages())
|
||||
binding.continueNowBtn.isEnabled = selectAll
|
||||
updateSelectAllState(selectAll)
|
||||
updateContinueNowBtnState(selectAll)
|
||||
}
|
||||
binding.continueNowBtn.setOnSingleClickListener {
|
||||
val selectedPages = pdfPageList.filter { it.isSelected }.map { it.copy() }
|
||||
val intent = PdfResultActivity.createIntentPdfToImageActivityToResult(
|
||||
context = this,
|
||||
filepath = filePath,
|
||||
list = ArrayList(selectedPages),
|
||||
source = PdfPickerSource.PDF_TO_IMAGES,
|
||||
password = currentPassword
|
||||
)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!password.isNullOrEmpty()) {
|
||||
currentPassword = password
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
pdfPageList.clear()
|
||||
// 先切换到 IO 线程执行删除,等待完成
|
||||
withContext(Dispatchers.IO) {
|
||||
PdfUtils.clearPdfThumbsCache(this@PdfToImageActivity)
|
||||
}
|
||||
// 删除完成后,开始收集数据
|
||||
var firstPageLoaded = false
|
||||
PdfUtils.splitPdfToPageItemsFlow(
|
||||
context = this@PdfToImageActivity,
|
||||
inputFile = file,
|
||||
password = password
|
||||
).collect { pageItem ->
|
||||
logDebug("splitPdfToPageItemsFlow pageItem->$pageItem")
|
||||
if (pdfPageList.size <= pageItem.pageIndex) {
|
||||
pdfPageList.add(pageItem)
|
||||
adapter.notifyItemInserted(pdfPageList.size - 1)
|
||||
} else {
|
||||
pdfPageList[pageItem.pageIndex] = pageItem
|
||||
adapter.updateItem(pageItem.pageIndex)
|
||||
}
|
||||
if (!firstPageLoaded) {
|
||||
binding.loadingRoot.root.visibility = View.GONE
|
||||
firstPageLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectAllState(b: Boolean) {
|
||||
binding.selectAll.setBackgroundResource(
|
||||
if (b) R.drawable.dr_circular_sel_on_bg
|
||||
else R.drawable.dr_circular_sel_off_bg
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateContinueNowBtnState(b: Boolean) {
|
||||
binding.continueNowBtn.setBackgroundResource(
|
||||
if (b) R.drawable.dr_click_btn_bg
|
||||
else R.drawable.dr_btn_not_clickable_bg
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@ -183,8 +183,9 @@ class SearchActivity : BaseActivity() {
|
||||
}).show(supportFragmentManager, "PdfRemovePasswordDialog")
|
||||
}
|
||||
|
||||
PdfPickerSource.TO_IMAGES -> {}
|
||||
PdfPickerSource.IMAGES_TO_PDF -> {}
|
||||
PdfPickerSource.TO_LONG_IMAGE -> {}
|
||||
PdfPickerSource.PDF_TO_IMAGES -> {}
|
||||
}
|
||||
}, onMoreClick = { pdf ->
|
||||
ListMoreDialogFragment(pdf.filePath).show(supportFragmentManager, FRAG_TAG)
|
||||
|
||||
@ -68,6 +68,10 @@ class ToolsFrag : BaseFrag() {
|
||||
binding.imgToPdfBtn.setOnClickListener {
|
||||
openImagePicker()
|
||||
}
|
||||
binding.pdfToImgBtn.setOnClickListener {
|
||||
val intent = PdfPickerActivity.createIntent(requireActivity(), PdfPickerSource.PDF_TO_IMAGES)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openImagePicker() {
|
||||
@ -154,7 +158,7 @@ class ToolsFrag : BaseFrag() {
|
||||
val intent = PdfResultActivity.createIntentInputFile(
|
||||
requireActivity(),
|
||||
ArrayList(paths),
|
||||
PdfPickerSource.TO_IMAGES
|
||||
PdfPickerSource.IMAGES_TO_PDF
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
package com.all.pdfreader.pro.app.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.print.PrintAttributes
|
||||
import android.print.PrintManager
|
||||
@ -271,4 +273,41 @@ object AppUtils {
|
||||
return spannable
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 打开图片文件(返回提示信息)
|
||||
*
|
||||
* @param context Context
|
||||
* @param file 要打开的图片文件
|
||||
* @return 提示信息
|
||||
*/
|
||||
fun openImageFile(context: Context, file: File): String {
|
||||
return try {
|
||||
val uri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
} else {
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "image/*")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.open)))
|
||||
"" // 成功返回空字符串
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.getString(R.string.no_app_to_open_image)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
context.getString(R.string.failed_to_open_image)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -15,7 +15,6 @@ 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
|
||||
@ -313,11 +312,7 @@ object PdfUtils {
|
||||
val pdImage = JPEGFactory.createFromImage(document, bitmap)
|
||||
PDPageContentStream(document, page).use { contentStream ->
|
||||
contentStream.drawImage(
|
||||
pdImage,
|
||||
offsetX,
|
||||
offsetY,
|
||||
targetWidth,
|
||||
targetHeight
|
||||
pdImage, offsetX, offsetY, targetWidth, targetHeight
|
||||
)
|
||||
}
|
||||
|
||||
@ -344,4 +339,84 @@ object PdfUtils {
|
||||
document?.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出选中的 PDF 页到图片文件(支持加密、进度回调、防重名、格式可选)
|
||||
*
|
||||
* @param context 上下文
|
||||
* @param inputFile 原 PDF 文件
|
||||
* @param selectedPages 选中的页对象列表(包含 pageIndex)
|
||||
* @param outputDir 输出目录
|
||||
* @param password PDF 密码(可为空)
|
||||
* @param format 输出格式:"JPG" 或 "PNG"
|
||||
* @param quality 图片质量(仅对 JPG 有效,0~100)
|
||||
* @param onProgress 当前进度回调 (current 页, total 页)
|
||||
* @return 导出的图片文件列表
|
||||
*/
|
||||
suspend fun exportSelectedPagesToImages(
|
||||
context: Context,
|
||||
inputFile: File,
|
||||
selectedPages: List<PdfPageItem>,
|
||||
outputDir: File,
|
||||
password: String? = null,
|
||||
format: String = "JPG",
|
||||
quality: Int = 100,
|
||||
onProgress: ((current: Int, total: Int) -> Unit)? = null
|
||||
): List<File> = withContext(Dispatchers.IO) {
|
||||
|
||||
if (selectedPages.isEmpty()) return@withContext emptyList()
|
||||
if (!outputDir.exists()) outputDir.mkdirs()
|
||||
|
||||
val pdfiumCore = PdfiumCore(context)
|
||||
val fd = ParcelFileDescriptor.open(inputFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
val document = if (password.isNullOrEmpty()) {
|
||||
pdfiumCore.newDocument(fd)
|
||||
} else {
|
||||
pdfiumCore.newDocument(fd, password)
|
||||
}
|
||||
|
||||
val sortedPages = selectedPages.sortedBy { it.pageIndex }
|
||||
val total = sortedPages.size
|
||||
val resultList = mutableListOf<File>()
|
||||
val baseName = inputFile.nameWithoutExtension
|
||||
|
||||
val compressFormat = if (format.equals("PNG", ignoreCase = true)) {
|
||||
Bitmap.CompressFormat.PNG
|
||||
} else {
|
||||
Bitmap.CompressFormat.JPEG
|
||||
}
|
||||
val extension = if (format.equals("PNG", ignoreCase = true)) "png" else "jpg"
|
||||
|
||||
sortedPages.forEachIndexed { index, pageItem ->
|
||||
try {
|
||||
pdfiumCore.openPage(document, pageItem.pageIndex)
|
||||
val width = pdfiumCore.getPageWidthPoint(document, pageItem.pageIndex)
|
||||
val height = pdfiumCore.getPageHeightPoint(document, pageItem.pageIndex)
|
||||
val bitmap = createBitmap(width, height)
|
||||
pdfiumCore.renderPageBitmap(document, bitmap, pageItem.pageIndex, 0, 0, width, height)
|
||||
|
||||
// 防止重名
|
||||
var outFile = File(outputDir, "${baseName}_page_${pageItem.pageIndex + 1}.$extension")
|
||||
var counter = 1
|
||||
while (outFile.exists()) {
|
||||
outFile = File(outputDir, "${baseName}_page_${pageItem.pageIndex + 1}($counter).$extension")
|
||||
counter++
|
||||
}
|
||||
|
||||
FileOutputStream(outFile).use {
|
||||
bitmap.compress(compressFormat, quality, it)
|
||||
}
|
||||
resultList.add(outFile)
|
||||
|
||||
onProgress?.invoke(index + 1, total)
|
||||
delay(1)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
pdfiumCore.closeDocument(document)
|
||||
fd.close()
|
||||
resultList
|
||||
}
|
||||
}
|
||||
|
||||
114
app/src/main/res/layout/activity_pdf_to_img.xml
Normal file
114
app/src/main/res/layout/activity_pdf_to_img.xml
Normal file
@ -0,0 +1,114 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bg_color"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/statusLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/backBtn"
|
||||
android:layout_width="40dp"
|
||||
android:background="@drawable/dr_click_effect_oval_transparent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/back_black" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextViewFont_PopMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/selected_page"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/selectAllBtn"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/selectAll"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="@drawable/dr_circular_sel_off_bg"
|
||||
android:padding="2dp"
|
||||
android:src="@drawable/gou_white" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/loadingRoot"
|
||||
layout="@layout/layout_loading"
|
||||
android:visibility="gone" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/splitListLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="visible">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/splitRv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@+id/continue_now_btn"
|
||||
android:layout_margin="8dp"
|
||||
android:overScrollMode="never"
|
||||
android:scrollbars="none"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:spanCount="2"
|
||||
tools:itemCount="6"
|
||||
tools:listitem="@layout/adapter_split_page_item" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/continue_now_btn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/dr_btn_not_clickable_bg"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
style="@style/TextViewFont_PopSemiBold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/convert"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
@ -41,17 +41,10 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/img_to_pdf" />
|
||||
|
||||
<LinearLayout
|
||||
<Button
|
||||
android:id="@+id/pdfToImgBtn"
|
||||
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>
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pdf_to_img" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -158,4 +158,9 @@
|
||||
<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>
|
||||
<string name="convert">CONVERT</string>
|
||||
<string name="converted_successfully">Converted successfully!</string>
|
||||
<string name="no_app_to_open_image">No app found to open image</string>
|
||||
<string name="failed_to_open_image">Failed to open image</string>
|
||||
<string name="press_again_to_exit">Press again to exit</string>
|
||||
</resources>
|
||||
@ -1,7 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 允许分享外部存储中的文件 -->
|
||||
|
||||
<!-- 外部公共存储目录,例如 Documents、Pictures、Download -->
|
||||
<external-path
|
||||
name="external_files"
|
||||
name="external_public"
|
||||
path="." />
|
||||
|
||||
<!-- 应用专属外部存储目录 -->
|
||||
<external-files-path
|
||||
name="external_private"
|
||||
path="." />
|
||||
|
||||
<!-- 应用内部 files 目录 -->
|
||||
<files-path
|
||||
name="internal_files"
|
||||
path="." />
|
||||
|
||||
<!-- 应用缓存目录 -->
|
||||
<cache-path
|
||||
name="cache"
|
||||
path="." />
|
||||
|
||||
</paths>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user