Skip to content

Commit eb7f2e1

Browse files
Fixed photos being rotated when uploaded (#88)
Co-authored-by: Thomas Ezan <[email protected]> Co-authored-by: lethargicpanda <[email protected]>
1 parent 7bb33c1 commit eb7f2e1

File tree

4 files changed

+96
-6
lines changed

4 files changed

+96
-6
lines changed

ai-catalog/gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ uiTooling = "1.8.3"
3232
firebaseAi = "16.2.0"
3333
lifecycleViewmodelAndroid = "2.8.7"
3434
material3 = "1.3.2"
35+
exifinterface = "1.4.1"
3536

3637
[libraries]
3738
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -78,6 +79,7 @@ ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref =
7879
google-firebase-ai = { group = "com.google.firebase", name = "firebase-ai", version.ref = "firebaseAi" }
7980
androidx-lifecycle-viewmodel-android = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-android", version.ref = "lifecycleViewmodelAndroid" }
8081
material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
82+
androidx-exifinterface = { group = "androidx.exifinterface", name = "exifinterface", version.ref = "exifinterface" }
8183

8284
[plugins]
8385
android-application = { id = "com.android.application", version.ref = "agp" }

ai-catalog/samples/gemini-image-chat/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ dependencies {
6767
implementation(libs.androidx.lifecycle.runtime.compose)
6868
implementation(libs.androidx.material3.android)
6969
implementation(libs.coil.compose)
70+
implementation(libs.androidx.exifinterface)
7071
ksp(libs.hilt.compiler)
7172

7273
testImplementation(libs.junit)

ai-catalog/samples/gemini-image-chat/src/main/java/com/android/ai/samples/geminiimagechat/GeminiImageChatScreen.kt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package com.android.ai.samples.geminiimagechat
1818
import android.content.Intent
1919
import android.graphics.Bitmap
2020
import android.net.Uri
21-
import android.provider.MediaStore
2221
import androidx.activity.compose.rememberLauncherForActivityResult
2322
import androidx.activity.result.PickVisualMediaRequest
2423
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
@@ -53,6 +52,7 @@ import androidx.compose.runtime.Composable
5352
import androidx.compose.runtime.getValue
5453
import androidx.compose.runtime.mutableStateOf
5554
import androidx.compose.runtime.remember
55+
import androidx.compose.runtime.rememberCoroutineScope
5656
import androidx.compose.runtime.saveable.rememberSaveable
5757
import androidx.compose.runtime.setValue
5858
import androidx.compose.ui.Alignment
@@ -67,6 +67,10 @@ import androidx.compose.ui.unit.sp
6767
import androidx.core.net.toUri
6868
import androidx.hilt.navigation.compose.hiltViewModel
6969
import androidx.lifecycle.compose.collectAsStateWithLifecycle
70+
import com.android.ai.samples.util.loadBitmapWithCorrectOrientation
71+
import kotlinx.coroutines.Dispatchers
72+
import kotlinx.coroutines.launch
73+
import kotlinx.coroutines.withContext
7074

7175
@OptIn(ExperimentalMaterial3Api::class)
7276
@Composable
@@ -84,6 +88,7 @@ fun GeminiImageChatScreen(viewModel: GeminiImageChatViewModel = hiltViewModel())
8488
}
8589

8690
val context = LocalContext.current
91+
val coroutineScope = rememberCoroutineScope()
8792

8893
Scaffold(
8994
modifier = Modifier
@@ -146,12 +151,16 @@ fun GeminiImageChatScreen(viewModel: GeminiImageChatViewModel = hiltViewModel())
146151
message = it
147152
},
148153
onSendClick = {
149-
val bitmap = imageUri?.let {
150-
MediaStore.Images.Media.getBitmap(context.contentResolver, imageUri)
154+
coroutineScope.launch {
155+
val bitmap = imageUri?.let {
156+
withContext(Dispatchers.IO) {
157+
loadBitmapWithCorrectOrientation(context, it)
158+
}
159+
}
160+
viewModel.sendMessage(message, bitmap)
161+
imageUri = null
162+
message = ""
151163
}
152-
viewModel.sendMessage(message, bitmap)
153-
imageUri = null
154-
message = ""
155164
},
156165
sendEnabled = uiState.geminiMessageState !is GeminiMessageState.Generating,
157166
addImage = {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.android.ai.samples.util
17+
18+
import android.content.Context
19+
import android.graphics.Bitmap
20+
import android.graphics.ImageDecoder
21+
import android.graphics.Matrix
22+
import android.net.Uri
23+
import android.os.Build.VERSION
24+
import android.os.Build.VERSION_CODES
25+
import android.provider.MediaStore
26+
import androidx.exifinterface.media.ExifInterface
27+
import java.io.IOException
28+
29+
internal fun loadBitmapWithCorrectOrientation(context: Context, uri: Uri): Bitmap? {
30+
return try {
31+
if (VERSION.SDK_INT >= VERSION_CODES.P) {
32+
val source = ImageDecoder.createSource(context.contentResolver, uri)
33+
ImageDecoder.decodeBitmap(source)
34+
} else {
35+
@Suppress("DEPRECATION")
36+
val bitmap = MediaStore.Images.Media.getBitmap(context.contentResolver, uri)
37+
38+
context.contentResolver.openInputStream(uri)?.use { inputStream ->
39+
val exifInterface = ExifInterface(inputStream)
40+
val orientation = exifInterface.getAttributeInt(
41+
ExifInterface.TAG_ORIENTATION,
42+
ExifInterface.ORIENTATION_NORMAL,
43+
)
44+
45+
if (orientation == ExifInterface.ORIENTATION_NORMAL) {
46+
return@use bitmap
47+
}
48+
49+
val matrix = Matrix()
50+
when (orientation) {
51+
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
52+
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
53+
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
54+
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1.0f, 1.0f)
55+
ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1.0f, -1.0f)
56+
ExifInterface.ORIENTATION_TRANSPOSE -> {
57+
matrix.postRotate(90f)
58+
matrix.preScale(-1.0f, 1.0f)
59+
}
60+
ExifInterface.ORIENTATION_TRANSVERSE -> {
61+
matrix.postRotate(-90f)
62+
matrix.preScale(-1.0f, 1.0f)
63+
}
64+
}
65+
66+
bitmap?.let {
67+
Bitmap.createBitmap(
68+
it, 0, 0, it.width, it.height, matrix, true,
69+
)
70+
}
71+
} ?: bitmap
72+
}
73+
} catch (e: IOException) {
74+
// In a real app, you should use a proper logging library.
75+
e.printStackTrace()
76+
null
77+
}
78+
}

0 commit comments

Comments
 (0)