diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ImageBytesResolver.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ImageBytesResolver.kt index 431c6898..84894f01 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ImageBytesResolver.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ImageBytesResolver.kt @@ -114,15 +114,12 @@ object ImageBytesResolver { }.getOrElse { e -> Result.Error(e.message ?: "Could not download image") } } + /** Image-only view of the shared MIME table: anything that isn't `image/` saves as `.img`. */ fun extensionForMime(mime: String): String = - when (mime.substringBefore(';').trim().lowercase()) { - "image/jpeg", "image/jpg" -> "jpg" - "image/png" -> "png" - "image/gif" -> "gif" - "image/webp" -> "webp" - "image/bmp" -> "bmp" - "image/heic", "image/heif" -> "heic" - "image/svg+xml" -> "svg" - else -> "img" + if (mime.trim().startsWith("image/", ignoreCase = true)) { + com.m57.hermescontrol.ui.chat + .extensionForMime(mime) + } else { + "img" } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/MediaTypes.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/MediaTypes.kt index 227447d8..e3c0bf09 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/MediaTypes.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/MediaTypes.kt @@ -140,67 +140,41 @@ fun normalizeFrameworkMime(mime: String?): String = ?.takeIf { it.isNotBlank() } ?: "application/octet-stream" +/** + * MIME aliases and preferred extensions that [MEDIA_BY_EXT] can't express + * (it's keyed by extension, so `image/jpeg` would otherwise resolve to its + * first entry, `jpeg`). HEIC is save-only here: it isn't classified as inline media. + */ +private val EXT_BY_MIME_ALIAS: Map = + mapOf( + "image/jpeg" to "jpg", + "image/jpg" to "jpg", + "image/heic" to "heic", + "image/heif" to "heic", + "audio/mp3" to "mp3", + "audio/x-m4a" to "m4a", + "audio/x-wav" to "wav", + "audio/wave" to "wav", + "application/x-flac" to "flac", + "application/ogg" to "ogg", + ) + /** Returns the canonical file extension for a given MIME type. */ fun extensionForMime(mime: String): String { val clean = mime.substringBefore(';').trim().lowercase() - for ((ext, pair) in MEDIA_BY_EXT) { - if (pair.second + EXT_BY_MIME_ALIAS[clean]?.let { return it } + MEDIA_BY_EXT.entries + .firstOrNull { (_, pair) -> + pair.second .substringBefore(';') .trim() .lowercase() == clean - ) { - return ext - } - } - return when (clean) { - "image/jpeg", "image/jpg" -> { - "jpg" - } - - "video/mp4" -> { - "mp4" - } - - "video/x-matroska" -> { - "mkv" - } - - "video/quicktime" -> { - "mov" - } - - "audio/mpeg", "audio/mp3" -> { - "mp3" - } - - "audio/mp4", "audio/x-m4a" -> { - "m4a" - } - - "audio/wav", "audio/x-wav", "audio/wave" -> { - "wav" - } - - "audio/flac", "application/x-flac" -> { - "flac" - } - - "audio/ogg", "application/ogg" -> { - "ogg" - } - - "audio/aac" -> { - "aac" - } - - else -> { - when { - clean.startsWith("video/") -> "mp4" - clean.startsWith("audio/") -> "mp3" - clean.startsWith("image/") -> "img" - else -> "bin" - } - } + }?.let { return it.key } + return when { + clean.startsWith("video/") -> "mp4" + clean.startsWith("audio/") -> "mp3" + clean.startsWith("image/") -> "img" + else -> "bin" } } diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ImageBytesResolverTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ImageBytesResolverTest.kt index 4b458a8e..b01aff74 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ImageBytesResolverTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ImageBytesResolverTest.kt @@ -35,6 +35,11 @@ class ImageBytesResolverTest { assertEquals("heic", ImageBytesResolver.extensionForMime("image/heic")) assertEquals("svg", ImageBytesResolver.extensionForMime("image/svg+xml")) assertEquals("img", ImageBytesResolver.extensionForMime("image/unknown")) + assertEquals("heic", ImageBytesResolver.extensionForMime("image/heif")) + assertEquals("bmp", ImageBytesResolver.extensionForMime("image/bmp")) + // Non-image content types stay `.img`, not `.bin`/`.pdf`. + assertEquals("img", ImageBytesResolver.extensionForMime("application/pdf")) + assertEquals("img", ImageBytesResolver.extensionForMime("text/html")) } @Test diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/MediaTypesTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/MediaTypesTest.kt index 7060ef35..86fb2a90 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/MediaTypesTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/MediaTypesTest.kt @@ -30,6 +30,30 @@ class MediaTypesTest { assertEquals("application/octet-stream", mediaMimeForPath("/tmp/x.weird")) } + @Test + fun `extension for mime uses one table plus aliases`() { + // Table entries, including params and case. + assertEquals("png", extensionForMime("image/png")) + assertEquals("png", extensionForMime(" IMAGE/PNG; charset=binary")) + assertEquals("ogg", extensionForMime("audio/ogg; codecs=opus")) + assertEquals("mkv", extensionForMime("video/x-matroska")) + assertEquals("pdf", extensionForMime("application/pdf")) + // Aliases win over first-table-match. + assertEquals("jpg", extensionForMime("image/jpeg")) + assertEquals("jpg", extensionForMime("image/jpg")) + assertEquals("heic", extensionForMime("image/heif")) + assertEquals("mp3", extensionForMime("audio/mp3")) + assertEquals("m4a", extensionForMime("audio/x-m4a")) + assertEquals("wav", extensionForMime("audio/wave")) + assertEquals("flac", extensionForMime("application/x-flac")) + assertEquals("ogg", extensionForMime("application/ogg")) + // Family fallbacks. + assertEquals("mp4", extensionForMime("video/unknown")) + assertEquals("mp3", extensionForMime("audio/unknown")) + assertEquals("img", extensionForMime("image/unknown")) + assertEquals("bin", extensionForMime("application/unknown")) + } + @Test fun `name extraction handles urls and paths`() { assertEquals("report.pdf", mediaNameFromPath("https://gw/api/files/download?path=%2Ftmp%2Freport.pdf"))