@@ -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