图片转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.jp2forandroid)
implementation(libs.flexbox)
implementation(libs.pictureselector)
implementation(libs.ucrop)
implementation(libs.compress)
}

View File

@ -21,3 +21,8 @@
#-renamesourcefileattribute SourceFile
-keep class com.shockwave.**
-keep class com.luck.picture.lib.** { *; }
-keep class com.luck.lib.camerax.** { *; }
-dontwarn com.yalantis.ucrop**
-keep class com.yalantis.ucrop** { *; }
-keep interface com.yalantis.ucrop** { *; }

View File

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

View File

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

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

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

View File

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

View File

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

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

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" />
<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>

View File

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

View File

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