@@ -8,7 +8,6 @@ import coil3.BitmapImage
88import coil3.ImageLoader
99import coil3.decode.DataSource
1010import coil3.decode.ImageSource
11- import coil3.fetch.FetchResult
1211import coil3.fetch.Fetcher
1312import coil3.fetch.SourceFetchResult
1413import coil3.request.CachePolicy
@@ -18,24 +17,30 @@ import coil3.request.allowConversionToBitmap
1817import coil3.request.allowHardware
1918import coil3.request.allowRgb565
2019import coil3.size.Precision
20+ import dagger.Lazy
2121import dagger.hilt.android.qualifiers.ApplicationContext
2222import network.loki.messenger.libsession_util.encrypt.Attachments
2323import network.loki.messenger.libsession_util.image.GifUtils
2424import network.loki.messenger.libsession_util.image.WebPUtils
2525import okio.BufferedSource
2626import okio.FileSystem
27+ import okio.buffer
28+ import okio.source
2729import org.session.libsession.utilities.Util
2830import org.session.libsignal.streams.AttachmentCipherInputStream
2931import org.session.libsignal.streams.AttachmentCipherOutputStream
3032import org.session.libsignal.streams.PaddingInputStream
3133import org.session.libsignal.utilities.ByteArraySlice
3234import org.session.libsignal.utilities.ByteArraySlice.Companion.view
3335import org.session.libsignal.utilities.Log
36+ import org.thoughtcrime.securesms.database.Storage
3437import org.thoughtcrime.securesms.util.AnimatedImageUtils
3538import org.thoughtcrime.securesms.util.BitmapUtil
3639import org.thoughtcrime.securesms.util.ImageUtils
40+ import java.io.ByteArrayInputStream
3741import java.io.ByteArrayOutputStream
3842import java.security.MessageDigest
43+ import java.util.concurrent.TimeoutException
3944import javax.inject.Inject
4045import javax.inject.Provider
4146import javax.inject.Singleton
@@ -50,13 +55,124 @@ typealias DigestResult = ByteArray
5055class AttachmentProcessor @Inject constructor(
5156 @param:ApplicationContext private val context : Context ,
5257 private val imageLoader : Provider <ImageLoader >,
58+ private val storage : Lazy <Storage >,
5359) {
5460 class ProcessResult (
5561 val data : ByteArray ,
5662 val mimeType : String ,
5763 val imageSize : IntSize
5864 )
5965
66+ suspend fun processAvatar (
67+ data : ByteArray ,
68+ ): ProcessResult ? {
69+ val buffer = ByteArrayInputStream (data).source().buffer()
70+ return when {
71+ AnimatedImageUtils .isAnimatedWebP(buffer) -> {
72+ val convertResult = runCatching {
73+ buffer.peek().use {
74+ processAnimatedWebP(
75+ data = it,
76+ maxImageResolution = MAX_AVATAR_SIZE_PX ,
77+ timeoutMills = 5_000L ,
78+ )
79+ }
80+ }
81+
82+ val processResult = when {
83+ convertResult.isSuccess -> convertResult.getOrThrow() ? : return null
84+ convertResult.exceptionOrNull() is TimeoutException -> {
85+ Log .w(TAG , " Animated WebP processing timed out, skipping" )
86+ return null
87+ }
88+
89+ else -> throw convertResult.exceptionOrNull()!!
90+ }
91+
92+ if (processResult.data.size > data.size) {
93+ Log .d(
94+ TAG ,
95+ " Avatar processing increased size from ${data.size} to ${processResult.data.size} , skipped result"
96+ )
97+ return null
98+ } else {
99+ processResult
100+ }
101+ }
102+
103+ AnimatedImageUtils .isAnimatedGif(data) -> {
104+ val origSize = ByteArrayInputStream (data).use(BitmapUtil ::getDimensions)
105+ .let { pair -> IntSize (pair.first, pair.second) }
106+
107+ val targetSize = if (origSize.width <= MAX_AVATAR_SIZE_PX .width &&
108+ origSize.height <= MAX_AVATAR_SIZE_PX .height) {
109+ origSize
110+ } else {
111+ scaleToFit(origSize, MAX_AVATAR_SIZE_PX ).first
112+ }
113+
114+ // First try to convert to webp in 5 seconds
115+ val convertResult = runCatching {
116+ " image/webp" to WebPUtils .encodeGifToWebP(
117+ input = data,
118+ timeoutMills = 5_000L ,
119+ targetWidth = targetSize.width, targetHeight = targetSize.height
120+ )
121+ }.recoverCatching { e ->
122+ if (e is TimeoutException ) {
123+ // If we timed out, try re-encoding as GIF in 2 seconds
124+ Log .w(TAG , " WebP conversion timed out, trying GIF re-encoding as fallback" )
125+ " image/gif" to GifUtils .reencodeGif(
126+ input = data,
127+ timeoutMills = 2_000L ,
128+ targetWidth = targetSize.width,
129+ targetHeight = targetSize.height
130+ )
131+ } else {
132+ throw e
133+ }
134+ }
135+
136+ val processResult = when {
137+ convertResult.isSuccess -> {
138+ val (mimeType, result) = convertResult.getOrThrow()
139+ ProcessResult (
140+ data = result,
141+ mimeType = mimeType,
142+ imageSize = targetSize
143+ )
144+ }
145+
146+ convertResult.exceptionOrNull() is TimeoutException -> {
147+ Log .w(TAG , " All operation times out, skipping avatar processing" )
148+ null
149+ }
150+
151+ else -> {
152+ throw convertResult.exceptionOrNull()!!
153+ }
154+ }
155+
156+ if (processResult != null && processResult.data.size > data.size) {
157+ Log .d(TAG , " Avatar processing increased size from ${data.size} to ${processResult.data.size} , skipped result" )
158+ return null
159+ }
160+
161+ processResult
162+ }
163+
164+ else -> {
165+ // All static images
166+ val (data, size) = processStaticImage(data, MAX_AVATAR_SIZE_PX , Bitmap .CompressFormat .WEBP , 90 )
167+ ProcessResult (
168+ data = data,
169+ mimeType = " image/webp" ,
170+ imageSize = size
171+ )
172+ }
173+ }
174+ }
175+
60176 /* *
61177 * Process a file based on its mime type and the given constraints.
62178 *
@@ -92,7 +208,11 @@ class AttachmentProcessor @Inject constructor(
92208 return null
93209 }
94210
95- return processAnimatedWebP(data = data, maxImageResolution)
211+ return processAnimatedWebP(
212+ data = data,
213+ maxImageResolution = maxImageResolution,
214+ timeoutMills = 30_000L
215+ )
96216 }
97217
98218 ImageUtils .isWebP(data) -> {
@@ -152,8 +272,16 @@ class AttachmentProcessor @Inject constructor(
152272 */
153273 fun encryptDeterministically (plaintext : ByteArray , domain : Attachments .Domain ): EncryptResult {
154274 val cipherOut = ByteArray (Attachments .encryptedSize(plaintext.size.toLong()).toInt())
275+ val privateKey = requireNotNull(storage.get().getUserED25519KeyPair()?.secretKey) {
276+ " No user identity available"
277+ }
278+ check(privateKey.data.size == 64 ) {
279+ " Invalid ED25519 private key size: ${privateKey.data.size} "
280+ }
281+ val seed = privateKey.data.sliceArray(0 until 32 )
282+
155283 val key = Attachments .encryptBytes(
156- seed = Util .getSecretBytes( 32 ) ,
284+ seed = seed ,
157285 plaintextIn = plaintext,
158286 cipherOut = cipherOut,
159287 domain = domain,
@@ -250,17 +378,15 @@ class AttachmentProcessor @Inject constructor(
250378
251379 private fun processAnimatedWebP (
252380 data : BufferedSource ,
253- maxImageResolution : IntSize ? ,
381+ maxImageResolution : IntSize ,
382+ timeoutMills : Long ,
254383 ): ProcessResult ? {
255384 val origSize = data.peek().inputStream().use(BitmapUtil ::getDimensions)
256385 .let { pair -> IntSize (pair.first, pair.second) }
257386
258387 val targetSize: IntSize
259388
260- if (maxImageResolution == null || (
261- origSize.width <= maxImageResolution.width &&
262- origSize.height <= maxImageResolution.height)
263- ) {
389+ if (origSize.width <= maxImageResolution.width && origSize.height <= maxImageResolution.height) {
264390 // No resizing needed hence no processing
265391 return null
266392 } else {
@@ -276,7 +402,7 @@ class AttachmentProcessor @Inject constructor(
276402 input = data.readByteArray(),
277403 targetWidth = targetSize.width,
278404 targetHeight = targetSize.height,
279- timeoutMills = 10_000L ,
405+ timeoutMills = timeoutMills ,
280406 )
281407
282408 Log .d(
@@ -345,14 +471,12 @@ class AttachmentProcessor @Inject constructor(
345471 ).first
346472 }
347473
348- val reencoded = data.peek().inputStream().use { input ->
349- GifUtils .reencodeGif(
350- input = input,
351- targetWidth = targetSize.width,
352- targetHeight = targetSize.height,
353- timeoutMills = 10_000L ,
354- )
355- }
474+ val reencoded = GifUtils .reencodeGif(
475+ input = data.readByteArray(),
476+ targetWidth = targetSize.width,
477+ targetHeight = targetSize.height,
478+ timeoutMills = 10_000L ,
479+ )
356480
357481 Log .d(
358482 TAG ,
@@ -376,14 +500,12 @@ class AttachmentProcessor @Inject constructor(
376500 options : Options ,
377501 imageLoader : ImageLoader
378502 ): Fetcher {
379- return object : Fetcher {
380- override suspend fun fetch (): FetchResult ? {
381- return SourceFetchResult (
382- source = ImageSource (data, FileSystem .SYSTEM ),
383- mimeType = null ,
384- dataSource = DataSource .MEMORY
385- )
386- }
503+ return Fetcher {
504+ SourceFetchResult (
505+ source = ImageSource (data, FileSystem .SYSTEM ),
506+ mimeType = null ,
507+ dataSource = DataSource .MEMORY
508+ )
387509 }
388510 }
389511 }
0 commit comments