diff --git a/build.gradle b/build.gradle index 8ba06304a6..ca7fa49e9b 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,8 @@ buildscript { ] ext.deps = [ - androidPlugin: 'com.android.tools.build:gradle:3.2.1', + androidPlugin: 'com.android.tools.build:gradle:3.3.0', + androidSvg: 'com.caverock:androidsvg-aar:1.3', okhttp: "com.squareup.okhttp3:okhttp:${versions.okhttp}", okio: "com.squareup.okio:okio:${versions.okio}", mockWebServer: "com.squareup.okhttp3:mockwebserver:${versions.okhttp}", @@ -99,7 +100,7 @@ subprojects { version = VERSION_NAME afterEvaluate { - tasks.findByName('check').dependsOn('checkstyle') + tasks.getByName('check').dependsOn('checkstyle') } apply plugin: 'net.ltgt.errorprone' diff --git a/decoders/svg/README.md b/decoders/svg/README.md new file mode 100644 index 0000000000..077b372dd4 --- /dev/null +++ b/decoders/svg/README.md @@ -0,0 +1,15 @@ +Picasso SVG Image Decoder +==================================== + +An image decoder that allows Picasso to decode SVG images. + +Usage +----- + +Provide an instance of `SvgImageDecoder` when creating a `Picasso` instance. + +```java +Picasso p = new Picasso.Builder(context) + .addImageDecoder(new SvgImageDecoder()) + .build(); +``` diff --git a/decoders/svg/build.gradle b/decoders/svg/build.gradle new file mode 100644 index 0000000000..6c0c84b6ee --- /dev/null +++ b/decoders/svg/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } + + compileOptions { + sourceCompatibility versions.sourceCompatibility + targetCompatibility versions.targetCompatibility + } + + lintOptions { + textOutput 'stdout' + textReport true + lintConfig file('lint.xml') + } +} + +dependencies { + api project(':picasso') + implementation deps.androidSvg + compileOnly deps.androidxAnnotations + testImplementation deps.junit + testImplementation deps.robolectric + testImplementation deps.truth + testImplementation deps.mockito + + annotationProcessor deps.nullaway +} + +apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/decoders/svg/gradle.properties b/decoders/svg/gradle.properties new file mode 100644 index 0000000000..1111190602 --- /dev/null +++ b/decoders/svg/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=picasso-decoder-svg +POM_NAME=Picasso SVG Descoder +POM_DESCRIPTION=An image decoder that supports SVG images. +POM_PACKAGING=aar diff --git a/decoders/svg/src/main/AndroidManifest.xml b/decoders/svg/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c5575853a8 --- /dev/null +++ b/decoders/svg/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/decoders/svg/src/main/java/com/squareup/picasso3/decoder/svg/SvgImageDecoder.java b/decoders/svg/src/main/java/com/squareup/picasso3/decoder/svg/SvgImageDecoder.java new file mode 100644 index 0000000000..8eb8610dfd --- /dev/null +++ b/decoders/svg/src/main/java/com/squareup/picasso3/decoder/svg/SvgImageDecoder.java @@ -0,0 +1,52 @@ +package com.squareup.picasso3.decoder.svg; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import com.caverock.androidsvg.SVG; +import com.caverock.androidsvg.SVGParseException; +import com.squareup.picasso3.ImageDecoder; +import com.squareup.picasso3.Request; +import java.io.IOException; +import okio.BufferedSource; + +class SvgImageDecoder implements ImageDecoder { + + @Override public boolean canHandleSource(BufferedSource source) { + try { + SVG.getFromInputStream(source.inputStream()); + return true; + } catch (SVGParseException e) { + return false; + } + } + + @Override public Image decodeImage(BufferedSource source, Request request) throws IOException { + try { + SVG svg = SVG.getFromInputStream(source.inputStream()); + if (request.hasSize()) { + if (request.targetWidth != 0) { + svg.setDocumentWidth(request.targetWidth); + } + if (request.targetHeight != 0) { + svg.setDocumentHeight(request.targetHeight); + } + } + + int width = (int) svg.getDocumentWidth(); + if (width == -1) { + width = (int) svg.getDocumentViewBox().width(); + } + int height = (int) svg.getDocumentHeight(); + if (height == -1) { + height = (int) svg.getDocumentViewBox().height(); + } + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + svg.renderToCanvas(canvas); + + return new Image(bitmap); + } catch (SVGParseException e) { + throw new IOException(e); + } + } +} diff --git a/decoders/svg/src/test/java/com/squareup/picasso3/decoder/svg/SvgImageDecoderTest.java b/decoders/svg/src/test/java/com/squareup/picasso3/decoder/svg/SvgImageDecoderTest.java new file mode 100644 index 0000000000..a2b2870b1d --- /dev/null +++ b/decoders/svg/src/test/java/com/squareup/picasso3/decoder/svg/SvgImageDecoderTest.java @@ -0,0 +1,67 @@ +package com.squareup.picasso3.decoder.svg; + +import android.net.Uri; +import com.squareup.picasso3.Request; +import java.io.IOException; +import java.io.InputStream; +import okio.BufferedSource; +import okio.Okio; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static com.google.common.truth.Truth.assertThat; +import static com.squareup.picasso3.ImageDecoder.Image; +import static org.mockito.Mockito.mock; + +@RunWith(RobolectricTestRunner.class) +public class SvgImageDecoderTest { + + private SvgImageDecoder decoder; + + @Before public void setup() { + decoder = new SvgImageDecoder(); + } + + @Test public void canHandleSource_forSvg_returnsTrue() { + BufferedSource svg = bufferResource("/android.svg"); + assertThat(decoder.canHandleSource(svg)).isTrue(); + } + + @Test public void canHandleSource_forBitmap_returnsFalse() { + BufferedSource jpg = bufferResource("/image.jpg"); + assertThat(decoder.canHandleSource(jpg)).isFalse(); + } + + @Test public void decodeImage_withoutTargetSize_returnsNativelySizedImage() throws IOException { + BufferedSource svg = bufferResource("/android.svg"); + Request request = new Request.Builder(mock(Uri.class)).build(); + Image image = decoder.decodeImage(svg, request); + + assertThat(image.bitmap).isNotNull(); + assertThat(image.bitmap.getWidth()).isEqualTo(96); + assertThat(image.bitmap.getHeight()).isEqualTo(105); + } + + @Test public void decodeImage_withTargetSize_returnsResizedImage() throws IOException { + BufferedSource svg = bufferResource("/android.svg"); + Request request = new Request.Builder(mock(Uri.class)) + .resize(50, 50) + .build(); + Image image = decoder.decodeImage(svg, request); + + assertThat(image.bitmap).isNotNull(); + assertThat(image.bitmap.getWidth()).isEqualTo(50); + assertThat(image.bitmap.getHeight()).isEqualTo(50); + } + + private BufferedSource bufferResource(String name) { + InputStream in = SvgImageDecoderTest.class.getResourceAsStream(name); + if (in == null) { + throw new IllegalArgumentException("Unknown resource for name: " + name); + } + return Okio.buffer(Okio.source(in)); + } + +} \ No newline at end of file diff --git a/decoders/svg/src/test/resources/android.svg b/decoders/svg/src/test/resources/android.svg new file mode 100644 index 0000000000..1db6886aaf --- /dev/null +++ b/decoders/svg/src/test/resources/android.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/decoders/svg/src/test/resources/image.jpg b/decoders/svg/src/test/resources/image.jpg new file mode 100644 index 0000000000..a3de9ea0b9 Binary files /dev/null and b/decoders/svg/src/test/resources/image.jpg differ diff --git a/decoders/svg/src/test/resources/robolectric.properties b/decoders/svg/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..84ab549693 --- /dev/null +++ b/decoders/svg/src/test/resources/robolectric.properties @@ -0,0 +1,3 @@ +sdk: 18 +constants: com.squareup.picasso3.BuildConfig +manifest: --default diff --git a/picasso-sample/src/main/java/com/example/picasso/Data.java b/picasso-sample/src/main/java/com/example/picasso/Data.java index 7eab87b3fc..90f6398404 100644 --- a/picasso-sample/src/main/java/com/example/picasso/Data.java +++ b/picasso-sample/src/main/java/com/example/picasso/Data.java @@ -15,7 +15,7 @@ final class Data { BASE + "Q54zMKT" + EXT, BASE + "9t6hLbm" + EXT, BASE + "F8n3Ic6" + EXT, BASE + "P5ZRSvT" + EXT, BASE + "jbemFzr" + EXT, BASE + "8B7haIK" + EXT, BASE + "aSeTYQr" + EXT, BASE + "OKvWoTh" + EXT, BASE + "zD3gT4Z" + EXT, - BASE + "z77CaIt" + EXT, + BASE + "z77CaIt" + EXT, "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/android.svg" }; private Data() { diff --git a/picasso/src/main/java/com/squareup/picasso3/AssetRequestHandler.java b/picasso/src/main/java/com/squareup/picasso3/AssetRequestHandler.java index 714a20572f..f18751c7f1 100644 --- a/picasso/src/main/java/com/squareup/picasso3/AssetRequestHandler.java +++ b/picasso/src/main/java/com/squareup/picasso3/AssetRequestHandler.java @@ -17,15 +17,13 @@ import android.content.Context; import android.content.res.AssetManager; -import android.graphics.Bitmap; import android.net.Uri; import androidx.annotation.NonNull; import java.io.IOException; +import okio.BufferedSource; import okio.Okio; -import okio.Source; import static android.content.ContentResolver.SCHEME_FILE; -import static com.squareup.picasso3.BitmapUtils.decodeStream; import static com.squareup.picasso3.Picasso.LoadedFrom.DISK; import static com.squareup.picasso3.Utils.checkNotNull; @@ -56,11 +54,18 @@ public void load(@NonNull Picasso picasso, @NonNull Request request, @NonNull Ca boolean signaledCallback = false; try { - Source source = Okio.source(assetManager.open(getFilePath(request))); + BufferedSource source = Okio.buffer(Okio.source(assetManager.open(getFilePath(request)))); try { - Bitmap bitmap = decodeStream(source, request); + ImageDecoder imageDecoder = request.decoderFactory.getImageDecoderForSource(source); + if (imageDecoder == null) { + callback.onError( + new IllegalStateException("No image decoder for source: " + getFilePath(request)) + ); + return; + } + ImageDecoder.Image image = imageDecoder.decodeImage(source, request); signaledCallback = true; - callback.onSuccess(new Result(bitmap, DISK)); + callback.onSuccess(new Result(image.bitmap, image.drawable, DISK, image.exifOrientation)); } finally { try { source.close(); diff --git a/picasso/src/main/java/com/squareup/picasso3/BitmapImageDecoder.java b/picasso/src/main/java/com/squareup/picasso3/BitmapImageDecoder.java new file mode 100644 index 0000000000..93d3b0aa34 --- /dev/null +++ b/picasso/src/main/java/com/squareup/picasso3/BitmapImageDecoder.java @@ -0,0 +1,33 @@ +package com.squareup.picasso3; + +import android.graphics.BitmapFactory; +import androidx.annotation.NonNull; +import java.io.IOException; +import okio.BufferedSource; + +import static com.squareup.picasso3.BitmapUtils.decodeStream; + +public final class BitmapImageDecoder implements ImageDecoder { + + @Override public boolean canHandleSource(@NonNull BufferedSource source) { + try { + if (Utils.isWebPFile(source)) { + return true; + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(source.inputStream(), null, options); + // we successfully decoded the bounds + return options.outWidth > 0 && options.outHeight > 0; + } catch (IOException e) { + return false; + } + } + + @NonNull @Override + public Image decodeImage(@NonNull BufferedSource source, @NonNull Request request) + throws IOException { + return new Image(decodeStream(source, request)); + } +} diff --git a/picasso/src/main/java/com/squareup/picasso3/BitmapUtils.java b/picasso/src/main/java/com/squareup/picasso3/BitmapUtils.java index a92e0aa83c..1dcc3be5bb 100644 --- a/picasso/src/main/java/com/squareup/picasso3/BitmapUtils.java +++ b/picasso/src/main/java/com/squareup/picasso3/BitmapUtils.java @@ -15,7 +15,6 @@ */ package com.squareup.picasso3; -import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; @@ -106,22 +105,21 @@ static Bitmap decodeStream(Source source, Request request) throws IOException { ExceptionCatchingSource exceptionCatchingSource = new ExceptionCatchingSource(source); BufferedSource bufferedSource = Okio.buffer(exceptionCatchingSource); Bitmap bitmap = SDK_INT >= 28 - ? decodeStreamP(request, bufferedSource) - : decodeStreamPreP(request, bufferedSource); + ? decodeStreamP(bufferedSource, request) + : decodeStreamPreP(bufferedSource, request); exceptionCatchingSource.throwIfCaught(); return bitmap; } @RequiresApi(28) - @SuppressLint("Override") - private static Bitmap decodeStreamP(Request request, BufferedSource bufferedSource) - throws IOException { - ImageDecoder.Source imageSource = - ImageDecoder.createSource(ByteBuffer.wrap(bufferedSource.readByteArray())); + private static Bitmap decodeStreamP(BufferedSource source, Request request) throws IOException { + android.graphics.ImageDecoder.Source imageSource = + android.graphics.ImageDecoder.createSource(ByteBuffer.wrap(source.readByteArray())); return decodeImageSource(imageSource, request); } - private static Bitmap decodeStreamPreP(Request request, BufferedSource bufferedSource) + @NonNull + private static Bitmap decodeStreamPreP(BufferedSource bufferedSource, Request request) throws IOException { boolean isWebPFile = Utils.isWebPFile(bufferedSource); boolean isPurgeable = request.purgeable && SDK_INT < Build.VERSION_CODES.LOLLIPOP; diff --git a/picasso/src/main/java/com/squareup/picasso3/ContactsPhotoRequestHandler.java b/picasso/src/main/java/com/squareup/picasso3/ContactsPhotoRequestHandler.java index dcc4698b2d..e4c71c4170 100644 --- a/picasso/src/main/java/com/squareup/picasso3/ContactsPhotoRequestHandler.java +++ b/picasso/src/main/java/com/squareup/picasso3/ContactsPhotoRequestHandler.java @@ -18,19 +18,18 @@ import android.content.ContentResolver; import android.content.Context; import android.content.UriMatcher; -import android.graphics.Bitmap; import android.net.Uri; import android.provider.ContactsContract; import androidx.annotation.NonNull; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import okio.BufferedSource; import okio.Okio; import okio.Source; import static android.content.ContentResolver.SCHEME_CONTENT; import static android.provider.ContactsContract.Contacts.openContactPhotoInputStream; -import static com.squareup.picasso3.BitmapUtils.decodeStream; import static com.squareup.picasso3.Picasso.LoadedFrom.DISK; import static com.squareup.picasso3.Utils.checkNotNull; @@ -78,9 +77,16 @@ public void load(@NonNull Picasso picasso, @NonNull Request request, @NonNull Ca try { Uri requestUri = checkNotNull(request.uri, "request.uri == null"); Source source = getSource(requestUri); - Bitmap bitmap = decodeStream(source, request); + + BufferedSource bufferedSource = Okio.buffer(source); + ImageDecoder imageDecoder = request.decoderFactory.getImageDecoderForSource(bufferedSource); + if (imageDecoder == null) { + callback.onError(new IllegalStateException("No image decoder for source: " + request)); + return; + } + ImageDecoder.Image image = imageDecoder.decodeImage(bufferedSource, request); signaledCallback = true; - callback.onSuccess(new Result(bitmap, DISK)); + callback.onSuccess(new Result(image.bitmap, image.drawable, DISK, image.exifOrientation)); } catch (Exception e) { if (!signaledCallback) { callback.onError(e); diff --git a/picasso/src/main/java/com/squareup/picasso3/ContentStreamRequestHandler.java b/picasso/src/main/java/com/squareup/picasso3/ContentStreamRequestHandler.java index 4a3057dc0a..76cbfc9392 100644 --- a/picasso/src/main/java/com/squareup/picasso3/ContentStreamRequestHandler.java +++ b/picasso/src/main/java/com/squareup/picasso3/ContentStreamRequestHandler.java @@ -17,20 +17,19 @@ import android.content.ContentResolver; import android.content.Context; -import android.graphics.Bitmap; import android.net.Uri; import androidx.annotation.NonNull; import androidx.exifinterface.media.ExifInterface; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import okio.BufferedSource; import okio.Okio; import okio.Source; import static android.content.ContentResolver.SCHEME_CONTENT; import static androidx.exifinterface.media.ExifInterface.ORIENTATION_NORMAL; import static androidx.exifinterface.media.ExifInterface.TAG_ORIENTATION; -import static com.squareup.picasso3.BitmapUtils.decodeStream; import static com.squareup.picasso3.Picasso.LoadedFrom.DISK; import static com.squareup.picasso3.Utils.checkNotNull; @@ -52,10 +51,17 @@ public void load(@NonNull Picasso picasso, @NonNull Request request, @NonNull Ca try { Uri requestUri = checkNotNull(request.uri, "request.uri == null"); Source source = getSource(requestUri); - Bitmap bitmap = decodeStream(source, request); + + BufferedSource bufferedSource = Okio.buffer(source); + ImageDecoder imageDecoder = request.decoderFactory.getImageDecoderForSource(bufferedSource); + if (imageDecoder == null) { + callback.onError(new IllegalStateException("No image decoder for request: " + request)); + return; + } + ImageDecoder.Image image = imageDecoder.decodeImage(bufferedSource, request); int exifRotation = getExifOrientation(requestUri); signaledCallback = true; - callback.onSuccess(new Result(bitmap, DISK, exifRotation)); + callback.onSuccess(new Result(image.bitmap, image.drawable, DISK, exifRotation)); } catch (Exception e) { if (!signaledCallback) { callback.onError(e); diff --git a/picasso/src/main/java/com/squareup/picasso3/FileRequestHandler.java b/picasso/src/main/java/com/squareup/picasso3/FileRequestHandler.java index 7fa0f92a00..a800acaf80 100644 --- a/picasso/src/main/java/com/squareup/picasso3/FileRequestHandler.java +++ b/picasso/src/main/java/com/squareup/picasso3/FileRequestHandler.java @@ -16,18 +16,17 @@ package com.squareup.picasso3; import android.content.Context; -import android.graphics.Bitmap; import android.net.Uri; import androidx.annotation.NonNull; import androidx.exifinterface.media.ExifInterface; import java.io.FileNotFoundException; import java.io.IOException; -import okio.Source; +import okio.BufferedSource; +import okio.Okio; import static android.content.ContentResolver.SCHEME_FILE; import static androidx.exifinterface.media.ExifInterface.ORIENTATION_NORMAL; import static androidx.exifinterface.media.ExifInterface.TAG_ORIENTATION; -import static com.squareup.picasso3.BitmapUtils.decodeStream; import static com.squareup.picasso3.Picasso.LoadedFrom.DISK; import static com.squareup.picasso3.Utils.checkNotNull; @@ -47,11 +46,16 @@ public void load(@NonNull Picasso picasso, @NonNull Request request, @NonNull Ca boolean signaledCallback = false; try { Uri requestUri = checkNotNull(request.uri, "request.uri == null"); - Source source = getSource(requestUri); - Bitmap bitmap = decodeStream(source, request); + BufferedSource source = Okio.buffer(getSource(requestUri)); + ImageDecoder imageDecoder = request.decoderFactory.getImageDecoderForSource(source); + if (imageDecoder == null) { + callback.onError(new IllegalStateException("No image decoder for request: " + request)); + return; + } + ImageDecoder.Image image = imageDecoder.decodeImage(source, request); int exifRotation = getExifOrientation(requestUri); signaledCallback = true; - callback.onSuccess(new Result(bitmap, DISK, exifRotation)); + callback.onSuccess(new Result(image.bitmap, image.drawable, DISK, exifRotation)); } catch (Exception e) { if (!signaledCallback) { callback.onError(e); diff --git a/picasso/src/main/java/com/squareup/picasso3/ImageDecoder.java b/picasso/src/main/java/com/squareup/picasso3/ImageDecoder.java new file mode 100644 index 0000000000..558388e6cc --- /dev/null +++ b/picasso/src/main/java/com/squareup/picasso3/ImageDecoder.java @@ -0,0 +1,36 @@ +package com.squareup.picasso3; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.io.IOException; +import okio.BufferedSource; + +public interface ImageDecoder { + + final class Image { + @Nullable public final Bitmap bitmap; + @Nullable public final Drawable drawable; + public final int exifOrientation; + + public Image(@NonNull Bitmap bitmap) { + this(bitmap, null, 0); + } + + public Image(@NonNull Drawable drawable) { + this(null, drawable, 0); + } + + public Image(@Nullable Bitmap bitmap, @Nullable Drawable drawable, int exifOrientation) { + this.bitmap = bitmap; + this.drawable = drawable; + this.exifOrientation = exifOrientation; + } + } + + boolean canHandleSource(@NonNull BufferedSource source); + + @NonNull Image decodeImage(@NonNull BufferedSource source, @NonNull Request request) + throws IOException; +} diff --git a/picasso/src/main/java/com/squareup/picasso3/ImageDecoderFactory.java b/picasso/src/main/java/com/squareup/picasso3/ImageDecoderFactory.java new file mode 100644 index 0000000000..e6666f95b4 --- /dev/null +++ b/picasso/src/main/java/com/squareup/picasso3/ImageDecoderFactory.java @@ -0,0 +1,29 @@ +package com.squareup.picasso3; + +import androidx.annotation.Nullable; +import java.util.List; +import okio.BufferedSource; + +final class ImageDecoderFactory { + + final List decoders; + + ImageDecoderFactory(List decoders) { + this.decoders = decoders; + } + + /** + * Returns the first {@link ImageDecoder} that can handle the supplied source. + * @param source The source of the image data. + * @return The first ImageDecoder that can decode the source, or null. + */ + @Nullable ImageDecoder getImageDecoderForSource(BufferedSource source) { + for (int i = 0, n = decoders.size(); i < n; i++) { + ImageDecoder decoder = decoders.get(i); + if (decoder.canHandleSource(source.peek())) { + return decoder; + } + } + return null; + } +} diff --git a/picasso/src/main/java/com/squareup/picasso3/MediaStoreRequestHandler.java b/picasso/src/main/java/com/squareup/picasso3/MediaStoreRequestHandler.java index e2a5fd2aa5..286e5f5051 100644 --- a/picasso/src/main/java/com/squareup/picasso3/MediaStoreRequestHandler.java +++ b/picasso/src/main/java/com/squareup/picasso3/MediaStoreRequestHandler.java @@ -23,7 +23,8 @@ import android.net.Uri; import android.provider.MediaStore; import androidx.annotation.NonNull; -import okio.Source; +import okio.BufferedSource; +import okio.Okio; import static android.content.ContentResolver.SCHEME_CONTENT; import static android.content.ContentUris.parseId; @@ -34,7 +35,6 @@ import static android.provider.MediaStore.Video; import static com.squareup.picasso3.BitmapUtils.calculateInSampleSize; import static com.squareup.picasso3.BitmapUtils.createBitmapOptions; -import static com.squareup.picasso3.BitmapUtils.decodeStream; import static com.squareup.picasso3.MediaStoreRequestHandler.PicassoKind.FULL; import static com.squareup.picasso3.MediaStoreRequestHandler.PicassoKind.MICRO; import static com.squareup.picasso3.MediaStoreRequestHandler.PicassoKind.MINI; @@ -71,10 +71,15 @@ public void load(@NonNull Picasso picasso, @NonNull Request request, @NonNull Ca if (request.hasSize()) { PicassoKind picassoKind = getPicassoKind(request.targetWidth, request.targetHeight); if (!isVideo && picassoKind == FULL) { - Source source = getSource(requestUri); - Bitmap bitmap = decodeStream(source, request); + BufferedSource source = Okio.buffer(getSource(requestUri)); + ImageDecoder imageDecoder = request.decoderFactory.getImageDecoderForSource(source); + if (imageDecoder == null) { + callback.onError(new IllegalStateException("No image decoder for request: " + request)); + return; + } + ImageDecoder.Image image = imageDecoder.decodeImage(source, request); signaledCallback = true; - callback.onSuccess(new Result(bitmap, DISK, exifOrientation)); + callback.onSuccess(new Result(image.bitmap, image.drawable, DISK, exifOrientation)); return; } @@ -106,10 +111,15 @@ public void load(@NonNull Picasso picasso, @NonNull Request request, @NonNull Ca } } - Source source = getSource(requestUri); - Bitmap bitmap = decodeStream(source, request); + BufferedSource source = Okio.buffer(getSource(requestUri)); + ImageDecoder imageDecoder = request.decoderFactory.getImageDecoderForSource(source); + if (imageDecoder == null) { + callback.onError(new IllegalStateException("No image decoder for request: " + request)); + return; + } + ImageDecoder.Image image = imageDecoder.decodeImage(source, request); signaledCallback = true; - callback.onSuccess(new Result(bitmap, DISK, exifOrientation)); + callback.onSuccess(new Result(image.bitmap, image.drawable, DISK, exifOrientation)); } catch (Exception e) { if (!signaledCallback) { callback.onError(e); diff --git a/picasso/src/main/java/com/squareup/picasso3/NetworkRequestHandler.java b/picasso/src/main/java/com/squareup/picasso3/NetworkRequestHandler.java index d937f6918f..26b04f2fc4 100644 --- a/picasso/src/main/java/com/squareup/picasso3/NetworkRequestHandler.java +++ b/picasso/src/main/java/com/squareup/picasso3/NetworkRequestHandler.java @@ -15,7 +15,6 @@ */ package com.squareup.picasso3; -import android.graphics.Bitmap; import android.net.NetworkInfo; import android.net.Uri; import androidx.annotation.NonNull; @@ -25,8 +24,8 @@ import okhttp3.Call; import okhttp3.Response; import okhttp3.ResponseBody; +import okio.BufferedSource; -import static com.squareup.picasso3.BitmapUtils.decodeStream; import static com.squareup.picasso3.Picasso.LoadedFrom.DISK; import static com.squareup.picasso3.Picasso.LoadedFrom.NETWORK; import static com.squareup.picasso3.Utils.checkNotNull; @@ -78,8 +77,14 @@ final class NetworkRequestHandler extends RequestHandler { stats.dispatchDownloadFinished(body.contentLength()); } try { - Bitmap bitmap = decodeStream(body.source(), request); - callback.onSuccess(new Result(bitmap, loadedFrom)); + BufferedSource source = body.source(); + ImageDecoder imageDecoder = request.decoderFactory.getImageDecoderForSource(source); + if (imageDecoder == null) { + callback.onError(new IllegalStateException("No image decoder for request: " + request)); + return; + } + ImageDecoder.Image image = imageDecoder.decodeImage(source, request); + callback.onSuccess(new Result(image.bitmap, image.drawable, loadedFrom, 0)); } catch (IOException e) { body.close(); callback.onError(e); diff --git a/picasso/src/main/java/com/squareup/picasso3/Picasso.java b/picasso/src/main/java/com/squareup/picasso3/Picasso.java index a916842333..d6bb9e0b04 100644 --- a/picasso/src/main/java/com/squareup/picasso3/Picasso.java +++ b/picasso/src/main/java/com/squareup/picasso3/Picasso.java @@ -136,6 +136,7 @@ public enum Priority { private final @Nullable okhttp3.Cache closeableCache; final PlatformLruCache cache; final Stats stats; + final ImageDecoderFactory imageDecoderFactory; final Map targetToAction; final Map targetToDeferredRequestCreator; @Nullable final Bitmap.Config defaultBitmapConfig; @@ -147,7 +148,8 @@ public enum Priority { Picasso(Context context, Dispatcher dispatcher, Call.Factory callFactory, @Nullable okhttp3.Cache closeableCache, PlatformLruCache cache, @Nullable Listener listener, - List requestTransformers, List extraRequestHandlers, + ImageDecoderFactory imageDecoderFactory, List requestTransformers, + List extraRequestHandlers, Stats stats, @Nullable Bitmap.Config defaultBitmapConfig, boolean indicatorsEnabled, boolean loggingEnabled) { this.context = context; @@ -156,6 +158,7 @@ public enum Priority { this.closeableCache = closeableCache; this.cache = cache; this.listener = listener; + this.imageDecoderFactory = imageDecoderFactory; this.requestTransformers = Collections.unmodifiableList(new ArrayList<>(requestTransformers)); this.defaultBitmapConfig = defaultBitmapConfig; @@ -664,6 +667,7 @@ public static class Builder { @Nullable private ExecutorService service; @Nullable private PlatformLruCache cache; @Nullable private Listener listener; + private final List imageDecoders = new ArrayList<>(); private final List requestTransformers = new ArrayList<>(); private final List requestHandlers = new ArrayList<>(); @Nullable private Bitmap.Config defaultBitmapConfig; @@ -688,6 +692,9 @@ public Builder(@NonNull Context context) { int numRequestHandlers = picasso.requestHandlers.size(); requestHandlers.addAll(picasso.requestHandlers.subList(2, numRequestHandlers - 6)); + int numImageDecoders = picasso.imageDecoderFactory.decoders.size(); + imageDecoders.addAll(picasso.imageDecoderFactory.decoders.subList(0, numImageDecoders - 1)); + defaultBitmapConfig = picasso.defaultBitmapConfig; indicatorsEnabled = picasso.indicatorsEnabled; loggingEnabled = picasso.loggingEnabled; @@ -774,6 +781,14 @@ public Builder listener(@NonNull Listener listener) { return this; } + /** Add an decoder that can decode custom image formats. */ + @NonNull + public Builder addImageDecoder(@NonNull ImageDecoder imageDecoder) { + checkNotNull(imageDecoder, "imageDecoder == null"); + imageDecoders.add(imageDecoder); + return this; + } + /** Add a transformer that observes and potentially modify all incoming requests. */ @NonNull public Builder addRequestTransformer(@NonNull RequestTransformer transformer) { @@ -830,13 +845,17 @@ public Picasso build() { service = new PicassoExecutorService(new PicassoThreadFactory()); } + ArrayList decoders = new ArrayList<>(imageDecoders); + decoders.add(new BitmapImageDecoder()); + ImageDecoderFactory decoderFactory = new ImageDecoderFactory(decoders); + Stats stats = new Stats(cache); Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, cache, stats); return new Picasso(context, dispatcher, callFactory, unsharedCache, cache, listener, - requestTransformers, requestHandlers, stats, defaultBitmapConfig, indicatorsEnabled, - loggingEnabled); + decoderFactory, requestTransformers, requestHandlers, stats, defaultBitmapConfig, + indicatorsEnabled, loggingEnabled); } } diff --git a/picasso/src/main/java/com/squareup/picasso3/Request.java b/picasso/src/main/java/com/squareup/picasso3/Request.java index bd5cb9a103..8f89a977be 100644 --- a/picasso/src/main/java/com/squareup/picasso3/Request.java +++ b/picasso/src/main/java/com/squareup/picasso3/Request.java @@ -66,6 +66,9 @@ public final class Request { */ @Nullable public final String stableKey; + /** The image decoder factory to use to decode the image. */ + @NonNull + public final ImageDecoderFactory decoderFactory; /** List of custom transformations to be applied after the built-in transformations. */ final List transformations; /** Target image width for resizing. */ @@ -114,6 +117,7 @@ public final class Request { this.uri = builder.uri; this.resourceId = builder.resourceId; this.stableKey = builder.stableKey; + this.decoderFactory = checkNotNull(builder.decoderFactory, "decoderFactory == null"); if (builder.transformations == null) { this.transformations = Collections.emptyList(); } else { @@ -282,6 +286,7 @@ public static final class Builder { float rotationPivotY; boolean hasRotationPivot; boolean purgeable; + @Nullable ImageDecoderFactory decoderFactory; @Nullable List transformations; @Nullable Bitmap.Config config; @Nullable Priority priority; @@ -299,9 +304,11 @@ public Builder(@DrawableRes int resourceId) { setResourceId(resourceId); } - Builder(@Nullable Uri uri, int resourceId, @Nullable Bitmap.Config bitmapConfig) { + Builder(@Nullable Uri uri, int resourceId, @Nullable ImageDecoderFactory decoderFactory, + @Nullable Bitmap.Config bitmapConfig) { this.uri = uri; this.resourceId = resourceId; + this.decoderFactory = decoderFactory; this.config = bitmapConfig; } @@ -320,6 +327,7 @@ public Builder(@DrawableRes int resourceId) { hasRotationPivot = request.hasRotationPivot; purgeable = request.purgeable; onlyScaleDown = request.onlyScaleDown; + decoderFactory = request.decoderFactory; if (request.transformations != null) { transformations = new ArrayList<>(request.transformations); } @@ -562,6 +570,18 @@ public Builder priority(@NonNull Priority priority) { return this; } + @NonNull + public Builder asBitmap() { + return imageDecoderFactory(new ImageDecoderFactory( + Collections.singletonList(new BitmapImageDecoder()))); + } + + @NonNull + public Builder imageDecoderFactory(@NonNull ImageDecoderFactory factory) { + decoderFactory = factory; + return this; + } + /** * Add a custom transformation to be applied to the image. *

@@ -657,6 +677,10 @@ public Request build() { if (priority == null) { priority = Priority.NORMAL; } + if (decoderFactory == null) { + decoderFactory = new ImageDecoderFactory( + Collections.singletonList(new BitmapImageDecoder())); + } return new Request(this); } diff --git a/picasso/src/main/java/com/squareup/picasso3/RequestCreator.java b/picasso/src/main/java/com/squareup/picasso3/RequestCreator.java index 9d686a8692..4acf1b3f36 100644 --- a/picasso/src/main/java/com/squareup/picasso3/RequestCreator.java +++ b/picasso/src/main/java/com/squareup/picasso3/RequestCreator.java @@ -32,6 +32,7 @@ import androidx.core.content.ContextCompat; import com.squareup.picasso3.RemoteViewsAction.RemoteViewsTarget; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -74,13 +75,14 @@ public class RequestCreator { "Picasso instance already shut down. Cannot submit new requests."); } this.picasso = picasso; - this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig); + this.data = new Request.Builder(uri, resourceId, picasso.imageDecoderFactory, + picasso.defaultBitmapConfig); } @SuppressWarnings("NullAway") @VisibleForTesting RequestCreator() { this.picasso = null; - this.data = new Request.Builder(null, 0, null); + this.data = new Request.Builder(null, 0, null, null); } /** @@ -334,6 +336,21 @@ public RequestCreator priority(@NonNull Priority priority) { return this; } + /** + * Specify that the image should be decoded as a bitmap. + */ + @NonNull + public RequestCreator asBitmap() { + return imageDecoderFactory(new ImageDecoderFactory( + Collections.singletonList(new BitmapImageDecoder()))); + } + + @NonNull + public RequestCreator imageDecoderFactory(@NonNull ImageDecoderFactory imageDecoderFactory) { + this.data.imageDecoderFactory(imageDecoderFactory); + return this; + } + /** * Add a custom transformation to be applied to the image. *

diff --git a/picasso/src/main/java/com/squareup/picasso3/RequestHandler.java b/picasso/src/main/java/com/squareup/picasso3/RequestHandler.java index f6c94765af..9257757a8f 100644 --- a/picasso/src/main/java/com/squareup/picasso3/RequestHandler.java +++ b/picasso/src/main/java/com/squareup/picasso3/RequestHandler.java @@ -68,7 +68,7 @@ public Result(@NonNull Drawable drawable, @NonNull Picasso.LoadedFrom loadedFrom this(null, checkNotNull(drawable, "drawable == null"), loadedFrom, 0); } - private Result( + Result( @Nullable Bitmap bitmap, @Nullable Drawable drawable, @NonNull Picasso.LoadedFrom loadedFrom, diff --git a/picasso/src/main/java/com/squareup/picasso3/ResourceDrawableRequestHandler.java b/picasso/src/main/java/com/squareup/picasso3/ResourceDrawableRequestHandler.java index cee2beaa91..110400e501 100644 --- a/picasso/src/main/java/com/squareup/picasso3/ResourceDrawableRequestHandler.java +++ b/picasso/src/main/java/com/squareup/picasso3/ResourceDrawableRequestHandler.java @@ -61,4 +61,5 @@ public static ResourceDrawableRequestHandler create(@NonNull final Context conte } }); } + } diff --git a/picasso/src/main/java/com/squareup/picasso3/StatsSnapshot.java b/picasso/src/main/java/com/squareup/picasso3/StatsSnapshot.java index 3cf4ff1015..5bca870e75 100644 --- a/picasso/src/main/java/com/squareup/picasso3/StatsSnapshot.java +++ b/picasso/src/main/java/com/squareup/picasso3/StatsSnapshot.java @@ -17,7 +17,6 @@ import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import java.io.IOException; import okio.Buffer; import okio.BufferedSink; @@ -129,7 +128,7 @@ public void dump(@NonNull BufferedSink sink) throws IOException { sink.writeUtf8("\n"); } - @Nullable + @NonNull @Override public String toString() { return "StatsSnapshot{" + "maxSize=" diff --git a/picasso/src/test/java/com/squareup/picasso3/BitmapHunterTest.java b/picasso/src/test/java/com/squareup/picasso3/BitmapHunterTest.java index 696fe935fd..ad7e47b8f1 100644 --- a/picasso/src/test/java/com/squareup/picasso3/BitmapHunterTest.java +++ b/picasso/src/test/java/com/squareup/picasso3/BitmapHunterTest.java @@ -60,6 +60,7 @@ import static com.squareup.picasso3.TestUtils.CONTENT_KEY_1; import static com.squareup.picasso3.TestUtils.CUSTOM_URI; import static com.squareup.picasso3.TestUtils.CUSTOM_URI_KEY; +import static com.squareup.picasso3.TestUtils.DEFAULT_DECODERS; import static com.squareup.picasso3.TestUtils.FILE_1_URL; import static com.squareup.picasso3.TestUtils.FILE_KEY_1; import static com.squareup.picasso3.TestUtils.MEDIA_STORE_CONTENT_1_URL; @@ -371,8 +372,9 @@ public final class BitmapHunterTest { List handlers = Collections.singletonList(handler); // Must use non-mock constructor because that is where Picasso's list of handlers is created. Picasso picasso = - new Picasso(context, dispatcher, UNUSED_CALL_FACTORY, null, cache, null, NO_TRANSFORMERS, - handlers, stats, ARGB_8888, false, false); + new Picasso(context, dispatcher, UNUSED_CALL_FACTORY, null, cache, null, + DEFAULT_DECODERS, NO_TRANSFORMERS, handlers, stats, ARGB_8888, false, + false); BitmapHunter hunter = forRequest(picasso, dispatcher, cache, stats, action); assertThat(hunter.requestHandler).isEqualTo(handler); } diff --git a/picasso/src/test/java/com/squareup/picasso3/BitmapTargetActionTest.java b/picasso/src/test/java/com/squareup/picasso3/BitmapTargetActionTest.java index 105b5b24a7..ace6a7a61f 100644 --- a/picasso/src/test/java/com/squareup/picasso3/BitmapTargetActionTest.java +++ b/picasso/src/test/java/com/squareup/picasso3/BitmapTargetActionTest.java @@ -25,6 +25,7 @@ import static android.graphics.Bitmap.Config.ARGB_8888; import static com.squareup.picasso3.Picasso.LoadedFrom.MEMORY; +import static com.squareup.picasso3.TestUtils.DEFAULT_DECODERS; import static com.squareup.picasso3.TestUtils.NO_HANDLERS; import static com.squareup.picasso3.TestUtils.NO_TRANSFORMERS; import static com.squareup.picasso3.TestUtils.RESOURCE_ID_1; @@ -77,8 +78,9 @@ public void invokesOnBitmapFailedIfTargetIsNotNullWithErrorResourceId() { Dispatcher dispatcher = mock(Dispatcher.class); PlatformLruCache cache = new PlatformLruCache(0); Picasso picasso = - new Picasso(context, dispatcher, UNUSED_CALL_FACTORY, null, cache, null, NO_TRANSFORMERS, - NO_HANDLERS, mock(Stats.class), ARGB_8888, false, false); + new Picasso(context, dispatcher, UNUSED_CALL_FACTORY, null, cache, + null, DEFAULT_DECODERS, NO_TRANSFORMERS, NO_HANDLERS, mock(Stats.class), + ARGB_8888, false, false); Resources res = mock(Resources.class); BitmapTargetAction request = new BitmapTargetAction(picasso, target, null, null, RESOURCE_ID_1); diff --git a/picasso/src/test/java/com/squareup/picasso3/ImageViewActionTest.java b/picasso/src/test/java/com/squareup/picasso3/ImageViewActionTest.java index cc74d5abe0..f93a7228f8 100644 --- a/picasso/src/test/java/com/squareup/picasso3/ImageViewActionTest.java +++ b/picasso/src/test/java/com/squareup/picasso3/ImageViewActionTest.java @@ -26,6 +26,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.squareup.picasso3.Picasso.LoadedFrom.MEMORY; +import static com.squareup.picasso3.TestUtils.DEFAULT_DECODERS; import static com.squareup.picasso3.TestUtils.NO_HANDLERS; import static com.squareup.picasso3.TestUtils.NO_TRANSFORMERS; import static com.squareup.picasso3.TestUtils.RESOURCE_ID_1; @@ -56,7 +57,7 @@ public void invokesTargetAndCallbackSuccessIfTargetIsNotNull() { PlatformLruCache cache = new PlatformLruCache(0); Picasso picasso = new Picasso(RuntimeEnvironment.application, dispatcher, UNUSED_CALL_FACTORY, null, cache, - null, NO_TRANSFORMERS, NO_HANDLERS, mock(Stats.class), Bitmap.Config.ARGB_8888, false, + null, DEFAULT_DECODERS, NO_TRANSFORMERS, NO_HANDLERS, mock(Stats.class), Bitmap.Config.ARGB_8888, false, false); ImageView target = mockImageViewTarget(); Callback callback = mockCallback(); diff --git a/picasso/src/test/java/com/squareup/picasso3/MediaStoreRequestHandlerTest.java b/picasso/src/test/java/com/squareup/picasso3/MediaStoreRequestHandlerTest.java index 025da393ff..848c371232 100644 --- a/picasso/src/test/java/com/squareup/picasso3/MediaStoreRequestHandlerTest.java +++ b/picasso/src/test/java/com/squareup/picasso3/MediaStoreRequestHandlerTest.java @@ -21,6 +21,7 @@ import static com.squareup.picasso3.MediaStoreRequestHandler.PicassoKind.MICRO; import static com.squareup.picasso3.MediaStoreRequestHandler.PicassoKind.MINI; import static com.squareup.picasso3.MediaStoreRequestHandler.getPicassoKind; +import static com.squareup.picasso3.TestUtils.DEFAULT_DECODERS; import static com.squareup.picasso3.TestUtils.MEDIA_STORE_CONTENT_1_URL; import static com.squareup.picasso3.TestUtils.MEDIA_STORE_CONTENT_KEY_1; import static com.squareup.picasso3.TestUtils.makeBitmap; @@ -45,7 +46,7 @@ public class MediaStoreRequestHandlerTest { @Test public void decodesVideoThumbnailWithVideoMimeType() { final Bitmap bitmap = makeBitmap(); Request request = - new Request.Builder(MEDIA_STORE_CONTENT_1_URL, 0, ARGB_8888) + new Request.Builder(MEDIA_STORE_CONTENT_1_URL, 0, DEFAULT_DECODERS, ARGB_8888) .stableKey(MEDIA_STORE_CONTENT_KEY_1).resize(100, 100).build(); Action action = mockAction(request); MediaStoreRequestHandler requestHandler = create("video/"); @@ -63,7 +64,7 @@ public class MediaStoreRequestHandlerTest { @Test public void decodesImageThumbnailWithImageMimeType() { final Bitmap bitmap = makeBitmap(20, 20); Request request = - new Request.Builder(MEDIA_STORE_CONTENT_1_URL, 0, ARGB_8888) + new Request.Builder(MEDIA_STORE_CONTENT_1_URL, 0, DEFAULT_DECODERS, ARGB_8888) .stableKey(MEDIA_STORE_CONTENT_KEY_1).resize(100, 100).build(); Action action = mockAction(request); MediaStoreRequestHandler requestHandler = create("image/png"); diff --git a/picasso/src/test/java/com/squareup/picasso3/PicassoTest.java b/picasso/src/test/java/com/squareup/picasso3/PicassoTest.java index e5f22b1c0c..0dc825d8c6 100644 --- a/picasso/src/test/java/com/squareup/picasso3/PicassoTest.java +++ b/picasso/src/test/java/com/squareup/picasso3/PicassoTest.java @@ -23,8 +23,10 @@ import androidx.annotation.NonNull; import com.squareup.picasso3.Picasso.RequestTransformer; import java.io.File; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import okio.BufferedSource; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -42,6 +44,7 @@ import static com.squareup.picasso3.Picasso.Listener; import static com.squareup.picasso3.Picasso.LoadedFrom.MEMORY; import static com.squareup.picasso3.RemoteViewsAction.RemoteViewsTarget; +import static com.squareup.picasso3.TestUtils.DEFAULT_DECODERS; import static com.squareup.picasso3.TestUtils.NOOP_REQUEST_HANDLER; import static com.squareup.picasso3.TestUtils.NOOP_TRANSFORMER; import static com.squareup.picasso3.TestUtils.NO_HANDLERS; @@ -88,7 +91,7 @@ public final class PicassoTest { @Before public void setUp() { initMocks(this); picasso = new Picasso(context, dispatcher, UNUSED_CALL_FACTORY, null, cache, listener, - NO_TRANSFORMERS, NO_HANDLERS, stats, ARGB_8888, false, false); + DEFAULT_DECODERS, NO_TRANSFORMERS, NO_HANDLERS, stats, ARGB_8888, false, false); } @Test public void submitWithTargetInvokesDispatcher() { @@ -192,7 +195,7 @@ public final class PicassoTest { } @Test public void resumeActionTriggersSubmitOnPausedAction() { - Request request = new Request.Builder(URI_1, 0, ARGB_8888).build(); + Request request = new Request.Builder(URI_1, 0, DEFAULT_DECODERS, ARGB_8888).build(); Action action = new Action(mockPicasso(), request) { @Override void complete(RequestHandler.Result result) { @@ -213,7 +216,7 @@ public final class PicassoTest { @Test public void resumeActionImmediatelyCompletesCachedRequest() { cache.set(URI_KEY_1, bitmap); - Request request = new Request.Builder(URI_1, 0, ARGB_8888).build(); + Request request = new Request.Builder(URI_1, 0, DEFAULT_DECODERS, ARGB_8888).build(); Action action = new Action(mockPicasso(), request) { @Override void complete(RequestHandler.Result result) { @@ -371,7 +374,7 @@ public final class PicassoTest { okhttp3.Cache cache = new okhttp3.Cache(temporaryFolder.getRoot(), 100); Picasso picasso = new Picasso(context, dispatcher, UNUSED_CALL_FACTORY, cache, this.cache, listener, - NO_TRANSFORMERS, NO_HANDLERS, stats, ARGB_8888, false, false); + DEFAULT_DECODERS, NO_TRANSFORMERS, NO_HANDLERS, stats, ARGB_8888, false, false); picasso.shutdown(); assertThat(cache.isClosed()).isTrue(); } @@ -403,7 +406,8 @@ public final class PicassoTest { } }; Picasso picasso = new Picasso(context, dispatcher, UNUSED_CALL_FACTORY, null, cache, listener, - Collections.singletonList(brokenTransformer), NO_HANDLERS, stats, ARGB_8888, false, false); + DEFAULT_DECODERS, Collections.singletonList(brokenTransformer), NO_HANDLERS, stats, + ARGB_8888, false, false); Request request = new Request.Builder(URI_1).build(); try { picasso.transformRequest(request); @@ -557,6 +561,31 @@ public final class PicassoTest { assertThat(original.requestHandlers).hasSize(NUM_BUILTIN_HANDLERS); } + @Test public void clonedImageDecodersAreRetained() { + Picasso parent = defaultPicasso(RuntimeEnvironment.application, false, false); + + ImageDecoder newDecoder = new ImageDecoder() { + @Override public boolean canHandleSource(@NonNull BufferedSource source) { + return false; + } + + @NonNull @Override + public Image decodeImage(@NonNull BufferedSource source, @NonNull Request request) + throws IOException { + return null; + } + }; + + Picasso child = parent.newBuilder() + .addImageDecoder(newDecoder) + .build(); + + assertThat(child.imageDecoderFactory.decoders).hasSize(3); + ImageDecoder parentCustomDecoder = parent.imageDecoderFactory.decoders.get(0); + assertThat(child.imageDecoderFactory.decoders).contains(parentCustomDecoder); + assertThat(child.imageDecoderFactory.decoders).contains(newDecoder); + } + @Test public void cloneSharesStatefulInstances() { Picasso parent = defaultPicasso(RuntimeEnvironment.application, true, true); @@ -576,6 +605,9 @@ public final class PicassoTest { parent.requestHandlers.get(i).getClass()); } + assertThat(child.imageDecoderFactory.decoders).hasSize( + parent.imageDecoderFactory.decoders.size()); + assertThat(child.defaultBitmapConfig).isEqualTo(parent.defaultBitmapConfig); assertThat(child.indicatorsEnabled).isEqualTo(parent.indicatorsEnabled); assertThat(child.loggingEnabled).isEqualTo(parent.loggingEnabled); diff --git a/picasso/src/test/java/com/squareup/picasso3/RemoteViewsActionTest.java b/picasso/src/test/java/com/squareup/picasso3/RemoteViewsActionTest.java index f96753da52..e311e8a11a 100644 --- a/picasso/src/test/java/com/squareup/picasso3/RemoteViewsActionTest.java +++ b/picasso/src/test/java/com/squareup/picasso3/RemoteViewsActionTest.java @@ -30,6 +30,7 @@ import static android.graphics.Bitmap.Config.ARGB_8888; import static com.google.common.truth.Truth.assertThat; import static com.squareup.picasso3.Picasso.LoadedFrom.NETWORK; +import static com.squareup.picasso3.TestUtils.DEFAULT_DECODERS; import static com.squareup.picasso3.TestUtils.NO_HANDLERS; import static com.squareup.picasso3.TestUtils.NO_TRANSFORMERS; import static com.squareup.picasso3.TestUtils.UNUSED_CALL_FACTORY; @@ -102,8 +103,9 @@ private TestableRemoteViewsAction createAction(int errorResId, Callback callback private Picasso createPicasso() { Dispatcher dispatcher = mock(Dispatcher.class); PlatformLruCache cache = new PlatformLruCache(0); - return new Picasso(RuntimeEnvironment.application, dispatcher, UNUSED_CALL_FACTORY, null, cache, - null, NO_TRANSFORMERS, NO_HANDLERS, mock(Stats.class), ARGB_8888, false, false); + return new Picasso(RuntimeEnvironment.application, dispatcher, UNUSED_CALL_FACTORY, + null, cache, null, DEFAULT_DECODERS, NO_TRANSFORMERS, NO_HANDLERS, + mock(Stats.class), ARGB_8888, false, false); } static class TestableRemoteViewsAction extends RemoteViewsAction { diff --git a/picasso/src/test/java/com/squareup/picasso3/RequestCreatorTest.java b/picasso/src/test/java/com/squareup/picasso3/RequestCreatorTest.java index 636b7f6474..46ddd67ba3 100644 --- a/picasso/src/test/java/com/squareup/picasso3/RequestCreatorTest.java +++ b/picasso/src/test/java/com/squareup/picasso3/RequestCreatorTest.java @@ -42,6 +42,7 @@ import static com.squareup.picasso3.Picasso.Priority.NORMAL; import static com.squareup.picasso3.RemoteViewsAction.AppWidgetAction; import static com.squareup.picasso3.RemoteViewsAction.NotificationAction; +import static com.squareup.picasso3.TestUtils.DEFAULT_DECODERS; import static com.squareup.picasso3.TestUtils.NO_HANDLERS; import static com.squareup.picasso3.TestUtils.NO_TRANSFORMERS; import static com.squareup.picasso3.TestUtils.STABLE_1; @@ -273,8 +274,8 @@ public void intoImageViewWithQuickMemoryCacheCheckDoesNotSubmit() { PlatformLruCache cache = new PlatformLruCache(0); Picasso picasso = spy(new Picasso(RuntimeEnvironment.application, mock(Dispatcher.class), UNUSED_CALL_FACTORY, - null, cache, null, NO_TRANSFORMERS, NO_HANDLERS, mock(Stats.class), ARGB_8888, false, - false)); + null, cache, null, DEFAULT_DECODERS, NO_TRANSFORMERS, NO_HANDLERS, + mock(Stats.class), ARGB_8888, false, false)); doReturn(bitmap).when(picasso).quickMemoryCacheCheck(URI_KEY_1); ImageView target = mockImageViewTarget(); Callback callback = mockCallback(); @@ -290,8 +291,8 @@ public void intoImageViewSetsPlaceholderDrawable() { PlatformLruCache cache = new PlatformLruCache(0); Picasso picasso = spy(new Picasso(RuntimeEnvironment.application, mock(Dispatcher.class), UNUSED_CALL_FACTORY, - null, cache, null, NO_TRANSFORMERS, NO_HANDLERS, mock(Stats.class), ARGB_8888, false, - false)); + null, cache, null, DEFAULT_DECODERS, NO_TRANSFORMERS, NO_HANDLERS, + mock(Stats.class), ARGB_8888, false, false)); ImageView target = mockImageViewTarget(); Drawable placeHolderDrawable = mock(Drawable.class); new RequestCreator(picasso, URI_1, 0).placeholder(placeHolderDrawable).into(target); @@ -305,8 +306,8 @@ public void intoImageViewNoPlaceholderDrawable() { PlatformLruCache cache = new PlatformLruCache(0); Picasso picasso = spy(new Picasso(RuntimeEnvironment.application, mock(Dispatcher.class), UNUSED_CALL_FACTORY, - null, cache, null, NO_TRANSFORMERS, NO_HANDLERS, mock(Stats.class), ARGB_8888, false, - false)); + null, cache, null, DEFAULT_DECODERS, NO_TRANSFORMERS, NO_HANDLERS, + mock(Stats.class), ARGB_8888, false, false)); ImageView target = mockImageViewTarget(); new RequestCreator(picasso, URI_1, 0).noPlaceholder().into(target); verifyNoMoreInteractions(target); @@ -319,8 +320,8 @@ public void intoImageViewSetsPlaceholderWithResourceId() { PlatformLruCache cache = new PlatformLruCache(0); Picasso picasso = spy(new Picasso(RuntimeEnvironment.application, mock(Dispatcher.class), UNUSED_CALL_FACTORY, - null, cache, null, NO_TRANSFORMERS, NO_HANDLERS, mock(Stats.class), ARGB_8888, false, - false)); + null, cache, null, DEFAULT_DECODERS, NO_TRANSFORMERS, NO_HANDLERS, + mock(Stats.class), ARGB_8888, false, false)); ImageView target = mockImageViewTarget(); new RequestCreator(picasso, URI_1, 0).placeholder(android.R.drawable.picture_frame) .into(target); diff --git a/picasso/src/test/java/com/squareup/picasso3/TestUtils.java b/picasso/src/test/java/com/squareup/picasso3/TestUtils.java index 00d5faf341..ecd138843c 100644 --- a/picasso/src/test/java/com/squareup/picasso3/TestUtils.java +++ b/picasso/src/test/java/com/squareup/picasso3/TestUtils.java @@ -34,10 +34,12 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.Collections; import java.util.List; import okhttp3.Call; import okhttp3.Response; +import okio.BufferedSource; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -166,7 +168,7 @@ static Action mockAction(String key, Uri uri, Object target, int resourceId) { static Action mockAction(String key, Uri uri, Object target, int resourceId, Priority priority, String tag) { - Request.Builder builder = new Request.Builder(uri, resourceId, DEFAULT_CONFIG).stableKey(key); + Request.Builder builder = new Request.Builder(uri, resourceId, DEFAULT_DECODERS, DEFAULT_CONFIG).stableKey(key); if (priority != null) { builder.priority(priority); } @@ -337,6 +339,20 @@ static DrawableLoader makeLoaderWithDrawable(final Drawable drawable) { } }; + static final ImageDecoder NOOP_IMAGE_DECODER = new ImageDecoder() { + @Override public boolean canHandleSource(@NonNull BufferedSource source) { + return false; + } + + @NonNull @Override + public Image decodeImage(@NonNull BufferedSource source, @NonNull Request request) + throws IOException { + return null; + } + }; + + static final ImageDecoderFactory DEFAULT_DECODERS = new ImageDecoderFactory( + Collections.singletonList(new BitmapImageDecoder())); static final List NO_TRANSFORMERS = Collections.emptyList(); static final List NO_HANDLERS = Collections.emptyList(); @@ -353,6 +369,7 @@ static Picasso defaultPicasso(Context context, boolean hasRequestHandlers, return builder .callFactory(UNUSED_CALL_FACTORY) .defaultBitmapConfig(DEFAULT_CONFIG) + .addImageDecoder(NOOP_IMAGE_DECODER) .executor(new PicassoExecutorService(new PicassoThreadFactory())) .indicatorsEnabled(true) .listener(NOOP_LISTENER) diff --git a/settings.gradle b/settings.gradle index 94c282c758..875720cb7b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,3 +4,4 @@ include 'picasso' include 'picasso-pollexor' include 'picasso-provider' include 'picasso-sample' +include 'decoders:svg'