From 80bfbf68b4191c830b0583621eb41634fec08f70 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 2 May 2023 10:02:45 -0400 Subject: [PATCH] Sending dimensions to Image files. --- .../vitorpamplona/amethyst/model/Account.kt | 2 + .../amethyst/service/FileHeader.kt | 15 +- .../amethyst/service/model/FileHeaderEvent.kt | 4 + .../service/model/FileStorageHeaderEvent.kt | 4 + .../ui/components/ZoomableContentView.kt | 346 ++++++++---------- .../amethyst/ui/note/NoteCompose.kt | 24 +- 6 files changed, 188 insertions(+), 207 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index b6f30a581..7d5952379 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -417,6 +417,7 @@ class Account( mimeType = headerInfo.mimeType, hash = headerInfo.hash, size = headerInfo.size.toString(), + dimensions = headerInfo.dim, blurhash = headerInfo.blurHash, description = headerInfo.description, privateKey = loggedIn.privKey!! @@ -445,6 +446,7 @@ class Account( mimeType = headerInfo.mimeType, hash = headerInfo.hash, size = headerInfo.size.toString(), + dimensions = headerInfo.dim, blurhash = headerInfo.blurHash, description = headerInfo.description, privateKey = loggedIn.privKey!! diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt index 537b4626e..941aab60e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/FileHeader.kt @@ -14,6 +14,7 @@ class FileHeader( val mimeType: String?, val hash: String, val size: Int, + val dim: String?, val blurHash: String?, val description: String? = null ) { @@ -43,7 +44,7 @@ class FileHeader( val hash = sha256.digest(data).toHexKey() val size = data.size - val blurHash = if (mimeType?.startsWith("image/") == true) { + val (blurHash, dim) = if (mimeType?.startsWith("image/") == true) { val opt = BitmapFactory.Options() opt.inPreferredConfig = Bitmap.Config.ARGB_8888 val mBitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opt) @@ -59,20 +60,22 @@ class FileHeader( mBitmap.height ) + val dim = "${mBitmap.width}x${mBitmap.height}" + val aspectRatio = (mBitmap.width).toFloat() / (mBitmap.height).toFloat() if (aspectRatio > 1) { - BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 9, (9 * (1 / aspectRatio)).roundToInt()) + Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 9, (9 * (1 / aspectRatio)).roundToInt()), dim) } else if (aspectRatio < 1) { - BlurHash.encode(intArray, mBitmap.width, mBitmap.height, (9 * aspectRatio).roundToInt(), 9) + Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, (9 * aspectRatio).roundToInt(), 9), dim) } else { - BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 4, 4) + Pair(BlurHash.encode(intArray, mBitmap.width, mBitmap.height, 4, 4), dim) } } else { - null + Pair(null, null) } - onReady(FileHeader(fileUrl, mimeType, hash, size, blurHash, description)) + onReady(FileHeader(fileUrl, mimeType, hash, size, dim, blurHash, description)) } catch (e: Exception) { Log.e("ImageDownload", "Couldn't convert image in to File Header: ${e.message}") onError() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt index 5c91fcc80..a33344af6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt @@ -19,6 +19,7 @@ class FileHeaderEvent( fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1) fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1) fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1) + fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1) fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1) fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1) fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1) @@ -30,6 +31,7 @@ class FileHeaderEvent( private const val ENCRYPTION_KEY = "aes-256-gcm" private const val MIME_TYPE = "m" private const val FILE_SIZE = "size" + private const val DIMENSION = "dim" private const val HASH = "x" private const val MAGNET_URI = "magnet" private const val TORRENT_INFOHASH = "i" @@ -41,6 +43,7 @@ class FileHeaderEvent( description: String? = null, hash: String? = null, size: String? = null, + dimensions: String? = null, blurhash: String? = null, magnetURI: String? = null, torrentInfoHash: String? = null, @@ -53,6 +56,7 @@ class FileHeaderEvent( mimeType?.let { listOf(MIME_TYPE, mimeType) }, hash?.let { listOf(HASH, it) }, size?.let { listOf(FILE_SIZE, it) }, + dimensions?.let { listOf(DIMENSION, it) }, blurhash?.let { listOf(BLUR_HASH, it) }, magnetURI?.let { listOf(MAGNET_URI, it) }, torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt index ae2fd5cdf..f2ba729bb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt @@ -20,6 +20,7 @@ class FileStorageHeaderEvent( fun mimeType() = tags.firstOrNull { it.size > 1 && it[0] == MIME_TYPE }?.get(1) fun hash() = tags.firstOrNull { it.size > 1 && it[0] == HASH }?.get(1) fun size() = tags.firstOrNull { it.size > 1 && it[0] == FILE_SIZE }?.get(1) + fun dimensions() = tags.firstOrNull { it.size > 1 && it[0] == DIMENSION }?.get(1) fun magnetURI() = tags.firstOrNull { it.size > 1 && it[0] == MAGNET_URI }?.get(1) fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1) fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1) @@ -30,6 +31,7 @@ class FileStorageHeaderEvent( private const val ENCRYPTION_KEY = "aes-256-gcm" private const val MIME_TYPE = "m" private const val FILE_SIZE = "size" + private const val DIMENSION = "dim" private const val HASH = "x" private const val MAGNET_URI = "magnet" private const val TORRENT_INFOHASH = "i" @@ -41,6 +43,7 @@ class FileStorageHeaderEvent( description: String? = null, hash: String? = null, size: String? = null, + dimensions: String? = null, blurhash: String? = null, magnetURI: String? = null, torrentInfoHash: String? = null, @@ -53,6 +56,7 @@ class FileStorageHeaderEvent( mimeType?.let { listOf(MIME_TYPE, mimeType) }, hash?.let { listOf(HASH, it) }, size?.let { listOf(FILE_SIZE, it) }, + dimensions?.let { listOf(DIMENSION, it) }, blurhash?.let { listOf(BLUR_HASH, it) }, magnetURI?.let { listOf(MAGNET_URI, it) }, torrentInfoHash?.let { listOf(TORRENT_INFOHASH, it) }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index ec0d94d49..00e208aa9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -78,53 +79,62 @@ import java.io.File import java.security.MessageDigest abstract class ZoomableContent( - val description: String? = null + val description: String? = null, + val dim: String? = null ) abstract class ZoomableUrlContent( val url: String, description: String? = null, val hash: String? = null, + dim: String? = null, val uri: String? = null -) : ZoomableContent(description) +) : ZoomableContent(description, dim) class ZoomableUrlImage( url: String, description: String? = null, hash: String? = null, val bluehash: String? = null, + dim: String? = null, uri: String? = null -) : ZoomableUrlContent(url, description, hash, uri) +) : ZoomableUrlContent(url, description, hash, dim, uri) class ZoomableUrlVideo( url: String, description: String? = null, hash: String? = null, + dim: String? = null, uri: String? = null -) : ZoomableUrlContent(url, description, hash, uri) +) : ZoomableUrlContent(url, description, hash, dim, uri) abstract class ZoomablePreloadedContent( + val localFile: File, description: String? = null, + val mimeType: String? = null, val isVerified: Boolean? = null, + dim: String? = null, val uri: String -) : ZoomableContent(description) +) : ZoomableContent(description, dim) -class ZoomableBitmapImage( - val localFile: File, - val mimeType: String? = null, +class ZoomableLocalImage( + localFile: File, + mimeType: String? = null, description: String? = null, - val bluehash: String? = null, + val blurhash: String? = null, + dim: String? = null, isVerified: Boolean? = null, uri: String -) : ZoomablePreloadedContent(description, isVerified, uri) +) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri) -class ZoomableBytesVideo( - val localFile: File, - val mimeType: String? = null, +class ZoomableLocalVideo( + localFile: File, + mimeType: String? = null, description: String? = null, + dim: String? = null, isVerified: Boolean? = null, uri: String -) : ZoomablePreloadedContent(description, isVerified, uri) +) : ZoomablePreloadedContent(localFile, description, mimeType, isVerified, dim, uri) fun figureOutMimeType(fullUrl: String): ZoomableContent { val removedParamsFromUrl = fullUrl.split("?")[0].lowercase() @@ -144,45 +154,12 @@ fun figureOutMimeType(fullUrl: String): ZoomableContent { @OptIn(ExperimentalFoundationApi::class) fun ZoomableContentView(content: ZoomableContent, images: List = listOf(content)) { val clipboardManager = LocalClipboardManager.current - val scope = rememberCoroutineScope() - val context = LocalContext.current // store the dialog open or close state var dialogOpen by remember { mutableStateOf(false) } - // store the dialog open or close state - var imageState by remember { - mutableStateOf(null) - } - - var verifiedHash by remember { - mutableStateOf(null) - } - - if (content is ZoomableUrlContent) { - LaunchedEffect(key1 = content.url, key2 = imageState) { - if (imageState is AsyncImagePainter.State.Success) { - scope.launch(Dispatchers.IO) { - verifiedHash = verifyHash(content, context) - } - } - } - } else if (content is ZoomableBitmapImage) { - LaunchedEffect(key1 = content.localFile, key2 = imageState) { - if (imageState is AsyncImagePainter.State.Success) { - scope.launch(Dispatchers.IO) { - verifiedHash = content.isVerified - } - } - } - } else if (content is ZoomableBytesVideo) { - LaunchedEffect(key1 = content.localFile, key2 = imageState) { - verifiedHash = content.isVerified - } - } - var mainImageModifier = Modifier .fillMaxWidth() .clip(shape = RoundedCornerShape(15.dp)) @@ -192,17 +169,17 @@ fun ZoomableContentView(content: ZoomableContent, images: List RoundedCornerShape(15.dp) ) + val ratio = aspectRatio(content.dim) + if (ratio != null) { + mainImageModifier = mainImageModifier.aspectRatio(ratio) + } + if (content is ZoomableUrlContent) { mainImageModifier = mainImageModifier.combinedClickable( onClick = { dialogOpen = true }, onLongClick = { clipboardManager.setText(AnnotatedString(content.uri ?: content.url)) } ) - } else if (content is ZoomableBitmapImage) { - mainImageModifier = mainImageModifier.combinedClickable( - onClick = { dialogOpen = true }, - onLongClick = { clipboardManager.setText(AnnotatedString(content.uri)) } - ) - } else if (content is ZoomableBytesVideo) { + } else if (content is ZoomablePreloadedContent) { mainImageModifier = mainImageModifier.combinedClickable( onClick = { dialogOpen = true }, onLongClick = { clipboardManager.setText(AnnotatedString(content.uri)) } @@ -213,71 +190,127 @@ fun ZoomableContentView(content: ZoomableContent, images: List } } - if (content is ZoomableUrlImage) { - Box(contentAlignment = Alignment.Center) { - AsyncImage( - model = content.url, - contentDescription = content.description, - contentScale = ContentScale.FillWidth, - modifier = mainImageModifier, - onLoading = { - imageState = it - }, - onSuccess = { - imageState = it - } - ) + when (content) { + is ZoomableUrlImage -> UrlImageView(content, mainImageModifier) + is ZoomableUrlVideo -> VideoView(content.url, content.description) { dialogOpen = true } + is ZoomableLocalImage -> LocalImageView(content, mainImageModifier) + is ZoomableLocalVideo -> VideoView(content.localFile, content.description) { dialogOpen = true } + } - if (imageState is AsyncImagePainter.State.Success) { - HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd)) - } + if (dialogOpen) { + ZoomableImageDialog(content, images, onDismiss = { dialogOpen = false }) + } +} - AnimatedVisibility( - visible = imageState !is AsyncImagePainter.State.Success, - exit = fadeOut(animationSpec = tween(200)) - ) { - if (content.bluehash != null) { - DisplayBlueHash(content, mainImageModifier) - } else { - DisplayUrlWithLoadingSymbol(content) - } - } - } - } else if (content is ZoomableUrlVideo) { - VideoView(content.url, content.description) { dialogOpen = true } - } else if (content is ZoomableBitmapImage) { - Box() { - AsyncImage( - model = content.localFile, - contentDescription = content.description, - contentScale = ContentScale.FillWidth, - modifier = mainImageModifier, - onLoading = { - imageState = it - }, - onSuccess = { - imageState = it - } - ) +@Composable +private fun LocalImageView( + content: ZoomableLocalImage, + mainImageModifier: Modifier +) { + // store the dialog open or close state + var imageState by remember { + mutableStateOf(null) + } - if (imageState is AsyncImagePainter.State.Success) { - HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd)) + Box(contentAlignment = Alignment.Center) { + AsyncImage( + model = content.localFile, + contentDescription = content.description, + contentScale = ContentScale.FillWidth, + modifier = mainImageModifier, + onLoading = { + imageState = it + }, + onSuccess = { + imageState = it } + ) + + if (imageState is AsyncImagePainter.State.Success) { + HashVerificationSymbol(content.isVerified, Modifier.align(Alignment.TopEnd)) } - if (imageState !is AsyncImagePainter.State.Success) { + AnimatedVisibility( + visible = imageState !is AsyncImagePainter.State.Success, + exit = fadeOut(animationSpec = tween(200)) + ) { + if (content.blurhash != null) { + DisplayBlueHash(content, mainImageModifier) + } else { + DisplayUrlWithLoadingSymbol(content) + } + } + } +} + +@Composable +private fun UrlImageView( + content: ZoomableUrlImage, + mainImageModifier: Modifier +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + + // store the dialog open or close state + var imageState by remember { + mutableStateOf(null) + } + + var verifiedHash by remember { + mutableStateOf(null) + } + + LaunchedEffect(key1 = content.url, key2 = imageState) { + if (imageState is AsyncImagePainter.State.Success) { + scope.launch(Dispatchers.IO) { + verifiedHash = verifyHash(content, context) + } + } + } + + Box(contentAlignment = Alignment.Center) { + AsyncImage( + model = content.url, + contentDescription = content.description, + contentScale = ContentScale.FillWidth, + modifier = mainImageModifier, + onLoading = { + imageState = it + }, + onSuccess = { + imageState = it + } + ) + + if (imageState is AsyncImagePainter.State.Success) { + HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd)) + } + + AnimatedVisibility( + visible = imageState !is AsyncImagePainter.State.Success, + exit = fadeOut(animationSpec = tween(200)) + ) { if (content.bluehash != null) { DisplayBlueHash(content, mainImageModifier) } else { DisplayUrlWithLoadingSymbol(content) } } - } else if (content is ZoomableBytesVideo) { - VideoView(content.localFile, content.description) { dialogOpen = true } } +} - if (dialogOpen) { - ZoomableImageDialog(content, images, onDismiss = { dialogOpen = false }) +private fun aspectRatio(dim: String?): Float? { + if (dim == null) return null + + val parts = dim.split("x") + if (parts.size != 2) return null + + return try { + val width = parts[0].toFloat() + val height = parts[1].toFloat() + width / height + } catch (e: Exception) { + null } } @@ -341,16 +374,16 @@ private fun DisplayBlueHash( @Composable private fun DisplayBlueHash( - content: ZoomableBitmapImage, + content: ZoomableLocalImage, modifier: Modifier ) { - if (content.bluehash == null) return + if (content.blurhash == null) return val context = LocalContext.current AsyncImage( model = BlurHashRequester.imageRequest( context, - content.bluehash + content.blurhash ), contentDescription = content.description, contentScale = ContentScale.FillWidth, @@ -388,7 +421,7 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: List(null) - } - - var verifiedHash by remember { - mutableStateOf(null) - } - - if (content is ZoomableUrlContent) { - LaunchedEffect(key1 = content.url, key2 = imageState) { - if (imageState is AsyncImagePainter.State.Success) { - scope.launch(Dispatchers.IO) { - verifiedHash = verifyHash(content, context) - } - } - } - } else if (content is ZoomableBitmapImage) { - LaunchedEffect(key1 = content.localFile, key2 = imageState) { - if (imageState is AsyncImagePainter.State.Success) { - scope.launch(Dispatchers.IO) { - verifiedHash = content.isVerified - } - } - } - } else if (content is ZoomableBytesVideo) { - LaunchedEffect(key1 = content.localFile, key2 = imageState) { - verifiedHash = content.isVerified - } - } + val mainModifier = Modifier + .fillMaxSize() + .zoomable(rememberZoomState()) if (content is ZoomableUrlImage) { - Box() { - AsyncImage( - model = content.url, - contentDescription = content.description, - contentScale = ContentScale.FillWidth, - modifier = Modifier - .fillMaxSize() - .zoomable(rememberZoomState()), - onLoading = { - imageState = it - }, - onSuccess = { - imageState = it - } - ) - - if (imageState is AsyncImagePainter.State.Success) { - HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd)) - } - - AnimatedVisibility( - visible = imageState !is AsyncImagePainter.State.Success, - exit = fadeOut(animationSpec = tween(200)) - ) { - if (content.bluehash != null) { - DisplayBlueHash(content = content, modifier = Modifier.fillMaxWidth()) - } else { - DisplayUrlWithLoadingSymbol(content) - } - } - } + UrlImageView(content = content, mainImageModifier = mainModifier) } else if (content is ZoomableUrlVideo) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) { VideoView(content.url, content.description) } - } else if (content is ZoomableBitmapImage) { - Box() { - AsyncImage( - model = content.localFile, - contentDescription = content.description, - contentScale = ContentScale.FillWidth, - modifier = Modifier - .fillMaxSize() - .zoomable(rememberZoomState()), - onLoading = { - imageState = it - }, - onSuccess = { - imageState = it - } - ) - - if (imageState !is AsyncImagePainter.State.Success) { - DisplayBlueHash(content = content, modifier = Modifier.fillMaxWidth()) - } else { - HashVerificationSymbol(verifiedHash, Modifier.align(Alignment.TopEnd)) - } - } - } else if (content is ZoomableBytesVideo) { + } else if (content is ZoomableLocalImage) { + LocalImageView(content = content, mainImageModifier = mainModifier) + } else if (content is ZoomableLocalVideo) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) { VideoView(content.localFile, content.description) } @@ -512,7 +464,7 @@ fun RenderImageOrVideo(content: ZoomableContent) { } @OptIn(ExperimentalCoilApi::class) -private suspend fun verifyHash(content: ZoomableUrlContent, context: Context): Boolean? { +private fun verifyHash(content: ZoomableUrlContent, context: Context): Boolean? { if (content.hash == null) return null context.imageLoader.diskCache?.get(content.url)?.use { snapshot -> diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 16da10c3e..0c88ee92b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -873,13 +873,13 @@ fun FileHeaderDisplay(note: Note) { withContext(Dispatchers.IO) { val blurHash = event.blurhash() val hash = event.hash() + val dimensions = event.dimensions() val description = event.content val removedParamsFromUrl = fullUrl.split("?")[0].lowercase() val isImage = imageExtensions.any { removedParamsFromUrl.endsWith(it) } - val isVideo = videoExtensions.any { removedParamsFromUrl.endsWith(it) } val uri = "nostr:" + note.toNEvent() content = if (isImage) { - ZoomableUrlImage(fullUrl, description, hash, blurHash, uri) + ZoomableUrlImage(fullUrl, description, hash, blurHash, dimensions, uri) } else { ZoomableUrlVideo(fullUrl, description, hash, uri) } @@ -911,14 +911,30 @@ fun FileStorageHeaderDisplay(baseNote: Note) { val localDir = File(File(appContext.externalCacheDir, "NIP95"), fileNote.idHex) val bytes = eventBytes?.decode() val blurHash = eventHeader.blurhash() + val dimensions = eventHeader.dimensions() val description = eventHeader.content val mimeType = eventHeader.mimeType() content = if (mimeType?.startsWith("image") == true) { - ZoomableBitmapImage(localDir, mimeType, description, blurHash, true, uri) + ZoomableLocalImage( + localFile = localDir, + mimeType = mimeType, + description = description, + blurhash = blurHash, + dim = dimensions, + isVerified = true, + uri = uri + ) } else { if (bytes != null) { - ZoomableBytesVideo(localDir, mimeType, description, true, uri) + ZoomableLocalVideo( + localFile = localDir, + mimeType = mimeType, + description = description, + dim = dimensions, + isVerified = true, + uri = uri + ) } else { null }