/* Copyright 2017 Andrew Dawson * * This file is a part of Tusky. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ package com.keylesspalace.tusky.components.common; import android.content.ContentResolver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; import com.keylesspalace.tusky.util.IOUtils; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import static com.keylesspalace.tusky.util.MediaUtilsKt.calculateInSampleSize; import static com.keylesspalace.tusky.util.MediaUtilsKt.getImageOrientation; import static com.keylesspalace.tusky.util.MediaUtilsKt.reorientBitmap; /** * Reduces the file size of images to fit under a given limit by resizing them, maintaining both * aspect ratio and orientation. */ public class DownsizeImageTask extends AsyncTask { private int sizeLimit; private ContentResolver contentResolver; private Listener listener; private File tempFile; /** * @param sizeLimit the maximum number of bytes each image can take * @param contentResolver to resolve the specified images' URIs * @param tempFile the file where the result will be stored * @param listener to whom the results are given */ public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) { this.sizeLimit = sizeLimit; this.contentResolver = contentResolver; this.tempFile = tempFile; this.listener = listener; } @Override protected Boolean doInBackground(Uri... uris) { boolean result = DownsizeImageTask.resize(uris, sizeLimit, contentResolver, tempFile); if (isCancelled()) { return false; } return result; } @Override protected void onPostExecute(Boolean successful) { if (successful) { listener.onSuccess(tempFile); } else { listener.onFailure(); } super.onPostExecute(successful); } public static boolean resize(Uri[] uris, long sizeLimit, ContentResolver contentResolver, File tempFile) { for (Uri uri : uris) { InputStream inputStream; try { inputStream = contentResolver.openInputStream(uri); } catch (FileNotFoundException e) { return false; } // Initially, just get the image dimensions. BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(inputStream, null, options); IOUtils.closeQuietly(inputStream); // Get EXIF data, for orientation info. int orientation = getImageOrientation(uri, contentResolver); /* Unfortunately, there isn't a determined worst case compression ratio for image * formats. So, the only way to tell if they're too big is to compress them and * test, and keep trying at smaller sizes. The initial estimate should be good for * many cases, so it should only iterate once, but the loop is used to be absolutely * sure it gets downsized to below the limit. */ int scaledImageSize = 1024; do { OutputStream stream; try { stream = new FileOutputStream(tempFile); } catch (FileNotFoundException e) { return false; } try { inputStream = contentResolver.openInputStream(uri); } catch (FileNotFoundException e) { return false; } options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize); options.inJustDecodeBounds = false; Bitmap scaledBitmap; try { scaledBitmap = BitmapFactory.decodeStream(inputStream, null, options); } catch (OutOfMemoryError error) { return false; } finally { IOUtils.closeQuietly(inputStream); } if (scaledBitmap == null) { return false; } Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation); if (reorientedBitmap == null) { scaledBitmap.recycle(); return false; } Bitmap.CompressFormat format; /* It's not likely the user will give transparent images over the upload limit, but * if they do, make sure the transparency is retained. */ if (!reorientedBitmap.hasAlpha()) { format = Bitmap.CompressFormat.JPEG; } else { format = Bitmap.CompressFormat.PNG; } reorientedBitmap.compress(format, 85, stream); reorientedBitmap.recycle(); scaledImageSize /= 2; } while (tempFile.length() > sizeLimit); } return true; } /** * Used to communicate the results of the task. */ public interface Listener { void onSuccess(File file); void onFailure(); } }