Verified Commit 2dbe6adf authored by Sebastian's avatar Sebastian
Browse files

Create ImageCompressor amd ImageCompressorImpl

* Methods from AttachmentCreationTask have been moved into them:
  * compressImage()
  * createBitmap()
* ImageCompressor is availabe via AttachmentModule
parent 36116c73
Pipeline #5222 passed with stage
in 9 minutes and 50 seconds
package org.briarproject.briar.android.attachment;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {
AttachmentModule.class
})
interface AbstractImageCompressorComponent {
void inject(AbstractImageCompressorTest test);
}
......@@ -2,34 +2,31 @@ package org.briarproject.briar.android.attachment;
import android.content.res.AssetManager;
import org.junit.Before;
import java.io.IOException;
import java.io.InputStream;
import static androidx.test.InstrumentationRegistry.getContext;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import javax.inject.Inject;
abstract class AbstractAttachmentCreationTaskTest {
import static androidx.test.InstrumentationRegistry.getContext;
private final ImageHelper imageHelper = new ImageHelperImpl();
private final ImageSizeCalculator imageSizeCalculator =
new ImageSizeCalculator(imageHelper);
public abstract class AbstractImageCompressorTest {
private AttachmentCreationTask task;
@Inject
ImageCompressor imageCompressor;
@Before
@SuppressWarnings("ConstantConditions") // add real objects when needed
public void setUp() {
task = new AttachmentCreationTask(null,
getApplicationContext().getContentResolver(), null,
imageSizeCalculator, null, null, true);
public AbstractImageCompressorTest() {
AbstractImageCompressorComponent component =
DaggerAbstractImageCompressorComponent.builder().build();
component.inject(this);
}
protected abstract void inject(
AbstractImageCompressorComponent component);
void testCompress(String filename, String contentType)
throws IOException {
InputStream is = getAssetManager().open(filename);
task.compressImage(is, contentType);
imageCompressor.compressImage(is, contentType);
}
static AssetManager getAssetManager() {
......
......@@ -9,8 +9,13 @@ import static android.os.Build.VERSION.SDK_INT;
import static org.junit.Assume.assumeTrue;
@RunWith(AndroidJUnit4.class)
public class AttachmentCreationTaskTest
extends AbstractAttachmentCreationTaskTest {
public class ImageCompressorTest
extends AbstractImageCompressorTest {
@Override
protected void inject(AbstractImageCompressorComponent component) {
component.inject(this);
}
@Test
public void testCompressSmallPng() throws Exception {
......
......@@ -17,11 +17,16 @@ import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
public class PngSuiteAttachmentCreationTaskTest
extends AbstractAttachmentCreationTaskTest {
public class PngSuiteImageCompressorTest
extends AbstractImageCompressorTest {
private static final Logger LOG =
getLogger(PngSuiteAttachmentCreationTaskTest.class.getName());
getLogger(PngSuiteImageCompressorTest.class.getName());
@Override
protected void inject(AbstractImageCompressorComponent component) {
component.inject(this);
}
@Parameters
public static Iterable<String> data() throws IOException {
......@@ -34,14 +39,14 @@ public class PngSuiteAttachmentCreationTaskTest
private final String filename;
public PngSuiteAttachmentCreationTaskTest(String filename) {
public PngSuiteImageCompressorTest(String filename) {
this.filename = filename;
}
@Test
public void testPngSuiteCompress() throws Exception {
assumeTrue(isOptionalTestEnabled(
PngSuiteAttachmentCreationTaskTest.class));
PngSuiteImageCompressorTest.class));
LOG.info("Testing " + filename);
if (filename.startsWith("x")) {
try {
......
......@@ -14,17 +14,15 @@ import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import org.briarproject.briar.android.attachment.ImageCompressor;
import org.briarproject.briar.api.test.TestAvatarCreator;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;
import javax.annotation.Nullable;
import static android.graphics.Bitmap.CompressFormat.JPEG;
import static org.briarproject.briar.api.media.MediaConstants.MAX_IMAGE_SIZE;
import javax.inject.Inject;
public class TestAvatarCreatorImpl implements TestAvatarCreator {
private final int WIDTH = 800;
......@@ -34,20 +32,18 @@ public class TestAvatarCreatorImpl implements TestAvatarCreator {
private final float[] hsv = new float[3];
private final Random random = new Random();
private final ImageCompressor imageCompressor;
@Inject
TestAvatarCreatorImpl(ImageCompressor imageCompressor) {
this.imageCompressor = imageCompressor;
}
@Nullable
@Override
public InputStream getAvatarInputStream() {
public InputStream getAvatarInputStream() throws IOException {
Bitmap bitmap = generateBitmap();
ByteArrayOutputStream out = new ByteArrayOutputStream();
// TODO maybe use ImageCompressor once available standalone
for (int quality = 100; quality >= 0; quality -= 10) {
if (!bitmap.compress(JPEG, quality, out)) return null;
if (out.size() <= MAX_IMAGE_SIZE) {
return new ByteArrayInputStream(out.toByteArray());
}
out.reset();
}
return new ByteArrayInputStream(out.toByteArray());
return imageCompressor.compressImage(bitmap);
}
private Bitmap generateBitmap() {
......
......@@ -178,8 +178,9 @@ public class AppModule {
}
@Provides
TestAvatarCreator provideTestAvatarCreator() {
return new TestAvatarCreatorImpl();
TestAvatarCreator provideTestAvatarCreator(
TestAvatarCreatorImpl testAvatarCreator) {
return testAvatarCreator;
}
@Provides
......
package org.briarproject.briar.android.attachment;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory.Options;
import android.net.Uri;
import org.briarproject.bramble.api.db.DbException;
......@@ -13,21 +11,14 @@ import org.briarproject.briar.api.media.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager;
import org.jsoup.UnsupportedMimeTypeException;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.logging.Logger;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import static android.graphics.Bitmap.CompressFormat.JPEG;
import static android.graphics.BitmapFactory.decodeStream;
import static java.util.Arrays.asList;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes;
......@@ -35,19 +26,16 @@ import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.api.media.MediaConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
class AttachmentCreationTask {
private static Logger LOG =
private static final Logger LOG =
getLogger(AttachmentCreationTask.class.getName());
private static final int MAX_ATTACHMENT_DIMENSION = 1000;
private final MessagingManager messagingManager;
private final ContentResolver contentResolver;
private final ImageSizeCalculator imageSizeCalculator;
private final ImageCompressor imageCompressor;
private final GroupId groupId;
private final Collection<Uri> uris;
private final boolean needsSize;
......@@ -59,11 +47,11 @@ class AttachmentCreationTask {
AttachmentCreationTask(MessagingManager messagingManager,
ContentResolver contentResolver,
AttachmentCreator attachmentCreator,
ImageSizeCalculator imageSizeCalculator,
ImageCompressor imageCompressor,
GroupId groupId, Collection<Uri> uris, boolean needsSize) {
this.messagingManager = messagingManager;
this.contentResolver = contentResolver;
this.imageSizeCalculator = imageSizeCalculator;
this.imageCompressor = imageCompressor;
this.groupId = groupId;
this.uris = uris;
this.needsSize = needsSize;
......@@ -115,7 +103,8 @@ class AttachmentCreationTask {
}
InputStream is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
is = compressImage(is, contentType);
is = imageCompressor
.compressImage(is, contentType);
contentType = "image/jpeg";
long timestamp = System.currentTimeMillis();
AttachmentHeader h = messagingManager
......@@ -125,51 +114,4 @@ class AttachmentCreationTask {
return h;
}
@VisibleForTesting
InputStream compressImage(InputStream is, String contentType)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
Bitmap bitmap = createBitmap(is, contentType);
for (int quality = 100; quality >= 0; quality -= 10) {
if (!bitmap.compress(JPEG, quality, out))
throw new IOException();
if (out.size() <= MAX_IMAGE_SIZE) {
if (LOG.isLoggable(INFO)) {
LOG.info("Compressed image to "
+ out.size() + " bytes, quality " + quality);
}
return new ByteArrayInputStream(out.toByteArray());
}
out.reset();
}
throw new IOException();
} finally {
tryToClose(is, LOG, WARNING);
}
}
private Bitmap createBitmap(InputStream is, String contentType)
throws IOException {
is = new BufferedInputStream(is);
Size size = imageSizeCalculator.getSize(is, contentType);
if (size.error) throw new IOException();
if (LOG.isLoggable(INFO))
LOG.info("Original image size: " + size.width + "x" + size.height);
int dimension = Math.max(size.width, size.height);
int inSampleSize = 1;
while (dimension > MAX_ATTACHMENT_DIMENSION) {
inSampleSize *= 2;
dimension /= 2;
}
if (LOG.isLoggable(INFO))
LOG.info("Scaling attachment by factor of " + inSampleSize);
Options options = new Options();
options.inSampleSize = inSampleSize;
if (contentType.equals("image/png"))
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = decodeStream(is, null, options);
if (bitmap == null) throw new IOException();
return bitmap;
}
}
......@@ -41,7 +41,7 @@ import static org.briarproject.briar.api.media.MediaConstants.MAX_IMAGE_SIZE;
@NotNullByDefault
class AttachmentCreatorImpl implements AttachmentCreator {
private static Logger LOG =
private static final Logger LOG =
getLogger(AttachmentCreatorImpl.class.getName());
private final Application app;
......@@ -49,7 +49,7 @@ class AttachmentCreatorImpl implements AttachmentCreator {
private final Executor ioExecutor;
private final MessagingManager messagingManager;
private final AttachmentRetriever retriever;
private final ImageSizeCalculator imageSizeCalculator;
private final ImageCompressor imageCompressor;
private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
......@@ -64,12 +64,12 @@ class AttachmentCreatorImpl implements AttachmentCreator {
@Inject
AttachmentCreatorImpl(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager, AttachmentRetriever retriever,
ImageSizeCalculator imageSizeCalculator) {
ImageCompressor imageCompressor) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.retriever = retriever;
this.imageSizeCalculator = imageSizeCalculator;
this.imageCompressor = imageCompressor;
}
@Override
......@@ -89,7 +89,7 @@ class AttachmentCreatorImpl implements AttachmentCreator {
if (id == null) throw new IllegalStateException();
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(messagingManager,
app.getContentResolver(), this, imageSizeCalculator, id,
app.getContentResolver(), this, imageCompressor, id,
uris, needsSize);
ioExecutor.execute(() -> task.storeAttachments());
});
......
......@@ -22,6 +22,12 @@ public class AttachmentModule {
return new ImageSizeCalculator(imageHelper);
}
@Provides
ImageCompressor provideImageCompressor(
ImageCompressorImpl imageCompressor) {
return imageCompressor;
}
@Provides
AttachmentDimensions provideAttachmentDimensions(Application app) {
return getAttachmentDimensions(app.getResources());
......
package org.briarproject.briar.android.attachment;
import android.graphics.Bitmap;
import android.net.Uri;
import java.io.IOException;
import java.io.InputStream;
public interface ImageCompressor {
/**
* Load an image from {@code is}, compress it and return an InputStream
* from which the resulting image can be read. The image will be compressed
* such that it fits into a message.
*
* @param is the stream to read the source image from
* @param contentType the mimetype of the source image such as "image/jpeg"
* as obtained by {@link android.content.ContentResolver#getType(Uri)}
* @return a stream from which the resulting image can be read
*/
InputStream compressImage(InputStream is, String contentType)
throws IOException;
/**
* Compress an image and return an InputStream from which the resulting
* image can be read. The image will be compressed such that it fits into
* a message.
*
* @param bitmap the source image
* @return a stream from which the resulting image can be read
*/
InputStream compressImage(Bitmap bitmap) throws IOException;
}
package org.briarproject.briar.android.attachment;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
import javax.inject.Inject;
import static android.graphics.Bitmap.CompressFormat.JPEG;
import static android.graphics.BitmapFactory.decodeStream;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.IoUtils.tryToClose;
import static org.briarproject.briar.api.media.MediaConstants.MAX_IMAGE_SIZE;
class ImageCompressorImpl implements ImageCompressor {
private static final Logger LOG =
getLogger(ImageCompressorImpl.class.getName());
private static final int MAX_ATTACHMENT_DIMENSION = 1000;
private final ImageSizeCalculator imageSizeCalculator;
@Inject
ImageCompressorImpl(ImageSizeCalculator imageSizeCalculator) {
this.imageSizeCalculator = imageSizeCalculator;
}
@Override
public InputStream compressImage(InputStream is, String contentType)
throws IOException {
try {
Bitmap bitmap =
createBitmap(is, contentType, MAX_ATTACHMENT_DIMENSION);
return compressImage(bitmap);
} finally {
tryToClose(is, LOG, WARNING);
}
}
@Override
public InputStream compressImage(Bitmap bitmap) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (int quality = 100; quality >= 0; quality -= 10) {
if (!bitmap.compress(JPEG, quality, out))
throw new IOException();
if (out.size() <= MAX_IMAGE_SIZE) {
if (LOG.isLoggable(INFO)) {
LOG.info("Compressed image to "
+ out.size() + " bytes, quality " + quality);
}
return new ByteArrayInputStream(out.toByteArray());
}
out.reset();
}
throw new IOException();
}
private Bitmap createBitmap(InputStream is, String contentType, int maxSize)
throws IOException {
is = new BufferedInputStream(is);
Size size = imageSizeCalculator.getSize(is, contentType);
if (size.error) throw new IOException();
if (LOG.isLoggable(INFO))
LOG.info("Original image size: " + size.width + "x" + size.height);
int dimension = Math.max(size.width, size.height);
int inSampleSize = 1;
while (dimension > maxSize) {
inSampleSize *= 2;
dimension /= 2;
}
if (LOG.isLoggable(INFO))
LOG.info("Scaling attachment by factor of " + inSampleSize);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = inSampleSize;
if (contentType.equals("image/png"))
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = decodeStream(is, null, options);
if (bitmap == null) throw new IOException();
return bitmap;
}
}
package org.briarproject.briar.android.test;
import android.util.Log;
import org.briarproject.briar.api.test.TestAvatarCreator;
import java.io.InputStream;
import javax.annotation.Nullable;
import javax.inject.Inject;
public class TestAvatarCreatorImpl implements TestAvatarCreator {
@Inject
TestAvatarCreatorImpl() {
}
@Nullable
@Override
public InputStream getAvatarInputStream() {
......
package org.briarproject.briar.api.test;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.Nullable;
public interface TestAvatarCreator {
@Nullable
InputStream getAvatarInputStream();
InputStream getAvatarInputStream() throws IOException;
}
......@@ -302,7 +302,13 @@ public class TestDataCreatorImpl implements TestDataCreator {
AuthorId authorId = c.getAuthor().getId();
GroupId groupId = groupFactory.createGroup(AvatarManager.CLIENT_ID,
AvatarManager.MAJOR_VERSION, authorId.getBytes()).getId();
InputStream is = testAvatarCreator.getAvatarInputStream();
InputStream is;
try {
is = testAvatarCreator.getAvatarInputStream();
} catch (IOException e) {
logException(LOG, WARNING, e);
return;
}
if (is == null) return;
Message m;
try {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment