Glide load into SimpleTarget<Bitmap> not honoring the specified width and height

Loudenvier picture Loudenvier · Jun 16, 2016 · Viewed 7.3k times · Source

I'm using Glide to load an image, resize it and save it to a file by means of a SimpleTarget<Bitmap>. These images will be uploaded to Amazon S3, but that's besides the point. I'm resizing the images prior to uploading as to save as much user's bandwidth as possible. For my app needs a 1024 pixel wide image is more than enough, so I'm using the following code to accomplish that:

final String to = getMyImageUrl();
final Context appCtx = context.getApplicationContext();

Glide.with(appCtx)
        .load(sourceImageUri)
        .asBitmap()
        .into(new SimpleTarget<Bitmap>(1024, 768) {
            @Override
            public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
                try {
                    FileOutputStream out = new FileOutputStream(to);
                    resource.compress(Bitmap.CompressFormat.JPEG, 70, out);
                    out.flush();
                    out.close();
                    MediaScannerConnection.scanFile(appCtx, new String[]{to}, null, null);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

It works almost perfectly, but the size of the resulting image is not 1024 pixels wide. Testing it with a source image with dimensions of 4160 x 2340 pixels the dimensions of the resulting saved image is 2080 x 1170 pixels.

I've tried playing with the width and height parameters passed to new SimpleTarget<Bitmap>(350, 350) and with these parameters the resulting image dimensions are 1040 x 585 pixels.

I really don't know what to do to make Glide respect the passed dimensions. In fact I'd like to resize the image proportionally so that the bigger dimension (either width or height) will be restricted to 1024 pixels and the smaller one resized accordingly (I believe I'll have to find a way to get the original image dimensions and then pass the width and height to SimpleTarget, but to do that I need Glide to respect the passed width and height!).

Does anyone have a clue what's going on? I'm using Glide 3.7.0.


Since this question itself may be useful for people trying to use Glide to resize and save images I believe it is in everyone's interest to provide my actual "solution" which relies on a new SimpleTargetimplementation that automatically saves the resized image:

import android.graphics.Bitmap;

import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;

import java.io.FileOutputStream;
import java.io.IOException;

public class FileTarget extends SimpleTarget<Bitmap> {
    public FileTarget(String fileName, int width, int height) {
        this(fileName, width, height, Bitmap.CompressFormat.JPEG, 70);
    }
    public FileTarget(String fileName, int width, int height, Bitmap.CompressFormat format, int quality) {
        super(width, height);
        this.fileName = fileName;
        this.format = format;
        this.quality = quality;
    }
    String fileName;
    Bitmap.CompressFormat format;
    int quality;
    public void onResourceReady(Bitmap bitmap, GlideAnimation anim) {
            try {
                FileOutputStream out = new FileOutputStream(fileName);
                bitmap.compress(format, quality, out);
                out.flush();
                out.close();
                onFileSaved();
            } catch (IOException e) {
                e.printStackTrace();
                onSaveException(e);
            }
    }
    public void onFileSaved() {
        // do nothing, should be overriden (optional)
    }
    public void onSaveException(Exception e) {
        // do nothing, should be overriden (optional)
    }

}

Using it is as simple as:

Glide.with(appCtx)
        .load(sourceImageUri)
        .asBitmap()
        .into(new FileTarget(to, 1024, 768) {
            @Override
            public void onFileSaved() {
                // do anything, or omit this override if you want
            }
        });

Answer

Loudenvier picture Loudenvier · Jun 16, 2016

After a good night's sleep I just figured it out! I had stumbled on an issue in Glide's github page which had the answer but I didn't realize it: I missed something in the explanation, which I now fully understood after resting for 10 hours. You should never underestimate the power of sleeping! But I digress. Here is the answer found on Glide's Github issue tracker:

Sizing the image usually has two phases:

  • Decoding/Downsampler read image from stream with inSampleSize
  • Transforming/BitmapTransformation take the Bitmap and match the exact target size

The decoding is always needed and is included in the flow, the default case is to match the target size with the "at least" downsampler, so when it comes to the transformation the image can be downsized more without quality loss (each pixel in the source will match at least 1.0 pixels and at most ~1.999 pixels) this can be controlled by asBitmap().at least|atMost|asIs|decoder(with downsampler)

The transformation and target size is automatic by default, but only when using a ViewTarget. When you load into an ImageView the size of that will be detected even when it has match_parent. Also if there's no explicit transformation there'll be one applied from scaleType. Thus results in a pixel perfect Bitmap for that image, meaning 1 pixel in Bitmap = 1 pixel on screen resulting in the best possible quality with the best memory usage and fast rendering (because there's no pixel mapping needed when drawing the image).

With a SimpleTarget you take on these responsibilities by providing a size on the constructor or via override() or implementing getSize if the sizing info is async-ly available only.

To fix your load add a transformation: .fitCenter|centerCrop(), your current applied transformation is .dontTransform() (Answer by Róbert Papp)

I got confused by this answer because of this:

With a SimpleTarget you take on these responsibilities by providing a size on the constructor or via override() or implementing getSize if the sizing info is async-ly available only.

Since I was passing the size I thought I got this covered already and such size should have been respected. I missed this important concept:

  • Decoding/Downsampler read image from stream with inSampleSize
  • Transforming/BitmapTransformation take the Bitmap and match the exact target size

And this:

To fix your load add a transformation: .fitCenter|centerCrop(), your current applied transformation is .dontTransform()

Now that I pieced it together it makes sense. Glide was only downsampling the image (first step in the sizing image flow as explained by Róbert) which gives an image with approximated dimensions. Let me say that Glide is very smart in this aspect. By using the downsampling method prior to resizing it avoids working with unnecessarily large bitmaps in memory and improves the resizing quality because downsampling to the exact size would compromise too many "important" pixels!

Since I didn't have any transformation applied to this loading pipeline, the sizing flow stopped in this first step (downsampling), and the resulting image had only approximate dimensions to my expected target size.

To solve it I just applied a .fitCenter() transform as shown bellow:

Glide.with(appCtx)
        .load(sourceImageUri)
        .asBitmap()
        .fitCenter()
        .into(new FileTarget(to, 1024, 768) {
            @Override
            public void onFileSaved() {
                // do anything, or omit this override if you want
            }
        });

The resulting image now have dimensions of 1024 x 576 pixels, which is exactly what I expected.

Glide is a very cool library!