Use WebpSanitizer.

This commit is contained in:
Cody Henthorne 2024-01-17 09:42:32 -05:00 committed by Greyson Parrelli
parent debf964b5f
commit 15afaeabe3
56 changed files with 32 additions and 936 deletions

View file

@ -487,7 +487,6 @@ dependencies {
implementation(project(":sms-exporter"))
implementation(project(":sticky-header-grid"))
implementation(project(":photoview"))
implementation(project(":glide-webp"))
implementation(project(":core-ui"))
implementation(libs.androidx.fragment.ktx)

View file

@ -1,25 +1,27 @@
/*
* Copyright 2023 Signal Messenger, LLC
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp
package org.thoughtcrime.securesms.glide.cache
import android.graphics.Bitmap
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapResource
import org.signal.core.util.StreamUtil
import org.signal.core.util.logging.Log
import org.signal.libsignal.media.WebpSanitizer
import java.io.IOException
import java.io.InputStream
class WebpInputStreamResourceDecoder(private val bitmapPool: BitmapPool) : ResourceDecoder<InputStream, Bitmap> {
/**
* Uses WebpSanitizer to check for invalid webp.
*/
class WebpSanDecoder : ResourceDecoder<InputStream, Bitmap> {
companion object {
private const val TAG = "WebpResourceDecoder" // Name > 23 characters
private val TAG = Log.tag(WebpSanDecoder::class.java)
private val MAGIC_NUMBER_P1 = byteArrayOf(0x52, 0x49, 0x46, 0x46) // "RIFF"
private val MAGIC_NUMBER_P2 = byteArrayOf(0x57, 0x45, 0x42, 0x50) // "WEBP"
@ -34,10 +36,12 @@ class WebpInputStreamResourceDecoder(private val bitmapPool: BitmapPool) : Resou
* [4-7]: File length
* [8-11]: "WEBP"
*
* We're not verifying the file length here, so we just need to check the first and last
* We're not verifying the file length here, so we just need to check the first and last.
*
* We then sanitize the webp and block the load if the check fails.
*/
override fun handles(source: InputStream, options: Options): Boolean {
return try {
try {
val magicNumberP1 = ByteArray(4)
StreamUtil.readFully(source, magicNumberP1)
@ -47,24 +51,27 @@ class WebpInputStreamResourceDecoder(private val bitmapPool: BitmapPool) : Resou
val magicNumberP2 = ByteArray(4)
StreamUtil.readFully(source, magicNumberP2)
magicNumberP1.contentEquals(MAGIC_NUMBER_P1) && magicNumberP2.contentEquals(MAGIC_NUMBER_P2)
if (magicNumberP1.contentEquals(MAGIC_NUMBER_P1) && magicNumberP2.contentEquals(MAGIC_NUMBER_P2)) {
try {
source.reset()
source.mark(MAX_WEBP_COMPRESSED_SIZE)
WebpSanitizer.sanitize(source, Long.MAX_VALUE)
source.reset()
} catch (e: Exception) {
Log.w(TAG, "Sanitize check failed or mark position invalidated by reset", e)
return true
}
}
} catch (e: IOException) {
Log.w(TAG, "Failed to read magic number from stream!", e)
false
return true
}
return false
}
override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource<Bitmap>? {
Log.d(TAG, "decode()")
val webp: ByteArray = try {
StreamUtil.readFully(source, MAX_WEBP_COMPRESSED_SIZE)
} catch (e: IOException) {
Log.w(TAG, "Unexpected IOException hit while reading image data", e)
throw e
}
val bitmap: Bitmap? = WebpDecoder().nativeDecodeBitmapScaled(webp, width, height)
return BitmapResource.obtain(bitmap, bitmapPool)
Log.w(TAG, "Image did not pass sanitizer")
throw IOException("Unable to load image")
}
}

View file

@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder;
import org.thoughtcrime.securesms.glide.cache.WebpSanDecoder;
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
@ -64,6 +65,8 @@ public class SignalGlideComponents implements RegisterGlideComponents {
registry.prepend(File.class, File.class, UnitModelLoader.Factory.getInstance());
registry.prepend(InputStream.class, Bitmap.class, new WebpSanDecoder());
registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool()));
registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool())));

View file

@ -100,8 +100,6 @@ task("qa") {
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
// Because gradle is weird, we delete here for glide-webp/lib project so the clean tasks there doesn"t barf
delete(fileTree("glide-webp/lib/.cxx"))
}
task("format") {

View file

@ -70,7 +70,7 @@ fun InputStream.readNBytesOrThrow(length: Int): ByteArray {
}
@Throws(IOException::class)
fun InputStream.readLength(): Long? {
fun InputStream.readLength(): Long {
val buffer = ByteArray(4096)
var count = 0L

View file

@ -10,6 +10,4 @@ android {
dependencies {
implementation(libs.glide.glide)
kapt(libs.glide.compiler)
implementation(project(":glide-webp"))
}

View file

@ -1 +0,0 @@
/build

View file

@ -1,22 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
plugins {
id("signal-sample-app")
kotlin("kapt")
}
android {
namespace = "org.signal.glide.webp.app"
}
dependencies {
implementation(project(":glide-webp"))
implementation(libs.androidx.fragment.ktx)
implementation(libs.glide.glide)
kapt(libs.glide.compiler)
}

View file

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Signal">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Signal">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -1,86 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp.app
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.load.engine.DiskCacheStrategy
import org.signal.core.util.dp
/**
* Main activity for this sample app.
*/
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activitiy)
Glide.init(
this,
GlideBuilder()
.setLogLevel(Log.VERBOSE)
)
val context = this
findViewById<RecyclerView>(R.id.list).apply {
adapter = ImageAdapter()
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
}
class ImageAdapter : RecyclerView.Adapter<ImageViewHolder>() {
private val data: List<String> = listOf(
"test_01.webp",
"test_02.webp",
"test_03.webp",
"test_04.webp",
"test_05.webp",
"test_06_lossless.webp",
"test_06_lossy.webp",
"test_07_lossless.webp",
"test_09_large.webp"
)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
return ImageViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.image_item, parent, false))
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
holder.bind(data[position])
}
}
class ImageViewHolder(itemView: View) : ViewHolder(itemView) {
val image: ImageView by lazy { itemView.findViewById(R.id.image) }
fun bind(filename: String) {
Glide.with(itemView)
.load(Uri.parse("file:///android_asset/$filename"))
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.override(250.dp)
.into(image)
}
}
}

View file

@ -1,24 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp.app
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.AppGlideModule
import org.signal.core.util.logging.Log
@GlideModule
class SampleAppGlideModule : AppGlideModule() {
companion object {
private val TAG = Log.tag(SampleAppGlideModule::class.java)
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
Log.e(TAG, "AppModule - registerComponents")
}
}

View file

@ -1,16 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp.app.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View file

@ -1,75 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp.app.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun SignalTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View file

@ -1,39 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp.app.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View file

@ -1,35 +0,0 @@
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -1,175 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp">
</ImageView>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View file

@ -1,8 +0,0 @@
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<resources>
<string name="app_name">Glide WebP</string>
</resources>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<resources>
<style name="Theme.Signal" parent="Theme.AppCompat.NoActionBar" />
</resources>

View file

@ -1,2 +0,0 @@
/build
.cxx

View file

@ -1,45 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
plugins {
id("signal-library")
kotlin("kapt")
}
android {
namespace = "org.signal.glide.webp"
defaultConfig {
externalNativeBuild {
cmake {
cppFlags("-std=c++17", "-fvisibility=hidden")
arguments("-DLIBWEBP_PATH=$rootDir/libwebp")
}
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
externalNativeBuild {
cmake {
path = file("$projectDir/src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
}
dependencies {
implementation(project(":core-util"))
implementation(libs.glide.glide)
kapt(libs.glide.compiler)
androidTestImplementation(testLibs.androidx.test.core)
androidTestImplementation(testLibs.androidx.test.core.ktx)
androidTestImplementation(testLibs.androidx.test.ext.junit)
androidTestImplementation(testLibs.androidx.test.ext.junit.ktx)
androidTestImplementation(testLibs.androidx.test.monitor)
androidTestImplementation(testLibs.androidx.test.runner)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

View file

@ -1,102 +0,0 @@
package org.signal.glide.webp
import android.os.Build
import android.os.Debug
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.StreamUtil
@RunWith(AndroidJUnit4::class)
class WebpDecoderTest {
private fun readAsset(name: String): ByteArray {
val source = InstrumentationRegistry.getInstrumentation().context.assets.open(name)
return StreamUtil.readFully(source)
}
@Test
fun landscapeUnscaled() {
val input = readAsset("landscape_550x368.webp")
val outputUnconstrained = WebpDecoder().nativeDecodeBitmapScaled(input, 1000, 1000)!!
assertEquals(550, outputUnconstrained.width)
assertEquals(368, outputUnconstrained.height)
val outputSquareConstraints = WebpDecoder().nativeDecodeBitmapScaled(input, 550, 550)!!
assertEquals(550, outputSquareConstraints.width)
assertEquals(368, outputSquareConstraints.height)
val outputExactConstraints = WebpDecoder().nativeDecodeBitmapScaled(input, 550, 368)!!
assertEquals(550, outputExactConstraints.width)
assertEquals(368, outputExactConstraints.height)
}
@Test
fun landscapeScaledBasedOnWidth() {
val input = readAsset("landscape_550x368.webp")
val outputUnconstrainedHeight = WebpDecoder().nativeDecodeBitmapScaled(input, 275, 1000)!!
assertEquals(275, outputUnconstrainedHeight.width)
assertEquals(184, outputUnconstrainedHeight.height)
val outputSquareConstraints = WebpDecoder().nativeDecodeBitmapScaled(input, 275, 275)!!
assertEquals(275, outputSquareConstraints.width)
assertEquals(184, outputSquareConstraints.height)
val outputSmallHeight = WebpDecoder().nativeDecodeBitmapScaled(input, 275, 200)!!
assertEquals(275, outputSmallHeight.width)
assertEquals(184, outputSmallHeight.height)
}
@Test
fun landscapeScaledBasedOnHeight() {
val input = readAsset("landscape_550x368.webp")
val outputUnconstrainedWidth = WebpDecoder().nativeDecodeBitmapScaled(input, 1000, 184)!!
assertEquals(275, outputUnconstrainedWidth.width)
assertEquals(184, outputUnconstrainedWidth.height)
val outputSmallWidth = WebpDecoder().nativeDecodeBitmapScaled(input, 300, 184)!!
assertEquals(275, outputSmallWidth.width)
assertEquals(184, outputSmallWidth.height)
}
private fun getNativeHeapSize(): Long {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Debug.getNativeHeapSize()
} else {
0
}
}
@Test
fun canScaleMassiveImagesWithoutAllocatingLargeBuffers() {
val nativeHeapSizeAtStart = getNativeHeapSize()
val input = readAsset("solid_white_10Kx10K.webp")
val output = WebpDecoder().nativeDecodeBitmapScaled(input, 1000, 1000)!!
assertEquals(1000, output.width)
assertEquals(1000, output.height)
var nativeHeapSizeAtEnd = getNativeHeapSize()
assertTrue("heap size: " + nativeHeapSizeAtStart + " -> " + nativeHeapSizeAtEnd, nativeHeapSizeAtStart + 10_000 * 10_000 > nativeHeapSizeAtEnd)
}
@Test
fun canHandleMassiveBrokenImagesWithoutAllocatingLargeBuffers() {
val nativeHeapSizeAtStart = getNativeHeapSize()
val input = readAsset("broken_10Kx10K.webp")
val output = WebpDecoder().nativeDecodeBitmapScaled(input, 1000, 1000)
assertNull(output)
var nativeHeapSizeAtEnd = getNativeHeapSize()
assertTrue("heap size: " + nativeHeapSizeAtStart + " -> " + nativeHeapSizeAtEnd, nativeHeapSizeAtStart + 10_000 * 10_000 > nativeHeapSizeAtEnd)
}
}

View file

@ -1,25 +0,0 @@
cmake_minimum_required(VERSION 3.22.1)
project(signal-webp)
add_compile_options(-Oz)
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
set(BUILD_SHARED_LIBS ON)
set(WEBP_BUILD_ANIM_UTILS OFF)
set(WEBP_BUILD_CWEBP OFF)
set(WEBP_BUILD_DWEBP OFF)
set(WEBP_BUILD_VWEBP OFF)
set(WEBP_BUILD_WEBPINFO OFF)
set(WEBP_BUILD_LIBWEBPMUX OFF)
set(WEBP_BUILD_EXTRAS OFF)
set(WEBP_NEAR_LOSSLESS, OFF)
file(GLOB SOURCES ${CMAKE_SOURCE_DIR}/*.cpp)
add_subdirectory(${LIBWEBP_PATH} ${CMAKE_CURRENT_BINARY_DIR}/libwebp)
add_library(signalwebp SHARED ${SOURCES})
target_include_directories(signalwebp PRIVATE ${LIBWEBP_PATH}/src)
find_library(androidlog log)
target_link_libraries(signalwebp webpdemux ${androidlog})

View file

@ -1,103 +0,0 @@
#include <android/log.h>
#include <jni.h>
#include <webp/demux.h>
#include <algorithm>
#include <string>
#define TAG "WebpResourceDecoder"
jobject createBitmap(JNIEnv *env, int width, int height, const uint8_t *pixels) {
static auto jbitmapConfigClass = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass("android/graphics/Bitmap$Config")));
static jfieldID jbitmapConfigARGB8888Field = env->GetStaticFieldID(jbitmapConfigClass, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
static auto jbitmapClass = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass("android/graphics/Bitmap")));
static jmethodID jbitmapCreateBitmapMethod = env->GetStaticMethodID(jbitmapClass, "createBitmap", "([IIIIILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jintArray intArray = env->NewIntArray(width * height);
env->SetIntArrayRegion(intArray, 0, width * height, reinterpret_cast<const jint *>(pixels));
jobject argb8888Config = env->GetStaticObjectField(jbitmapConfigClass, jbitmapConfigARGB8888Field);
jobject jbitmap = env->CallStaticObjectMethod(jbitmapClass, jbitmapCreateBitmapMethod, intArray, 0, width, width, height, argb8888Config);
return jbitmap;
}
jobject nativeDecodeBitmapScaled(JNIEnv *env, jobject, jbyteArray data, jint requestedWidth, jint requestedHeight) {
jbyte *javaBytes = env->GetByteArrayElements(data, nullptr);
auto *buffer = reinterpret_cast<uint8_t *>(javaBytes);
jsize bufferLength = env->GetArrayLength(data);
WebPBitstreamFeatures features;
if (WebPGetFeatures(buffer, bufferLength, &features) != VP8_STATUS_OK) {
__android_log_write(ANDROID_LOG_WARN, TAG, "GetFeatures");
env->ReleaseByteArrayElements(data, javaBytes, JNI_ABORT);
return nullptr;
}
WebPDecoderConfig config;
if (!WebPInitDecoderConfig(&config)) {
__android_log_write(ANDROID_LOG_WARN, TAG, "Init decoder config");
env->ReleaseByteArrayElements(data, javaBytes, JNI_ABORT);
return nullptr;
}
config.options.no_fancy_upsampling = 1;
config.output.colorspace = MODE_BGRA;
if (requestedWidth > 0 && requestedHeight > 0 && features.width > 0 && features.height > 0 && (requestedWidth < features.width || requestedHeight < features.height)) {
double widthScale = static_cast<double>(requestedWidth) / features.width;
double heightScale = static_cast<double>(requestedHeight) / features.height;
double requiredScale = std::min(widthScale, heightScale);
config.options.use_scaling = 1;
config.options.scaled_width = static_cast<int>(requiredScale * features.width);
config.options.scaled_height = static_cast<int>(requiredScale * features.height);
}
uint8_t *pixels = nullptr;
int width = 0;
int height = 0;
VP8StatusCode result = WebPDecode(buffer, bufferLength, &config);
if (result != VP8_STATUS_OK) {
__android_log_write(ANDROID_LOG_WARN, TAG, ("Scaled WebPDecode failed (" + std::to_string(result) + ")").c_str());
} else {
pixels = config.output.u.RGBA.rgba;
width = config.output.width;
height = config.output.height;
}
jobject jbitmap = nullptr;
if (pixels != nullptr) {
jbitmap = createBitmap(env, width, height, pixels);
}
WebPFree(pixels);
env->ReleaseByteArrayElements(data, javaBytes, JNI_ABORT);
return jbitmap;
}
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) {
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass c = env->FindClass("org/signal/glide/webp/WebpDecoder");
if (c == nullptr) {
return JNI_ERR;
}
static const JNINativeMethod methods[] = {
{"nativeDecodeBitmapScaled", "([BII)Landroid/graphics/Bitmap;", reinterpret_cast<void *>(nativeDecodeBitmapScaled)}
};
int rc = env->RegisterNatives(c, methods, sizeof(methods) / sizeof(JNINativeMethod));
if (rc != JNI_OK) {
return rc;
}
return JNI_VERSION_1_6;
}

View file

@ -1,17 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp
import android.graphics.Bitmap
class WebpDecoder {
init {
System.loadLibrary("signalwebp")
}
external fun nativeDecodeBitmapScaled(data: ByteArray, requestedWidth: Int, requestedHeight: Int): Bitmap?
}

View file

@ -1,30 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp
import android.content.Context
import android.graphics.Bitmap
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.LibraryGlideModule
import org.signal.core.util.logging.Log
import java.io.InputStream
/**
* Registers a custom handler for webp images.
*/
@GlideModule
class WebpLibraryGlideModule : LibraryGlideModule() {
companion object {
private val TAG = Log.tag(WebpLibraryGlideModule::class.java)
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
Log.d(TAG, "registerComponents()")
registry.prepend(InputStream::class.java, Bitmap::class.java, WebpInputStreamResourceDecoder(glide.bitmapPool))
}
}

View file

@ -58,8 +58,6 @@ include(":benchmark")
include(":microbenchmark")
include(":video")
include(":video-app")
include(":glide-webp")
include(":glide-webp-app")
project(":app").name = "Signal-Android"
project(":paging").projectDir = file("paging/lib")
@ -89,9 +87,6 @@ project(":qr-app").projectDir = file("qr/app")
project(":video").projectDir = file("video/lib")
project(":video-app").projectDir = file("video/app")
project(":glide-webp").projectDir = file("glide-webp/lib")
project(":glide-webp-app").projectDir = file("glide-webp/app")
rootProject.name = "Signal"
apply(from = "dependencies.gradle.kts")