Skip to content

Commit 042f6b5

Browse files
committed
Avoid wrapping byte[] and ByteBuffer in streams.
The streams use a buffered stream where the limited size buffer can cause failures if parsing metadata requries reading past the buffer size. This is unnecessary for these two types because both already have all data in memory or otherwise handle buffering internally. Avoiding the buffer also saves some memory overhead.
1 parent a295828 commit 042f6b5

File tree

8 files changed

+166
-9
lines changed

8 files changed

+166
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.bumptech.glide;
2+
3+
import static com.google.common.truth.Truth.assertThat;
4+
5+
import android.content.ContentResolver;
6+
import android.content.Context;
7+
import android.content.res.Resources;
8+
import android.graphics.Bitmap;
9+
import android.net.Uri;
10+
import androidx.test.core.app.ApplicationProvider;
11+
import androidx.test.ext.junit.runners.AndroidJUnit4;
12+
import com.bumptech.glide.load.model.UnitModelLoader;
13+
import com.bumptech.glide.test.ConcurrencyHelper;
14+
import com.bumptech.glide.test.ResourceIds;
15+
import com.bumptech.glide.test.TearDownGlide;
16+
import java.io.ByteArrayOutputStream;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.nio.ByteBuffer;
20+
import java.util.Objects;
21+
import org.junit.Rule;
22+
import org.junit.Test;
23+
import org.junit.rules.TestRule;
24+
import org.junit.runner.RunWith;
25+
26+
@RunWith(AndroidJUnit4.class)
27+
public class LargeImageTest {
28+
@Rule public final TestRule tearDownGlide = new TearDownGlide();
29+
private final ConcurrencyHelper concurrency = new ConcurrencyHelper();
30+
private final Context context = ApplicationProvider.getApplicationContext();
31+
32+
@Test
33+
public void loadLargeJpeg_asByteArray_succeeds() throws IOException {
34+
byte[] data = getLargeImageBytes();
35+
Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(data).submit());
36+
assertThat(bitmap).isNotNull();
37+
}
38+
39+
@Test
40+
public void loadLargeJpeg_asByteBuffer_succeeds() throws IOException {
41+
// Using UnitModelLoader lets us mimic loading the ByteBuffer from some other data type, which
42+
// reduces the scope of our test.
43+
Glide.get(context)
44+
.getRegistry()
45+
.append(
46+
ByteBuffer.class, ByteBuffer.class, UnitModelLoader.Factory.<ByteBuffer>getInstance());
47+
48+
ByteBuffer buffer = ByteBuffer.wrap(getLargeImageBytes());
49+
Bitmap bitmap = concurrency.get(Glide.with(context).asBitmap().load(buffer).submit());
50+
assertThat(bitmap).isNotNull();
51+
}
52+
53+
private byte[] getLargeImageBytes() throws IOException {
54+
Resources resources = context.getResources();
55+
int resourceId = ResourceIds.raw.canonical_large;
56+
Uri uri =
57+
new Uri.Builder()
58+
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
59+
.authority(resources.getResourcePackageName(resourceId))
60+
.appendPath(resources.getResourceTypeName(resourceId))
61+
.appendPath(resources.getResourceEntryName(resourceId))
62+
.build();
63+
64+
InputStream is = Objects.requireNonNull(context.getContentResolver().openInputStream(uri));
65+
ByteArrayOutputStream os = new ByteArrayOutputStream();
66+
byte[] buffer = new byte[1024 * 1024 * 5];
67+
int read;
68+
while ((read = is.read(buffer, 0, buffer.length)) != -1) {
69+
os.write(buffer, 0, read);
70+
}
71+
return os.toByteArray();
72+
}
73+
}

instrumentation/src/androidTest/java/com/bumptech/glide/test/ResourceIds.java

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ private ResourceIds() {
1616
public interface raw {
1717
int dl_world_anim = getResourceId("raw", "dl_world_anim");
1818
int canonical = getResourceId("raw", "canonical");
19+
int canonical_large = getResourceId("raw", "canonical_large");
1920
int canonical_png = getResourceId("raw", "canonical_png");
2021
int canonical_transparent_png = getResourceId("raw", "canonical_transparent_png");
2122
int interlaced_transparent_gif = getResourceId("raw", "interlaced_transparent_gif");
Loading

library/src/main/java/com/bumptech/glide/load/ImageHeaderParserUtils.java

+26
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,32 @@ private static ImageType getTypeInternal(
123123
return ImageType.UNKNOWN;
124124
}
125125

126+
/**
127+
* Returns the result from the first of {@code parsers} that returns something other than {@link
128+
* ImageHeaderParser#UNKNOWN_ORIENTATION}.
129+
*
130+
* <p>If {@code buffer} is null, the parers list is empty, or none of the parsers returns a valid
131+
* value, {@link ImageHeaderParser#UNKNOWN_ORIENTATION} is returned.
132+
*/
133+
public static int getOrientation(
134+
@NonNull List<ImageHeaderParser> parsers,
135+
@Nullable final ByteBuffer buffer,
136+
@NonNull final ArrayPool arrayPool)
137+
throws IOException {
138+
if (buffer == null) {
139+
return ImageHeaderParser.UNKNOWN_ORIENTATION;
140+
}
141+
142+
return getOrientationInternal(
143+
parsers,
144+
new OrientationReader() {
145+
@Override
146+
public int getOrientation(ImageHeaderParser parser) throws IOException {
147+
return parser.getOrientation(buffer, arrayPool);
148+
}
149+
});
150+
}
151+
126152
/** Returns the orientation for the given InputStream. */
127153
public static int getOrientation(
128154
@NonNull List<ImageHeaderParser> parsers,

library/src/main/java/com/bumptech/glide/load/resource/bitmap/ByteBufferBitmapDecoder.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
import com.bumptech.glide.load.Options;
66
import com.bumptech.glide.load.ResourceDecoder;
77
import com.bumptech.glide.load.engine.Resource;
8-
import com.bumptech.glide.util.ByteBufferUtil;
98
import java.io.IOException;
10-
import java.io.InputStream;
119
import java.nio.ByteBuffer;
1210

1311
/** Decodes {@link android.graphics.Bitmap Bitmaps} from {@link java.nio.ByteBuffer ByteBuffers}. */
@@ -27,7 +25,6 @@ public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) {
2725
public Resource<Bitmap> decode(
2826
@NonNull ByteBuffer source, int width, int height, @NonNull Options options)
2927
throws IOException {
30-
InputStream is = ByteBufferUtil.toStream(source);
31-
return downsampler.decode(is, width, height, options);
28+
return downsampler.decode(source, width, height, options);
3229
}
3330
}

library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java

+15
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,21 @@ public Resource<Bitmap> decode(InputStream is, int outWidth, int outHeight, Opti
177177
return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS);
178178
}
179179

180+
/**
181+
* Identical to {@link #decode(InputStream, int, int, Options)}, except that it accepts a {@link
182+
* ByteBuffer} in place of an {@link InputStream}.
183+
*/
184+
public Resource<Bitmap> decode(
185+
ByteBuffer buffer, int requestedWidth, int requestedHeight, Options options)
186+
throws IOException {
187+
return decode(
188+
new ImageReader.ByteBufferReader(buffer, parsers, byteArrayPool),
189+
requestedWidth,
190+
requestedHeight,
191+
options,
192+
EMPTY_CALLBACKS);
193+
}
194+
180195
/**
181196
* Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF
182197
* data present in the stream and that is downsampled according to the given dimensions and any

library/src/main/java/com/bumptech/glide/load/resource/bitmap/ImageReader.java

+41
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@
22

33
import android.graphics.Bitmap;
44
import android.graphics.BitmapFactory;
5+
import android.graphics.BitmapFactory.Options;
56
import android.os.Build;
67
import android.os.ParcelFileDescriptor;
78
import androidx.annotation.Nullable;
89
import androidx.annotation.RequiresApi;
910
import com.bumptech.glide.load.ImageHeaderParser;
11+
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
1012
import com.bumptech.glide.load.ImageHeaderParserUtils;
1113
import com.bumptech.glide.load.data.DataRewinder;
1214
import com.bumptech.glide.load.data.InputStreamRewinder;
1315
import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder;
1416
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
17+
import com.bumptech.glide.util.ByteBufferUtil;
1518
import com.bumptech.glide.util.Preconditions;
1619
import java.io.IOException;
1720
import java.io.InputStream;
21+
import java.nio.ByteBuffer;
1822
import java.util.List;
1923

2024
/**
@@ -31,6 +35,43 @@ interface ImageReader {
3135

3236
void stopGrowingBuffers();
3337

38+
final class ByteBufferReader implements ImageReader {
39+
40+
private final ByteBuffer buffer;
41+
private final List<ImageHeaderParser> parsers;
42+
private final ArrayPool byteArrayPool;
43+
44+
ByteBufferReader(ByteBuffer buffer, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool) {
45+
this.buffer = buffer;
46+
this.parsers = parsers;
47+
this.byteArrayPool = byteArrayPool;
48+
}
49+
50+
@Nullable
51+
@Override
52+
public Bitmap decodeBitmap(Options options) {
53+
return BitmapFactory.decodeStream(stream(), /* outPadding= */ null, options);
54+
}
55+
56+
@Override
57+
public ImageType getImageType() throws IOException {
58+
return ImageHeaderParserUtils.getType(parsers, ByteBufferUtil.rewind(buffer));
59+
}
60+
61+
@Override
62+
public int getImageOrientation() throws IOException {
63+
return ImageHeaderParserUtils.getOrientation(
64+
parsers, ByteBufferUtil.rewind(buffer), byteArrayPool);
65+
}
66+
67+
@Override
68+
public void stopGrowingBuffers() {}
69+
70+
private InputStream stream() {
71+
return ByteBufferUtil.toStream(ByteBufferUtil.rewind(buffer));
72+
}
73+
}
74+
3475
final class InputStreamImageReader implements ImageReader {
3576
private final InputStreamRewinder dataRewinder;
3677
private final ArrayPool byteArrayPool;

library/src/main/java/com/bumptech/glide/util/ByteBufferUtil.java

+9-5
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public static ByteBuffer fromFile(@NonNull File file) throws IOException {
6060
}
6161

6262
public static void toFile(@NonNull ByteBuffer buffer, @NonNull File file) throws IOException {
63-
buffer.position(0);
63+
rewind(buffer);
6464
RandomAccessFile raf = null;
6565
FileChannel channel = null;
6666
try {
@@ -120,7 +120,7 @@ public static byte[] toBytes(@NonNull ByteBuffer byteBuffer) {
120120
} else {
121121
ByteBuffer toCopy = byteBuffer.asReadOnlyBuffer();
122122
result = new byte[toCopy.limit()];
123-
toCopy.position(0);
123+
rewind(toCopy);
124124
toCopy.get(result);
125125
}
126126
return result;
@@ -150,7 +150,11 @@ public static ByteBuffer fromStream(@NonNull InputStream stream) throws IOExcept
150150
byte[] bytes = outStream.toByteArray();
151151

152152
// Some resource decoders require a direct byte buffer. Prefer allocateDirect() over wrap()
153-
return (ByteBuffer) ByteBuffer.allocateDirect(bytes.length).put(bytes).position(0);
153+
return rewind(ByteBuffer.allocateDirect(bytes.length).put(bytes));
154+
}
155+
156+
public static ByteBuffer rewind(ByteBuffer buffer) {
157+
return (ByteBuffer) buffer.position(0);
154158
}
155159

156160
@Nullable
@@ -208,7 +212,7 @@ public boolean markSupported() {
208212
}
209213

210214
@Override
211-
public int read(@NonNull byte[] buffer, int byteOffset, int byteCount) throws IOException {
215+
public int read(@NonNull byte[] buffer, int byteOffset, int byteCount) {
212216
if (!byteBuffer.hasRemaining()) {
213217
return -1;
214218
}
@@ -227,7 +231,7 @@ public synchronized void reset() throws IOException {
227231
}
228232

229233
@Override
230-
public long skip(long byteCount) throws IOException {
234+
public long skip(long byteCount) {
231235
if (!byteBuffer.hasRemaining()) {
232236
return -1;
233237
}

0 commit comments

Comments
 (0)