From 63c00a12a85e0ff8d9017666d72280edb236557b Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sat, 31 Jul 2021 17:59:34 +0200 Subject: [PATCH] Recode images to save cache space --- .../core/glide/ApOkHttpUrlLoader.java | 3 +- .../glide/ResizingOkHttpStreamFetcher.java | 133 ++++++++++++++++++ 2 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java index 8c80e9151..a0ec16578 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java @@ -4,7 +4,6 @@ import android.content.ContentResolver; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; @@ -77,7 +76,7 @@ class ApOkHttpUrlLoader implements ModelLoader { @Nullable @Override public LoadData buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) { - return new LoadData<>(new ObjectKey(model), new OkHttpStreamFetcher(client, new GlideUrl(model))); + return new LoadData<>(new ObjectKey(model), new ResizingOkHttpStreamFetcher(client, new GlideUrl(model))); } @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java new file mode 100644 index 000000000..26f50e42f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java @@ -0,0 +1,133 @@ +package de.danoeh.antennapod.core.glide; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.bumptech.glide.Priority; +import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher; +import com.bumptech.glide.load.model.GlideUrl; +import com.google.android.exoplayer2.util.Log; +import okhttp3.Call; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ResizingOkHttpStreamFetcher extends OkHttpStreamFetcher { + private static final String TAG = "ResizingOkHttpStreamFetcher"; + private static final int MAX_DIMENSIONS = 2000; + private static final int MAX_FILE_SIZE = 1024 * 1024; // 1 MB + + private FileInputStream stream; + private File tempIn; + private File tempOut; + + public ResizingOkHttpStreamFetcher(Call.Factory client, GlideUrl url) { + super(client, url); + } + + @Override + public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { + super.loadData(priority, new DataCallback() { + @Override + public void onDataReady(@Nullable InputStream data) { + if (data == null) { + callback.onDataReady(null); + return; + } + try { + tempIn = File.createTempFile("resize_", null); + tempOut = File.createTempFile("resize_", null); + OutputStream outputStream = new FileOutputStream(tempIn); + IOUtils.copy(data, outputStream); + outputStream.close(); + IOUtils.closeQuietly(data); + + if (tempIn.length() <= MAX_FILE_SIZE) { + try { + stream = new FileInputStream(tempIn); + callback.onDataReady(stream); // Just deliver the original, non-scaled image + } catch (FileNotFoundException fileNotFoundException) { + callback.onLoadFailed(fileNotFoundException); + } + return; + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + FileInputStream in = new FileInputStream(tempIn); + BitmapFactory.decodeStream(in, null, options); + IOUtils.closeQuietly(in); + + if (Math.max(options.outHeight, options.outWidth) >= MAX_DIMENSIONS) { + double sampleSize = (double) Math.max(options.outHeight, options.outWidth) / MAX_DIMENSIONS; + options.inSampleSize = (int) Math.pow(2d, Math.floor(Math.log(sampleSize) / Math.log(2d))); + } + + options.inJustDecodeBounds = false; + in = new FileInputStream(tempIn); + Bitmap bitmap = BitmapFactory.decodeStream(in, null, options); + IOUtils.closeQuietly(in); + + Bitmap.CompressFormat format = Build.VERSION.SDK_INT < 30 + ? Bitmap.CompressFormat.WEBP : Bitmap.CompressFormat.WEBP_LOSSY; + + int quality = 100; + while (true) { + FileOutputStream out = new FileOutputStream(tempOut); + bitmap.compress(format, quality, out); + IOUtils.closeQuietly(out); + + if (tempOut.length() > 3 * MAX_FILE_SIZE && quality >= 45) { + quality -= 40; + } else if (tempOut.length() > 2 * MAX_FILE_SIZE && quality >= 25) { + quality -= 20; + } else if (tempOut.length() > MAX_FILE_SIZE && quality >= 15) { + quality -= 10; + } else if (tempOut.length() > MAX_FILE_SIZE && quality >= 10) { + quality -= 5; + } else { + break; + } + } + + stream = new FileInputStream(tempOut); + callback.onDataReady(stream); + Log.d(TAG, "Compressed image from " + tempIn.length() / 1024 + + " to " + tempOut.length() / 1024 + " kB (quality: " + quality + "%)"); + } catch (IOException e) { + e.printStackTrace(); + + try { + stream = new FileInputStream(tempIn); + callback.onDataReady(stream); // Just deliver the original, non-scaled image + } catch (FileNotFoundException fileNotFoundException) { + e.printStackTrace(); + callback.onLoadFailed(fileNotFoundException); + } + } + } + + @Override + public void onLoadFailed(@NonNull Exception e) { + callback.onLoadFailed(e); + } + }); + } + + @Override + public void cleanup() { + IOUtils.closeQuietly(stream); + FileUtils.deleteQuietly(tempIn); + FileUtils.deleteQuietly(tempOut); + super.cleanup(); + } +}