mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 02:10:44 +01:00
Use dynamic quality and webp for archive thumbnail generation.
This commit is contained in:
@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
import org.thoughtcrime.securesms.util.ImageCompressionUtil
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream
|
||||
@@ -31,6 +32,8 @@ import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
@@ -45,6 +48,11 @@ class ArchiveThumbnailUploadJob private constructor(
|
||||
const val KEY = "ArchiveThumbnailUploadJob"
|
||||
private val TAG = Log.tag(ArchiveThumbnailUploadJob::class.java)
|
||||
|
||||
private const val STARTING_IMAGE_QUALITY = 75f
|
||||
private const val MINIMUM_IMAGE_QUALITY = 10f
|
||||
private const val MAX_PIXEL_DIMENSION = 256
|
||||
private const val ADDITIONAL_QUALITY_DECREASE = 10f
|
||||
|
||||
fun enqueueIfNecessary(attachmentId: AttachmentId) {
|
||||
if (SignalStore.backup.backsUpMedia) {
|
||||
AppDependencies.jobManager.add(ArchiveThumbnailUploadJob(attachmentId))
|
||||
@@ -137,7 +145,7 @@ class ArchiveThumbnailUploadJob private constructor(
|
||||
// save attachment thumbnail
|
||||
SignalDatabase.attachments.finalizeAttachmentThumbnailAfterUpload(
|
||||
attachmentId = attachmentId,
|
||||
attachmentDigest = attachment.remoteDigest!!,
|
||||
attachmentDigest = attachment.remoteDigest,
|
||||
data = thumbnailResult.data
|
||||
)
|
||||
|
||||
@@ -163,16 +171,34 @@ class ArchiveThumbnailUploadJob private constructor(
|
||||
val uri: DecryptableUri = attachment.uri?.let { DecryptableUri(it) } ?: return null
|
||||
|
||||
return if (MediaUtil.isImageType(attachment.contentType)) {
|
||||
ImageCompressionUtil.compress(context, attachment.contentType ?: "", uri, 256, 50)
|
||||
compress(uri, attachment.contentType ?: "")
|
||||
} else if (Build.VERSION.SDK_INT >= 23 && MediaUtil.isVideoType(attachment.contentType)) {
|
||||
MediaUtil.getVideoThumbnail(context, attachment.uri)?.let {
|
||||
ImageCompressionUtil.compress(context, attachment.contentType ?: "", uri, 256, 50)
|
||||
compress(uri, attachment.contentType ?: "")
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun compress(uri: DecryptableUri, contentType: String): ImageCompressionUtil.Result? {
|
||||
val maxFileSize = RemoteConfig.backupMaxThumbnailFileSize.inWholeBytes.toFloat()
|
||||
var attempts = 0
|
||||
var quality = STARTING_IMAGE_QUALITY
|
||||
|
||||
var result: ImageCompressionUtil.Result? = ImageCompressionUtil.compress(context, contentType, MediaUtil.IMAGE_WEBP, uri, MAX_PIXEL_DIMENSION, quality.toInt())
|
||||
|
||||
while (result != null && result.data.size > maxFileSize && attempts < 5 && quality > MINIMUM_IMAGE_QUALITY) {
|
||||
val maxSizeToActualRatio = maxFileSize / result.data.size.toFloat()
|
||||
val newQuality = quality * maxSizeToActualRatio - ADDITIONAL_QUALITY_DECREASE
|
||||
|
||||
quality = floor(max(MINIMUM_IMAGE_QUALITY, newQuality))
|
||||
result = ImageCompressionUtil.compress(context, contentType, MediaUtil.IMAGE_WEBP, uri, MAX_PIXEL_DIMENSION, quality.toInt())
|
||||
attempts++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun buildSignalServiceAttachmentStream(result: ImageCompressionUtil.Result, uploadSpec: ResumableUpload): SignalServiceAttachmentStream {
|
||||
return SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(ByteArrayInputStream(result.data))
|
||||
|
||||
@@ -373,12 +373,12 @@ public abstract class PushSendJob extends SendJob {
|
||||
|
||||
try {
|
||||
if (MediaUtil.isImageType(attachment.contentType) && attachment.getUri() != null) {
|
||||
thumbnailData = ImageCompressionUtil.compress(context, attachment.contentType, new DecryptableUri(attachment.getUri()), 100, 50);
|
||||
thumbnailData = ImageCompressionUtil.compress(context, attachment.contentType, attachment.contentType, new DecryptableUri(attachment.getUri()), 100, 50);
|
||||
} else if (Build.VERSION.SDK_INT >= 23 && MediaUtil.isVideoType(attachment.contentType) && attachment.getUri() != null) {
|
||||
Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getUri(), 1000);
|
||||
|
||||
if (bitmap != null) {
|
||||
thumbnailData = ImageCompressionUtil.compress(context, attachment.contentType, new DecryptableUri(attachment.getUri()), 100, 50);
|
||||
thumbnailData = ImageCompressionUtil.compress(context, attachment.contentType, attachment.contentType, new DecryptableUri(attachment.getUri()), 100, 50);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ public final class ImageCompressionUtil {
|
||||
@IntRange(from = 0, to = 100) int quality)
|
||||
throws BitmapDecodingException
|
||||
{
|
||||
Result result = compress(context, mimeType, glideModel, maxDimension, quality);
|
||||
Result result = compress(context, mimeType, mimeType, glideModel, maxDimension, quality);
|
||||
|
||||
if (result.getData().length <= maxBytes) {
|
||||
return result;
|
||||
@@ -74,6 +74,7 @@ public final class ImageCompressionUtil {
|
||||
@WorkerThread
|
||||
public static @NonNull Result compress(@NonNull Context context,
|
||||
@Nullable String contentType,
|
||||
@Nullable String targetContentType,
|
||||
@NonNull Object glideModel,
|
||||
int maxDimension,
|
||||
@IntRange(from = 0, to = 100) int quality)
|
||||
@@ -121,7 +122,7 @@ public final class ImageCompressionUtil {
|
||||
}
|
||||
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
Bitmap.CompressFormat format = mimeTypeToCompressFormat(contentType);
|
||||
Bitmap.CompressFormat format = mimeTypeToCompressFormat(targetContentType);
|
||||
scaledBitmap.compress(format, quality, output);
|
||||
|
||||
byte[] data = output.toByteArray();
|
||||
@@ -137,6 +138,8 @@ public final class ImageCompressionUtil {
|
||||
MediaUtil.isAvifType(mimeType) ||
|
||||
MediaUtil.isVideoType(mimeType)) {
|
||||
return Bitmap.CompressFormat.JPEG;
|
||||
} else if (MediaUtil.isWebpType(mimeType)) {
|
||||
return Bitmap.CompressFormat.WEBP;
|
||||
} else {
|
||||
return Bitmap.CompressFormat.PNG;
|
||||
}
|
||||
@@ -148,6 +151,8 @@ public final class ImageCompressionUtil {
|
||||
return MediaUtil.IMAGE_JPEG;
|
||||
case PNG:
|
||||
return MediaUtil.IMAGE_PNG;
|
||||
case WEBP:
|
||||
return MediaUtil.IMAGE_WEBP;
|
||||
default:
|
||||
throw new AssertionError("Unsupported format!");
|
||||
}
|
||||
|
||||
@@ -347,6 +347,10 @@ public class MediaUtil {
|
||||
return !TextUtils.isEmpty(contentType) && contentType.trim().equals(IMAGE_AVIF);
|
||||
}
|
||||
|
||||
public static boolean isWebpType(String contentType) {
|
||||
return !TextUtils.isEmpty(contentType) && contentType.trim().equals(IMAGE_WEBP);
|
||||
}
|
||||
|
||||
public static boolean isFile(Attachment attachment) {
|
||||
return !isGif(attachment) && !isImage(attachment) && !isAudio(attachment) && !isVideo(attachment);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.gibiBytes
|
||||
import org.signal.core.util.kibiBytes
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.mebiBytes
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
@@ -989,6 +992,15 @@ object RemoteConfig {
|
||||
defaultValue = 3
|
||||
)
|
||||
|
||||
/** Max plaintext unpadded file size for backup thumbnails. */
|
||||
val backupMaxThumbnailFileSize: ByteSize by remoteValue(
|
||||
key = "global.backups.maxThumbnailFileSizeBytes",
|
||||
hotSwappable = true,
|
||||
active = true
|
||||
) { value ->
|
||||
value.asLong(8.kibiBytes.inWholeBytes).bytes
|
||||
}
|
||||
|
||||
/** Whether unauthenticated chat web socket is backed by libsignal-net */
|
||||
@JvmStatic
|
||||
@get:JvmName("libSignalWebSocketEnabled")
|
||||
|
||||
Reference in New Issue
Block a user