From f9f1e1981900743f1e5ff7274a36385f7981cef2 Mon Sep 17 00:00:00 2001 From: Christopher Drury Date: Mon, 28 Nov 2022 15:01:59 -0500 Subject: [PATCH] Add gif support to network requests --- .../src/main/java/com/example/picasso/Data.kt | 3 +- .../java/com/squareup/picasso3/BitmapUtils.kt | 43 +++++++++++ .../com/squareup/picasso3/MovieDrawable.kt | 71 +++++++++++++++++++ .../picasso3/NetworkRequestHandler.kt | 13 +++- .../main/java/com/squareup/picasso3/Utils.kt | 7 ++ .../java/com/squareup/picasso3/UtilsTest.kt | 6 ++ 6 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 picasso/src/main/java/com/squareup/picasso3/MovieDrawable.kt diff --git a/picasso-sample/src/main/java/com/example/picasso/Data.kt b/picasso-sample/src/main/java/com/example/picasso/Data.kt index f465dc650b..5e219edb49 100644 --- a/picasso-sample/src/main/java/com/example/picasso/Data.kt +++ b/picasso-sample/src/main/java/com/example/picasso/Data.kt @@ -18,6 +18,7 @@ package com.example.picasso internal object Data { private const val BASE = "https://i.imgur.com/" private const val EXT = ".jpg" + private const val GIF_EXT = ".gif" @JvmField val URLS = arrayOf( @@ -32,6 +33,6 @@ internal object 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, BASE + "B3wgCmW" + GIF_EXT ) } diff --git a/picasso/src/main/java/com/squareup/picasso3/BitmapUtils.kt b/picasso/src/main/java/com/squareup/picasso3/BitmapUtils.kt index 2814b155f3..742e8a1580 100644 --- a/picasso/src/main/java/com/squareup/picasso3/BitmapUtils.kt +++ b/picasso/src/main/java/com/squareup/picasso3/BitmapUtils.kt @@ -21,10 +21,14 @@ import android.content.res.Resources import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.ImageDecoder +import android.graphics.Movie +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import android.os.Build.VERSION import android.util.TypedValue import androidx.annotation.DrawableRes import androidx.annotation.RequiresApi +import com.squareup.picasso3.Utils.isGifFile import okio.Buffer import okio.BufferedSource import okio.ForwardingSource @@ -186,6 +190,45 @@ internal object BitmapUtils { } } + /** + * Decode a byte stream into a Drawable. This method will take into account additional information + * about the supplied request in order to do the decoding efficiently. + */ + fun decodeDrawableStream(source: Source, request: Request): Drawable { + val exceptionCatchingSource = ExceptionCatchingSource(source) + val bufferedSource = exceptionCatchingSource.buffer() + val drawable = if (VERSION.SDK_INT >= 28) { + val imageSource = ImageDecoder.createSource(ByteBuffer.wrap(bufferedSource.readByteArray())) + decodeDrawableSourceP(imageSource, request) + } else { + if (isGifFile(bufferedSource)) { + val movie = Movie.decodeStream(bufferedSource.inputStream()) + MovieDrawable(movie) + } else { + BitmapDrawable(decodeStreamPreP(request, bufferedSource)) + } + } + exceptionCatchingSource.throwIfCaught() + return drawable + } + + @RequiresApi(28) + fun decodeDrawableSourceP(imageSource: ImageDecoder.Source, request: Request): Drawable { + return ImageDecoder.decodeDrawable(imageSource) { imageDecoder, imageInfo, source -> + if (request.hasSize()) { + val size = imageInfo.size + val width = size.width + val height = size.height + val targetWidth = request.targetWidth + val targetHeight = request.targetHeight + if (shouldResize(request.onlyScaleDown, width, height, targetWidth, targetHeight)) { + val ratio = ratio(targetWidth, targetHeight, width, height, request) + imageDecoder.setTargetSize(width / ratio, height / ratio) + } + } + } + } + private fun ratio( requestWidth: Int, requestHeight: Int, diff --git a/picasso/src/main/java/com/squareup/picasso3/MovieDrawable.kt b/picasso/src/main/java/com/squareup/picasso3/MovieDrawable.kt new file mode 100644 index 0000000000..ae83adf7e9 --- /dev/null +++ b/picasso/src/main/java/com/squareup/picasso3/MovieDrawable.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.picasso3 + +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Movie +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.drawable.Animatable +import android.graphics.drawable.Drawable +import android.os.SystemClock + +internal class MovieDrawable(private val movie: Movie) : Drawable(), Animatable { + + private var paint = Paint(Paint.ANTI_ALIAS_FLAG) + private var start = 0 + + override fun draw(canvas: Canvas) { + if (start > 0) { + movie.setTime(((SystemClock.uptimeMillis() - start).toInt()) % movie.duration()) + invalidateSelf() + } + + movie.draw(canvas, 0f, 0f, paint) + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.colorFilter = colorFilter + } + + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun getIntrinsicWidth(): Int { + return movie.width() + } + + override fun getIntrinsicHeight(): Int { + return movie.height() + } + + override fun start() { + start = SystemClock.uptimeMillis().toInt() + invalidateSelf() + } + + override fun stop() { + start = 0 + } + + override fun isRunning() = start != 0 +} diff --git a/picasso/src/main/java/com/squareup/picasso3/NetworkRequestHandler.kt b/picasso/src/main/java/com/squareup/picasso3/NetworkRequestHandler.kt index b321b96dba..6c3e50cdbc 100644 --- a/picasso/src/main/java/com/squareup/picasso3/NetworkRequestHandler.kt +++ b/picasso/src/main/java/com/squareup/picasso3/NetworkRequestHandler.kt @@ -16,12 +16,14 @@ package com.squareup.picasso3 import android.net.NetworkInfo +import com.squareup.picasso3.BitmapUtils.decodeDrawableStream import com.squareup.picasso3.BitmapUtils.decodeStream import com.squareup.picasso3.NetworkPolicy.Companion.isOfflineOnly import com.squareup.picasso3.NetworkPolicy.Companion.shouldReadFromDiskCache import com.squareup.picasso3.NetworkPolicy.Companion.shouldWriteToDiskCache import com.squareup.picasso3.Picasso.LoadedFrom.DISK import com.squareup.picasso3.Picasso.LoadedFrom.NETWORK +import com.squareup.picasso3.Utils.isGifFile import okhttp3.CacheControl import okhttp3.Call import okhttp3.Response @@ -67,8 +69,15 @@ internal class NetworkRequestHandler( picasso.downloadFinished(body.contentLength()) } try { - val bitmap = decodeStream(body!!.source(), request) - callback.onSuccess(Result.Bitmap(bitmap, loadedFrom)) + val bufferedSource = body!!.source() + val result = if (isGifFile(bufferedSource)) { + val drawable = decodeDrawableStream(bufferedSource, request) + Result.Drawable(drawable, loadedFrom) + } else { + val bitmap = decodeStream(bufferedSource, request) + Result.Bitmap(bitmap, loadedFrom) + } + callback.onSuccess(result) } catch (e: IOException) { body!!.close() callback.onError(e) diff --git a/picasso/src/main/java/com/squareup/picasso3/Utils.kt b/picasso/src/main/java/com/squareup/picasso3/Utils.kt index 13ee0f57ef..026ffde5ac 100644 --- a/picasso/src/main/java/com/squareup/picasso3/Utils.kt +++ b/picasso/src/main/java/com/squareup/picasso3/Utils.kt @@ -82,6 +82,8 @@ internal object Utils { */ private val WEBP_FILE_HEADER_RIFF: ByteString = "RIFF".encodeUtf8() private val WEBP_FILE_HEADER_WEBP: ByteString = "WEBP".encodeUtf8() + private val GIF_FILE_HEADER_87A: ByteString = "GIF87a".encodeUtf8() + private val GIF_FILE_HEADER_89A: ByteString = "GIF89a".encodeUtf8() fun checkNotNull(value: T?, message: String?): T { if (value == null) { @@ -178,6 +180,11 @@ internal object Utils { source.rangeEquals(8, WEBP_FILE_HEADER_WEBP) } + fun isGifFile(source: BufferedSource): Boolean { + return source.rangeEquals(0, GIF_FILE_HEADER_87A) || + source.rangeEquals(0, GIF_FILE_HEADER_89A) + } + fun getResourceId(resources: Resources, data: Request): Int { if (data.resourceId != 0 || data.uri == null) { return data.resourceId diff --git a/picasso/src/test/java/com/squareup/picasso3/UtilsTest.kt b/picasso/src/test/java/com/squareup/picasso3/UtilsTest.kt index 68a70edc62..8114da3e5a 100644 --- a/picasso/src/test/java/com/squareup/picasso3/UtilsTest.kt +++ b/picasso/src/test/java/com/squareup/picasso3/UtilsTest.kt @@ -21,6 +21,7 @@ import com.squareup.picasso3.TestUtils.RESOURCE_ID_URI import com.squareup.picasso3.TestUtils.RESOURCE_TYPE_URI import com.squareup.picasso3.TestUtils.URI_1 import com.squareup.picasso3.TestUtils.mockPackageResourceContext +import com.squareup.picasso3.Utils.isGifFile import com.squareup.picasso3.Utils.isWebPFile import okio.Buffer import org.junit.Test @@ -61,6 +62,11 @@ class UtilsTest { assertThat(isWebPFile(Buffer().writeUtf8("RIFFxxWEBP"))).isFalse() } + @Test fun detectGifFile() { + assertThat(isGifFile(Buffer().writeUtf8("GIF87a"))).isTrue() + assertThat(isGifFile(Buffer().writeUtf8("GIF89a"))).isTrue() + } + @Test fun ensureBuilderIsCleared() { Request.Builder(RESOURCE_ID_URI).build() assertThat(Utils.MAIN_THREAD_KEY_BUILDER.length).isEqualTo(0)