3 min read 0 comments

Get first image from GIF with Fresco Sync & Async

Fresco did not provides a Sync way to get bitmap from DataSource, but we can use SimpleSettableFuture from ReactNative. However, we can write our own SettableFuture like what ReactNative done.

Sync, With Fresco

object FrescoUtils {
    private const val TAG = "FrescoUtils"

    // Inspired by https://github.com/facebook/fresco/issues/2229#issuecomment-434061873
    @WorkerThread
    fun getBitmapFromAnimatedImageUri(gifUri: Uri, timeout: Long, unit: TimeUnit): Bitmap? {
        val request = ImageRequest.fromUri(gifUri) ?: return null

        // ATTENTION!!! future.set() should be called
        val future = SimpleSettableFuture<Bitmap?>()

        val dataSource =Fresco.getImagePipeline().fetchDecodedImage(request, "animatedImage")
        if (dataSource != null) {
            dataSource.subscribe(object : BaseDataSubscriber<CloseableReference<CloseableImage?>?>() {
                override fun onNewResultImpl(paramDataSource: DataSource<CloseableReference<CloseableImage?>?>?) {
                    if (paramDataSource != null && paramDataSource.hasResult()) {
                        paramDataSource.result?.get()?.use {
                            if (it is CloseableAnimatedImage) {
                                val bitmap = FrescoUtils.getFirstBitmapFromAnimatedImage(it)
                                future.set(bitmap)
                            }
                        }
                    }
                }

                override fun onCancellation(dataSource: DataSource<CloseableReference<CloseableImage?>?>?) {
                    super.onCancellation(dataSource)
                    future.set(null)
                }

                override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage?>?>) {
                    future.set(null)
                }
            }, UiThreadImmediateExecutorService.getInstance())
        } else {
            future.set(null)
        }

        return try {
                future.get(timeout, unit)
            } catch (e: Exception) {
                null
            }
    }

    @WorkerThread
    fun getFirstBitmapFromAnimatedImage(animatedImg: CloseableAnimatedImage?): Bitmap? {
        if (animatedImg == null
            || animatedImg.imageResult == null
            || animatedImg.imageResult.image == null
            || animatedImg.imageResult.image.frameDurations == null
            || animatedImg.width <= 0
            || animatedImg.height <= 0
        ) {
            return null
        }

        var imageFrame: AnimatedImageFrame? = null
        var bitmap: Bitmap? = null
        try {
            bitmap = Bitmap.createBitmap(
                animatedImg.width,
                animatedImg.height,
                Bitmap.Config.ARGB_8888
            )
            imageFrame = animatedImg.image?.getFrame(0)

            imageFrame?.renderFrame(imageFrame.width, imageFrame.height, bitmap)
        } catch (e: IllegalArgumentException) {
            Log.printErrStackTrace(TAG, e, "exception when create bitmap from ImageFrame")
        } finally {
            imageFrame?.dispose()
        }

        return bitmap
    }
}

SimpleSettableFuture from ReactNative

/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.common.futures;

import javax.annotation.Nullable;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * A super simple Future-like class that can safely notify another Thread when a value is ready.
 * Does not support canceling.
 */
public class SimpleSettableFuture<T> implements Future<T> {
  private final CountDownLatch mReadyLatch = new CountDownLatch(1);
  private @Nullable T mResult;
  private @Nullable Exception mException;

  /**
   * Sets the result. If another thread has called {@link #get}, they will immediately receive the
   * value. set or setException must only be called once.
   */
  public void set(@Nullable T result) {
    checkNotSet();
    mResult = result;
    mReadyLatch.countDown();
  }

  /**
   * Sets the exception. If another thread has called {@link #get}, they will immediately receive
   * the exception. set or setException must only be called once.
   */
  public void setException(Exception exception) {
    checkNotSet();
    mException = exception;
    mReadyLatch.countDown();
  }

  @Override
  public boolean cancel(boolean mayInterruptIfRunning) {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean isCancelled() {
    return false;
  }

  @Override
  public boolean isDone() {
    return mReadyLatch.getCount() == 0;
  }

  @Override
  public @Nullable T get() throws InterruptedException, ExecutionException {
    mReadyLatch.await();
    if (mException != null) {
      throw new ExecutionException(mException);
    }

    return mResult;
  }

  /**
   * Wait up to the timeout time for another Thread to set a value on this future. If a value has
   * already been set, this method will return immediately.
   *
   * NB: For simplicity, we catch and wrap InterruptedException. Do NOT use this class if you
   * are in the 1% of cases where you actually want to handle that.
   */
  @Override
  public @Nullable T get(long timeout, TimeUnit unit) throws
      InterruptedException, ExecutionException, TimeoutException {
    if (!mReadyLatch.await(timeout, unit)) {
      throw new TimeoutException("Timed out waiting for result");
    }
    if (mException != null) {
      throw new ExecutionException(mException);
    }

    return mResult;
  }

  /**
   * Convenience wrapper for {@link #get()} that re-throws get()'s Exceptions as
   * RuntimeExceptions.
   */
  public @Nullable T getOrThrow() {
    try {
      return get();
    } catch (InterruptedException | ExecutionException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Convenience wrapper for {@link #get(long, TimeUnit)} that re-throws get()'s Exceptions as
   * RuntimeExceptions.
   */
  public @Nullable T getOrThrow(long timeout, TimeUnit unit) {
    try {
      return get(timeout, unit);
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
      throw new RuntimeException(e);
    }
  }

  private void checkNotSet() {
    if (mReadyLatch.getCount() == 0) {
      throw new RuntimeException("Result has already been set!");
    }
  }
}