diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bd1d9e6..48ca91a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,12 +7,13 @@ + - + @@ -104,6 +105,13 @@ android:label="@string/app_name" android:screenOrientation="portrait" /> + + + {} + 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 = { diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt index 998a991..84dec72 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfResultActivity.kt @@ -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, + 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 = + 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() } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfToImageActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfToImageActivity.kt new file mode 100644 index 0000000..910b0e9 --- /dev/null +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfToImageActivity.kt @@ -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 = 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SearchActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SearchActivity.kt index d104d91..c147cd9 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SearchActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/SearchActivity.kt @@ -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) diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt index 0eaf49b..9f5426a 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/fragment/ToolsFrag.kt @@ -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) } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/AppUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/AppUtils.kt index 23b9556..0751f9a 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/AppUtils.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/AppUtils.kt @@ -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) + } + } + + } \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt index 7e8db3a..c230db2 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfUtils.kt @@ -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, + outputDir: File, + password: String? = null, + format: String = "JPG", + quality: Int = 100, + onProgress: ((current: Int, total: Int) -> Unit)? = null + ): List = 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() + 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 + } } diff --git a/app/src/main/res/layout/activity_pdf_to_img.xml b/app/src/main/res/layout/activity_pdf_to_img.xml new file mode 100644 index 0000000..eeffffd --- /dev/null +++ b/app/src/main/res/layout/activity_pdf_to_img.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_tools.xml b/app/src/main/res/layout/fragment_tools.xml index 9fa1944..48ba139 100644 --- a/app/src/main/res/layout/fragment_tools.xml +++ b/app/src/main/res/layout/fragment_tools.xml @@ -41,17 +41,10 @@ android:layout_height="wrap_content" android:text="@string/img_to_pdf" /> - - - - - + android:layout_height="wrap_content" + android:text="@string/pdf_to_img" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d9e94b3..79e4d85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -158,4 +158,9 @@ Unknown Source Image to PDF PDF to image + CONVERT + Converted successfully! + No app found to open image + Failed to open image + Press again to exit \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 4b1e768..e222877 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -1,7 +1,24 @@ - + + + + + + + + + + + +