Glide: Glide can not display very long (tall) image.

Created on 20 Oct 2015  路  15Comments  路  Source: bumptech/glide

Glide Version/Integration library (if any): 3.6.0
Device/Android Version: 6.0
Issue details/Repro steps/Use case background:
Using Glide to load a very long image into an image view. Width of the ImageView would match screen width. Image is going to scroll vertically so the user can see the whole picture by scrolling up and down.

But when HW accelerate is enabled. Image would not show and error message from log says Bitmap too large to be uploaded into a texture.

Glide load line:

Glide.with(context)
       .load(url)
       .placeholder(R.drawable.placeholder)
       .into(imgView);

Layout XML:
N/A, Layout is dynamically added,

ImageView imgView = new ImageView(context);
imgView.setScaleType(ImageView.ScaleType.CENTER_CROP);
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, parent_width*image_height/image_width);
mParentView.addView(imgView, lp);

Android suggests using _BitmapRegionDecoder_ to decode images by regions. So how can I use _BitmapRegionDecoder_ with Glide? and will Glide support display long image in future?

question

Most helpful comment

I've just created a big image viewer supporting pan and zoom, with very little memory usage and full featured image loading choices. Powered by Subsampling Scale Image View, Fresco, Glide, and Picasso https://github.com/Piasy/BigImageViewer , hope it helps :)

All 15 comments

can see #547 similar to this issue, but it's solution dose't work at all.

Partial decoding is not supported and not planned. However I think this issue only happens when the ImageView tries to draw, so I see two possible options:

  • use software rendering on that ImageView
  • write a custom BitmapTransformation that cuts some part of the image, less effective than BRD, but could work.

Please describe what your intention is with the big image, e.g. is it going to vertical scroll?

What are the numbers exactly (log line)? What is the XML layout? Can you please fill in the issue template? (Feel free to edit original comment)

@TWiStErRob Thanks for your reply. I've edited original comment.
for the solutions you suggested,
Use software rendering on that ImageView
I tried by calling

imgView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

but still can't see long image, log says

ImageView not displayed because it is too large to fit into a software layer (or drawing cache), needs 19975200 bytes, only 8294400 available

And for the other solution, I need to show the whole picture, so I can't cut some parts.

One of the solutions I can think of is to download the image by calling Glide downloadOnly, then decode and display the image via BitmapRegionDecoder.

One of the solutions I can think of is to download the image by calling Glide downloadOnly, then decode and display the image via BitmapRegionDecoder.

Remember that BitmapRegionDecoder will only load part of the image. So you still can't show the whole image in a single ImageView. However building on that idea, how about this approach?

Create a list (ListView/RecyclerView) where you load fixed sized chunks of the image into memory. This way there's no need to have a little more than a full screen's amount of pixels in Bitmaps in memory at any time.

  • I suggest something like half or quarter of the shorter dimension of the screen so the images will be manageable to Android and the GPU. Using fixed sizes will also help Glide to reuse Bitmaps when scrolling.
  • The data for the list's adapter is the Url and 2 numbers: the Bitmap's height. Which you can acquire by executing a .downloadOnly().get() on a background thread (AsyncTask), then using inJustDecodeBounds. Don't forget to scale the height down like you did in your "XML".
  • The acquired number can then be returned from AsyncTask.doInBackground and used in onPostExecute to create the adapter and update the list
  • The adapter can then figure out the following:

java // calculate how big on the image half a screen would be int fixedHeight = (screenHeight / 2) / (screenWidth / (float)imageWidth); // field initialized in constructor public int getCount() { return imageHeight / fixedHeight + (imageHeight % fixedHeight == 0? 0 : 1); } // round up for last partial row public Integer getItem(int position) { return fixedHeight * position; } public long getItemId(int position) { return getItem(position); } public boolean hasStableIds() { return true; }

  • in getView inflate/create an imageView with no padding/margin/etc... also try to remove any separator or gap between items so parts of the image merge seamlessly
  • in getView when you bind the to the ImageView you load part of the image between pixels getItem(position) and getItem(position) + fixedHeight
    Now this last part is tricky, you can try BitmapRegionDecoder yourself, but don't forget to downsample to screen width, do it in the background. Or you can create a custom ResourceDecoder which means something like:

``` java
Glide.with(this)
.load(url)
.asBitmap()
.fitCenter()
.diskCacheStrategy(ALL)
.imageDecoder(new RegionBitmapResourceDecoder(context, rect))
.into(holder.imageView)
;

class RegionBitmapResourceDecoder implements ResourceDecoder {
private final BitmapPool bitmapPool;
private final Rect region;
public RegionBitmapResourceDecoder(Context context, Rect region) {
this(Glide.get(context).getBitmapPool(), region);
}
public RegionBitmapResourceDecoder(BitmapPool bitmapPool, Rect region) {
this.bitmapPool = bitmapPool;
this.region = region;
}
@Override public Resource decode(InputStream source, int width, int height) throws IOException {
Bitmap bitmap = BitmapRegionDecoder.newInstance(source, false).decodeRegion(region, ...);
return BitmapResource.obtain(bitmap, bitmapPool);
}
@Override public String getId() {
return getClass().getName() + region.toShortString(); // + region is important for RESULT caching
}
}
```

One thing you should pay extra attention to is whether you're in the source image's coordinate system or in the View's coordinate system.
The image chunk loaded by decode() should be later downsampled to exactly the image size by .fitCenter().

@TWiStErRob how does glide will manage memory with RegionBitmapResourceDecoder? It will only give us an InputStream? Can we get OOM with this solution?

I went ahead and made a POC out of my idea: https://github.com/TWiStErRob/glide-support/tree/master/src/glide3/java/com/bumptech/glide/supportapp/github/_700_tall_image
@Kalyaganov you can pass in the pool and even use Options.inBitmap to re-use Bitmaps. I didn't go that far. Check out Glide's Downsampler class. I managed to load a 7388x16711 image this way.

public class TestFragment extends GlideRecyclerFragment {
    protected RecyclerView listView;

    @Override public @Nullable View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        RecyclerView view = new RecyclerView(container.getContext());
        view.setId(android.R.id.list);
        view.setLayoutParams(new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        view.setLayoutManager(new LinearLayoutManager(container.getContext()));
        return view;
    }

    @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        listView = (RecyclerView)view.findViewById(android.R.id.list);
        new AsyncTask<Void, Void, Point>() {
            String url = "http://imgfave-herokuapp-com.global.ssl.fastly.net/image_cache/142083463797243_tall.jpg";
            //String url = "https://upload.wikimedia.org/wikipedia/commons/a/a3/Berliner_Fernsehturm,_Sicht_vom_Neptunbrunnen_-_Berlin_Mitte.jpg";
            @Override protected Point doInBackground(Void[] params) {
                try {
                    File image = Glide
                            .with(TestFragment.this)
                            .load(url)
                            .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
                            .get();
                    Options opts = new Options();
                    opts.inJustDecodeBounds = true;
                    BitmapFactory.decodeFile(image.getAbsolutePath(), opts);
                    return new Point(opts.outWidth, opts.outHeight);
                } catch (InterruptedException | ExecutionException ignored) {
                    return null;
                }
            }
            @Override protected void onPostExecute(Point imageSize) {
                if (imageSize != null) {
                    listView.setAdapter(new ImageChunkAdapter(getScreenSize(), url, imageSize));
                }
            }
        }.execute();
    }

    private Point getScreenSize() {
        WindowManager window = (WindowManager)getActivity().getSystemService(Context.WINDOW_SERVICE);
        Display display = window.getDefaultDisplay();
        Point screen = new Point();
        if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR2) {
            display.getSize(screen);
        } else {
            screen.set(display.getWidth(), display.getHeight());
        }
        return screen;
    }
}

public class ImageChunkAdapter extends RecyclerView.Adapter<ImageChunkAdapter.ImageChunkViewHolder> {
    private final String url;
    private final Point image;
    private final int imageChunkHeight;
    private final float ratio;

    public ImageChunkAdapter(Point screen, String url, Point image) {
        this.url = url;
        this.image = image;

        // calculate a chunk's height
        this.ratio = screen.x / (float)image.x; // image will be fit to width
        // this will result in having the chunkHeight between 1/3 and 2/3 of screen height, making sure it fits in memory
        int minScreenChunkHeight = screen.y / 3;
        int screenChunkHeight = leastMultiple(screen.x / gcd(screen.x, image.x), minScreenChunkHeight);
        // GCD helps to keep this a whole number
        // worst case GCD is 1 so screenChunkHeight == screen.x -> imageChunkHeight == image.x
        this.imageChunkHeight = Math.round(screenChunkHeight / ratio);
        // screen: Point(720, 1280), image: Point(500, 4784), ratio: 1.44, screenChunk: 396 (396.000031), imageChunk: 275 (275)
        // screen: Point(1280, 720), image: Point(7388, 16711), ratio: 0.173254, screenChunk: 320 (320.000000), imageChunk: 1847 (1847.000000)
        Log.wtf("GLIDE", String.format(Locale.ROOT,
                "screen: %s, image: %s, ratio: %f, screenChunk: %d (%f), imageChunk: %d (%f)",
                screen, image, ratio,
                screenChunkHeight, imageChunkHeight * ratio,
                imageChunkHeight, screenChunkHeight / ratio));
    }

    /** Greatest Common Divisor */
    private static int gcd(int a, int b) {
        while (b != 0) {
            int t = b;
            b = a % b;
            a = t;
        }
        return a;
    }

    /**
     * @param base positive whole number
     * @param threshold positive whole number
     * @return multiple of base that is >= threshold
     */
    private static int leastMultiple(int base, int threshold) {
        int minMul = Math.max(1, threshold / base);
        return base * minMul;
    }

    @Override public int getItemCount() {
        // round up for last partial row
        return image.y / imageChunkHeight + (image.y % imageChunkHeight == 0? 0 : 1);
    }

    @Override public long getItemId(int position) {
        return imageChunkHeight * position;
    }

    @Override public ImageChunkViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ImageView view = new ImageView(parent.getContext());
        view.setScaleType(ScaleType.CENTER);
        view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, (int)(imageChunkHeight * ratio)));
        return new ImageChunkViewHolder(view);
    }

    @Override public void onBindViewHolder(ImageChunkViewHolder holder, int position) {
        int left = 0, top = imageChunkHeight * position;
        int width = image.x, height = imageChunkHeight;
        if (position == getItemCount() - 1 && image.y % imageChunkHeight != 0) {
            height = image.y % imageChunkHeight; // height of last partial row, if any
        }
        Rect rect = new Rect(left, top, left + width, top + height);
        float viewWidth = width * ratio;
        float viewHeight = height * ratio;

        final String bind = String.format(Locale.ROOT, "Binding %s w=%d (%d->%f) h=%d (%d->%f)",
                rect.toShortString(),
                rect.width(), width, viewWidth,
                rect.height(), height, viewHeight);

        Context context = holder.itemView.getContext();
        // See https://docs.google.com/drawings/d/1KyOJkNd5Dlm8_awZpftzW7KtqgNR6GURvuF6RfB210g/edit?usp=sharing
        Glide
                .with(context)
                .load(url)
                .asBitmap()
                .placeholder(new ColorDrawable(Color.BLUE))
                .error(new ColorDrawable(Color.RED))
                // overshoot a little so fitCenter uses width's ratio (see minPercentage)
                .override(Math.round(viewWidth), (int)Math.ceil(viewHeight))
                .fitCenter()
                // Cannot use .imageDecoder, only decoder; see bumptech/glide#708
                //.imageDecoder(new RegionStreamDecoder(context, rect))
                .decoder(new RegionImageVideoDecoder(context, rect))
                .cacheDecoder(new RegionFileDecoder(context, rect))
                // Cannot use RESULT cache; see bumptech/glide#707
                .diskCacheStrategy(DiskCacheStrategy.SOURCE)
                .listener(new RequestListener<String, Bitmap>() {
                    @Override public boolean onException(Exception e, String model, Target<Bitmap> target,
                            boolean isFirstResource) {
                        Log.wtf("GLIDE", String.format(Locale.ROOT, "%s %s into %s failed: %s",
                                bind, model, target, e), e);
                        return false;
                    }
                    @Override public boolean onResourceReady(Bitmap resource, String model, Target<Bitmap> target,
                            boolean isFromMemoryCache, boolean isFirstResource) {
                        View v = ((ViewTarget)target).getView();
                        LayoutParams p = v.getLayoutParams();
                        String targetString = String.format("%s(%dx%d->%dx%d)",
                                target, p.width, p.height, v.getWidth(), v.getHeight());
                        Log.wtf("GLIDE", String.format(Locale.ROOT, "%s %s into %s result %dx%d",
                                bind, model, targetString, resource.getWidth(), resource.getHeight()));
                        return false;
                    }
                })
                .into(new BitmapImageViewTarget(holder.imageView) {
                    @Override protected void setResource(Bitmap resource) {
                        if (resource != null) {
                            LayoutParams params = view.getLayoutParams();
                            if (params.height != resource.getHeight()) {
                                params.height = resource.getHeight();
                            }
                            view.setLayoutParams(params);
                        }
                        super.setResource(resource);
                    }
                })
        ;
    }

    static class ImageChunkViewHolder extends RecyclerView.ViewHolder {
        ImageView imageView;

        public ImageChunkViewHolder(View itemView) {
            super(itemView);
            imageView = (ImageView)itemView;
        }
    }
}

abstract class RegionResourceDecoder<T> implements ResourceDecoder<T, Bitmap> {
    private final BitmapPool bitmapPool;
    private final Rect region;
    public RegionResourceDecoder(Context context, Rect region) {
        this(Glide.get(context).getBitmapPool(), region);
    }
    public RegionResourceDecoder(BitmapPool bitmapPool, Rect region) {
        this.bitmapPool = bitmapPool;
        this.region = region;
    }

    @Override public Resource<Bitmap> decode(T source, int width, int height) throws IOException {
        Options opts = new Options();
        // Algorithm from Glide's Downsampler.getRoundedSampleSize
        int sampleSize = (int)Math.ceil((double)region.width() / (double)width);
        sampleSize = sampleSize == 0? 0 : Integer.highestOneBit(sampleSize);
        sampleSize = Math.max(1, sampleSize);
        opts.inSampleSize = sampleSize;

        BitmapRegionDecoder decoder = createDecoder(source, width, height);
        Bitmap bitmap = decoder.decodeRegion(region, opts);
        // probably not worth putting it into the pool because we'd need to get from the pool too to be efficient
        return BitmapResource.obtain(bitmap, bitmapPool);
    }
    protected abstract BitmapRegionDecoder createDecoder(T source, int width, int height) throws IOException;

    @Override public String getId() {
        return getClass().getName() + region; // + region is important for RESULT caching
    }
}

class RegionImageVideoDecoder extends RegionResourceDecoder<ImageVideoWrapper> {
    public RegionImageVideoDecoder(Context context, Rect region) {
        super(context, region);
    }

    @Override protected BitmapRegionDecoder createDecoder(ImageVideoWrapper source, int width, int height) throws IOException {
        try {
            return BitmapRegionDecoder.newInstance(source.getStream(), false);
        } catch (Exception ignore) {
            return BitmapRegionDecoder.newInstance(source.getFileDescriptor().getFileDescriptor(), false);
        }
    }
}

class RegionFileDecoder extends RegionResourceDecoder<File> {
    public RegionFileDecoder(Context context, Rect region) {
        super(context, region);
    }

    @Override protected BitmapRegionDecoder createDecoder(File source, int width, int height) throws IOException {
        return BitmapRegionDecoder.newInstance(source.getAbsolutePath(), false);
    }
}

I'm closing this because there isn't much we can do about this Android system limitation.

If someone is not satisfied with the complex workaround I gave above, this may also help to load the biggest possible image with hardware layers (not tested):

.asBitmap()
.atMost()
.override(maxDeviceTextureWidth, maxDeviceTextureHeight)

Thank you for your solution. I took some interesting ideas from it.

@Kalyaganov Did you have solve the problem, i also encounter the same problem.

@t2314862168 Actualy right now i am using extended Picasso library for loading long images. You can see how it works in this app https://play.google.com/store/apps/details?id=ru.futurobot.pikabuclient
But my solution not flexible and i do not like it. So i think i`m going to use glide in the future. I will post link to my solution with glide later, when it will be done.

P.S. 9GAG app also have solution of displaying long images https://play.google.com/store/apps/details?id=com.ninegag.android.app but unfortunately app is obfuscated.

@t2314862168 https://github.com/futurobot/Long-Images-with-Glide here is my undone yet solution. It written on Kotlin. I need to work on performance.

I found a project https://github.com/LuckyJayce/LargeImage , but it can't operate like sina weibo app performance well.
@Kalyaganov Thank you for your prompt reply,I will test you projectl,if i have some question , i will tell me. Thank you

@t2314862168 take a look at this library https://github.com/futurobot/ByakuGallery

@futurobot if you have free time ,can you tell me how to solve the problem of https://github.com/diegocarloslima/ByakuGallery/issues/16

I've just created a big image viewer supporting pan and zoom, with very little memory usage and full featured image loading choices. Powered by Subsampling Scale Image View, Fresco, Glide, and Picasso https://github.com/Piasy/BigImageViewer , hope it helps :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sant527 picture sant527  路  3Comments

StefMa picture StefMa  路  3Comments

billy2271 picture billy2271  路  3Comments

mttmllns picture mttmllns  路  3Comments

Tryking picture Tryking  路  3Comments