Updated image compression parameters.

fork-5.53.8
Greyson Parrelli 2021-02-08 15:39:04 -05:00 zatwierdzone przez Cody Henthorne
rodzic 3bdf2e7e2c
commit 236e1ba885
7 zmienionych plików z 227 dodań i 32 usunięć

Wyświetl plik

@ -2,8 +2,10 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.media.MediaDataSource;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.google.android.exoplayer2.util.MimeTypes;
@ -31,8 +33,8 @@ import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ImageCompressionUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
import org.thoughtcrime.securesms.util.MemoryFileDescriptor.MemoryFileException;
@ -141,7 +143,7 @@ public final class AttachmentCompressionJob extends BaseJob {
MediaConstraints mediaConstraints = mms ? MediaConstraints.getMmsMediaConstraints(mmsSubscriptionId)
: MediaConstraints.getPushMediaConstraints();
scaleAndStripExif(database, mediaConstraints, databaseAttachment);
compress(database, mediaConstraints, databaseAttachment);
}
@Override
@ -152,30 +154,25 @@ public final class AttachmentCompressionJob extends BaseJob {
return exception instanceof IOException;
}
private void scaleAndStripExif(@NonNull AttachmentDatabase attachmentDatabase,
@NonNull MediaConstraints constraints,
@NonNull DatabaseAttachment attachment)
private void compress(@NonNull AttachmentDatabase attachmentDatabase,
@NonNull MediaConstraints constraints,
@NonNull DatabaseAttachment attachment)
throws UndeliverableMessageException
{
try {
if (MediaUtil.isVideo(attachment)) {
Log.i(TAG, "Compressing video.");
attachment = transcodeVideoIfNeededToDatabase(context, attachmentDatabase, attachment, constraints, EventBus.getDefault(), this::isCanceled);
if (!constraints.isSatisfied(context, attachment)) {
throw new UndeliverableMessageException("Size constraints could not be met on video!");
}
} else if (MediaUtil.isHeic(attachment) || MediaUtil.isHeif(attachment)) {
MediaStream converted = getResizedMedia(context, attachment, constraints);
} else if (constraints.canResize(attachment)) {
Log.i(TAG, "Compressing image.");
MediaStream converted = compressImage(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, converted, false);
attachmentDatabase.markAttachmentAsTransformed(attachmentId);
} else if (constraints.isSatisfied(context, attachment)) {
if (MediaUtil.isJpeg(attachment)) {
MediaStream stripped = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, stripped, false);
}
attachmentDatabase.markAttachmentAsTransformed(attachmentId);
} else if (constraints.canResize(attachment)) {
MediaStream resized = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, resized, false);
Log.i(TAG, "Not compressing.");
attachmentDatabase.markAttachmentAsTransformed(attachmentId);
} else {
throw new UndeliverableMessageException("Size constraints could not be met!");
@ -295,27 +292,48 @@ public final class AttachmentCompressionJob extends BaseJob {
return attachment;
}
private static MediaStream getResizedMedia(@NonNull Context context,
@NonNull Attachment attachment,
@NonNull MediaConstraints constraints)
throws IOException
/**
* Compresses the images. Given that we compress every image, this has the fun side effect of
* stripping all EXIF data.
*/
@WorkerThread
private static MediaStream compressImage(@NonNull Context context,
@NonNull Attachment attachment,
@NonNull MediaConstraints mediaConstraints)
throws UndeliverableMessageException
{
if (!constraints.canResize(attachment)) {
throw new UnsupportedOperationException("Cannot resize this content type");
Uri uri = attachment.getUri();
if (uri == null) {
throw new UndeliverableMessageException("No attachment URI!");
}
ImageCompressionUtil.Result result = null;
try {
BitmapUtil.ScaleResult scaleResult = BitmapUtil.createScaledBytes(context,
new DecryptableStreamUriLoader.DecryptableUri(attachment.getUri()),
constraints);
return new MediaStream(new ByteArrayInputStream(scaleResult.getBitmap()),
MediaUtil.IMAGE_JPEG,
scaleResult.getWidth(),
scaleResult.getHeight());
for (int size : mediaConstraints.getImageDimensionTargets(context)) {
result = ImageCompressionUtil.compressWithinConstraints(context,
attachment.getContentType(),
new DecryptableStreamUriLoader.DecryptableUri(uri),
size,
mediaConstraints.getImageMaxSize(context),
70);
if (result != null) {
break;
}
}
} catch (BitmapDecodingException e) {
throw new IOException(e);
throw new UndeliverableMessageException(e);
}
if (result == null) {
throw new UndeliverableMessageException("Somehow couldn't meet the constraints!");
}
return new MediaStream(new ByteArrayInputStream(result.getData()),
result.getMimeType(),
result.getWidth(),
result.getHeight());
}
public static final class Factory implements Job.Factory<AttachmentCompressionJob> {

Wyświetl plik

@ -33,6 +33,13 @@ public abstract class MediaConstraints {
public abstract int getImageMaxHeight(Context context);
public abstract int getImageMaxSize(Context context);
/**
* Provide a list of dimensions that should be attempted during compression. We will keep moving
* down the list until the image can be scaled to fit under {@link #getImageMaxSize(Context)}.
* The first entry in the list should match your max width/height.
*/
public abstract int[] getImageDimensionTargets(Context context);
public abstract int getGifMaxSize(Context context);
public abstract int getVideoMaxSize(Context context);

Wyświetl plik

@ -24,6 +24,19 @@ final class MmsMediaConstraints extends MediaConstraints {
return Math.max(MIN_IMAGE_DIMEN, getOverriddenMmsConfig(context).getMaxImageHeight());
}
@Override
public int[] getImageDimensionTargets(Context context) {
int[] targets = new int[4];
targets[0] = getImageMaxHeight(context);
for (int i = 1; i < targets.length; i++) {
targets[i] = targets[i - 1] / 2;
}
return targets;
}
@Override
public int getImageMaxSize(Context context) {
return getMaxMessageSize(context);

Wyświetl plik

@ -7,10 +7,13 @@ import org.thoughtcrime.securesms.util.Util;
public class PushMediaConstraints extends MediaConstraints {
private static final int MAX_IMAGE_DIMEN_LOWMEM = 768;
private static final int MAX_IMAGE_DIMEN = 4096;
private static final int MAX_IMAGE_DIMEN = 1599;
private static final int KB = 1024;
private static final int MB = 1024 * KB;
private static final int[] FALLBACKS = { MAX_IMAGE_DIMEN, 1024, 768, 512 };
private static final int[] FALLBACKS_LOWMEM = { MAX_IMAGE_DIMEN_LOWMEM, 512 };
@Override
public int getImageMaxWidth(Context context) {
return Util.isLowMemory(context) ? MAX_IMAGE_DIMEN_LOWMEM : MAX_IMAGE_DIMEN;
@ -23,7 +26,13 @@ public class PushMediaConstraints extends MediaConstraints {
@Override
public int getImageMaxSize(Context context) {
return 6 * MB;
//noinspection PointlessArithmeticExpression
return 1 * MB;
}
@Override
public int[] getImageDimensionTargets(Context context) {
return Util.isLowMemory(context) ? FALLBACKS_LOWMEM : FALLBACKS;
}
@Override

Wyświetl plik

@ -21,6 +21,11 @@ public class ProfileMediaConstraints extends MediaConstraints {
return 5 * 1024 * 1024;
}
@Override
public int[] getImageDimensionTargets(Context context) {
return new int[] { getImageMaxWidth(context) };
}
@Override
public int getGifMaxSize(Context context) {
return 0;

Wyświetl plik

@ -48,6 +48,11 @@ public class BitmapUtil {
private static final int MIN_COMPRESSION_QUALITY_DECREASE = 5;
private static final int MAX_IMAGE_HALF_SCALES = 3;
/**
* @deprecated You probably want to use {@link ImageCompressionUtil} instead, which has a clearer
* contract and handles mimetypes properly.
*/
@Deprecated
@WorkerThread
public static <T> ScaleResult createScaledBytes(@NonNull Context context, @NonNull T model, @NonNull MediaConstraints constraints)
throws BitmapDecodingException
@ -58,6 +63,10 @@ public class BitmapUtil {
constraints.getImageMaxSize(context));
}
/**
* @deprecated You probably want to use {@link ImageCompressionUtil} instead, which has a clearer
* contract and handles mimetypes properly.
*/
@WorkerThread
public static <T> ScaleResult createScaledBytes(@NonNull Context context,
@NonNull T model,
@ -69,6 +78,10 @@ public class BitmapUtil {
return createScaledBytes(context, model, maxImageWidth, maxImageHeight, maxImageSize, CompressFormat.JPEG);
}
/**
* @deprecated You probably want to use {@link ImageCompressionUtil} instead, which has a clearer
* contract and handles mimetypes properly.
*/
@WorkerThread
public static <T> ScaleResult createScaledBytes(Context context,
T model,

Wyświetl plik

@ -0,0 +1,130 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.Bitmap;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.mms.GlideApp;
import java.io.ByteArrayOutputStream;
import java.util.concurrent.ExecutionException;
public final class ImageCompressionUtil {
private ImageCompressionUtil () {}
/**
* A result satisfying the provided constraints, or null if they could not be met.
*/
@WorkerThread
public static @Nullable Result compressWithinConstraints(@NonNull Context context,
@NonNull String mimeType,
@NonNull Object glideModel,
int maxDimension,
int maxBytes,
@IntRange(from = 0, to = 100) int quality)
throws BitmapDecodingException
{
Result result = compress(context, mimeType, glideModel, maxDimension, quality);
if (result.getData().length <= maxBytes) {
return result;
} else {
return null;
}
}
/**
* Compresses the image to match the requested parameters.
*/
@WorkerThread
public static @NonNull Result compress(@NonNull Context context,
@NonNull String mimeType,
@NonNull Object glideModel,
int maxDimension,
@IntRange(from = 0, to = 100) int quality)
throws BitmapDecodingException
{
Bitmap scaledBitmap;
try {
scaledBitmap = GlideApp.with(context.getApplicationContext())
.asBitmap()
.load(glideModel)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.centerInside()
.submit(maxDimension, maxDimension)
.get();
} catch (ExecutionException | InterruptedException e) {
throw new BitmapDecodingException(e);
}
if (scaledBitmap == null) {
throw new BitmapDecodingException("Unable to decode image");
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
Bitmap.CompressFormat format = mimeTypeToCompressFormat(mimeType);
scaledBitmap.compress(format, quality, output);
byte[] data = output.toByteArray();
return new Result(data, compressFormatToMimeType(format), scaledBitmap.getWidth(), scaledBitmap.getHeight());
}
private static @NonNull Bitmap.CompressFormat mimeTypeToCompressFormat(@NonNull String mimeType) {
if (MediaUtil.isJpegType(mimeType) || MediaUtil.isHeicType(mimeType) || MediaUtil.isHeifType(mimeType)) {
return Bitmap.CompressFormat.JPEG;
} else {
return Bitmap.CompressFormat.PNG;
}
}
private static @NonNull String compressFormatToMimeType(@NonNull Bitmap.CompressFormat format) {
switch (format) {
case JPEG:
return MediaUtil.IMAGE_JPEG;
case PNG:
return MediaUtil.IMAGE_PNG;
default:
throw new AssertionError("Unsupported format!");
}
}
public static final class Result {
private final byte[] data;
private final String mimeType;
private final int height;
private final int width;
public Result(@NonNull byte[] data, @NonNull String mimeType, int width, int height) {
this.data = data;
this.mimeType = mimeType;
this.width = width;
this.height = height;
}
public byte[] getData() {
return data;
}
public @NonNull String getMimeType() {
return mimeType;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
}