Merge pull request #591 from vector-im/feature/image_orientation

Fix issue with image orientation
This commit is contained in:
Benoit Marty 2019-10-08 10:53:46 +02:00 committed by GitHub
commit ecdb3c3326
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 116 additions and 36 deletions

View File

@ -17,6 +17,7 @@ Bugfix:
- Fix opening a permalink: the targeted event is displayed twice (#556) - Fix opening a permalink: the targeted event is displayed twice (#556)
- Fix opening a permalink paginates all the history up to the last event (#282) - Fix opening a permalink paginates all the history up to the last event (#282)
- after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267) - after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267)
- Picture uploads are unreliable, pictures are shown in wrong aspect ratio on desktop client (#517)
Translations: Translations:
- -

View File

@ -118,6 +118,9 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:core:$markwon_version"
// Image
implementation 'androidx.exifinterface:exifinterface:1.0.0'
// Database // Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
kapt 'dk.ilios:realmfieldnameshelper:1.1.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1'

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.content package im.vector.matrix.android.api.session.content
import android.os.Parcelable import android.os.Parcelable
import androidx.exifinterface.media.ExifInterface
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@Parcelize @Parcelize
@ -26,6 +27,7 @@ data class ContentAttachmentData(
val date: Long = 0, val date: Long = 0,
val height: Long? = 0, val height: Long? = 0,
val width: Long? = 0, val width: Long? = 0,
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
val name: String? = null, val name: String? = null,
val path: String, val path: String,
val mimeType: String, val mimeType: String,

View File

@ -42,16 +42,6 @@ data class ImageInfo(
*/ */
@Json(name = "size") val size: Int = 0, @Json(name = "size") val size: Int = 0,
/**
* Not documented
*/
@Json(name = "rotation") val rotation: Int = 0,
/**
* Not documented
*/
@Json(name = "orientation") val orientation: Int = 0,
/** /**
* Metadata about the image referred to in thumbnail_url. * Metadata about the image referred to in thumbnail_url.
*/ */

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room.send package im.vector.matrix.android.internal.session.room.send
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import androidx.exifinterface.media.ExifInterface
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R import im.vector.matrix.android.R
import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.permalinks.PermalinkFactory
@ -173,13 +174,27 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event { private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event {
var width = attachment.width
var height = attachment.height
when (attachment.exifOrientation) {
ExifInterface.ORIENTATION_ROTATE_90,
ExifInterface.ORIENTATION_TRANSVERSE,
ExifInterface.ORIENTATION_ROTATE_270,
ExifInterface.ORIENTATION_TRANSPOSE -> {
val tmp = width
width = height
height = tmp
}
}
val content = MessageImageContent( val content = MessageImageContent(
type = MessageType.MSGTYPE_IMAGE, type = MessageType.MSGTYPE_IMAGE,
body = attachment.name ?: "image", body = attachment.name ?: "image",
info = ImageInfo( info = ImageInfo(
mimeType = attachment.mimeType, mimeType = attachment.mimeType,
width = attachment.width?.toInt() ?: 0, width = width?.toInt() ?: 0,
height = attachment.height?.toInt() ?: 0, height = height?.toInt() ?: 0,
size = attachment.size.toInt() size = attachment.size.toInt()
), ),
url = attachment.path url = attachment.path

View File

@ -0,0 +1,74 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.images
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import androidx.exifinterface.media.ExifInterface
import timber.log.Timber
import javax.inject.Inject
class ImageTools @Inject constructor(private val context: Context) {
/**
* Gets the [ExifInterface] value for the orientation for this local bitmap Uri.
*
* @param uri The URI to find the orientation for. Must be local.
* @return The orientation value, which may be [ExifInterface.ORIENTATION_UNDEFINED].
*/
fun getOrientationForBitmap(uri: Uri): Int {
var orientation = ExifInterface.ORIENTATION_UNDEFINED
if (uri.scheme == "content") {
val proj = arrayOf(MediaStore.Images.Media.DATA)
var cursor: Cursor? = null
try {
cursor = context.contentResolver.query(uri, proj, null, null, null)
if (cursor != null && cursor.count > 0) {
cursor.moveToFirst()
val idxData = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val path = cursor.getString(idxData)
if (path.isNullOrBlank()) {
Timber.w("Cannot find path in media db for uri $uri")
return orientation
}
val exif = ExifInterface(path)
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
}
} catch (e: Exception) {
// eg SecurityException from com.google.android.apps.photos.content.GooglePhotosImageProvider URIs
// eg IOException from trying to parse the returned path as a file when it is an http uri.
Timber.e(e, "Cannot get orientation for bitmap")
} finally {
cursor?.close()
}
} else if (uri.scheme == "file") {
try {
val exif = ExifInterface(uri.path)
orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
} catch (e: Exception) {
Timber.e(e, "Cannot get EXIF for file uri $uri")
}
}
return orientation
}
}

View File

@ -54,6 +54,7 @@ import im.vector.matrix.rx.unwrap
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.images.ImageTools
import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.resources.UserPreferencesProvider
@ -76,6 +77,7 @@ import java.util.concurrent.TimeUnit
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
private val userPreferencesProvider: UserPreferencesProvider, private val userPreferencesProvider: UserPreferencesProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val imageTools: ImageTools,
private val session: Session private val session: Session
) : VectorViewModel<RoomDetailViewState>(initialState) { ) : VectorViewModel<RoomDetailViewState>(initialState) {
@ -470,7 +472,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleSendMedia(action: RoomDetailActions.SendMedia) { private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
val attachments = action.mediaFiles.map { val attachments = action.mediaFiles.map {
val nameWithExtension = getFilenameFromUri(null, Uri.parse(it.path)) val pathWithScheme = if (it.path.startsWith("/")) {
"file://" + it.path
} else {
it.path
}
val uri = Uri.parse(pathWithScheme)
val nameWithExtension = getFilenameFromUri(null, uri)
ContentAttachmentData( ContentAttachmentData(
size = it.size, size = it.size,
@ -478,6 +487,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
date = it.date, date = it.date,
height = it.height, height = it.height,
width = it.width, width = it.width,
exifOrientation = imageTools.getOrientationForBitmap(uri),
name = nameWithExtension ?: it.name, name = nameWithExtension ?: it.name,
path = it.path, path = it.path,
mimeType = it.mimeType, mimeType = it.mimeType,

View File

@ -45,7 +45,7 @@ sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconRes
data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit) data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit)
data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote) data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote)
data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply) data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply)
data class Share(val imageUrl: String?) : SimpleAction(R.string.share, R.drawable.ic_share) data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share)
data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw) data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw)
data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash) data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash)
data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete) data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete)
@ -166,7 +166,9 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
if (canShare(type)) { if (canShare(type)) {
if (messageContent is MessageImageContent) { if (messageContent is MessageImageContent) {
add(SimpleAction.Share(session.contentUrlResolver().resolveFullSize(messageContent.url))) session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
add(SimpleAction.Share(url))
}
} }
//TODO //TODO
} }

View File

@ -189,9 +189,7 @@ class MessageItemFactory @Inject constructor(
height = messageContent.info?.height, height = messageContent.info?.height,
maxHeight = maxHeight, maxHeight = maxHeight,
width = messageContent.info?.width, width = messageContent.info?.width,
maxWidth = maxWidth, maxWidth = maxWidth
orientation = messageContent.info?.orientation,
rotation = messageContent.info?.rotation
) )
return MessageImageVideoItem_() return MessageImageVideoItem_()
.attributes(attributes) .attributes(attributes)

View File

@ -20,7 +20,6 @@ import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import android.widget.ImageView import android.widget.ImageView
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@ -49,9 +48,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
val height: Int?, val height: Int?,
val maxHeight: Int, val maxHeight: Int,
val width: Int?, val width: Int?,
val maxWidth: Int, val maxWidth: Int
val orientation: Int? = null,
val rotation: Int? = null
) : Parcelable { ) : Parcelable {
fun isLocalFile() = url.isLocalFile() fun isLocalFile() = url.isLocalFile()
@ -152,26 +149,14 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
private fun processSize(data: Data, mode: Mode): Pair<Int, Int> { private fun processSize(data: Data, mode: Mode): Pair<Int, Int> {
val maxImageWidth = data.maxWidth val maxImageWidth = data.maxWidth
val maxImageHeight = data.maxHeight val maxImageHeight = data.maxHeight
val rotationAngle = data.rotation ?: 0 val width = data.width ?: maxImageWidth
val orientation = data.orientation ?: ExifInterface.ORIENTATION_NORMAL val height = data.height ?: maxImageHeight
var width = data.width ?: maxImageWidth
var height = data.height ?: maxImageHeight
var finalHeight = -1 var finalHeight = -1
var finalWidth = -1 var finalWidth = -1
// if the image size is known // if the image size is known
// compute the expected height // compute the expected height
if (width > 0 && height > 0) { if (width > 0 && height > 0) {
// swap width and height if the image is side oriented
if (rotationAngle == 90 || rotationAngle == 270) {
val tmp = width
width = height
height = tmp
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270) {
val tmp = width
width = height
height = tmp
}
if (mode == Mode.FULL_SIZE) { if (mode == Mode.FULL_SIZE) {
finalHeight = height finalHeight = height
finalWidth = width finalWidth = width