Recognize image contents and preserve image dimensions in Android conversations
This commit is contained in:
1 parent
233a3f78d1
commit
4228f37cf5
3 files changed
+66
-16
No files matched your search
@@ -594,6 +594,8 @@ class ChatRepository {
|
||||
videoDurationMs = obj.optInt("duration_ms"),
|
||||
videoWidth = obj.optInt("width"),
|
||||
videoHeight = obj.optInt("height"),
|
||||
imageWidth = obj.optInt("width"),
|
||||
imageHeight = obj.optInt("height"),
|
||||
downloadState = if (!file) "" else if (path.isNotEmpty()) "downloaded" else if (status == "expired") "error" else "downloading",
|
||||
deliveryStatusRes = delivery
|
||||
))
|
||||
|
||||
@@ -11,6 +11,31 @@ import android.media.ExifInterface
|
||||
* Отсутствующий/нечитаемый EXIF трактуется как обычная ориентация; допуск источника проверяет вызывающий код.
|
||||
*/
|
||||
object PhotoUtil {
|
||||
data class Metadata(val width: Int, val height: Int, val mime: String)
|
||||
|
||||
/** Заголовок изображения, размеры для отображения и фактический формат; пиксели не выделяются. */
|
||||
fun metadata(path: String): Metadata {
|
||||
val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(path, opts)
|
||||
require(opts.outWidth in 1..16384 && opts.outHeight in 1..16384) { "Invalid image dimensions ${opts.outWidth}x${opts.outHeight}" }
|
||||
val mime = opts.outMimeType.orEmpty()
|
||||
require(mime in setOf("image/jpeg", "image/png", "image/gif", "image/bmp", "image/webp")) { "Unsupported image format: $mime" }
|
||||
return if (isRotated90(orientation(path))) Metadata(opts.outHeight, opts.outWidth, mime)
|
||||
else Metadata(opts.outWidth, opts.outHeight, mime)
|
||||
}
|
||||
|
||||
/** Ограниченное декодирование для превью/просмотрщика, с EXIF и сохранением пропорций. */
|
||||
fun decodeFitted(path: String, maxDimension: Int): Bitmap? {
|
||||
require(maxDimension > 0)
|
||||
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(path, bounds)
|
||||
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) {
|
||||
LogManager.addLog("ERROR", "Image", "Cannot read image dimensions path=$path"); return null
|
||||
}
|
||||
var sample = 1
|
||||
while (maxOf(bounds.outWidth, bounds.outHeight) / sample > maxDimension) sample *= 2
|
||||
return decodeOriented(path, sample)
|
||||
}
|
||||
|
||||
private fun orientation(path: String): Int = try {
|
||||
ExifInterface(path).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||
|
||||
@@ -1620,6 +1620,9 @@ class ChatViewModel : ViewModel() {
|
||||
|
||||
fun sendAttachment(filePath: String, target: ConversationTarget, name: String = File(filePath).name,
|
||||
reply: ChannelReply? = replyFor(target)): Deferred<Boolean> = viewModelScope.async {
|
||||
val ext = filePath.substringAfterLast('.', "").lowercase()
|
||||
if (ext in IMAGE_EXTENSIONS && (target is ConversationTarget.Channel || ext in setOf("jpg", "jpeg", "png")))
|
||||
return@async sendPhotoMessage(filePath, target, reply, clearReply = false).await()
|
||||
if (target is ConversationTarget.Pm) {
|
||||
val current = dmTargetCurrent(target)
|
||||
val accepted = current && NativeLib.dmSendFile(target.id, filePath, name, target.generation)
|
||||
@@ -1632,17 +1635,14 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
return@async accepted
|
||||
}
|
||||
val chId = target.id
|
||||
val ext = filePath.substringAfterLast('.', "").lowercase()
|
||||
val dims = if (ext in IMAGE_EXTENSIONS) withContext(Dispatchers.IO) {
|
||||
val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeFile(filePath, opts)
|
||||
opts.outWidth to opts.outHeight
|
||||
} else 0 to 0
|
||||
submitChannel(chId, reply) { request ->
|
||||
if (dims.first > 0 && dims.second > 0)
|
||||
NativeLib.photoSend(chId, filePath, dims.first, dims.second, reply?.message?.messageId.orEmpty(), request)
|
||||
else NativeLib.attachmentSend(chId, filePath, reply?.message?.messageId.orEmpty(), request)
|
||||
try {
|
||||
submitChannel(target.id, reply) { request ->
|
||||
NativeLib.attachmentSend(target.id, filePath, reply?.message?.messageId.orEmpty(), request)
|
||||
}
|
||||
} finally {
|
||||
withContext(kotlinx.coroutines.NonCancellable + Dispatchers.IO) {
|
||||
if (!File(filePath).delete() && File(filePath).exists()) LogManager.addLog("ERROR", "Attachment", "Cache cleanup failed: $filePath")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1690,12 +1690,35 @@ class ChatViewModel : ViewModel() {
|
||||
saved
|
||||
}
|
||||
|
||||
fun sendPhotoMessage(filePath: String, width: Int, height: Int, channel: String,
|
||||
reply: ChannelReply?): Deferred<Boolean> = viewModelScope.async {
|
||||
val saved = submitChannel(channel, reply) {
|
||||
NativeLib.photoSend(channel, filePath, width, height, reply?.message?.messageId.orEmpty(), it)
|
||||
/** Снимок камеры и выбранный JPG/PNG имеют один путь и неизменного адресата. */
|
||||
fun sendPhotoMessage(filePath: String, target: ConversationTarget, reply: ChannelReply?,
|
||||
clearReply: Boolean = true): Deferred<Boolean> = viewModelScope.async {
|
||||
var accepted = false
|
||||
var saved = false
|
||||
var legacy = false
|
||||
try {
|
||||
val meta = withContext(Dispatchers.IO) { PhotoUtil.metadata(filePath) }
|
||||
legacy = meta.mime !in setOf("image/jpeg", "image/png")
|
||||
val queue: (Long) -> Boolean = { request ->
|
||||
if (target is ConversationTarget.Pm && !dmTargetCurrent(target)) {
|
||||
LogManager.addLog("INFO", "Image", "Discarded image for deleted PM target=${target.nativeId}"); false
|
||||
} else NativeLib.photoSend(target.nativeId, filePath, meta.width, meta.height, meta.mime,
|
||||
reply?.message?.messageId.orEmpty(), request, (target as? ConversationTarget.Pm)?.generation ?: 0).also { accepted = it }
|
||||
}
|
||||
saved = if (target is ConversationTarget.Channel) submitChannel(target.id, reply, queue = queue) else queue(0)
|
||||
if (!saved) _messageSendFailed.value = true
|
||||
if (saved && clearReply) finishReply(reply)
|
||||
LogManager.addLog("INFO", "Image", "Image submission target=${target.nativeId} mime=${meta.mime} " +
|
||||
"dimensions=${meta.width}x${meta.height} accepted=$accepted saved=$saved")
|
||||
} catch (e: kotlinx.coroutines.CancellationException) { throw e }
|
||||
catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "Image", "Image submission failed target=${target.nativeId}: ${e.message}")
|
||||
_messageSendFailed.value = true
|
||||
} finally {
|
||||
if (!accepted || legacy) withContext(kotlinx.coroutines.NonCancellable + Dispatchers.IO) {
|
||||
if (!File(filePath).delete() && File(filePath).exists()) LogManager.addLog("ERROR", "Image", "Cache cleanup failed: $filePath")
|
||||
}
|
||||
}
|
||||
if (saved) finishReply(reply)
|
||||
saved
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user