android, desktop: constrain image sizes for previews (#6726)

* android, desktop: constrain image sizes for previews

* use correct JSON parser

* more JSON fixes

* constrain ratio in image decoder

* constrain max height in layout

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
Evgeny
2026-03-30 12:24:16 +01:00
committed by GitHub
parent 47f82c10da
commit c3663ae285
4 changed files with 30 additions and 6 deletions

View File

@@ -21,12 +21,19 @@ import java.net.URI
import kotlin.math.min
import kotlin.math.sqrt
private const val MAX_IMAGE_DIMENSION = 4320
actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
val imageString = base64ImageString
.removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,")
return try {
val imageBytes = Base64.decode(imageString, Base64.NO_WRAP)
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
if (options.outWidth <= 0 || options.outHeight <= 0 || options.outWidth > MAX_IMAGE_DIMENSION || options.outHeight > MAX_IMAGE_DIMENSION || options.outHeight > options.outWidth * 256) {
return errorBitmap.asImageBitmap()
}
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap()
} catch (e: Exception) {
Log.e(TAG, "base64ToBitmap error: $e")

View File

@@ -3834,7 +3834,7 @@ object MsgReactionSerializer : KSerializer<MsgReaction> {
when(val t = json["type"]?.jsonPrimitive?.content ?: "") {
"emoji" -> {
val msgReaction = try {
val emoji = Json.decodeFromString<MREmojiChar>(json["emoji"].toString())
val emoji = decoder.json.decodeFromString<MREmojiChar>(json["emoji"].toString())
MsgReaction.Emoji(emoji)
} catch (e: Throwable) {
MsgReaction.Unknown(t, json)
@@ -4276,7 +4276,7 @@ object MsgContentSerializer : KSerializer<MsgContent> {
when (t) {
"text" -> MsgContent.MCText(text)
"link" -> {
val preview = Json.decodeFromString<LinkPreview>(json["preview"].toString())
val preview = decoder.json.decodeFromString<LinkPreview>(json["preview"].toString())
MsgContent.MCLink(text, preview)
}
"image" -> {
@@ -4294,11 +4294,11 @@ object MsgContentSerializer : KSerializer<MsgContent> {
}
"file" -> MsgContent.MCFile(text)
"report" -> {
val reason = Json.decodeFromString<ReportReason>(json["reason"].toString())
val reason = decoder.json.decodeFromString<ReportReason>(json["reason"].toString())
MsgContent.MCReport(text, reason)
}
"chat" -> {
val chatLink = Json.decodeFromString<MsgChatLink>(json["chatLink"].toString())
val chatLink = decoder.json.decodeFromString<MsgChatLink>(json["chatLink"].toString())
MsgContent.MCChat(text, chatLink)
}
else -> MsgContent.MCUnknown(t, text, json)

View File

@@ -437,7 +437,10 @@ fun PriorityLayout(
) { measureable, constraints ->
// Find important element which should tell what max width other elements can use
// Expecting only one such element. Can be less than one but not more
val imagePlaceable = measureable.firstOrNull { it.layoutId == priorityLayoutId }?.measure(constraints)
// Constrain max image height to prevent crashes and scroll issues from images with extreme aspect ratios
val maxImageHeight = (constraints.maxWidth * 2.33f).toInt().coerceAtMost(constraints.maxHeight)
val imageConstraints = constraints.copy(maxHeight = maxImageHeight)
val imagePlaceable = measureable.firstOrNull { it.layoutId == priorityLayoutId }?.measure(imageConstraints)
val placeables: List<Placeable> = measureable.map {
if (it.layoutId == priorityLayoutId)
imagePlaceable!!

View File

@@ -21,12 +21,26 @@ import kotlin.math.sqrt
private fun errorBitmap(): ImageBitmap =
ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg=="))).toComposeImageBitmap()
private const val MAX_IMAGE_DIMENSION = 4320
actual fun base64ToBitmap(base64ImageString: String): ImageBitmap {
val imageString = base64ImageString
.removePrefix("data:image/png;base64,")
.removePrefix("data:image/jpg;base64,")
return try {
ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap()
val bytes = Base64.getMimeDecoder().decode(imageString)
val stream = ImageIO.createImageInputStream(ByteArrayInputStream(bytes))
val reader = ImageIO.getImageReaders(stream).next()
reader.setInput(stream)
val width = reader.getWidth(0)
val height = reader.getHeight(0)
if (width <= 0 || height <= 0 || width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION || height > width * 256) {
reader.dispose()
return errorBitmap()
}
val image = reader.read(0)
reader.dispose()
image.toComposeImageBitmap()
} catch (e: Throwable) {
Log.e(TAG, "base64ToBitmap error: $e")
errorBitmap()