I'm giving uri to Glide then it resizes image to view's size. That's great but how can i get the original image size?
I want Glide to resize image. But i just want to get original image size. My current code is
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
Glide.with(this).load(uri).into(imageView).getSize(new SizeReadyCallback() {
@Override
public void onSizeReady(int width, int height) {
mEditDeskLayout.setImageSize(width,height);
}
});
Although image size is wrap content, SizeReadyCallBack always returns screen size even original image size is smaller or larger than screen.
That's not possible like this. getSize is an internal mechanism to get the Target's size, not the image's size. The target in this case wraps your imageView so it returns the view size after layout. wrap_content is not handled yet, the fallback is screen width/height. The best approach for Android is that the ImageView should be filling the space without any image loaded in it, which is match_parent or a fixed size. With wrapping Glide doesn't really know how much space is available and can't effectively load the perfect-sized image. All phones should be able to handle images which are not greater than screen, hence the fallback.
Add a .listener() before .into() and ask the Drawable (default) or Bitmap (.asBitmap()) how big it is; this will still return the same results though, because Glide loads the smallest possible size.
Getting the orginal size can be done with BitmapFactory with the correct options, if you really want it here's how to do it with Glide. Continue reading at your own risk ;)
(_check out Glide Resource Flow for more info_)
Notice how .diskCacheStrategy is added to both loads. It's necessary to prevent double-downloading from the network.
// inside a Fragment
GenericRequestBuilder<Uri, InputStream, Options, Options> SIZE_REQUEST;
@Override public void onAttach(Activity activity) {
super.onAttach(activity);
SIZE_REQUEST = Glide // cache for effectiveness (re-use in lists for example) and readability at usage
.with(this)
.using(new StreamUriLoader(activity), InputStream.class)
.from(Uri.class)
.as(Options.class)
.sourceEncoder(new StreamEncoder())
.cacheDecoder(new BitmapSizeDecoder())
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.listener(new LoggingListener<Uri, Options>())
;
}
void load(ImageView imageView, Uri uri) {
// normal load to display
Glide
.with(this)
.load(uri)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.listener(new LoggingListener<Uri, GlideDrawable>())
.into(imageView)
;
// get original size
SIZE_REQUEST
.load(uri)
.into(new SimpleTarget<Options>() { // Target.SIZE_ORIGINAL is hidden in ctor
@Override public void onResourceReady(Options resource, GlideAnimation glideAnimation) {
Log.wtf("SIZE", String.format(Locale.ROOT, "%dx%d", resource.outWidth, resource.outHeight));
}
})
;
}
LoggingListener from wiki the other classes are built-in, and the key to the whole operation is the following class:
class BitmapSizeDecoder implements ResourceDecoder<File, Options> {
@Override public Resource<Options> decode(File source, int width, int height) throws IOException {
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(source.getAbsolutePath(), options);
return new SimpleResource<>(options);
}
@Override public String getId() {
return getClass().getName();
}
}
It would make sense to cache the result, so the original file doesn't even need to be stored (or downloaded for that matter, only the partial header needs to be read from the network stream). Sadly the cacheDecoder decodes both SOURCE and RESULT cache items, so you have to either peek at contents to decide if you need to decode a bitmap or just 8 bytes of two ints. See #707 for more info.
If you don't want to display it, you can really save traffic by using .diskCacheStrategy(NONE) it'll likely just read the header which is much much faster. It then also becomes possible to cache the result, making it possible to use the size even offline, if it was requested before... if you're interested in this, just write below and I'll show you how to do it.
If you don't want to receive an Options in the effective version above you can hide the conversion by transcoding:
-GenericRequestBuilder<Uri, InputStream, Options, Options> SIZE_REQUEST;
+GenericRequestBuilder<Uri, InputStream, Options, Size> SIZE_REQUEST;
.as(Options.class)
+ .transcode(new OptionsSizeResourceTranscoder(), Size.class)
.sourceEncoder(new StreamEncoder())
.cacheDecoder(new BitmapSizeDecoder())
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
- .listener(new LoggingListener<Uri, Options>())
+ .listener(new LoggingListener<Uri, Size>())
-.into(new SimpleTarget<Options>() {
- @Override public void onResourceReady(Options resource, GlideAnimation glideAnimation) {
- Log.wtf("SIZE", String.format(Locale.ROOT, "%dx%d", resource.outWidth, resource.outHeight));
- }
-})
+.into(new SimpleTarget<Size>() {
+ @Override public void onResourceReady(Size resource, GlideAnimation glideAnimation) {
+ Log.wtf("SIZE", String.format(Locale.ROOT, "%dx%d", resource.width, resource.height));
+ }
+})
There's no Size class in Android before API 21, so roll you own (this class could be anything you already have):
class Size {
int width, height;
}
and the fast transcoder:
class OptionsSizeResourceTranscoder implements ResourceTranscoder<Options, Size> {
@Override public Resource<Size> transcode(Resource<Options> toTranscode) {
Options options = toTranscode.get();
Size size = new Size();
size.width = options.outWidth;
size.height = options.outHeight;
return new SimpleResource<>(size);
}
@Override public String getId() {
return getClass().getName();
}
}
The above code is the most efficient way to achieve what you wanted. There's an alternative, but it requires to load the image into memory, which I strongly advise against (just for proof of concept or debugging, please don't use it in production!):
Glide
.with(this)
.load(uri)
.asBitmap()
.transcode(new BitmapSizeTranscoder(), Size.class)
.into(new SimpleTarget<Size>() {
@Override public void onResourceReady(Size resource, GlideAnimation glideAnimation) {
Log.wtf("SIZE", String.format(Locale.ROOT, "%dx%d", resource.width, resource.height));
}
})
;
class BitmapSizeTranscoder implements ResourceTranscoder<Bitmap, Size> {
@Override public Resource<Size> transcode(Resource<Bitmap> toTranscode) {
Bitmap bitmap = toTranscode.get();
Size size = new Size();
size.width = bitmap.getWidth();
size.height = bitmap.getHeight();
return new SimpleResource<>(size);
}
@Override public String getId() {
return getClass().getName();
}
}
Great answer! Thank you!
This issue was referenced from https://groups.google.com/forum/m/#!msg/glidelibrary/GNiGFBMwyfE/ae9PXuYXAgAJ
I think will not work in multi threads, for SIZE_REQUEST
will this work in recyclerview @TWiStErRob
@mawenge it'll work as long as you don't add more stuff to it at usage. .load().into() is safe here, because all the item binding is done on the UI thread, sequentially. It is a request builder after all, so with load you change the model in the builder and with into you create the target and build the request; after which the builder can be changed again when the next bind needs it.
If you want to reuse it, but use a different placeholder/listener/diskCacheStrategy/..., you can always do SIZE_REQUEST.clone().load.....into.
so nice of your answer, there is another problem, for me锛孲IZE_REQUEST uses Uri to load source, but my image resource is from network by url, how do I handle this situation? thanks a lot @TWiStErRob
@mawenge Uri.parse((Strung)url) or change all the Uris in the code to String.
Thank you very much, you just solved my problem completely, thank you again @TWiStErRob
@mawenge glad I could help. _On behalf of your users I thank you for not using BitmapSizeTranscoder!_
If you don't want to display it, you can really save traffic by using .diskCacheStrategy(NONE) it'll likely just read the header which is much much faster. It then also becomes possible to cache the result, making it possible to use the size even offline, if it was requested before... if you're interested in this, just write below and I'll show you how to do it.
this means glide can just download first part of pic to know its size without loading whole pic in memory or cached in disk?
is that true?
Hi @TWiStErRob,
I am a little bit lost with all the changes brought by Glide v4. Is it possible to do the same thing with that new version of Glide? Is it simpler with the generated API?
Thanks a lot! :)
Also wondering how to do it in Glide v4. I need to show the image from a url and also know its original size.
Also needing the v4 implementation
I needed to port this from v3 to v4 also and I'll put code snippets here to show how. Note to developers: I imagine there are lots of folks like me that have no control over the source of images yet need to allow the user to select images based on size for better resource management. Examples would be artwork from Last.fm, Coverart, or more importantly a general Google image search.
This code is Kotlin but Java folks should have no trouble. The code to do the load would look something like:
Glide.with(activity)
.`as`(Size2::class.java)
.apply(sizeOptions)
.load(uri)
.into(object : SimpleTarget<Size2>() {
override fun onResourceReady(size: Size2, glideAnimation: Transition<in Size2>) {
imageToSizeMap.put(image, size)
holder.albumArtDescription.text = size.toString()
}
override fun onLoadFailed(errorDrawable: Drawable?) {
imageToSizeMap.put(image, Size2(-1, -1))
holder.albumArtDescription.setText(R.string.Unknown)
}
})
The resusable options are:
private val sizeOptions by lazy {
RequestOptions()
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.DATA)
}
My size class is approximately:
data class Size2(val width: Int, val height: Int) : Parcelable {
companion object {
@JvmField val CREATOR = createParcel { Size2(it) }
}
private constructor(parcelIn: Parcel) : this(parcelIn.readInt(), parcelIn.readInt())
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeInt(width)
dest.writeInt(height)
}
override fun describeContents() = 0
override fun toString(): String = "$width x $height"
}
Here's the relevant part of my AppGlideModule (less my loaders):
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry.prepend(File::class.java, BitmapFactory.Options::class.java, BitmapSizeDecoder())
registry.register(BitmapFactory.Options::class.java, Size2::class.java, OptionsSizeResourceTranscoder())
}
The decoder and transcoder are basically what TWiStErRob provide above. Thanks TWiStErRob.
Not large so I'll include these, ported to v4 and also Kotlin
class BitmapSizeDecoder : ResourceDecoder<File, BitmapFactory.Options> {
@Throws(IOException::class)
override fun handles(file: File, options: Options): Boolean {
return true
}
override fun decode(file: File, width: Int, height: Int, options: Options): Resource<BitmapFactory.Options>? {
val bmOptions: BitmapFactory.Options = BitmapFactory.Options()
bmOptions.inJustDecodeBounds = true
BitmapFactory.decodeFile(file.absolutePath, bmOptions)
return SimpleResource(bmOptions)
}
}
and
class OptionsSizeResourceTranscoder : ResourceTranscoder<BitmapFactory.Options, Size2> {
override fun transcode(resource: Resource<BitmapFactory.Options>, options: Options): Resource<Size2> {
val bmOptions = resource.get()
val size = Size2(bmOptions.outWidth, bmOptions.outHeight)
return SimpleResource(size)
}
}
@TWiStErRob @pandasys That looks cool code, but quite a hack method to me. Is it possible to provide a simple way like getImageSize() in Glide?
Glide supports EXIF by default. But the BitmapSizeDecoder doesn't have the support and we need to take care of it on our own. If you don't do this, when the image's EXIF orientation is 90 or 270, your width and height will be interchanged. You need to do something like below:
override fun decode(file: File, width: Int, height: Int, options: Options): Resource<BitmapFactory.Options>? {
val bmOptions: BitmapFactory.Options = BitmapFactory.Options()
bmOptions.inJustDecodeBounds = true
BitmapFactory.decodeFile(file.absolutePath, bmOptions)
val orientation = ExifInterface(file).getAttribute(TAG_ORIENTATION)?.toInt() ?: ORIENTATION_NORMAL
if (orientation == ORIENTATION_ROTATE_90 || orientation == ORIENTATION_ROTATE_270) {
val outHeight = bmOptions.outHeight
val outWidth = bmOptions.outWidth
bmOptions.outHeight = outWidth
bmOptions.outWidth = outHeight
}
return SimpleResource(bmOptions)
}
By the way, ExifInterface comes from androidx.exifinterface:exifinterface:1.1.0
Update the exception:
2020-09-01 17:48:04.711 14856-14891/com.vnindie.customglide E/GlideExecutor: Request threw uncaught throwable
java.lang.IllegalStateException: Failed to find any load path from class java.lang.String to class com.vnindie.customglide.MainActivity$ImageSize
at com.bumptech.glide.load.engine.ResourceCacheGenerator.startNext(ResourceCacheGenerator.java:57)
at com.bumptech.glide.load.engine.DecodeJob.runGenerators(DecodeJob.java:310)
at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:276)
at com.bumptech.glide.load.engine.DecodeJob.run(DecodeJob.java:234)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
at com.bumptech.glide.load.engine.executor.GlideExecutor$DefaultThreadFactory$1.run(GlideExecutor.java:393)
Not large so I'll include these, ported to v4 and also Kotlin
class BitmapSizeDecoder : ResourceDecoder<File, BitmapFactory.Options> { @Throws(IOException::class) override fun handles(file: File, options: Options): Boolean { return true } override fun decode(file: File, width: Int, height: Int, options: Options): Resource<BitmapFactory.Options>? { val bmOptions: BitmapFactory.Options = BitmapFactory.Options() bmOptions.inJustDecodeBounds = true BitmapFactory.decodeFile(file.absolutePath, bmOptions) return SimpleResource(bmOptions) } }and
class OptionsSizeResourceTranscoder : ResourceTranscoder<BitmapFactory.Options, Size2> { override fun transcode(resource: Resource<BitmapFactory.Options>, options: Options): Resource<Size2> { val bmOptions = resource.get() val size = Size2(bmOptions.outWidth, bmOptions.outHeight) return SimpleResource(size) } }
Sorry for disturbing, after follow your code, onResourceReady is not being called. Only onLoadFailed is called and errorDrawable return null.
Do you have any idea about this?
Thank you!
Most helpful comment
That's not possible like this.
getSizeis an internal mechanism to get theTarget's size, not the image's size. The target in this case wraps yourimageViewso it returns the view size after layout.wrap_contentis not handled yet, the fallback is screen width/height. The best approach for Android is that the ImageView should be filling the space without any image loaded in it, which ismatch_parentor a fixed size. With wrapping Glide doesn't really know how much space is available and can't effectively load the perfect-sized image. All phones should be able to handle images which are not greater than screen, hence the fallback.Add a
.listener()before.into()and ask theDrawable(default) orBitmap(.asBitmap()) how big it is; this will still return the same results though, because Glide loads the smallest possible size.Get original size
Getting the orginal size can be done with
BitmapFactorywith the correct options, if you really want it here's how to do it with Glide. Continue reading at your own risk ;)(_check out Glide Resource Flow for more info_)
Notice how
.diskCacheStrategyis added to both loads. It's necessary to prevent double-downloading from the network.LoggingListenerfrom wiki the other classes are built-in, and the key to the whole operation is the following class:It would make sense to cache the result, so the original file doesn't even need to be stored (or downloaded for that matter, only the partial header needs to be read from the network stream). Sadly the
cacheDecoderdecodes bothSOURCEandRESULTcache items, so you have to either peek at contents to decide if you need to decode a bitmap or just 8 bytes of two ints. See #707 for more info.If you don't want to display it, you can really save traffic by using
.diskCacheStrategy(NONE)it'll likely just read the header which is much much faster. It then also becomes possible to cache the result, making it possible to use the size even offline, if it was requested before... if you're interested in this, just write below and I'll show you how to do it.Taking it a step further
If you don't want to receive an
Optionsin the effective version above you can hide the conversion by transcoding:There's no
Sizeclass in Android before API 21, so roll you own (this class could be anything you already have):and the fast transcoder:
Much slower hacky alternative
The above code is the most efficient way to achieve what you wanted. There's an alternative, but it requires to load the image into memory, which I strongly advise against (just for proof of concept or debugging, please don't use it in production!):