如何压缩从科特林画廊挑选的照片

Posted

技术标签:

【中文标题】如何压缩从科特林画廊挑选的照片【英文标题】:How to compress a photo picked from gallery in kotlin 【发布时间】:2021-04-26 03:08:57 【问题描述】:

我发现了很多关于这个主题的问题,但没有一个有完整的工作示例。

我的情况很简单:

    获取选取的图像 压缩图片 将压缩图像上传到 Firebase 存储

我当前的代码(未压缩):

片段:

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) 
        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode) 
            RC_PICK_IMAGE ->
                if (resultCode == Activity.RESULT_OK)
                    data?.data?.let  viewModel.updateUserPicture(it) 
        
    

视图模型:

    fun updatePhotoUrl(photoUrl: Uri?): Task<Void> =
        Storage.uploadFile("/$PS_USERS/$uid", photoUrl)
                    .continueWithTask  ... 

存储:(包装 Firebase 交互的对象)

    fun uploadFile(path: String, uri: Uri): Task<Uri> 
        val imageRef = storageRef.child(path)
        val uploadTask = imageRef.putFile(uri)

        // Return the task getting the download URL
        return uploadTask.continueWithTask  task ->
            if (!task.isSuccessful) 
                task.exception?.let 
                    throw it
                
            
            imageRef.downloadUrl
        
    

这很好用。

现在我的问题是:在这个过程中添加压缩的正确方法是什么?

我找到了许多指向Compressor 库的答案(如here 和here)并尝试了它,但它不适用于图库结果uris。我找到了从中获取实际 uri 的方法(例如 here),但它们感觉像很多样板代码,所以感觉不是最佳实践。 我还使用bitmap.compress(如here 和here)找到了许多答案并尝试过,但它要求提供位图。使用MediaStore.Images.Media.getBitmap 获取位图很容易,但它已被弃用,this 解决方案让我怀疑它是否是正确的方向。此外,将位图保存在我的 LiveData 对象中(在屏幕上显示直到实际保存,在编辑屏幕中)而不是 uri,感觉很奇怪(我使用 Glide 来呈现图像)。

最重要的是,这两种解决方案都需要上下文。我认为压缩是一个应该属于后端(或 Repository 类。在我的例子中是 Storage 对象)的过程,所以他们感觉有点不对劲。

有人可以分享这个简单用例的完整工作示例吗?

【问题讨论】:

【参考方案1】:

使用它可能会有所帮助: 通过传递 compressAndSetImage(data.data) 从 onActivityResult 调用 compressAndSetImage

fun compressAndSetImage(result: Uri)
        val job = Job()
        val uiScope = CoroutineScope(Dispatchers.IO + job)
        val fileUri = getFilePathFromUri(result, context!!) 
        uiScope.launch 
            val compressedImageFile = Compressor.compress(context!!, File(fileUri.path))
                quality(50) // combine with compressor constraint
                format(Bitmap.CompressFormat.JPEG)
            
            resultUri = Uri.fromFile(compressedImageFile)

            activity!!.runOnUiThread 
                resultUri?.let 
                    //set image here 
                
            
        
    

要解决此问题,我必须先将此路径转换为真实路径,然后我才能通过这种方式解决此问题.. 首先这是要在 build.gradle (app) 中添加的依赖项: //图片压缩依赖

implementation 'id.zelory:compressor:3.0.0'

//将uri路径转换为真实路径

@Throws(IOException::class)
fun getFilePathFromUri(uri: Uri?, context: Context?): Uri? 
    val fileName: String? = getFileName(uri, context)
    val file = File(context?.externalCacheDir, fileName)
    file.createNewFile()
    FileOutputStream(file).use  outputStream ->
        context?.contentResolver?.openInputStream(uri).use  inputStream ->
            copyFile(inputStream, outputStream)
            outputStream.flush()
        
    
    return Uri.fromFile(file)


@Throws(IOException::class)
private fun copyFile(`in`: InputStream?, out: OutputStream) 
    val buffer = ByteArray(1024)
    var read: Int? = null
    while (`in`?.read(buffer).also( read = it!! ) != -1) 
        read?.let  out.write(buffer, 0, it) 
    
//copyFile ends

fun getFileName(uri: Uri?, context: Context?): String? 
    var fileName: String? = getFileNameFromCursor(uri, context)
    if (fileName == null) 
        val fileExtension: String? = getFileExtension(uri, context)
        fileName = "temp_file" + if (fileExtension != null) ".$fileExtension" else ""
     else if (!fileName.contains(".")) 
        val fileExtension: String? = getFileExtension(uri, context)
        fileName = "$fileName.$fileExtension"
    
    return fileName


fun getFileExtension(uri: Uri?, context: Context?): String? 
    val fileType: String? = context?.contentResolver?.getType(uri)
    return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType)


fun getFileNameFromCursor(uri: Uri?, context: Context?): String? 
    val fileCursor: Cursor? = context?.contentResolver
        ?.query(uri, arrayOf<String>(OpenableColumns.DISPLAY_NAME), null, null, null)
    var fileName: String? = null
    if (fileCursor != null && fileCursor.moveToFirst()) 
        val cIndex: Int = fileCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
        if (cIndex != -1) 
            fileName = fileCursor.getString(cIndex)
        
    
    return fileName

【讨论】:

您好,感谢您的回答!我试过了(已经和现在再次分享错误),但它不适用于图像选择器的 uri 结果,因为它不是一个真正的文件(我理解这是我找到的所有解决方案中的核心问题)。 Compressor.compress(context!!, File(result.path)) 抛出:kotlin.io.NoSuchFileException: /-1/1/content:/media/external/images/media/25/ORIGINAL/NONE/1161564586: The source file doesn't exist.。如何获得适用于该函数的 uri?? 我已经编辑了我的答案。请查看它,如果仍然出现任何问题,请告诉我。但希望您的问题可以通过这种方式解决。 好的,现在可以了。我对这个解决方案的问题是它就像 75 行样板代码,实际上打开了一个新文件并将所有数据从 uri 复制到它。这真的是从标准图库选择器压缩图像的最佳方式吗?请查看我的回答 below - 它在不到 10 行代码的情况下实现了相同的结果 - 不使用 Compressor 库。我想知道为什么我从来没有看到有人建议过它,而大多数人都指向 Compressor 库 - 你能告诉我你的解决方案的优势吗? 实际上,您之前遇到的关于 NoSuchFleException 的问题,要解决我们必须像我们一样解决的问题。我不会称其为最佳解决方案,但要解决该问题,我想这是这样做的方式,因为我自己也遇到过很多次同样的问题。除了举个例子,如果我们想从驱动器或在线某个地方而不是在画廊中选择一张照片,那么这就是让事情顺利进行的方式.. 是的,您的解决方案也符合要求,但您不能再次尝试从画廊中挑选图像并查看您的解决方案是否有效.. 如果需要进一步的帮助,请告诉我,让我知道,以便我也获得更多信息 :)【参考方案2】:

流程应该是:

    用户选择图像并显示在ImageView 中(scaleTyle 应为 centerCrop)。

    用户点击保存按钮,我们开始上传图片bytes,如下所示:

     val uploadTask = profilePicturesReference.putBytes(getImageBytes())
    
     private fun getImageBytes(): ByteArray 
         val bitmap = Bitmap.createBitmap(my_profile_imageView.width, my_profile_imageView.height, Bitmap.Config.ARGB_8888)
    
         val canvas = Canvas(bitmap)
    
         my_profile_imageView.draw(canvas)
    
         val outputStream = ByteArrayOutputStream()
    
         bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)    //here, 100 is the quality in %
    
         return outputStream.toByteArray()
     
    

演示:https://www.youtube.com/watch?v=iTXCn3NVqDM


将原始大小的图片上传到 Firebase 存储:

val uploadTask = profilePicturesReference.putFile(fileUri)
    

【讨论】:

感谢您的回答!有趣的方法,但是如果我想要原始图片的压缩版本怎么办?在我的例子中,编辑屏幕在一个小的 ImageView 中显示图像,而在显示屏幕中它显示在一个更大的视图中。此外,它是圆形裁剪的,将来可能会显示为方形裁剪。如果我按照你的方法,我会得到一张特别匹配小编辑屏幕 ImageView 大小和裁剪(或更小的视图)的图片。【参考方案3】:

我能想到的最佳解决方案是:

片段:

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) 
        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode) 
            RC_PICK_IMAGE ->
                if (resultCode == Activity.RESULT_OK)
                    data?.data?.let  uri ->
                        context?.let 
                        viewModel.updateUserPicture(uriToCompressedBitmap(it, uri)) 
                
            
        
    

    fun uriToCompressedBitmap(context: Context, uri: Uri): ByteArray 
        val pfd = context.contentResolver.openFileDescriptor(uri, "r")
        val bitmap =
            BitmapFactory.decodeFileDescriptor(pfd?.fileDescriptor, null, null)
        val baos = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 75, baos)
        return baos.toByteArray()
    

(当然:updateUserPictureuploadFileUri参数改为ByteArray,对putFile的调用改为putBytes

这个link 对我很有帮助。

我不太喜欢在整个应用程序中使用位图而不是 uris,但这是迄今为止对我最有效的方法。如果有人有更好的解决方案,请分享:)

编辑:

此解决方案会丢失元数据,如here 所述。当然是可以解决的(here),但是我认为因为元数据字段可以在android版本之间改变,所以Hascher7的解决方案above更加健壮。

【讨论】:

以上是关于如何压缩从科特林画廊挑选的照片的主要内容,如果未能解决你的问题,请参考以下文章

如何在流中使用来自另一个流数据的数据? (科特林流程)

如果 MutableList 为空,如何更改 TextView 的可见性? (安卓/科特林)

如何在firestore中检索当前auth用户的所有字段? - 科特林

科特林 |杰克逊注解 |如何修复超出 START_ARRAY 令牌错误

如何根据将单击的推送通知的确切内容来选择将成为 startActivity 的 Activity ??? (科特林)

如何在 for 循环中使用动态键值从 JSON 数组获取值?科特林