使用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,性能最优:
- 旋转前先从原文件缓存需要保留的EXIF经纬度、海拔等属性
- 旋转Bitmap并压缩得到字节数组后,直接在内存中修改EXIF属性
- 将修改后的字节数组转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
相关产品推荐
相关产品推荐

