kopia lustrzana https://github.com/ryukoposting/Signal-Android
Updated image compression parameters.
rodzic
3bdf2e7e2c
commit
236e1ba885
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue