You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Bitmap.createBitmap旋转位图时如何保留EXIF经纬度等元信息

问题:Bitmap旋转后EXIF经纬度丢失的保留方案

场景说明

我正在开发一款应用,其中一项功能是使用androidx.camera库拍照,为照片打EXIF latLong标签,通过Bitmap.createBitmap按设备相关的预设值旋转照片后编码为Base64 String,再通过API上传。
目前遇到的问题是:上述处理过程中EXIF经纬度信息丢失,需要找到最优的保留或重写该信息的方案。

现有代码片段

拍照逻辑(EXIF写入正常)

private fun takePhoto(
    orderResult: OrderResult?,
    categoryString: String
)
{
    Log.i(TAG, "P100: takePhoto: current mediaDirectory = '${getMediaDirectory(requireActivity())}'")

    if(orderResult==null){
        Message.error("Order not ready... try again later")
        return
    }

    val mediaDirectory = getMediaDirectory(requireActivity())


    val filename = "${SimpleDateFormat(UPLOAD_MSLAM_FILENAME_FORMAT, Locale.ROOT).format(Date().time)}${categoryString}.jpg"

    val photoFile = File(mediaDirectory,filename)

    offlinePhotoViewModel.imageCaptureMLD.value?.takePicture(
        ImageCapture.OutputFileOptions.Builder(photoFile).build(),
        ContextCompat.getMainExecutor(requireContext()),
        object : ImageCapture.OnImageSavedCallback{
            override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                val savedUri = Uri.fromFile(photoFile)

                Log.i(TAG, "P100: onImageSaved: saved file attempt: ${outputFileResults}")
                Log.i(TAG, "P100: onImageSaved: savedUri = $savedUri")

                orderResult.apply {
                    siteId?.let {safeSiteId->
                        assignmentId?.let {safeAssignmentId->
                            savedUri.lastPathSegment?.let {safeFilename->
                                savedUri.path?.let {safePath->

                                    CoroutineScope(Dispatchers.IO).launch {
                                        val exif = ExifInterface(safePath)

                                        val deviceLocation = Repository.getDeviceLocationResult()
                                        deviceLocation?.apply {
                                            latitude?.let { safeLatitude->
                                                longitude?.let { safeLongitude->
                                                    exif.setLatLong(safeLatitude,safeLongitude)
                                                }
                                            }
                                            altitude?.let { safeAltitude->
                                                exif.setAltitude(safeAltitude)
                                            }

                                        }

                                        exif.saveAttributes()
                                    }


                                    offlinePhotoViewModel.insertReplacePictureEntity(
                                        PictureEntity(
                                            siteId = safeSiteId,
                                            assignmentId = safeAssignmentId,
                                            filename = safeFilename,
                                            uriString = safePath
                                        )
                                    )
                                }
                            }
                        }
                    }
                }
            }

            override fun onError(exception: ImageCaptureException) {
                Log.e(TAG, "onError: takePhoto error", exception)
                Message.error(exception.message?:"Unknown error capturing photo")
                //TODO: Consider record file store attempt as an error
            }

        }
    )?:let {
        Message.error("Camera not ready yet, please try again later")
    }

}

本地EXIF读取验证逻辑

localPictureResult?.apply {
    val imgFile = File(uriString)
    if(imgFile.exists())
    {
        listItemNewPicturesGalleryAppCompatImageView.apply {
            setImageURI(Uri.fromFile(imgFile))
            visibility = View.VISIBLE
            val exif = ExifInterface(imgFile)
            exif.latLong?.let {
                listItemNewPicturesGalleryMaterialTextViewExifLocationContents.text =
                    formatLatLngPosition(LatLng(it[0],it[1]))
            }

        }
        listItemNewPicturesGalleryCircularProgressIndicator.visibility = View.GONE
    }
    else
    {
        listItemNewPicturesGalleryAppCompatImageView.apply {
            setImageDrawable(
                ContextCompat.getDrawable(binding.root.context,
                R.drawable.ic_baseline_image_not_supported_24)
            )
            visibility = View.VISIBLE
        }
        listItemNewPicturesGalleryCircularProgressIndicator.visibility = View.GONE
    }
}

上传旋转逻辑(EXIF丢失环节)

private suspend fun postPictureWithPath(
    savedPath: String,
    siteId: Long,
    carId: Long?,
    assignmentId: Long,
    siteHash: String,
    assignmentHash: String,
    uploadFilename: String
):Int?
{
    APIClientFactory.makeAPIInterfaceService()?.let {apiInterfaceService ->
        try {
            val bm = BitmapFactory.decodeFile(savedPath)

            val rotation = PreferenceManager
                .getDefaultSharedPreferences(applicationContext)
                .getString(applicationContext.resources.getString(
                    R.string.preference_key_fragment_settings_camera_picture_gallery_drop_down_preference_rotation
                ),"0.0")?.toFloatOrNull()?:0.0f

            val rotatedBitmap = Bitmap.createBitmap(
                bm,
                0,
                0,
                bm.width,
                bm.height,
                Matrix().apply { postRotate(rotation) },
                true
            )

            val byteArrayOutputStream = ByteArrayOutputStream();
            rotatedBitmap.compress(Bitmap.CompressFormat.JPEG,100,byteArrayOutputStream) //Consider value in settings

            val byteArray = byteArrayOutputStream.toByteArray()

            val encodedImage = Base64.encodeToString(byteArray,Base64.NO_WRAP)

            val req = apiInterfaceService.postSlamSiteSiteIdAssignmentAssignmentIdPictureOriginalFilename(
                siteId = siteId,
                assignmentId = assignmentId,
                carId = carId,
                originalFilename = uploadFilename,
                siteHash = siteHash,
                assignmentHash = assignmentHash,
                base64String = encodedImage
            )

            if(req?.success==true)
            {
                if(req.data!=null)
                {
                    req.data?.apply {
                        appDatabase.withTransaction {
                            appDatabase.pictureDao().insertReplacePictureInfo(
                                PictureInfoEntity(
                                    siteId = siteId,
                                    assignmentId = assignmentId,
                                    originalFilename = originalFilename,
                                    fileDate = fileDate,
                                    hash = hash?:"no hash present",
                                    upload = true
                                )
                            )
                        }
                    }
                }
                else
                {
                    Message.error("Missing data record")
                    return -5
                }
            }
            else if(req?.success==false)
            {
                Message.error(req.message?:"Unknown picture upload error")
                return -1
            }
            else
            {
                Message.error(req?.message?:"Unknown connection error")
                return null
            }
        }
        catch (httpException:HttpException)
        {
            Log.e(TAG, "postPictureWithPath: httpException",httpException)
            Message.error(httpException.message()?:"Unknown HTTP error posting picture")
            return -2
        }
        catch (throwable: Throwable)
        {
            Log.e(TAG, "postPictureWithPath: throwable", throwable)
            Message.error(throwable.message?:"Unknown throwable error posting picture")
            return -3
        }
        catch (exception: Exception)
        {
            Log.e(TAG, "postPictureWithPath: exception", exception)
            Message.error(exception.message?:"Unknown exception posting picture info")
            return -4
        }

    }?:let {
        return null
    }

    return 0
}

现有备选方案

  • 方案A:提取EXIF→旋转照片→保存照片到文件→写入EXIF到文件→上传,缺点是会产生额外存储和文件操作,拖慢整体处理速度。
  • 方案B:提取EXIF→旋转照片→将EXIF写入流→上传,缺点是原生ExifInterface旧版本不支持直接向OutputStream写入EXIF。

最优解决方案

使用AndroidX ExifInterface 1.3.0及以上版本提供的内存读写能力,全程无额外文件IO,性能最优:

  1. 旋转前先从原文件缓存需要保留的EXIF经纬度、海拔等属性
  2. 旋转Bitmap并压缩得到字节数组后,直接在内存中修改EXIF属性
  3. 将修改后的字节数组转Base64上传即可

修改后的上传逻辑核心代码:

// 先读取原文件的EXIF信息缓存
val originalExif = ExifInterface(savedPath)
val latLong = originalExif.latLong
val altitude = originalExif.getAltitude(0.0)

// 原有旋转逻辑不变
val bm = BitmapFactory.decodeFile(savedPath)
val rotation = PreferenceManager
    .getDefaultSharedPreferences(applicationContext)
    .getString(applicationContext.resources.getString(
        R.string.preference_key_fragment_settings_camera_picture_gallery_drop_down_preference_rotation
    ),"0.0")?.toFloatOrNull()?:0.0f
val rotatedBitmap = Bitmap.createBitmap(
    bm,
    0,
    0,
    bm.width,
    bm.height,
    Matrix().apply { postRotate(rotation) },
    true
)

// 压缩到字节数组
val byteArrayOutputStream = ByteArrayOutputStream()
rotatedBitmap.compress(Bitmap.CompressFormat.JPEG,100,byteArrayOutputStream)
val imageBytes = byteArrayOutputStream.toByteArray()

// 内存中写入EXIF
val modifiedBytes = ByteArrayInputStream(imageBytes).use { inputStream ->
    val newExif = ExifInterface(inputStream)
    // 写入缓存的经纬度、海拔
    latLong?.let { newExif.setLatLong(it[0], it[1]) }
    newExif.setAltitude(altitude)
    // 保存修改并输出新的字节数组
    ByteArrayOutputStream().use { outputStream ->
        newExif.saveAttributes()
        newExif.writeOutput(outputStream)
        outputStream.toByteArray()
    }
}

// 转Base64上传
val encodedImage = Base64.encodeToString(modifiedBytes, Base64.NO_WRAP)
// 后续原有上传逻辑不变

注意事项

  • 需要在build.gradle中引入依赖:implementation "androidx.exifinterface:exifinterface:1.3.6"(使用最新稳定版即可)
  • 所有操作都在内存完成,不会产生额外本地文件,性能和方案B一致,无额外开销

内容的提问来源于stack exchange,提问作者Roar Grønmo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.29 14:36:05