kotlinjpeginputstreamexifoutputstream

How do I preserve EXIF when rotating bitmap with createbitmap


I am working on an app which one of its task is is to take pictures (androidx.camera library), tag it with 'EXIF' 'latLong', rotate the picture with some predefined (device dependent) value through Bitmap.createBitmap before encode it as Base64 String, and then upload it with an API.

The trouble is that the 'EXIF' 'latLong' info get lost during this process, and how do I in the best way keep it (or how to rewrite it) ?

Snippet of the code which takes the picture: Callback when picture file is saved. Catch that filepath and add EXIF with my preserved devicelocation (from an androidx room database), ths snippet works well.

    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")
        }

    }

...and I can easily recover that information when presenting the picture:

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

Before I upload the picture, I need to rotate it (the server side needs the picture in a particular direction, and only way to do that is to rotate it using Bitmap.createBitmap method, but here I suspect the EXIF information to be stripped during that process.) The 'EXIF' 'latLong' should later be used when the pictures are further processed in the server.

    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
    }


-> I think I have two methods to preserve the EXIF:

A. Extract->rotate->save picture to file->add exif to file->send picture

  1. save the EXIF before rotation.
  2. rotate picture
  3. save the picture to file
  4. add EXIF in the same way as the picture was taken.
  5. Send picture. (Actually the whole process without rotation)

This cause extra storing and file-handling, and slows the entire process down.

B. Extract->rotate->add exif to stream->send picture

  1. Save EXIF before rotation
  2. rotate picture
  3. save EXIF to stream
  4. Send picture

This was more promising, but I found that there was no "save EXIF to" OutputStream, but there is for InputStream.

Is there a way to "convert" the stream from Output to Input temporary ?

Or is there another way to rotate the picture keeping the EXIF put ?


Solution

  • I havent found any good non-file inclusive way of doing it, so I came up with a solution which mimicks the A. method above:

        private suspend fun postPictureWithPath(
            _companyId: Long?=null,
            savedPath: String,
            siteId: Long,
            carId: Long?,
            assignmentId: Long,
            siteHash: String,
            assignmentHash: String,
            uploadFilename: String,
            hq: Boolean?
        ):Int?
        {
            val resolution = if(hq==true) CAMERA_HI_RESOLUTION else CAMERA_LO_RESOLUTION
    
            APIClientFactory.makeAPIInterfaceService()?.let {apiInterfaceService ->
                try {
                    val companyId = _companyId?:getUserSelection().departmentId
    
                    var oldExif = ExifInterface(savedPath)
    
                    Log.i(TAG, "postPictureWithPath: exif = ${oldExif.latLong?.get(0)}, ${oldExif.latLong?.get(1)}")
    
                    var 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
                        ),"90.0")?.toFloatOrNull()?:90.0f  //Changed to standard 90 on 2022.11.25
    
                    var newWidth = bm.width;
                    var newHeight = bm.height;
    
                    Log.i(TAG, "B901: postPictureWithPath: before: height = ${bm.height}, width = ${bm.width}")
    
                    if(bm.height>=bm.width)
                    {
                        if(newHeight > resolution.height)
                        {
                            val ratio = resolution.height.toDouble()/newHeight.toDouble();
                            newHeight = (ratio*newHeight.toDouble()).roundToInt()
                            newWidth = (ratio*newWidth.toDouble()).roundToInt()
    
                        }
                    } else {
                        if(newWidth > resolution.height) //using height since it should not be wider than 1280
                        {
                            val ratio = resolution.height.toDouble()/newWidth.toDouble();
                            newWidth = (ratio*newWidth.toDouble()).roundToInt()
                            newHeight = (ratio*newHeight.toDouble()).roundToInt()
    
                        }
                    }
    
                    bm = Bitmap.createScaledBitmap(bm, newWidth, newHeight, true)
    
                    Log.i(TAG, "B901: postPictureWithPath: after: height = ${bm.height}, width = ${bm.width}")
    
                    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
    
                    //Saving file to write exif info after conversion
    
                    val outputStream = FileOutputStream(savedPath)
                    byteArrayOutputStream.writeTo(outputStream)
                    outputStream.close()
    
                    val newExif = ExifInterface(savedPath)
    
                    oldExif.latLong?.let{it
                        newExif.setLatLong(it[0], it[1])
                    }
                    newExif.saveAttributes()
    
                    //Opening file after exif write (note 2GB limit !)
    
                    val byteArray = File(savedPath).readBytes()
    
                    //Convert to Base64 and send.
    
                    val encodedImage = Base64.encodeToString(byteArray,Base64.NO_WRAP)
                    Log.i(TAG, "postPictureWithPath: P5020 companyId = $companyId")
                    val req = apiInterfaceService.postSlamSiteSiteIdAssignmentAssignmentIdPictureOriginalFilename(
                        companyId = companyId,
                        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(applicationContext.getString(R.string.repository_post_picture_with_path_missing_data_record))
                            return -5
                        }
                    }
                    else if(req?.success==false)
                    {
                        Message.error(req.message?:applicationContext.getString(R.string.repository_post_picture_with_path_unknown_picture_upload_error))
                        return -1
                    }
                    else
                    {
                        Message.error(req?.message?:applicationContext.getString(R.string.repository_post_picture_with_path_unknown_connection_error))
                        return null
                    }
    
    
    
                }
                catch (httpException:HttpException)
                {
                    Log.e(TAG, "postPictureWithPath: httpException",httpException)
                    Message.error(httpException.message()?:applicationContext.getString(R.string.repository_post_picture_with_path_uknown_http_error))
                    return -2
                }
                catch (throwable: Throwable)
                {
                    Log.e(TAG, "postPictureWithPath: throwable", throwable)
                    Message.error(throwable.message?:applicationContext.getString(R.string.repository_post_picture_with_path_uknown_throwable_error))
                    return -3
                }
                catch (exception: Exception)
                {
                    Log.e(TAG, "postPictureWithPath: exception", exception)
                    Message.error(exception.message?:applicationContext.getString(R.string.repository_post_picture_with_path_uknown_error))
    
                    return -4
                }
    
    
    
    
            }?:let {
                return null
            }
    
            return 0
        }
    
    
    

    This is a snippet from my code, so there may be much extra.

    My use of exif here is just location, but you may use other tags of course.