From 57f51110b531dc71a9039b4d910b620e3da50126 Mon Sep 17 00:00:00 2001
From: Rinaldy Adin <16521390@mahasiswa.itb.ac.id>
Date: Fri, 5 Apr 2024 13:17:06 +0700
Subject: [PATCH] feat: transform preview correctly

---
 app/build.gradle.kts                          |   2 +
 .../example/abe/ui/twibbon/TwibbonFragment.kt | 135 ++++++++++++++----
 app/src/main/res/layout/fragment_twibbon.xml  |  53 ++++---
 gradle/libs.versions.toml                     |   2 +
 4 files changed, 139 insertions(+), 53 deletions(-)

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index cbf15de..5fc3d1e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -58,6 +58,7 @@ dependencies {
     implementation(libs.camera.lifecycle)
     implementation(libs.camera.view)
     implementation(libs.glide)
+    implementation(libs.androidx.exifinterface)
     annotationProcessor(libs.glideCompiler)
     implementation(libs.androidx.activity)
     implementation(libs.play.services.location)
@@ -72,4 +73,5 @@ dependencies {
     androidTestImplementation(libs.androidx.espresso.core)
     implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
     implementation("org.jsoup:jsoup:1.14.3")
+
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt b/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt
index 766cbb6..ffbf96d 100644
--- a/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt
+++ b/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt
@@ -1,7 +1,6 @@
 package com.example.abe.ui.twibbon
 
 import android.Manifest
-import android.R.attr
 import android.app.Dialog
 import android.content.pm.PackageManager
 import android.graphics.Bitmap
@@ -11,7 +10,6 @@ import android.graphics.Canvas
 import android.graphics.Color
 import android.graphics.Matrix
 import android.graphics.drawable.ColorDrawable
-import android.media.ExifInterface
 import android.net.Uri
 import android.os.Bundle
 import android.util.Log
@@ -28,6 +26,7 @@ import androidx.camera.core.ImageCaptureException
 import androidx.camera.core.Preview
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.core.content.ContextCompat
+import androidx.exifinterface.media.ExifInterface
 import androidx.fragment.app.Fragment
 import com.bumptech.glide.Glide
 import com.example.abe.R
@@ -36,7 +35,6 @@ import java.io.ByteArrayOutputStream
 import java.io.File
 import java.io.FileNotFoundException
 import java.io.FileOutputStream
-import java.io.InputStream
 import java.net.URI
 import java.text.SimpleDateFormat
 import java.util.Locale
@@ -48,7 +46,6 @@ class TwibbonFragment : Fragment() {
     private val binding get() = _binding!!
 
     private var imageCapture: ImageCapture? = null
-    private lateinit var cameraExecutor: ExecutorService
 
     companion object {
         private const val TAG = "ABE-TWB"
@@ -68,7 +65,6 @@ class TwibbonFragment : Fragment() {
         }
 
         binding.btnCaptureTwibbon.setOnClickListener { previewTwibbon() }
-        binding.ibtnCustomTwibbon.setOnClickListener { setCustomTwibbon() }
 
         return binding.root
     }
@@ -92,9 +88,10 @@ class TwibbonFragment : Fragment() {
                 cameraProvider.unbindAll()
 
                 cameraProvider.bindToLifecycle(
-                    this, cameraSelector, preview, imageCapture)
+                    this, cameraSelector, preview, imageCapture
+                )
 
-            } catch(exc: Exception) {
+            } catch (exc: Exception) {
                 Log.e(TAG, "Use case binding failed", exc)
             }
 
@@ -103,14 +100,17 @@ class TwibbonFragment : Fragment() {
 
     private val cameraPermissionsLauncher =
         registerForActivityResult(
-            ActivityResultContracts.RequestPermission())
+            ActivityResultContracts.RequestPermission()
+        )
         { isGranted ->
             if (isGranted) {
                 startCamera()
             } else {
-                Toast.makeText(requireContext(),
+                Toast.makeText(
+                    requireContext(),
                     "Please allow camera to use Twibbon",
-                    Toast.LENGTH_SHORT).show()
+                    Toast.LENGTH_SHORT
+                ).show()
             }
         }
 
@@ -120,7 +120,12 @@ class TwibbonFragment : Fragment() {
 
         val photoFile = File(
             requireContext().cacheDir,
-            "twibbon_${SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())}.jpg"
+            "twibbon_${
+                SimpleDateFormat(
+                    FILENAME_FORMAT,
+                    Locale.US
+                ).format(System.currentTimeMillis())
+            }.jpg"
         )
 
         val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
@@ -146,28 +151,100 @@ class TwibbonFragment : Fragment() {
     private fun overlayTwibbonToImage(imageUri: Uri) {
         try {
             Log.d(TAG, "Start overlaying twibbon")
-            val ims: InputStream = requireActivity().contentResolver.openInputStream(imageUri) ?: return
-
-            val photoBitmap = BitmapFactory.decodeStream(ims)
-            val orientation = ExifInterface(ims).getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
-            val photoMatrix = Matrix()
-            if (orientation == 6) {
-                photoMatrix.postRotate(90f)
-            } else if (orientation == 3) {
-                photoMatrix.postRotate(180f)
-            } else if (orientation == 8) {
-                photoMatrix.postRotate(270f)
+
+            val exifIms = requireActivity().contentResolver.openInputStream(imageUri) ?: return
+            val exif = ExifInterface(exifIms)
+            val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
+
+            val overlayBitmap = BitmapFactory.decodeResource(
+                requireContext().resources,
+                R.drawable.img_default_twibbon
+            )
+
+            val bitmapIms = requireActivity().contentResolver.openInputStream(imageUri) ?: return
+            val originalPhotoBitmap = BitmapFactory.decodeStream(bitmapIms)
+            val rotationMatrix = Matrix()
+
+            when (orientation) {
+                6 -> {
+                    rotationMatrix.postRotate(90f)
+                }
+
+                3 -> {
+                    rotationMatrix.postRotate(180f)
+                }
+
+                8 -> {
+                    rotationMatrix.postRotate(270f)
+                }
+            }
+
+            val rotatedBitmap = Bitmap.createBitmap(
+                originalPhotoBitmap,
+                0,
+                0,
+                originalPhotoBitmap.width,
+                originalPhotoBitmap.height,
+                rotationMatrix,
+                true
+            )
+
+            val flipMatrix = Matrix().apply {
+                postScale(
+                    -1f,
+                    1f,
+                    rotatedBitmap.width.toFloat() / 2,
+                    rotatedBitmap.height.toFloat()
+                )
+            }
+            val flippedBitmap = Bitmap.createBitmap(
+                rotatedBitmap,
+                0,
+                0,
+                rotatedBitmap.width,
+                rotatedBitmap.height,
+                flipMatrix,
+                true
+            )
+
+            val scaleMatrix = Matrix().apply {
+                val scale = overlayBitmap.width.toFloat() / flippedBitmap.width.toFloat()
+                postScale(
+                    scale,
+                    scale,
+                    flippedBitmap.width.toFloat(),
+                    flippedBitmap.height.toFloat()
+                )
             }
 
-            val overlayBitmap = BitmapFactory.decodeResource(requireContext().resources, R.drawable.img_default_twibbon)
+            val photoBitmap = Bitmap.createBitmap(
+                flippedBitmap,
+                0,
+                0,
+                flippedBitmap.width,
+                flippedBitmap.height,
+                scaleMatrix,
+                true
+            )
+            val translateMatrix = Matrix().apply {
+                postTranslate(
+                    0f,
+                    (overlayBitmap.height.toFloat() - photoBitmap.height.toFloat()) / 2f
+                )
+            }
 
-            val bmOverlay = Bitmap.createBitmap(overlayBitmap.height, overlayBitmap.width, photoBitmap.getConfig())
-            val canvas = Canvas(bmOverlay)
-            canvas.drawBitmap(photoBitmap, photoMatrix, null)
+            val resultBitmap =
+                Bitmap.createBitmap(
+                    overlayBitmap.width,
+                    overlayBitmap.height,
+                    overlayBitmap.getConfig()
+                )
+            val canvas = Canvas(resultBitmap)
+            canvas.drawBitmap(photoBitmap, translateMatrix, null)
             canvas.drawBitmap(overlayBitmap, Matrix(), null)
 
             val bos = ByteArrayOutputStream()
-            bmOverlay.compress(CompressFormat.JPEG, 100 /*ignored for PNG*/, bos)
+            resultBitmap.compress(CompressFormat.JPEG, 100, bos)
             val bitmapData = bos.toByteArray()
 
             val f = File(URI(imageUri.toString()))
@@ -223,8 +300,4 @@ class TwibbonFragment : Fragment() {
     private fun requestCameraPermissions() {
         cameraPermissionsLauncher.launch(Manifest.permission.CAMERA)
     }
-
-    private fun setCustomTwibbon() {
-        TODO("Not yet implemented")
-    }
 }
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_twibbon.xml b/app/src/main/res/layout/fragment_twibbon.xml
index 80f3b08..1d197f8 100644
--- a/app/src/main/res/layout/fragment_twibbon.xml
+++ b/app/src/main/res/layout/fragment_twibbon.xml
@@ -1,29 +1,53 @@
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context=".ui.twibbon.TwibbonFragment">
 
-    <androidx.camera.view.PreviewView
-        android:id="@+id/prvTwibbon"
+    <androidx.constraintlayout.widget.ConstraintLayout
         android:layout_width="match_parent"
         android:layout_height="0dp"
+        android:background="@color/grayLight"
         app:layout_constraintBottom_toTopOf="@+id/clPreviewBottonBar"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintHorizontal_bias="0.5"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintVertical_chainStyle="packed">
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.camera.view.PreviewView
+            android:id="@+id/prvTwibbon"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintHorizontal_bias="0.5"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_chainStyle="packed" />
 
-    </androidx.camera.view.PreviewView>
+        <ImageView
+            android:id="@+id/ivTwibbonOverlay"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:src="@drawable/img_default_twibbon"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintHorizontal_bias="0.5"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_bias="0.5" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/clPreviewBottonBar"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:background="@drawable/bottom_bar_scan"
+        android:background="@color/primaryActive"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent">
@@ -41,21 +65,6 @@
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent" />
 
-        <ImageButton
-            android:id="@+id/ibtnCustomTwibbon"
-            android:layout_width="52dp"
-            android:layout_height="52dp"
-            android:adjustViewBounds="true"
-            android:background="?attr/selectableItemBackgroundBorderless"
-            android:contentDescription="Get Photo from Gallery"
-            android:padding="10dp"
-            android:scaleType="fitCenter"
-            android:src="@drawable/ic_gallery"
-            app:layout_constraintBottom_toBottomOf="@+id/btnCaptureTwibbon"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toEndOf="@+id/btnCaptureTwibbon"
-            app:layout_constraintTop_toTopOf="@+id/btnCaptureTwibbon" />
-
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e5e09b1..df9a53d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -27,6 +27,7 @@ playServicesLocation = "21.2.0"
 camerax = "1.4.0-alpha04"
 glide = "4.12.0"
 okhttp = "4.9.0"
+exifinterface = "1.3.7"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -59,6 +60,7 @@ glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
 glideCompiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
 okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
 logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
+androidx-exifinterface = { group = "androidx.exifinterface", name = "exifinterface", version.ref = "exifinterface" }
 
 [plugins]
 androidApplication = { id = "com.android.application", version.ref = "agp" }
-- 
GitLab