Skip to content

Instantly share code, notes, and snippets.

@davemorrissey
Forked from gokhanbarisaker/PicassoDecoder.java
Last active November 26, 2021 14:17
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save davemorrissey/e2781ba5b966c9e95539 to your computer and use it in GitHub Desktop.
Save davemorrissey/e2781ba5b966c9e95539 to your computer and use it in GitHub Desktop.
Picasso decoder for subsampling-scale-image-view
/**
* Created by gokhanbarisaker on 8/30/15.
*/
public class PicassoDecoder implements ImageDecoder
{
private String tag;
private String picasso;
public PicassoDecoder(String tag, Picasso picasso) {
this.tag = tag;
this.picasso = picasso;
}
@Override
public Bitmap decode(Context context, Uri uri) throws Exception {
return picasso
.load(uri)
.tag(tag)
.config(Bitmap.Config.RGB_565)
.memoryPolicy(MemoryPolicy.NO_CACHE)
.get();
}
}
/**
* Created by gokhanbarisaker on 8/30/15.
*/
public class PicassoRegionDecoder implements ImageRegionDecoder {
private OkHttpClient client;
private BitmapRegionDecoder decoder;
private final Object decoderLock = new Object();
public PicassoRegionDecoder (OkHttpClient client) {
this.client = client;
}
@Override
public Point init(Context context, Uri uri) throws Exception {
OkHttpDownloader downloader = new OkHttpDownloader(client);
InputStream inputStream = downloader.load(uri, 0).getInputStream();
this.decoder = BitmapRegionDecoder.newInstance(inputStream, false);
return new Point(this.decoder.getWidth(), this.decoder.getHeight());
}
@Override
public Bitmap decodeRegion(Rect rect, int sampleSize) {
synchronized(this.decoderLock) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = this.decoder.decodeRegion(rect, options);
if(bitmap == null) {
throw new RuntimeException("Region decoder returned null bitmap - image format may not be supported");
} else {
return bitmap;
}
}
}
@Override
public boolean isReady() {
return this.decoder != null && !this.decoder.isRecycled();
}
@Override
public void recycle() {
this.decoder.recycle();
}
}
@ktchernov
Copy link

private String picasso; should be private Picasso picasso;

@ThePromoter
Copy link

If CompatDecoderFactory expects your custom Decoder to have a no-arg constructor, how can we go about setting the tag when using this decoder? Do you have an example where you set this custom decoder as the bitmapDecoderClass, and properly use it with picasso?

@gokhanbarisaker
Copy link

Let's assume that you are using your image url string as a tag. You have 2 options;

  1. Declaring url & picasso at outer scope of your factory

final String url = "http://example.com/image001.jpg";
final Picasso picasso = Picasso.with(imageView.getContext());

and accessing them within factory

imageView.setBitmapDecoderFactory(new DecoderFactory<ImageDecoder>() {
public ImageDecoder make() {
return new PicassoDecoder(url, picasso);
}});

  1. Subclassing factory & adding a constructor that takes tag and Picasso instance as argument

public class PicassoDecoderFactory implements new DecoderFactory<ImageDecoder> {
private final String tag;
private final Picasso picasso;
public PicassoDecoderFactory(String tag, Picasso picasso) { this.tag = tag; this.picasso = picasso; }

public ImageDecoder make() {
return new PicassoDecoder(tag, picasso);
}
}

and using that as

String url = "http://example.com/image001.jpg";
Picasso picasso = Picasso.with(imageView.getContext());
imageView.setBitmapDecoderFactory(new PicassoDecoderFactory(url, picasso));

@VladimirYugay
Copy link

Can you provide us with full example of loading an image with this decoder please?

@dinesh-tech1990
Copy link

Can you provide us with full example of loading an image with this decoder please?

@Orabig
Copy link

Orabig commented May 16, 2016

+1 : I've managed to declare these Factories into my imageView, but how to load an image with this ? :/
Edit : I've found out how to do this. Simply write imageView.setImage( ImageSource.uri( url ) );

@okacat
Copy link

okacat commented May 17, 2016

Thank you, this helps a lot. I'd like to add that you need to set both a setBitmapDecoderFactory and setRegionDecoderFactory. Also, if someone else is having issues with the new OkHttp3Downloader(client) part inside PicassoRegionDecoder, take a look at this https://github.com/JakeWharton/picasso2-okhttp3-downloader

@gmikhail
Copy link

gmikhail commented Jun 7, 2016

If someone will be helpful - that's a complete example:

final Picasso picasso = Picasso.with(imageView.getContext());

imageView.setBitmapDecoderFactory(new DecoderFactory<ImageDecoder>() {
    public ImageDecoder make() {
        return new PicassoDecoder(imageUrl, picasso);
    }});

imageView.setRegionDecoderFactory(new DecoderFactory<ImageRegionDecoder>() {
    @Override
    public ImageRegionDecoder make() throws IllegalAccessException, InstantiationException {
        return new PicassoRegionDecoder(new OkHttpClient());
    }
});

imageView.setImage(ImageSource.uri(imageUrl));

@timothythlau
Copy link

timothythlau commented Jun 29, 2016

Just a heads up for anyone using this view in a view pager with Picasso integration, when you do set the RegionDecoderFactory in this line:

imageView.setRegionDecoderFactory(new DecoderFactory<ImageRegionDecoder>() {
    @Override
    public ImageRegionDecoder make() throws IllegalAccessException, InstantiationException {
        return new PicassoRegionDecoder(new OkHttpClient());
    }
});

Be careful of the OkHttpClient here. Was running into an issue where the number of open connections were exhausted with a view pager count of around 70 images with this view, due to what I suspect are new OkHttpClients creating new open connections and not dropping them fast enough when the page was destroyed once a user scrolled away from the page. Instantiated a new OkHttpClient in the ViewPager adapter and passed it to each view page on the instantiateView call fixed this issue for me.

Final code that is being used is as follows:

SubsamplingScaleImageView scaleImageView;

 public void loadImageByUrl(final String url, final OkHttpClient okHttpClient) {
        scaleImageView.setMaxScale(5.0f);
        final Picasso picasso = Picasso.with(getContext());

        scaleImageView.setBitmapDecoderFactory(new DecoderFactory<ImageDecoder>() {
            @Override
            public ImageDecoder make() throws IllegalAccessException, java.lang.InstantiationException {

                return new PicassoDecoder(url, picasso);
            }
        });

        scaleImageView.setRegionDecoderFactory(new DecoderFactory<ImageRegionDecoder>() {
            @Override
            public ImageRegionDecoder make() throws IllegalAccessException, java.lang.InstantiationException {
                return new PicassoRegionDecoder(okHttpClient);
            }
        });

        scaleImageView.setOnImageEventListener(new SubScalingImageViewListener());
        scaleImageView.setImage(ImageSource.uri(url));
    }

@kalyaganov
Copy link

kalyaganov commented Nov 22, 2016

I don't really understand how PicassoRegionDecoder connected with Picasso library?

@sregg
Copy link

sregg commented Jan 16, 2017

Has anyone tried to do this for Glide?

@markini
Copy link

markini commented Feb 27, 2017

With the new Picasso snapshot the init function of the PicassoRegionDecoder looks like this:

@Override
public Point init(Context context, Uri uri) throws Exception {
	OkHttp3Downloader downloader = new OkHttp3Downloader(client);
	okhttp3.Request request = new Request.Builder().url(uri.toString()).build();
	InputStream inputStream = downloader.load(request).body().byteStream();
	this.decoder = BitmapRegionDecoder.newInstance(inputStream, false);

	return new Point(this.decoder.getWidth(), this.decoder.getHeight());
}

@lordscales91
Copy link

Jesus! Combine libraries can be a nightmare sometimes. You need to line up the proper versions of each library.

These are the versions that work for me:

implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.squareup.picasso:picasso:2.5.2'
implementation 'com.squareup.okhttp:okhttp:2.1.0'

@tickerguy
Copy link

tickerguy commented May 16, 2018

Anyone got a code snippet that uses the isImageLoaded() override so I can have a "downloading...." display until the image comes up when using this with Picasso/Okhttp?

(Nevermind -- figured it out ;-) I like this a lot....)

@aldrinjoemathew
Copy link

@tickerguy how did you do that?

@arnaudlvq
Copy link

arnaudlvq commented Apr 14, 2020

@aldrinjoemathew

use the SSIV listener

image.setOnImageEventListener(new SubsamplingScaleImageView.OnImageEventListener() {

        @Override
        public void onReady() {

            //You could start loading your image here, so the ssiv is ready.

        }
        @Override
        public void onImageLoaded() {

            //Do your stuff here.

        }
        @Override
        public void onPreviewLoadError(Exception e) {}
        @Override
        public void onImageLoadError(Exception e) {}
        @Override
        public void onTileLoadError(Exception e) {}
        @Override
        public void onPreviewReleased() {}
    });

@arnaudlvq
Copy link

arnaudlvq commented Apr 20, 2020

** How to intercept the bitmap before the SSIV** : using Glide : (Working on 8k images with latest implementation 4.11.0)

Glide.with(ssiv.getContext())
            .asBitmap()
            .load("https://vah.dy.fi/testcard/7680x4320.png")     //Your own link
            .into(new CustomTarget<Bitmap>() {
                @Override
                public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                    
                       //Do whatever you want with the bitmap (like saving it to storage)
                       //ex :    ssiv.setImage(ImageSource.bitmap(resource);

                }

                @Override
                public void onLoadCleared(@Nullable Drawable placeholder) {
                }
            });

@kyze8439690
Copy link

Provide a decoder based on HttpUrlConnection, not OkHttp or Picasso dependency needed. Use only if you don't need image cache.

    class URLImageDecoder : ImageDecoder {

        private var bitmapConfig: Bitmap.Config =
            SubsamplingScaleImageView.getPreferredBitmapConfig() ?: Bitmap.Config.RGB_565

        @Throws(java.lang.Exception::class)
        override fun decode(context: Context, uri: Uri): Bitmap {
            val options = BitmapFactory.Options().apply { inPreferredConfig = bitmapConfig }
            val bitmap: Bitmap?
            var inputStream: InputStream? = null
            try {
                inputStream = URL(uri.toString()).openStream()
                bitmap = BitmapFactory.decodeStream(inputStream, null, options)
            } finally {
                try {
                    inputStream?.close()
                } catch (ignored: Exception) {
                }
            }
            return bitmap
                ?: throw RuntimeException("Region decoder returned null bitmap - image format may not be supported")
        }
    }

    class URLImageRegionDecoder : ImageRegionDecoder {
        private lateinit var decoder: BitmapRegionDecoder
        private val decoderLock: ReadWriteLock = ReentrantReadWriteLock(true)
        private val bitmapConfig: Bitmap.Config =
            SubsamplingScaleImageView.getPreferredBitmapConfig() ?: Bitmap.Config.RGB_565

        @Throws(Exception::class)
        override fun init(context: Context, uri: Uri): Point {
            var inputStream: InputStream? = null
            try {
                inputStream = URL(uri.toString()).openStream()
                decoder = BitmapRegionDecoder.newInstance(inputStream, false)
            } finally {
                try {
                    inputStream?.close()
                } catch (ignored: Exception) {
                }
            }
            return Point(decoder.width, decoder.height)
        }

        override fun decodeRegion(rect: Rect, sampleSize: Int): Bitmap {
            getDecodeLock().lock()
            return try {
                if (!decoder.isRecycled) {
                    val options = BitmapFactory.Options()
                    options.inSampleSize = sampleSize
                    options.inPreferredConfig = bitmapConfig
                    decoder.decodeRegion(rect, options) ?: throw java.lang.RuntimeException(
                        "Skia image decoder returned null bitmap - image format may not be supported")
                } else {
                    throw IllegalStateException("Cannot decode region after decoder has been recycled")
                }
            } finally {
                getDecodeLock().unlock()
            }
        }

        override fun isReady(): Boolean = !decoder.isRecycled
        override fun recycle() = decoder.recycle()

        /**
         * Before SDK 21, BitmapRegionDecoder was not synchronized internally. Any attempt to decode
         * regions from multiple threads with one decoder instance causes a segfault. For old versions
         * use the write lock to enforce single threaded decoding.
         */
        private fun getDecodeLock(): Lock {
            return if (Build.VERSION.SDK_INT < 21) {
                decoderLock.writeLock()
            } else {
                decoderLock.readLock()
            }
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment