图片转pdf,优化拆分功能,拆分加密pdf流程。

This commit is contained in:
ocean 2025-10-16 14:32:59 +08:00
parent 85a66a9911
commit 0cded6a58d
17 changed files with 500 additions and 13 deletions

View File

@ -63,4 +63,7 @@ dependencies {
implementation(libs.pdfbox.android) implementation(libs.pdfbox.android)
implementation(libs.jp2forandroid) implementation(libs.jp2forandroid)
implementation(libs.flexbox) implementation(libs.flexbox)
implementation(libs.pictureselector)
implementation(libs.ucrop)
implementation(libs.compress)
} }

View File

@ -20,4 +20,9 @@
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-keep class com.shockwave.** -keep class com.shockwave.**
-keep class com.luck.picture.lib.** { *; }
-keep class com.luck.lib.camerax.** { *; }
-dontwarn com.yalantis.ucrop**
-keep class com.yalantis.ucrop** { *; }
-keep interface com.yalantis.ucrop** { *; }

View File

@ -4,8 +4,15 @@
android:theme="@style/Theme.PDFReaderPro" android:theme="@style/Theme.PDFReaderPro"
tools:ignore="DiscouragedApi,LockedOrientationActivity,RedundantLabel"> tools:ignore="DiscouragedApi,LockedOrientationActivity,RedundantLabel">
<!-- Android 11+ 存储权限 --> <uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE"/>
<!-- Android 13+ 媒体权限 --> <!-- Android 13+ 媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
@ -107,5 +114,16 @@
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
</application> </application>
<queries package="${applicationId}">
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE">
</action>
</intent>
<intent>
<action android:name="android.media.action.ACTION_VIDEO_CAPTURE">
</action>
</intent>
</queries>
</manifest> </manifest>

View File

@ -10,6 +10,7 @@ import android.view.View
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.all.pdfreader.pro.app.PRApp
import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitResultBinding import com.all.pdfreader.pro.app.databinding.ActivityPdfSplitResultBinding
import com.all.pdfreader.pro.app.model.PdfPickerSource import com.all.pdfreader.pro.app.model.PdfPickerSource
@ -105,7 +106,7 @@ class PdfResultActivity : BaseActivity() {
finish() finish()
return return
} }
} else if (source == PdfPickerSource.MERGE) { } else if (source == PdfPickerSource.MERGE || source == PdfPickerSource.TO_IMAGES) {
if (inputFile.isEmpty()) { if (inputFile.isEmpty()) {
showToast(getString(R.string.pdf_loading_failed)) showToast(getString(R.string.pdf_loading_failed))
finish() finish()
@ -229,6 +230,34 @@ class PdfResultActivity : BaseActivity() {
) )
resultList.add(result) resultList.add(result)
} }
} else if(source == PdfPickerSource.TO_IMAGES){
val inputFiles: List<File> = inputFile.map { path -> File(path) }
val outputDir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
"PDFReaderPro/img2Pdf"
).apply { if (!exists()) mkdirs() }
val outputFileName =
getString(R.string.app_name) + "_" + System.currentTimeMillis()
.toUnderscoreDateTime() + ".pdf"
PdfUtils.imgToPdfFilesSafe(
inputFiles = inputFiles,
outputDir = outputDir,
outputFileName = outputFileName,
onProgress = { current, total ->
logDebug("current->$current total->$total")
val progressPercent = current * 100 / total
logDebug("progressPercent->$progressPercent")
runOnUiThread {
binding.progressBar.progress = progressPercent
binding.progressTv.text = "$progressPercent"
}
})?.let { resultFile ->
val thumbnails = generateFastThumbnail(this@PdfResultActivity, resultFile)
val result = PdfSplitResultItem(resultFile.absolutePath, thumbnails, false)
pdfScanner.addNewPdfToDatabase(result.filePath, result.thumbnailPath) {
resultList.add(result)
}
}
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.processingLayout.visibility = View.GONE binding.processingLayout.visibility = View.GONE

View File

@ -16,9 +16,11 @@ import com.all.pdfreader.pro.app.model.PdfSelectedPagesItem
import com.all.pdfreader.pro.app.model.RenameType import com.all.pdfreader.pro.app.model.RenameType
import com.all.pdfreader.pro.app.ui.adapter.SplitPdfAdapter import com.all.pdfreader.pro.app.ui.adapter.SplitPdfAdapter
import com.all.pdfreader.pro.app.ui.adapter.SplitSelectedPdfAdapter import com.all.pdfreader.pro.app.ui.adapter.SplitSelectedPdfAdapter
import com.all.pdfreader.pro.app.ui.dialog.PdfPasswordProtectionDialogFragment
import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment import com.all.pdfreader.pro.app.ui.dialog.PromptDialogFragment
import com.all.pdfreader.pro.app.ui.dialog.RenameDialogFragment import com.all.pdfreader.pro.app.ui.dialog.RenameDialogFragment
import com.all.pdfreader.pro.app.util.AppUtils.setOnSingleClickListener import com.all.pdfreader.pro.app.util.AppUtils.setOnSingleClickListener
import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted
import com.all.pdfreader.pro.app.util.FileUtils.toUnderscoreDateTime import com.all.pdfreader.pro.app.util.FileUtils.toUnderscoreDateTime
import com.all.pdfreader.pro.app.util.PdfUtils import com.all.pdfreader.pro.app.util.PdfUtils
import com.gyf.immersionbar.ImmersionBar import com.gyf.immersionbar.ImmersionBar
@ -149,6 +151,24 @@ class SplitPdfActivity : BaseActivity() {
} }
private fun initSplitData(file: File) { private fun initSplitData(file: File) {
lifecycleScope.launch {
// 是否存在密码
val isEncrypted = withContext(Dispatchers.IO) {
isPdfEncrypted(file)
}
if (isEncrypted) {
PdfPasswordProtectionDialogFragment(file, onOkClick = { password ->
initSplitDataWithPassword(file, password)
}, onCancelClick = {
finish()
}).show(supportFragmentManager, TAG)
} else {
initSplitDataWithPassword(file)
}
}
}
private fun initSplitDataWithPassword(file: File, password: String? = null) {
lifecycleScope.launch { lifecycleScope.launch {
splitList.clear() splitList.clear()
// 先切换到 IO 线程执行删除,等待完成 // 先切换到 IO 线程执行删除,等待完成
@ -157,7 +177,11 @@ class SplitPdfActivity : BaseActivity() {
} }
// 删除完成后,开始收集数据 // 删除完成后,开始收集数据
var firstPageLoaded = false var firstPageLoaded = false
PdfUtils.splitPdfToPageItemsFlow(this@SplitPdfActivity, file).collect { pageItem -> PdfUtils.splitPdfToPageItemsFlow(
context = this@SplitPdfActivity,
inputFile = file,
password = password
).collect { pageItem ->
logDebug("splitPdfToPageItemsFlow pageItem->$pageItem") logDebug("splitPdfToPageItemsFlow pageItem->$pageItem")
if (splitList.size <= pageItem.pageIndex) { if (splitList.size <= pageItem.pageIndex) {
splitList.add(pageItem) splitList.add(pageItem)

View File

@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment
import com.all.pdfreader.pro.app.databinding.FragmentFavoriteBinding import com.all.pdfreader.pro.app.databinding.FragmentFavoriteBinding
import com.all.pdfreader.pro.app.room.repository.PdfRepository import com.all.pdfreader.pro.app.room.repository.PdfRepository
import com.all.pdfreader.pro.app.sp.AppStore import com.all.pdfreader.pro.app.sp.AppStore
import com.all.pdfreader.pro.app.util.ToastUtils
abstract class BaseFrag : Fragment() { abstract class BaseFrag : Fragment() {
protected abstract val TAG: String protected abstract val TAG: String
@ -59,6 +60,10 @@ abstract class BaseFrag : Fragment() {
Log.w("ocean", "$TAG: $message") Log.w("ocean", "$TAG: $message")
} }
protected fun showToast(message: String) {
ToastUtils.show(requireActivity(), message)
}
//获取数据库实例 //获取数据库实例
protected fun getRepository(): PdfRepository { protected fun getRepository(): PdfRepository {
return PdfRepository.getInstance() return PdfRepository.getInstance()

View File

@ -1,15 +1,39 @@
package com.all.pdfreader.pro.app.ui.fragment package com.all.pdfreader.pro.app.ui.fragment
import android.content.Context
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.all.pdfreader.pro.app.PRApp
import com.all.pdfreader.pro.app.R
import com.all.pdfreader.pro.app.databinding.FragmentToolsBinding import com.all.pdfreader.pro.app.databinding.FragmentToolsBinding
import com.all.pdfreader.pro.app.model.PdfPickerSource import com.all.pdfreader.pro.app.model.PdfPickerSource
import com.all.pdfreader.pro.app.ui.act.PdfPickerActivity import com.all.pdfreader.pro.app.ui.act.PdfPickerActivity
import com.all.pdfreader.pro.app.ui.act.PdfResultActivity
import com.all.pdfreader.pro.app.util.GlideEngine
import com.luck.picture.lib.basic.PictureSelector
import com.luck.picture.lib.config.PictureMimeType
import com.luck.picture.lib.config.SelectMimeType
import com.luck.picture.lib.config.SelectModeConfig
import com.luck.picture.lib.engine.CompressFileEngine
import com.luck.picture.lib.entity.LocalMedia
import com.luck.picture.lib.interfaces.OnKeyValueResultCallbackListener
import com.luck.picture.lib.interfaces.OnMediaEditInterceptListener
import com.luck.picture.lib.interfaces.OnResultCallbackListener
import com.luck.picture.lib.style.PictureSelectorStyle
import com.luck.picture.lib.style.SelectMainStyle
import com.luck.picture.lib.utils.DateUtils
import com.yalantis.ucrop.UCrop
import top.zibin.luban.Luban
import top.zibin.luban.OnNewCompressListener
import java.io.File
class ToolsFrag : Fragment() { class ToolsFrag : BaseFrag() {
override val TAG: String = "ToolsFrag"
private lateinit var binding: FragmentToolsBinding private lateinit var binding: FragmentToolsBinding
override fun onCreateView( override fun onCreateView(
@ -41,5 +65,110 @@ class ToolsFrag : Fragment() {
val intent = PdfPickerActivity.createIntent(requireActivity(), PdfPickerSource.UNLOCK) val intent = PdfPickerActivity.createIntent(requireActivity(), PdfPickerSource.UNLOCK)
startActivity(intent) startActivity(intent)
} }
binding.imgToPdfBtn.setOnClickListener {
openImagePicker()
}
}
private fun openImagePicker() {
val selectorStyle = PictureSelectorStyle().apply {
selectMainStyle = SelectMainStyle().apply {
isSelectNumberStyle = true
isPreviewSelectNumberStyle = true
selectBackground = R.drawable.num_checkbox_selector
}
}
PictureSelector.create(this)
.openGallery(SelectMimeType.ofImage())
.setSelectorUIStyle(selectorStyle)
.setImageEngine(GlideEngine.createGlideEngine())
.setImageSpanCount(3)
.isGif(false)
.isFastSlidingSelect(true)
.setSelectionMode(SelectModeConfig.MULTIPLE)
.setMaxSelectNum(50)
.setCompressEngine(object : CompressFileEngine {
override fun onStartCompress(
context: Context?,
source: ArrayList<Uri?>?,
call: OnKeyValueResultCallbackListener?
) {
Luban.with(context)
.load(source)
.ignoreBy(100)
.setCompressListener(object : OnNewCompressListener {
override fun onStart() {
}
override fun onSuccess(
source: String?,
compressFile: File?
) {
call?.onCallback(source, compressFile?.absolutePath)
}
override fun onError(source: String?, e: Throwable?) {
call?.onCallback(source, null)
}
})
.launch()
}
})
.setEditMediaInterceptListener(object : OnMediaEditInterceptListener {
override fun onStartMediaEdit(
fragment: Fragment, currentLocalMedia: LocalMedia, requestCode: Int
) {
val currentEditPath = currentLocalMedia.getAvailablePath()
val inputUri = if (PictureMimeType.isContent(currentEditPath)) {
currentEditPath.toUri()
} else {
Uri.fromFile(File(currentEditPath))
}
val destinationUri = Uri.fromFile(
File(
getSandboxPath(), DateUtils.getCreateFileName("CROP_") + ".jpeg"
)
)
val uCrop = UCrop.of<Any>(inputUri, destinationUri)
val buildOptions = UCrop.Options().apply {
isDarkStatusBarBlack(true)
}
uCrop.withOptions(buildOptions)
uCrop.start(requireActivity(), fragment, requestCode)
}
}).forResult(object : OnResultCallbackListener<LocalMedia> {
override fun onResult(result: ArrayList<LocalMedia>) {
logDebug("forResult ->${result.size}")
if (result.isNotEmpty()) {
// result 中可能有带裁剪的 Uri 或原图 Uri
// 取出路径列表
val paths = result.mapNotNull { media ->
media.availablePath
}
if (paths.isEmpty()) {
return
}
val intent = PdfResultActivity.createIntentInputFile(
requireActivity(),
ArrayList(paths),
PdfPickerSource.TO_IMAGES
)
startActivity(intent)
}
}
override fun onCancel() {
logDebug("onCancel")
}
})
}
fun getSandboxPath(): String {
val dir = File(PRApp.getContext().externalCacheDir, "SandboxPdfPro")
if (!dir.exists()) dir.mkdirs()
return dir.absolutePath
} }
} }

View File

@ -6,7 +6,7 @@ import android.graphics.pdf.PdfRenderer
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.Log import android.util.Log
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
@ -21,6 +21,7 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import com.all.pdfreader.pro.app.model.RenameResult import com.all.pdfreader.pro.app.model.RenameResult
import androidx.core.net.toUri
object FileUtils { object FileUtils {
@ -330,6 +331,7 @@ object FileUtils {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH) val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH)
return sdf.format(Date(this)) return sdf.format(Date(this))
} }
data class FileInfo( data class FileInfo(
val name: String, val size: Long, val uri: Uri val name: String, val size: Long, val uri: Uri
) )
@ -368,11 +370,12 @@ object FileUtils {
*/ */
fun isPdfPasswordCorrect(context: Context, file: File, password: String): Boolean { fun isPdfPasswordCorrect(context: Context, file: File, password: String): Boolean {
return try { return try {
val descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { descriptor ->
val pdfiumCore = PdfiumCore(context) val pdfiumCore = PdfiumCore(context)
val doc = pdfiumCore.newDocument(descriptor, password) val doc = pdfiumCore.newDocument(descriptor, password)
pdfiumCore.closeDocument(doc) pdfiumCore.closeDocument(doc)
true // 没抛异常说明密码正确 }
true// 没抛异常说明密码正确
} catch (e: Exception) { } catch (e: Exception) {
false // 抛异常说明密码错误 false // 抛异常说明密码错误
} }
@ -557,4 +560,28 @@ object FileUtils {
return newFile return newFile
} }
fun uriToFile(context: Context, path: String): File? {
return try {
if (path.startsWith("content://")) {
// 是 content:// Uri需要拷贝到临时文件
val uri = path.toUri()
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val tempFile = File(context.cacheDir, "temp_${System.currentTimeMillis()}.jpg")
tempFile.outputStream().use { output ->
inputStream.copyTo(output)
}
inputStream.close()
tempFile
} else {
// 是普通文件路径
val file = File(path)
if (file.exists()) file else null
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
} }

View File

@ -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()
}
}

View File

@ -2,6 +2,7 @@ package com.all.pdfreader.pro.app.util
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
@ -10,6 +11,11 @@ import com.shockwave.pdfium.PdfiumCore
import com.tom_roush.pdfbox.io.MemoryUsageSetting import com.tom_roush.pdfbox.io.MemoryUsageSetting
import com.tom_roush.pdfbox.multipdf.PDFMergerUtility import com.tom_roush.pdfbox.multipdf.PDFMergerUtility
import com.tom_roush.pdfbox.pdmodel.PDDocument import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.pdmodel.PDPage
import com.tom_roush.pdfbox.pdmodel.PDPageContentStream
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle
import com.tom_roush.pdfbox.pdmodel.graphics.image.JPEGFactory
import com.tom_roush.pdfbox.pdmodel.graphics.image.LosslessFactory
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -44,11 +50,17 @@ object PdfUtils {
inputFile: File, inputFile: File,
dpi: Float = 72f, dpi: Float = 72f,
chunkSize: Int = 5, chunkSize: Int = 5,
thumbWidth: Int = 200 thumbWidth: Int = 200,
password: String? = null
): Flow<PdfPageItem> = flow { ): Flow<PdfPageItem> = flow {
val pdfiumCore = PdfiumCore(context) val pdfiumCore = PdfiumCore(context)
ParcelFileDescriptor.open(inputFile, ParcelFileDescriptor.MODE_READ_ONLY).use { fd -> ParcelFileDescriptor.open(inputFile, ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
val pdfDocument = pdfiumCore.newDocument(fd) val pdfDocument = if (password.isNullOrEmpty()) {
pdfiumCore.newDocument(fd)
} else {
pdfiumCore.newDocument(fd, password)
}
val pageCount = pdfiumCore.getPageCount(pdfDocument) val pageCount = pdfiumCore.getPageCount(pdfDocument)
val pages = List(pageCount) { val pages = List(pageCount) {
@ -208,4 +220,83 @@ object PdfUtils {
null null
} }
} }
/**
* 图片转 PDF优化版安全快速低内存
*/
suspend fun imgToPdfFilesSafe(
inputFiles: List<File>,
outputDir: File,
outputFileName: String,
onProgress: ((current: Int, total: Int) -> Unit)? = null
): File? = withContext(Dispatchers.IO) {
if (inputFiles.isEmpty()) return@withContext null
val outputFile = File(outputDir, outputFileName)
if (!outputDir.exists()) outputDir.mkdirs()
var document: PDDocument? = null
try {
document = PDDocument()
val total = inputFiles.size
inputFiles.forEachIndexed { index, imageFile ->
try {
//安全加载压缩后的图片
val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.RGB_565
}
val bitmap = BitmapFactory.decodeFile(imageFile.absolutePath, options)
?: return@forEachIndexed
//按 A4 比例缩放
val a4Width = PDRectangle.A4.width
val a4Height = PDRectangle.A4.height
val imageRatio = bitmap.width.toFloat() / bitmap.height
val pageRatio = a4Width / a4Height
val targetWidth: Float
val targetHeight: Float
if (imageRatio > pageRatio) {
targetWidth = a4Width
targetHeight = a4Width / imageRatio
} else {
targetHeight = a4Height
targetWidth = a4Height * imageRatio
}
val offsetX = (a4Width - targetWidth) / 2
val offsetY = (a4Height - targetHeight) / 2
val page = PDPage(PDRectangle.A4)
document.addPage(page)
val pdImage = JPEGFactory.createFromImage(document, bitmap)
PDPageContentStream(document, page).use { contentStream ->
contentStream.drawImage(pdImage, offsetX, offsetY, targetWidth, targetHeight)
}
bitmap.recycle()
} catch (e: OutOfMemoryError) {
e.printStackTrace()
System.gc() // 手动GC一次极端情况下
return@forEachIndexed
} catch (e: Exception) {
e.printStackTrace()
}
onProgress?.invoke(index + 1, total)
}
document.save(outputFile)
outputFile
} catch (e: Exception) {
e.printStackTrace()
null
} finally {
document?.close()
}
}
} }

View 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>

View 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>

View 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>

View 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>

View File

@ -35,4 +35,23 @@
android:text="@string/unlock_pdf" /> android:text="@string/unlock_pdf" />
<Button
android:id="@+id/imgToPdfBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/img_to_pdf" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<FrameLayout
android:id="@+id/fragment_fl"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -113,6 +113,7 @@
<string name="duplicate_created_successfully">Duplicate file created successfully</string> <string name="duplicate_created_successfully">Duplicate file created successfully</string>
<string name="duplicate_created_failed">Duplicate file created failed</string> <string name="duplicate_created_failed">Duplicate file created failed</string>
<string name="processing">Processing…</string> <string name="processing">Processing…</string>
<string name="image_processing">Image Processing…</string>
<string name="view_model">View Model</string> <string name="view_model">View Model</string>
<string name="page_by_page">Page by page</string> <string name="page_by_page">Page by page</string>
<string name="color_inversion">color inversion</string> <string name="color_inversion">color inversion</string>
@ -154,4 +155,6 @@
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="please_select_a_file">Please select a file</string> <string name="please_select_a_file">Please select a file</string>
<string name="unknown_source">Unknown Source</string> <string name="unknown_source">Unknown Source</string>
<string name="img_to_pdf">Image to PDF</string>
<string name="pdf_to_img">PDF to image</string>
</resources> </resources>

View File

@ -16,6 +16,7 @@ lifecycleRuntimeKtx = "2.9.2"
immersionbar = "3.2.2" immersionbar = "3.2.2"
immersionbarKtx = "3.2.2" immersionbarKtx = "3.2.2"
pdfboxAndroid = "2.0.27.0" pdfboxAndroid = "2.0.27.0"
pictureselector = "v3.11.2"
room_version = "2.7.2" room_version = "2.7.2"
swiperefreshlayout = "1.1.0" swiperefreshlayout = "1.1.0"
recyclerview = "1.4.0" recyclerview = "1.4.0"
@ -31,6 +32,7 @@ androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room_version" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room_version" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room_version" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room_version" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room_version" } androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room_version" }
compress = { module = "io.github.lucksiege:compress", version.ref = "pictureselector" }
flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" } flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
jp2forandroid = { module = "com.github.Tgo1014:JP2ForAndroid", version.ref = "jp2forandroid" } jp2forandroid = { module = "com.github.Tgo1014:JP2ForAndroid", version.ref = "jp2forandroid" }
@ -43,9 +45,11 @@ immersionbar-ktx = { group = "com.geyifeng.immersionbar", name = "immersionbar-k
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" } androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" } androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
pdfbox-android = { module = "com.tom-roush:pdfbox-android", version.ref = "pdfboxAndroid" } pdfbox-android = { module = "com.tom-roush:pdfbox-android", version.ref = "pdfboxAndroid" }
pictureselector = { module = "io.github.lucksiege:pictureselector", version.ref = "pictureselector" }
protolite-well-known-types = { group = "com.google.firebase", name = "protolite-well-known-types", version.ref = "protoliteWellKnownTypes" } protolite-well-known-types = { group = "com.google.firebase", name = "protolite-well-known-types", version.ref = "protoliteWellKnownTypes" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" } androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" }
ucrop = { module = "io.github.lucksiege:ucrop", version.ref = "pictureselector" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }