添加pdf转换图片功能
优化一些其余项
This commit is contained in:
parent
2c88b4271d
commit
d9dfa75b9b
@ -7,12 +7,13 @@
|
|||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.camera"
|
android:name="android.hardware.camera"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<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.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<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" />
|
||||||
@ -104,6 +105,13 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:screenOrientation="portrait" />
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.act.PdfToImageActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
|||||||
@ -6,6 +6,7 @@ enum class PdfPickerSource {
|
|||||||
SPLIT, // PDF拆分
|
SPLIT, // PDF拆分
|
||||||
LOCK, // PDF加密
|
LOCK, // PDF加密
|
||||||
UNLOCK, // PDF解密
|
UNLOCK, // PDF解密
|
||||||
TO_IMAGES, // PDF转图片
|
IMAGES_TO_PDF,// 图片转PDF
|
||||||
|
PDF_TO_IMAGES,// PDF转图片
|
||||||
TO_LONG_IMAGE // PDF转长图
|
TO_LONG_IMAGE // PDF转长图
|
||||||
}
|
}
|
||||||
@ -3,6 +3,7 @@ package com.all.pdfreader.pro.app.ui.act
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
@ -55,6 +56,7 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
setupDoubleBackExit()
|
||||||
initObserve()
|
initObserve()
|
||||||
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
|
ImmersionBar.with(this).statusBarView(binding.view).statusBarDarkFont(true)
|
||||||
.navigationBarColor(R.color.white).init()
|
.navigationBarColor(R.color.white).init()
|
||||||
@ -475,4 +477,25 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback
|
|||||||
binding.multiSelectMergeBtn.isClickable = false
|
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")
|
}).show(supportFragmentManager, "PdfRemovePasswordDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
PdfPickerSource.TO_IMAGES -> {}
|
PdfPickerSource.IMAGES_TO_PDF -> {}
|
||||||
PdfPickerSource.TO_LONG_IMAGE -> {}
|
PdfPickerSource.TO_LONG_IMAGE -> {}
|
||||||
PdfPickerSource.NONE -> {}
|
PdfPickerSource.NONE -> {}
|
||||||
|
PdfPickerSource.PDF_TO_IMAGES -> {
|
||||||
|
val intent = PdfToImageActivity.createIntent(this, pdf.filePath)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSelectModelItemClick = {
|
onSelectModelItemClick = {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import com.all.pdfreader.pro.app.PRApp
|
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.PdfPageItem
|
||||||
import com.all.pdfreader.pro.app.model.PdfPickerSource
|
import com.all.pdfreader.pro.app.model.PdfPickerSource
|
||||||
import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
|
import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
|
||||||
import com.all.pdfreader.pro.app.model.PdfSplitResultItem
|
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_LOCK_UNLOCK_PASSWORD = "extra_lock_unlock_password"
|
||||||
private const val EXTRA_FILE_PATH = "extra_file_path"
|
private const val EXTRA_FILE_PATH = "extra_file_path"
|
||||||
private const val EXTRA_SPLIT_PASSWORD = "extra_split_password"
|
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(
|
fun createIntentSplitPdfActivityToResult(
|
||||||
context: Context,
|
context: Context,
|
||||||
@ -84,6 +87,21 @@ class PdfResultActivity : BaseActivity() {
|
|||||||
putExtra(EXTRA_SOURCE, source)
|
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
|
private lateinit var binding: ActivityPdfSplitResultBinding
|
||||||
@ -248,7 +266,7 @@ class PdfResultActivity : BaseActivity() {
|
|||||||
)
|
)
|
||||||
resultList.add(result)
|
resultList.add(result)
|
||||||
}
|
}
|
||||||
} else if (source == PdfPickerSource.TO_IMAGES) {
|
} else if (source == PdfPickerSource.IMAGES_TO_PDF) {
|
||||||
val files = requireStringArrayList(EXTRA_FILE_LIST)
|
val files = requireStringArrayList(EXTRA_FILE_LIST)
|
||||||
if (files.isEmpty()) {
|
if (files.isEmpty()) {
|
||||||
showToast(getString(R.string.pdf_loading_failed))
|
showToast(getString(R.string.pdf_loading_failed))
|
||||||
@ -267,9 +285,7 @@ class PdfResultActivity : BaseActivity() {
|
|||||||
outputDir = outputDir,
|
outputDir = outputDir,
|
||||||
outputFileName = outputFileName,
|
outputFileName = outputFileName,
|
||||||
onProgress = { current, total ->
|
onProgress = { current, total ->
|
||||||
logDebug("current->$current total->$total")
|
|
||||||
val progressPercent = current * 100 / total
|
val progressPercent = current * 100 / total
|
||||||
logDebug("progressPercent->$progressPercent")
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
binding.progressBar.progress = progressPercent
|
binding.progressBar.progress = progressPercent
|
||||||
binding.progressTv.text = "$progressPercent"
|
binding.progressTv.text = "$progressPercent"
|
||||||
@ -281,6 +297,41 @@ class PdfResultActivity : BaseActivity() {
|
|||||||
resultList.add(result)
|
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) {
|
withContext(Dispatchers.Main) {
|
||||||
binding.processingLayout.visibility = View.GONE
|
binding.processingLayout.visibility = View.GONE
|
||||||
@ -307,6 +358,16 @@ class PdfResultActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.okBtn.setOnClickListener {
|
binding.okBtn.setOnClickListener {
|
||||||
|
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()
|
val selectedItem = adapter.getSelectedItem()
|
||||||
selectedItem?.let {
|
selectedItem?.let {
|
||||||
val intent = PdfViewActivity.createIntent(this, selectedItem.filePath)
|
val intent = PdfViewActivity.createIntent(this, selectedItem.filePath)
|
||||||
@ -315,6 +376,7 @@ class PdfResultActivity : BaseActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|||||||
@ -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")
|
}).show(supportFragmentManager, "PdfRemovePasswordDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
PdfPickerSource.TO_IMAGES -> {}
|
PdfPickerSource.IMAGES_TO_PDF -> {}
|
||||||
PdfPickerSource.TO_LONG_IMAGE -> {}
|
PdfPickerSource.TO_LONG_IMAGE -> {}
|
||||||
|
PdfPickerSource.PDF_TO_IMAGES -> {}
|
||||||
}
|
}
|
||||||
}, onMoreClick = { pdf ->
|
}, onMoreClick = { pdf ->
|
||||||
ListMoreDialogFragment(pdf.filePath).show(supportFragmentManager, FRAG_TAG)
|
ListMoreDialogFragment(pdf.filePath).show(supportFragmentManager, FRAG_TAG)
|
||||||
|
|||||||
@ -68,6 +68,10 @@ class ToolsFrag : BaseFrag() {
|
|||||||
binding.imgToPdfBtn.setOnClickListener {
|
binding.imgToPdfBtn.setOnClickListener {
|
||||||
openImagePicker()
|
openImagePicker()
|
||||||
}
|
}
|
||||||
|
binding.pdfToImgBtn.setOnClickListener {
|
||||||
|
val intent = PdfPickerActivity.createIntent(requireActivity(), PdfPickerSource.PDF_TO_IMAGES)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openImagePicker() {
|
private fun openImagePicker() {
|
||||||
@ -154,7 +158,7 @@ class ToolsFrag : BaseFrag() {
|
|||||||
val intent = PdfResultActivity.createIntentInputFile(
|
val intent = PdfResultActivity.createIntentInputFile(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
ArrayList(paths),
|
ArrayList(paths),
|
||||||
PdfPickerSource.TO_IMAGES
|
PdfPickerSource.IMAGES_TO_PDF
|
||||||
)
|
)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
package com.all.pdfreader.pro.app.util
|
package com.all.pdfreader.pro.app.util
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.print.PrintAttributes
|
import android.print.PrintAttributes
|
||||||
import android.print.PrintManager
|
import android.print.PrintManager
|
||||||
@ -271,4 +273,41 @@ object AppUtils {
|
|||||||
return spannable
|
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.PDPageContentStream
|
||||||
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle
|
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.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
|
||||||
@ -313,11 +312,7 @@ object PdfUtils {
|
|||||||
val pdImage = JPEGFactory.createFromImage(document, bitmap)
|
val pdImage = JPEGFactory.createFromImage(document, bitmap)
|
||||||
PDPageContentStream(document, page).use { contentStream ->
|
PDPageContentStream(document, page).use { contentStream ->
|
||||||
contentStream.drawImage(
|
contentStream.drawImage(
|
||||||
pdImage,
|
pdImage, offsetX, offsetY, targetWidth, targetHeight
|
||||||
offsetX,
|
|
||||||
offsetY,
|
|
||||||
targetWidth,
|
|
||||||
targetHeight
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,4 +339,84 @@ object PdfUtils {
|
|||||||
document?.close()
|
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:layout_height="wrap_content"
|
||||||
android:text="@string/img_to_pdf" />
|
android:text="@string/img_to_pdf" />
|
||||||
|
|
||||||
<LinearLayout
|
<Button
|
||||||
|
android:id="@+id/pdfToImgBtn"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:text="@string/pdf_to_img" />
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/fragment_fl"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@ -158,4 +158,9 @@
|
|||||||
<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="img_to_pdf">Image to PDF</string>
|
||||||
<string name="pdf_to_img">PDF to image</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>
|
</resources>
|
||||||
@ -1,7 +1,24 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<!-- 允许分享外部存储中的文件 -->
|
|
||||||
|
<!-- 外部公共存储目录,例如 Documents、Pictures、Download -->
|
||||||
<external-path
|
<external-path
|
||||||
name="external_files"
|
name="external_public"
|
||||||
path="." />
|
path="." />
|
||||||
|
|
||||||
|
<!-- 应用专属外部存储目录 -->
|
||||||
|
<external-files-path
|
||||||
|
name="external_private"
|
||||||
|
path="." />
|
||||||
|
|
||||||
|
<!-- 应用内部 files 目录 -->
|
||||||
|
<files-path
|
||||||
|
name="internal_files"
|
||||||
|
path="." />
|
||||||
|
|
||||||
|
<!-- 应用缓存目录 -->
|
||||||
|
<cache-path
|
||||||
|
name="cache"
|
||||||
|
path="." />
|
||||||
|
|
||||||
</paths>
|
</paths>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user