mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Merge pull request #7020 from bxdxnn/misc/svg-support
Add full SVG support
This commit is contained in:
+25
-5
@@ -67,7 +67,7 @@ class AndroidMediaPreProcessor(
|
||||
*/
|
||||
private const val IMAGE_SCALE_REF_SIZE = 640
|
||||
|
||||
private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP, MimeTypes.Svg)
|
||||
private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP)
|
||||
}
|
||||
|
||||
private val contentResolver = context.contentResolver
|
||||
@@ -83,10 +83,7 @@ class AndroidMediaPreProcessor(
|
||||
): Result<MediaUploadInfo> = withContext(coroutineDispatchers.computation) {
|
||||
runCatchingExceptions {
|
||||
val result = when {
|
||||
// Special case for SVG, since Android can't read its metadata or create a thumbnail, it must be sent as a file
|
||||
mimeType == MimeTypes.Svg -> {
|
||||
processFile(uri, mimeType)
|
||||
}
|
||||
mimeType == MimeTypes.Svg -> processSvgImage(uri, mimeType)
|
||||
mimeType.isMimeTypeImage() -> {
|
||||
val shouldBeCompressed = mediaOptimizationConfig.compressImages && mimeType !in notCompressibleImageTypes
|
||||
processImage(uri, mimeType, shouldBeCompressed)
|
||||
@@ -160,6 +157,7 @@ class AndroidMediaPreProcessor(
|
||||
|
||||
private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo {
|
||||
Timber.d("Processing image ${uri.path.orEmpty().hash()}")
|
||||
|
||||
suspend fun processImageWithCompression(): MediaUploadInfo {
|
||||
// Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail.
|
||||
val orientation = contentResolver.openInputStream(uri).use { input ->
|
||||
@@ -220,6 +218,28 @@ class AndroidMediaPreProcessor(
|
||||
}
|
||||
}
|
||||
|
||||
private val svgDimensionExtractor = SvgDimensionExtractor()
|
||||
|
||||
private suspend fun processSvgImage(uri: Uri, mimeType: String): MediaUploadInfo {
|
||||
Timber.d("Processing SVG image ${uri.path.orEmpty().hash()}")
|
||||
val file = copyToTmpFile(uri)
|
||||
val size = svgDimensionExtractor.extractDimensions(file)
|
||||
val imageInfo = ImageInfo(
|
||||
width = size.width.toLong(),
|
||||
height = size.height.toLong(),
|
||||
mimetype = mimeType,
|
||||
size = file.length(),
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
)
|
||||
return MediaUploadInfo.Image(
|
||||
file = file,
|
||||
imageInfo = imageInfo,
|
||||
thumbnailFile = null,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun processVideo(uri: Uri, mimeType: String?, videoCompressionPreset: VideoCompressionPreset): MediaUploadInfo {
|
||||
Timber.d("Processing video ${uri.path.orEmpty().hash()}")
|
||||
val resultFile = runCatchingExceptions {
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaupload.impl
|
||||
|
||||
import android.util.Xml
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.File
|
||||
|
||||
class SvgDimensionExtractor(
|
||||
private val defaultWidth: Long = 640L,
|
||||
private val defaultHeight: Long = 480L,
|
||||
) {
|
||||
fun extractDimensions(file: File): android.util.Size {
|
||||
return file.inputStream().use { inputStream ->
|
||||
try {
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setInput(inputStream, null)
|
||||
var eventType = parser.eventType
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (eventType == XmlPullParser.START_TAG && parser.name.equals("svg", ignoreCase = true)) {
|
||||
val width = parser.getAttributeValue(null, "width")
|
||||
val height = parser.getAttributeValue(null, "height")
|
||||
val viewBox = parser.getAttributeValue(null, "viewBox")
|
||||
|
||||
val parsedWidth = width?.let { parseLength(it) }
|
||||
val parsedHeight = height?.let { parseLength(it) }
|
||||
|
||||
if (parsedWidth != null && parsedHeight != null) {
|
||||
return android.util.Size(parsedWidth.toInt(), parsedHeight.toInt())
|
||||
}
|
||||
|
||||
if (viewBox != null) {
|
||||
val parts = viewBox.trim().split("\\s+".toRegex()).map { it.toFloatOrNull() }
|
||||
if (parts.size == 4 && parts[2] != null && parts[3] != null) {
|
||||
val vbWidth = parts[2]!!.toLong().coerceAtLeast(1)
|
||||
val vbHeight = parts[3]!!.toLong().coerceAtLeast(1)
|
||||
return android.util.Size(vbWidth.toInt(), vbHeight.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
return android.util.Size((parsedWidth ?: defaultWidth).toInt(), (parsedHeight ?: defaultHeight).toInt())
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
android.util.Size(defaultWidth.toInt(), defaultHeight.toInt())
|
||||
} catch (_: Exception) {
|
||||
android.util.Size(defaultWidth.toInt(), defaultHeight.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLength(value: String): Long? {
|
||||
val trimmed = value.trim()
|
||||
if (trimmed.isEmpty()) return null
|
||||
return try {
|
||||
val numericPart = trimmed.replace(Regex("[^\\d.]"), "")
|
||||
if (numericPart.isEmpty()) null else numericPart.toFloat().toLong().coerceAtLeast(1)
|
||||
} catch (_: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
+48
@@ -399,6 +399,54 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing svg`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImageSvg,
|
||||
mediaOptimizationConfig = MediaOptimizationConfig(
|
||||
compressImages = true,
|
||||
videoCompressionPreset = VideoCompressionPreset.STANDARD,
|
||||
),
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
width = 800,
|
||||
height = 600,
|
||||
mimetype = MimeTypes.Svg,
|
||||
size = 210,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
)
|
||||
)
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing svg without compression`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImageSvg,
|
||||
mediaOptimizationConfig = MediaOptimizationConfig(
|
||||
compressImages = false,
|
||||
videoCompressionPreset = VideoCompressionPreset.STANDARD,
|
||||
),
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
width = 800,
|
||||
height = 600,
|
||||
mimetype = MimeTypes.Svg,
|
||||
size = 210,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
)
|
||||
)
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing audio`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
|
||||
+11
@@ -83,3 +83,14 @@ val assetAnimatedGif = Asset(
|
||||
width = 800,
|
||||
height = 600,
|
||||
)
|
||||
|
||||
/**
|
||||
* "image.svg" is an 800 x 600 SVG image with a size of 210 bytes.
|
||||
*/
|
||||
val assetImageSvg = Asset(
|
||||
filename = "image.svg",
|
||||
mimeType = MimeTypes.Svg,
|
||||
size = 210,
|
||||
width = 800,
|
||||
height = 600,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user