1
Fork 0
mirror of https://github.com/Steffo99/twom.git synced 2024-11-25 17:44:24 +00:00

Modify bitmaps before using them to create a room

This commit is contained in:
Steffo 2023-12-07 01:31:47 +01:00
parent 5ca508f645
commit 63bf438b1b
Signed by: steffo
GPG key ID: 2A24051445686895
5 changed files with 157 additions and 75 deletions

View file

@ -0,0 +1,54 @@
package eu.steffo.twom.create
import android.graphics.Bitmap
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.matrix.avatar.AvatarFromBitmap
@Composable
@Preview
fun AvatarSelector(
modifier: Modifier = Modifier,
onSelectAvatar: (bitmap: Bitmap) -> Unit = {},
) {
val context = LocalContext.current
val resolver = context.contentResolver
var selection by rememberSaveable { mutableStateOf<Bitmap?>(null) }
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) ImageSelect@{
it ?: return@ImageSelect
val rawBitmap = ImageHandler.getRawBitmap(resolver, it) ?: return@ImageSelect
val orientation = ImageHandler.getOrientation(resolver, it) ?: return@ImageSelect
val correctedBitmap = ImageHandler.squareAndOrient(rawBitmap, orientation)
selection = correctedBitmap
onSelectAvatar(correctedBitmap)
}
Box(
modifier = modifier
.clickable {
launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
) {
AvatarFromBitmap(
bitmap = selection?.asImageBitmap(),
)
}
}

View file

@ -26,11 +26,10 @@ class CreateActivity : ComponentActivity() {
val resultIntent = Intent() val resultIntent = Intent()
resultIntent.putExtra(NAME_EXTRA, name) resultIntent.putExtra(NAME_EXTRA, name)
resultIntent.putExtra(DESCRIPTION_EXTRA, description) resultIntent.putExtra(DESCRIPTION_EXTRA, description)
// Kotlin cannot use nullable types in Java interop generics
if (avatarUri != null) { if (avatarUri != null) {
// Kotlin cannot use nullable types in Java interop generics
resultIntent.putExtra(AVATAR_EXTRA, avatarUri) resultIntent.putExtra(AVATAR_EXTRA, avatarUri)
} }
setResult(RESULT_OK, resultIntent) setResult(RESULT_OK, resultIntent)
finish() finish()
}, },

View file

@ -1,11 +1,6 @@
package eu.steffo.twom.create package eu.steffo.twom.create
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -23,16 +18,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.steffo.twom.R import eu.steffo.twom.R
import eu.steffo.twom.matrix.avatar.AvatarFromBitmap
import eu.steffo.twom.theme.TwoMPadding import eu.steffo.twom.theme.TwoMPadding
@Composable @Composable
@ -41,40 +32,28 @@ fun CreateActivityContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClickCreate: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> }, onClickCreate: (name: String, description: String, avatarUri: Uri?) -> Unit = { _, _, _ -> },
) { ) {
val context = LocalContext.current
var name by rememberSaveable { mutableStateOf("") } var name by rememberSaveable { mutableStateOf("") }
var description by rememberSaveable { mutableStateOf("") } var description by rememberSaveable { mutableStateOf("") }
var avatarUri by rememberSaveable { mutableStateOf<Uri?>(null) } var avatarUri by rememberSaveable { mutableStateOf<Uri?>(null) }
var avatarBitmap by rememberSaveable { mutableStateOf<ImageBitmap?>(null) }
// val avatarBitmap = if(avatarUri != null) BitmapFactory.decodeFile(avatarUri.toString()).asImageBitmap() else null // val avatarBitmap = if(avatarUri != null) BitmapFactory.decodeFile(avatarUri.toString()).asImageBitmap() else null
val avatarSelectLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) {
avatarUri = it
avatarBitmap = if (it != null) ImageHandler.uriToBitmap(context.contentResolver, it)
?.asImageBitmap() else null
}
Column(modifier) { Column(modifier) {
Row(TwoMPadding.base) { Row(TwoMPadding.base) {
val avatarContentDescription = stringResource(R.string.create_avatar_label) val avatarContentDescription = stringResource(R.string.create_avatar_label)
Box( AvatarSelector(
modifier = Modifier modifier = Modifier
.size(60.dp) .size(60.dp)
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.clickable {
avatarSelectLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}
.semantics { .semantics {
this.contentDescription = avatarContentDescription this.contentDescription = avatarContentDescription
} },
) { onSelectAvatar = SelectAvatar@{
AvatarFromBitmap( val cache = ImageHandler.bitmapToCache("createAvatar", it)
bitmap = avatarBitmap, avatarUri = Uri.fromFile(cache)
) },
} )
TextField( TextField(
modifier = Modifier modifier = Modifier
.height(60.dp) .height(60.dp)

View file

@ -6,73 +6,84 @@ import android.graphics.BitmapFactory
import android.graphics.Matrix import android.graphics.Matrix
import android.net.Uri import android.net.Uri
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import java.io.File
class ImageHandler { class ImageHandler {
companion object { companion object {
fun uriToBitmap(contentResolver: ContentResolver, uri: Uri): Bitmap? { fun getOrientation(contentResolver: ContentResolver, uri: Uri): Int? {
// Open two streams... contentResolver.openInputStream(uri).use {
// One to read the EXIF metadata from: if (it == null) {
val exifStream = contentResolver.openInputStream(uri) return null
// One to read the image data itself from: } else {
val bitmapStream = contentResolver.openInputStream(uri) return ExifInterface(it).getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
}
if (exifStream == null || bitmapStream == null) {
return null
} }
}
// Use the EXIF metadata to determine the orientation of the image fun getRawBitmap(contentResolver: ContentResolver, uri: Uri): Bitmap? {
val exifInterface = ExifInterface(exifStream) contentResolver.openInputStream(uri).use {
val orientation = if (it == null) {
exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1) return null
exifStream.close() } else {
return BitmapFactory.decodeStream(it)
// Parse the image data as-is }
val originalBitmap = BitmapFactory.decodeStream(bitmapStream) }
bitmapStream.close() }
fun squareAndOrient(
bitmap: Bitmap,
orientation: Int = ExifInterface.ORIENTATION_NORMAL
): Bitmap {
// Determine the starting points and the size to crop the image to a 1:1 square // Determine the starting points and the size to crop the image to a 1:1 square
val xStart: Int val xStart: Int
val yStart: Int val yStart: Int
val size: Int val size: Int
if (originalBitmap.width > originalBitmap.height) { if (bitmap.width > bitmap.height) {
yStart = 0 yStart = 0
xStart = (originalBitmap.width - originalBitmap.height) / 2 xStart = (bitmap.width - bitmap.height) / 2
size = originalBitmap.height size = bitmap.height
} else { } else {
xStart = 0 xStart = 0
yStart = (originalBitmap.height - originalBitmap.width) / 2 yStart = (bitmap.height - bitmap.width) / 2
size = originalBitmap.width size = bitmap.width
} }
// Create a transformation matrix to rotate the bitmap based on the orientation // Create a transformation matrix to rotate the bitmap based on the orientation
val transformationMatrix = Matrix() val transformationMatrix = Matrix()
// TODO: Make sure these transformations are valid // TODO: Make sure all these transformations are valid
when (orientation) { when (orientation) {
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> transformationMatrix.postScale( ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> {
-1f, transformationMatrix.postScale(-1f, 1f)
1f
)
ExifInterface.ORIENTATION_ROTATE_180 -> transformationMatrix.postRotate(180f)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> transformationMatrix.postScale(
1f,
-1f
)
ExifInterface.ORIENTATION_TRANSPOSE -> {/* TODO: Transpose the image Matrix */
} }
ExifInterface.ORIENTATION_ROTATE_90 -> transformationMatrix.postRotate(90f) ExifInterface.ORIENTATION_ROTATE_180 -> {
ExifInterface.ORIENTATION_TRANSVERSE -> {/* TODO: Flip horizontally the image Matrix, then transpose it */ transformationMatrix.postRotate(180f)
} }
ExifInterface.ORIENTATION_ROTATE_270 -> transformationMatrix.postRotate(270f) ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
transformationMatrix.postScale(1f, -1f)
}
ExifInterface.ORIENTATION_TRANSPOSE -> {
/* TODO: Transpose the image Matrix */
}
ExifInterface.ORIENTATION_ROTATE_90 -> {
transformationMatrix.postRotate(90f)
}
ExifInterface.ORIENTATION_TRANSVERSE -> {
/* TODO: Flip horizontally the image Matrix, then transpose it */
}
ExifInterface.ORIENTATION_ROTATE_270 -> {
transformationMatrix.postRotate(270f)
}
} }
// Crop the bitmap return Bitmap.createBitmap(
val croppedBitmap = Bitmap.createBitmap( bitmap,
originalBitmap,
xStart, xStart,
yStart, yStart,
size, size,
@ -80,8 +91,21 @@ class ImageHandler {
transformationMatrix, transformationMatrix,
true true
) )
}
return croppedBitmap fun bitmapToCache(id: String, bitmap: Bitmap): File {
val file = File.createTempFile("bitmap_$id", ".jpg")
file.outputStream().use {
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, it)
it.flush()
}
return file
}
fun bitmapFromCache(file: File): Bitmap {
file.inputStream().use {
return BitmapFactory.decodeStream(it)
}
} }
} }
} }

View file

@ -9,6 +9,7 @@ import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toFile
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import eu.steffo.twom.create.CreateActivity import eu.steffo.twom.create.CreateActivity
import eu.steffo.twom.login.LoginActivity import eu.steffo.twom.login.LoginActivity
@ -160,17 +161,42 @@ class MainActivity : ComponentActivity() {
val currentSession = session val currentSession = session
val createRoomParams = CreateRoomParams() val createRoomParams = CreateRoomParams()
createRoomParams.name = name createRoomParams.name = name
createRoomParams.topic = description createRoomParams.topic = description
createRoomParams.avatarUri = avatarUri
createRoomParams.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT createRoomParams.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
createRoomParams.roomType = TwoMMatrix.ROOM_TYPE createRoomParams.roomType = TwoMMatrix.ROOM_TYPE
when (avatarUri?.toFile()?.isFile) {
false -> {
Log.e(
"Main",
"Avatar has been deleted from cache before room could possibly be created, ignoring..."
)
}
true -> {
Log.d(
"Main",
"Avatar seems to exist at: $avatarUri"
)
createRoomParams.avatarUri = avatarUri
}
null -> {
Log.d(
"Main",
"Avatar was not set, ignoring..."
)
}
}
Log.d( Log.d(
"Main", "Main",
"Creating room '$name' with description '$description' and avatar '$avatarUri'..." "Creating room '$name' with description '$description' and avatar '$avatarUri'..."
) )
val roomId = currentSession!!.roomService().createRoom(createRoomParams) val roomId = currentSession!!.roomService().createRoom(createRoomParams)
Log.d( Log.d(
"Main", "Main",
"Created room '$name' with description '$description' and avatar '$avatarUri': $roomId" "Created room '$name' with description '$description' and avatar '$avatarUri': $roomId"