diff --git a/buildSrc/src/main/java/com/facebook/fresco/buildsrc/dependencies.kt b/buildSrc/src/main/java/com/facebook/fresco/buildsrc/dependencies.kt index 03f83399bb..d3bdf3d2ed 100644 --- a/buildSrc/src/main/java/com/facebook/fresco/buildsrc/dependencies.kt +++ b/buildSrc/src/main/java/com/facebook/fresco/buildsrc/dependencies.kt @@ -17,6 +17,8 @@ object Deps { const val volley = "com.android.volley:volley:1.2.1" + const val avifAndroid = "org.aomedia.avif.android:avif:1.0.1.262e11d" + object AndroidX { const val androidxAnnotation = "androidx.annotation:annotation:1.1.0" const val core = "androidx.core:core:1.3.1" diff --git a/imagepipeline-base/src/main/java/com/facebook/imageformat/DefaultImageFormatChecker.kt b/imagepipeline-base/src/main/java/com/facebook/imageformat/DefaultImageFormatChecker.kt index 6d13c3bb25..f5338b423f 100644 --- a/imagepipeline-base/src/main/java/com/facebook/imageformat/DefaultImageFormatChecker.kt +++ b/imagepipeline-base/src/main/java/com/facebook/imageformat/DefaultImageFormatChecker.kt @@ -9,6 +9,7 @@ package com.facebook.imageformat import com.facebook.common.webp.WebpSupportStatus import com.facebook.imageformat.ImageFormat.FormatChecker +import java.nio.charset.Charset /** Default image format checker that is able to determine all [DefaultImageFormats]. */ class DefaultImageFormatChecker : FormatChecker { @@ -61,6 +62,9 @@ class DefaultImageFormatChecker : FormatChecker { if (isHeifHeader(headerBytes, headerSize)) { return DefaultImageFormats.HEIF } + if (isAvifHeader(headerBytes)) { + return DefaultImageFormats.AVIF + } return if (isDngHeader(headerBytes, headerSize)) { DefaultImageFormats.DNG } else { @@ -266,5 +270,17 @@ class DefaultImageFormatChecker : FormatChecker { headerSize >= DNG_HEADER_LENGTH && (ImageFormatCheckerUtils.startsWithPattern(imageHeaderBytes, DNG_HEADER_II) || ImageFormatCheckerUtils.startsWithPattern(imageHeaderBytes, DNG_HEADER_MM)) + + /** + * Checks if [imageHeaderBytes] contains 'avif'. + * + * This check may not be sufficient, though it works for most AVIF images. + * Details on AVIF can be found [here](https://aomediacodec.github.io/av1-avif/). + * */ + private fun isAvifHeader(imageHeaderBytes: ByteArray): Boolean { + return imageHeaderBytes + .toString(Charset.forName("UTF-8")) + .contains("avif") + } } } diff --git a/imagepipeline-base/src/main/java/com/facebook/imageformat/DefaultImageFormats.kt b/imagepipeline-base/src/main/java/com/facebook/imageformat/DefaultImageFormats.kt index e3ed98bf3a..b593cbc9d6 100644 --- a/imagepipeline-base/src/main/java/com/facebook/imageformat/DefaultImageFormats.kt +++ b/imagepipeline-base/src/main/java/com/facebook/imageformat/DefaultImageFormats.kt @@ -22,6 +22,7 @@ object DefaultImageFormats { @JvmField val WEBP_ANIMATED: ImageFormat = ImageFormat("WEBP_ANIMATED", "webp") @JvmField val HEIF: ImageFormat = ImageFormat("HEIF", "heif") @JvmField val DNG: ImageFormat = ImageFormat("DNG", "dng") + @JvmField val AVIF: ImageFormat = ImageFormat("AVIF", "avif") /** * Check if the given image format is a WebP image format (static or animated). @@ -34,6 +35,9 @@ object DefaultImageFormats { return isStaticWebpFormat(imageFormat) || imageFormat === WEBP_ANIMATED } + @JvmStatic + fun isAvifFormat(imageFormat: ImageFormat) = imageFormat === AVIF + /** * Check if the given image format is static WebP (not animated). * @@ -67,5 +71,6 @@ object DefaultImageFormats { WEBP_EXTENDED_WITH_ALPHA, WEBP_ANIMATED, HEIF, + AVIF, ) } diff --git a/imagepipeline-base/src/main/java/com/facebook/imagepipeline/decoder/factory/AvifDecoderFactory.kt b/imagepipeline-base/src/main/java/com/facebook/imagepipeline/decoder/factory/AvifDecoderFactory.kt new file mode 100644 index 0000000000..8ac1704204 --- /dev/null +++ b/imagepipeline-base/src/main/java/com/facebook/imagepipeline/decoder/factory/AvifDecoderFactory.kt @@ -0,0 +1,7 @@ +package com.facebook.imagepipeline.decoder.factory + +import com.facebook.imagepipeline.decoder.ImageDecoder + +interface AvifDecoderFactory { + val avifDecoder: ImageDecoder? +} diff --git a/imagepipeline-base/src/main/java/com/facebook/imagepipeline/image/EncodedImage.java b/imagepipeline-base/src/main/java/com/facebook/imagepipeline/image/EncodedImage.java index 7d6010e792..8b7e1f029d 100644 --- a/imagepipeline-base/src/main/java/com/facebook/imagepipeline/image/EncodedImage.java +++ b/imagepipeline-base/src/main/java/com/facebook/imagepipeline/image/EncodedImage.java @@ -369,6 +369,8 @@ private void internalParseMetaData() { final Pair dimensions; if (DefaultImageFormats.isWebpFormat(imageFormat)) { dimensions = readWebPImageSize(); + } else if (DefaultImageFormats.isAvifFormat(imageFormat)) { + dimensions = null; } else { dimensions = readImageMetaData().getDimensions(); } diff --git a/imagepipeline-base/src/main/java/com/facebook/imageutils/ByteBufferUtil.kt b/imagepipeline-base/src/main/java/com/facebook/imageutils/ByteBufferUtil.kt new file mode 100644 index 0000000000..2ef8502094 --- /dev/null +++ b/imagepipeline-base/src/main/java/com/facebook/imageutils/ByteBufferUtil.kt @@ -0,0 +1,35 @@ +package com.facebook.imageutils + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.util.concurrent.atomic.AtomicReference + +object ByteBufferUtil { + private const val BUFFER_SIZE = 16384 // 16KiB + private val BUFFER_REF = AtomicReference() + + @Throws(IOException::class) + fun fromStream(stream: InputStream): ByteBuffer { + val outStream = ByteArrayOutputStream(BUFFER_SIZE) + + var buffer = BUFFER_REF.getAndSet(null) + if (buffer == null) { + buffer = ByteArray(BUFFER_SIZE) + } + + var n: Int + while (stream.read(buffer).also { n = it } >= 0) { + outStream.write(buffer, 0, n) + } + + BUFFER_REF.set(buffer) + + val bytes = outStream.toByteArray() + + return rewind(ByteBuffer.allocateDirect(bytes.size).put(bytes)) + } + + private fun rewind(buffer: ByteBuffer) = buffer.position(0) as ByteBuffer +} diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineExperiments.kt b/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineExperiments.kt index c7406b2aea..9beda214ab 100644 --- a/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineExperiments.kt +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineExperiments.kt @@ -24,6 +24,7 @@ import com.facebook.imagepipeline.cache.CacheKeyFactory import com.facebook.imagepipeline.cache.MemoryCache import com.facebook.imagepipeline.decoder.ImageDecoder import com.facebook.imagepipeline.decoder.ProgressiveJpegConfig +import com.facebook.imagepipeline.decoder.factory.AvifDecoderFactory import com.facebook.imagepipeline.image.CloseableImage import com.facebook.imagepipeline.platform.PlatformDecoderOptions import com.facebook.imageutils.BitmapUtil @@ -41,6 +42,7 @@ class ImagePipelineExperiments private constructor(builder: Builder) { val webpErrorLogger: WebpErrorLogger? val isDecodeCancellationEnabled: Boolean val webpBitmapFactory: WebpBitmapFactory? + val avifDecoderFactory: AvifDecoderFactory? val useDownsamplingRatioForResizing: Boolean val useBitmapPrepareToDraw: Boolean val useBalancedAnimationStrategy: Boolean @@ -81,6 +83,7 @@ class ImagePipelineExperiments private constructor(builder: Builder) { @JvmField var webpErrorLogger: WebpErrorLogger? = null @JvmField var decodeCancellationEnabled = false @JvmField var webpBitmapFactory: WebpBitmapFactory? = null + @JvmField var avifDecoderFactory: AvifDecoderFactory? = null @JvmField var useDownsamplingRatioForResizing = false @JvmField var useBitmapPrepareToDraw = false @JvmField var useBalancedAnimationStrategy = false @@ -401,6 +404,7 @@ class ImagePipelineExperiments private constructor(builder: Builder) { webpErrorLogger = builder.webpErrorLogger isDecodeCancellationEnabled = builder.decodeCancellationEnabled webpBitmapFactory = builder.webpBitmapFactory + avifDecoderFactory = builder.avifDecoderFactory useDownsamplingRatioForResizing = builder.useDownsamplingRatioForResizing useBitmapPrepareToDraw = builder.useBitmapPrepareToDraw useBalancedAnimationStrategy = builder.useBalancedAnimationStrategy diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineFactory.java b/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineFactory.java index d53d96defd..4685433fa6 100644 --- a/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineFactory.java +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/core/ImagePipelineFactory.java @@ -31,6 +31,8 @@ import com.facebook.imagepipeline.cache.MemoryCache; import com.facebook.imagepipeline.decoder.DefaultImageDecoder; import com.facebook.imagepipeline.decoder.ImageDecoder; +import com.facebook.imagepipeline.decoder.factory.AvifDecoderFactory; +import com.facebook.imagepipeline.decoder.factory.provider.AvifDecoderFactoryProvider; import com.facebook.imagepipeline.drawable.DrawableFactory; import com.facebook.imagepipeline.image.CloseableImage; import com.facebook.imagepipeline.platform.PlatformDecoder; @@ -250,13 +252,22 @@ private ImageDecoder getImageDecoder() { webPDecoder = animatedFactory.getWebPDecoder(); } + final AvifDecoderFactory avifDecoderFactory = getAvifDecoderFactory(); + + ImageDecoder avifDecoder = null; + + if (avifDecoderFactory != null) { + avifDecoder = avifDecoderFactory.getAvifDecoder(); + } + if (mConfig.getImageDecoderConfig() == null) { - mImageDecoder = new DefaultImageDecoder(gifDecoder, webPDecoder, getPlatformDecoder()); + mImageDecoder = new DefaultImageDecoder(gifDecoder, webPDecoder, avifDecoder, getPlatformDecoder()); } else { mImageDecoder = new DefaultImageDecoder( gifDecoder, webPDecoder, + avifDecoder, getPlatformDecoder(), mConfig.getImageDecoderConfig().getCustomImageDecoders()); // Add custom image formats if needed @@ -269,6 +280,11 @@ private ImageDecoder getImageDecoder() { return mImageDecoder; } + @Nullable + private AvifDecoderFactory getAvifDecoderFactory() { + return AvifDecoderFactoryProvider.loadIfExists(mConfig.getPoolFactory().getBitmapPool()); + } + public BufferedDiskCache getMainBufferedDiskCache() { if (mMainBufferedDiskCache == null) { mMainBufferedDiskCache = diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/decoder/DefaultImageDecoder.java b/imagepipeline/src/main/java/com/facebook/imagepipeline/decoder/DefaultImageDecoder.java index 41f6806b4d..c82757d383 100644 --- a/imagepipeline/src/main/java/com/facebook/imagepipeline/decoder/DefaultImageDecoder.java +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/decoder/DefaultImageDecoder.java @@ -51,6 +51,7 @@ public class DefaultImageDecoder implements ImageDecoder { private final @Nullable ImageDecoder mAnimatedGifDecoder; private final @Nullable ImageDecoder mAnimatedWebPDecoder; + private final @Nullable ImageDecoder mAvifDecoder; private final PlatformDecoder mPlatformDecoder; private final Supplier mEnableEncodedImageColorSpaceUsage; @@ -76,6 +77,8 @@ public class DefaultImageDecoder implements ImageDecoder { return decodeGif(encodedImage, length, qualityInfo, options); } else if (imageFormat == DefaultImageFormats.WEBP_ANIMATED) { return decodeAnimatedWebp(encodedImage, length, qualityInfo, options); + } else if (imageFormat == DefaultImageFormats.AVIF) { + return decodeAvif(encodedImage, length, qualityInfo, options); } else if (imageFormat == ImageFormat.UNKNOWN) { throw new DecodeException("unknown image format", encodedImage); } @@ -88,17 +91,20 @@ public class DefaultImageDecoder implements ImageDecoder { public DefaultImageDecoder( @Nullable final ImageDecoder animatedGifDecoder, @Nullable final ImageDecoder animatedWebPDecoder, + @Nullable final ImageDecoder avifDecoder, final PlatformDecoder platformDecoder) { - this(animatedGifDecoder, animatedWebPDecoder, platformDecoder, null); + this(animatedGifDecoder, animatedWebPDecoder, avifDecoder, platformDecoder, null); } public DefaultImageDecoder( @Nullable final ImageDecoder animatedGifDecoder, @Nullable final ImageDecoder animatedWebPDecoder, + @Nullable final ImageDecoder avifDecoder, final PlatformDecoder platformDecoder, @Nullable Map customDecoders) { mAnimatedGifDecoder = animatedGifDecoder; mAnimatedWebPDecoder = animatedWebPDecoder; + mAvifDecoder = avifDecoder; mPlatformDecoder = platformDecoder; mCustomDecoders = customDecoders; mEnableEncodedImageColorSpaceUsage = Suppliers.BOOLEAN_FALSE; @@ -107,11 +113,13 @@ public DefaultImageDecoder( public DefaultImageDecoder( @Nullable final ImageDecoder animatedGifDecoder, @Nullable final ImageDecoder animatedWebPDecoder, + @Nullable final ImageDecoder avifDecoder, final PlatformDecoder platformDecoder, @Nullable Map customDecoders, final Supplier enableEncodedImageColorSpaceUsage) { mAnimatedGifDecoder = animatedGifDecoder; mAnimatedWebPDecoder = animatedWebPDecoder; + mAvifDecoder = avifDecoder; mPlatformDecoder = platformDecoder; mCustomDecoders = customDecoders; mEnableEncodedImageColorSpaceUsage = enableEncodedImageColorSpaceUsage; @@ -264,4 +272,15 @@ public CloseableStaticBitmap decodeJpeg( } return decodeStaticImage(encodedImage, options); } + + private @Nullable CloseableImage decodeAvif( + final EncodedImage encodedImage, + final int length, + final QualityInfo qualityInfo, + final ImageDecodeOptions options) { + if (mAvifDecoder != null) { + return mAvifDecoder.decode(encodedImage, length, qualityInfo, options); + } + return decodeStaticImage(encodedImage, options); + } } diff --git a/imagepipeline/src/main/java/com/facebook/imagepipeline/decoder/factory/provider/AvifDecoderFactoryProvider.kt b/imagepipeline/src/main/java/com/facebook/imagepipeline/decoder/factory/provider/AvifDecoderFactoryProvider.kt new file mode 100644 index 0000000000..f7a78e0ce9 --- /dev/null +++ b/imagepipeline/src/main/java/com/facebook/imagepipeline/decoder/factory/provider/AvifDecoderFactoryProvider.kt @@ -0,0 +1,28 @@ +package com.facebook.imagepipeline.decoder.factory.provider + +import com.facebook.imagepipeline.decoder.factory.AvifDecoderFactory +import com.facebook.imagepipeline.memory.BitmapPool + +object AvifDecoderFactoryProvider { + + private var checked = false + + private var avifDecoderFactory: AvifDecoderFactory? = null + + @JvmStatic + fun loadIfExists(bitmapPool: BitmapPool): AvifDecoderFactory? { + if (checked) return avifDecoderFactory + + try { + avifDecoderFactory = Class + .forName("com.facebook.avifsupport.AvifDecoderFactoryImpl") + .getConstructor(BitmapPool::class.java) + .newInstance(bitmapPool) as? AvifDecoderFactory + } catch (_: Throwable) { + // Not available + } + + checked = true + return avifDecoderFactory + } +} diff --git a/samples/showcase/build.gradle b/samples/showcase/build.gradle index f8a3602034..4a5b3cd0da 100644 --- a/samples/showcase/build.gradle +++ b/samples/showcase/build.gradle @@ -106,6 +106,7 @@ dependencies { implementation project(':animated-gif') implementation project(':animated-gif-lite') implementation project(':animated-webp') + implementation project(':static-avif') implementation project(':static-webp') implementation project(':native-filters') implementation project(':native-imagetranscoder') diff --git a/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/ExampleDatabase.kt b/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/ExampleDatabase.kt index 45287b40fb..8276449092 100644 --- a/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/ExampleDatabase.kt +++ b/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/ExampleDatabase.kt @@ -9,6 +9,7 @@ package com.facebook.fresco.samples.showcase import com.facebook.fresco.samples.showcase.drawee.* import com.facebook.fresco.samples.showcase.drawee.transition.DraweeTransitionFragment +import com.facebook.fresco.samples.showcase.imageformat.avif.ImageFormatAvifFragment import com.facebook.fresco.samples.showcase.imageformat.color.ImageFormatColorFragment import com.facebook.fresco.samples.showcase.imageformat.datauri.ImageFormatDataUriFragment import com.facebook.fresco.samples.showcase.imageformat.gif.ImageFormatGifFragment @@ -69,6 +70,7 @@ object ExampleDatabase { ExampleItem("Color") { ImageFormatColorFragment() }, ExampleItem("GIF") { ImageFormatGifFragment() }, ExampleItem("WebP") { ImageFormatWebpFragment() }, + ExampleItem("AVIF") { ImageFormatAvifFragment() }, ExampleItem("SVG") { ImageFormatSvgFragment() }, ExampleItem("Keyframes") { ImageFormatKeyframesFragment() }, ExampleItem("Decoder Override") { ImageFormatOverrideExample() }, diff --git a/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imageformat/avif/ImageFormatAvifFragment.kt b/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imageformat/avif/ImageFormatAvifFragment.kt new file mode 100644 index 0000000000..6bdd2d4f16 --- /dev/null +++ b/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imageformat/avif/ImageFormatAvifFragment.kt @@ -0,0 +1,21 @@ +package com.facebook.fresco.samples.showcase.imageformat.avif + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.facebook.drawee.view.SimpleDraweeView +import com.facebook.fresco.samples.showcase.BaseShowcaseFragment +import com.facebook.fresco.samples.showcase.R + +class ImageFormatAvifFragment : BaseShowcaseFragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_format_avif, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val draweeAvifStatic = view.findViewById(R.id.drawee_view_avif_static) + draweeAvifStatic.setImageURI(sampleUris().createAvifStaticUri()) + } +} diff --git a/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/misc/ImageUriProvider.kt b/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/misc/ImageUriProvider.kt index 45da3ea83d..3582e350b1 100644 --- a/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/misc/ImageUriProvider.kt +++ b/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/misc/ImageUriProvider.kt @@ -144,6 +144,9 @@ class ImageUriProvider constructor(context: Context) { fun createWebpStaticUri(): Uri = applyOverrideSettings(SAMPLE_URI_WEBP_STATIC, UriModification.NONE) + fun createAvifStaticUri(): Uri = + applyOverrideSettings(SAMPLE_URI_AVIF_STATIC, UriModification.NONE) + fun createWebpTranslucentUri(): Uri = applyOverrideSettings(SAMPLE_URI_WEBP_TRANSLUCENT, UriModification.NONE) @@ -282,6 +285,8 @@ class ImageUriProvider constructor(context: Context) { private val SAMPLE_URI_WEBP_STATIC = "https://www.gstatic.com/webp/gallery/2.webp" + private const val SAMPLE_URI_AVIF_STATIC = "https://raw.githubusercontent.com/link-u/avif-sample-images/master/hato.profile2.12bpc.yuv422.avif" + private val SAMPLE_URI_WEBP_TRANSLUCENT = "https://www.gstatic.com/webp/gallery3/5_webp_ll.webp" private val SAMPLE_URI_WEBP_ANIMATED = "https://www.gstatic.com/webp/animated/1.webp" diff --git a/samples/showcase/src/main/res/layout/fragment_format_avif.xml b/samples/showcase/src/main/res/layout/fragment_format_avif.xml new file mode 100644 index 0000000000..463feb57f8 --- /dev/null +++ b/samples/showcase/src/main/res/layout/fragment_format_avif.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/samples/showcase/src/main/res/values/strings.xml b/samples/showcase/src/main/res/values/strings.xml index ba3a8ca20d..6d7b1f72a1 100644 --- a/samples/showcase/src/main/res/values/strings.xml +++ b/samples/showcase/src/main/res/values/strings.xml @@ -92,6 +92,8 @@ The above Drawee displays a loss-less WebP image with semi-transparent areas. The above Drawee displays an animated WebP image + The above Drawee displays an AVIF image. + Select a progressive JPEG from the spinner and enable or disable progessive JPEG support with the switch. Progressive JPEG rendering enabled Small progressive JPEG diff --git a/settings.gradle b/settings.gradle index 3d91304914..4fa5e82314 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,6 +36,7 @@ include ':samples:showcase' include ':samples:zoomable' include ':samples:zoomableapp' include ':soloader' +include ':static-avif' include ':static-webp' include ':tools:flipper' include ':tools:flipper-fresco-plugin' diff --git a/static-avif/.gitignore b/static-avif/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/static-avif/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/static-avif/build.gradle.kts b/static-avif/build.gradle.kts new file mode 100644 index 0000000000..f58eb7397f --- /dev/null +++ b/static-avif/build.gradle.kts @@ -0,0 +1,28 @@ +import com.facebook.fresco.buildsrc.Deps + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("com.vanniktech.maven.publish") +} + +android { + namespace = "com.facebook.avifsupport" + + buildToolsVersion = FrescoConfig.buildToolsVersion + compileSdkVersion = "android-${FrescoConfig.compileSdkVersion}" + + defaultConfig { + minSdk = FrescoConfig.minSdkVersion + targetSdk = FrescoConfig.targetSdkVersion + } +} + +dependencies { + compileOnly(Deps.inferAnnotation) + + implementation(project(":middleware")) + implementation(project(":fbcore")) + implementation(project(":imagepipeline")) + implementation(Deps.avifAndroid) +} \ No newline at end of file diff --git a/static-avif/gradle.properties b/static-avif/gradle.properties new file mode 100644 index 0000000000..30ea1e06b5 --- /dev/null +++ b/static-avif/gradle.properties @@ -0,0 +1,4 @@ +POM_NAME=AvifSupport +POM_DESCRIPTION=Fresco AVIF Support +POM_ARTIFACT_ID=avifsupport +POM_PACKAGING=aar diff --git a/static-avif/src/main/AndroidManifest.xml b/static-avif/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4360be26dd --- /dev/null +++ b/static-avif/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/static-avif/src/main/java/com/facebook/avifsupport/AvifDecoder.kt b/static-avif/src/main/java/com/facebook/avifsupport/AvifDecoder.kt new file mode 100644 index 0000000000..650718533c --- /dev/null +++ b/static-avif/src/main/java/com/facebook/avifsupport/AvifDecoder.kt @@ -0,0 +1,58 @@ +package com.facebook.avifsupport + +import android.graphics.Bitmap +import android.util.Log +import com.facebook.common.logging.FLog +import com.facebook.imagepipeline.common.ImageDecodeOptions +import com.facebook.imagepipeline.decoder.ImageDecoder +import com.facebook.imagepipeline.image.CloseableImage +import com.facebook.imagepipeline.image.CloseableStaticBitmap +import com.facebook.imagepipeline.image.EncodedImage +import com.facebook.imagepipeline.image.QualityInfo +import com.facebook.imagepipeline.memory.BitmapPool +import com.facebook.imageutils.BitmapUtil +import com.facebook.imageutils.ByteBufferUtil +import org.aomedia.avif.android.AvifDecoder +import org.aomedia.avif.android.AvifDecoder.Info + +class AvifDecoder(private val bitmapPool: BitmapPool) : ImageDecoder { + override fun decode( + encodedImage: EncodedImage, + length: Int, + qualityInfo: QualityInfo, + options: ImageDecodeOptions + ): CloseableImage? = encodedImage.inputStream?.let { inputStream -> + val byteBuffer = ByteBufferUtil.fromStream(inputStream) + + val info = Info() + if (!AvifDecoder.getInfo(byteBuffer, length, info)) { + FLog.e(TAG, "Error when getting Info from AvifDecoder") + return null + } + + val bitmap = getBitmap(info.width, info.height, options.bitmapConfig) + + if (!AvifDecoder.decode(byteBuffer, byteBuffer.remaining(), bitmap)) { + FLog.e(TAG, "Error while decoding") + return null + } + + return CloseableStaticBitmap.of( + bitmap, + bitmapPool::release, + qualityInfo, + encodedImage.rotationAngle, + encodedImage.exifOrientation + ) + } + + private fun getBitmap(width: Int, height: Int, config: Bitmap.Config): Bitmap { + return bitmapPool[BitmapUtil.getSizeInByteForBitmap(width, height, config)].also { bitmap -> + bitmap.reconfigure(width, height, config) + } + } + + companion object { + private const val TAG = "AvifDecoder" + } +} diff --git a/static-avif/src/main/java/com/facebook/avifsupport/AvifDecoderFactoryImpl.kt b/static-avif/src/main/java/com/facebook/avifsupport/AvifDecoderFactoryImpl.kt new file mode 100644 index 0000000000..cc2ea7bafb --- /dev/null +++ b/static-avif/src/main/java/com/facebook/avifsupport/AvifDecoderFactoryImpl.kt @@ -0,0 +1,13 @@ +package com.facebook.avifsupport + +import com.facebook.common.internal.DoNotStrip +import com.facebook.imagepipeline.decoder.ImageDecoder +import com.facebook.imagepipeline.decoder.factory.AvifDecoderFactory +import com.facebook.imagepipeline.memory.BitmapPool +import com.facebook.infer.annotation.Nullsafe + +@Nullsafe(Nullsafe.Mode.STRICT) +@DoNotStrip +class AvifDecoderFactoryImpl(bitmapPool: BitmapPool) : AvifDecoderFactory { + override val avifDecoder: ImageDecoder = AvifDecoder(bitmapPool) +}