package com.bumptech.glide.load.resource.bitmap;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool;
import com.bumptech.glide.testutil.TestResourceUtil;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.Util;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 18)
public class DefaultImageHeaderParserTest {
private static final byte[] PNG_HEADER_WITH_IHDR_CHUNK =
new byte[] { (byte) 0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa, 0x0, 0x0, 0x0, 0xd, 0x49,
0x48, 0x44, 0x52, 0x0, 0x0, 0x1, (byte) 0x90, 0x0, 0x0, 0x1, 0x2c, 0x8, 0x6 };
private ArrayPool byteArrayPool;
@Before
public void setUp() {
byteArrayPool = new LruArrayPool();
}
@Test
public void testCanParsePngType() throws IOException {
// PNG magic number from: http://en.wikipedia.org/wiki/Portable_Network_Graphics.
byte[] data = new byte[] { (byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
runTest(data, new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.PNG, parser.getType(is));
}
@Override
public void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer,
ArrayPool byteArrayPool) throws IOException {
assertEquals(ImageType.PNG, parser.getType(byteBuffer));
}
});
}
@Test
public void testCanParsePngWithAlpha() throws IOException {
for (int i = 3; i <= 6; i++) {
byte[] pngHeaderWithIhdrChunk = generatePngHeaderWithIhdr(i);
runTest(pngHeaderWithIhdrChunk, new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.PNG_A, parser.getType(is));
}
@Override
public void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer,
ArrayPool byteArrayPool) throws IOException {
assertEquals(ImageType.PNG_A, parser.getType(byteBuffer));
}
});
}
}
@Test
public void testCanParsePngWithoutAlpha() throws IOException {
for (int i = 0; i < 3; i++) {
byte[] pngHeaderWithIhdrChunk = generatePngHeaderWithIhdr(i);
runTest(pngHeaderWithIhdrChunk, new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.PNG, parser.getType(is));
}
@Override
public void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer,
ArrayPool byteArrayPool) throws IOException {
assertEquals(ImageType.PNG, parser.getType(byteBuffer));
}
});
}
}
@Test
public void testCanParseJpegType() throws IOException {
byte[] data = new byte[] { (byte) 0xFF, (byte) 0xD8 };
runTest(data, new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.JPEG, parser.getType(is));
}
@Override
public void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer,
ArrayPool byteArrayPool) throws IOException {
assertEquals(ImageType.JPEG, parser.getType(byteBuffer));
}
});
}
@Test
public void testCanParseGifType() throws IOException {
byte[] data = new byte[] { 'G', 'I', 'F' };
runTest(data, new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.GIF, parser.getType(is));
}
@Override
public void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer,
ArrayPool byteArrayPool) throws IOException {
assertEquals(ImageType.GIF, parser.getType(byteBuffer));
}
});
}
@Test
public void testCanParseWebpWithAlpha() throws IOException {
byte[] data = new byte[] { 0x52, 0x49, 0x46, 0x46, 0x3c, 0x50, 0x00, 0x00, 0x57, 0x45, 0x42,
0x50, 0x56, 0x50, 0x38, 0x4c, 0x30, 0x50, 0x00, 0x00, 0x2f, (byte) 0xef, (byte) 0x80, 0x15,
0x10, (byte) 0x8d, 0x30, 0x68, 0x1b, (byte) 0xc9, (byte) 0x91, (byte) 0xb2 };
runTest(data, new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.WEBP_A, parser.getType(is));
}
@Override
public void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer,
ArrayPool byteArrayPool) throws IOException {
assertEquals(ImageType.WEBP_A, parser.getType(byteBuffer));
}
});
}
@Test
public void testCanParseWebpWithoutAlpha() throws IOException {
byte[] data = new byte[] { 0x52, 0x49, 0x46, 0x46, 0x72, 0x1c, 0x00, 0x00, 0x57, 0x45, 0x42,
0x50, 0x56, 0x50, 0x38, 0x20, 0x66, 0x1c, 0x00, 0x00, 0x30, 0x3c, 0x01, (byte) 0x9d, 0x01,
0x2a, 0x52, 0x02, (byte) 0x94, 0x03, 0x00, (byte) 0xc7 };
runTest(data, new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.WEBP, parser.getType(is));
}
@Override
public void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer,
ArrayPool byteArrayPool) throws IOException {
assertEquals(ImageType.WEBP, parser.getType(byteBuffer));
}
});
}
@Test
public void testReturnsUnknownTypeForUnknownImageHeaders() throws IOException {
byte[] data = new byte[] { 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 };
runTest(data, new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.UNKNOWN, parser.getType(is));
}
@Override
public void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer,
ArrayPool byteArrayPool) throws IOException {
assertEquals(ImageType.UNKNOWN, parser.getType(byteBuffer));
}
});
}
// Test for #286.
@Test
public void testHandlesParsingOrientationWithMinimalExifSegment() throws IOException {
byte[] data =
Util.readBytes(TestResourceUtil.openResource(getClass(), "short_exif_sample.jpg"));
runTest(data, new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(-1, parser.getOrientation(is, byteArrayPool));
}
@Override
public void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer,
ArrayPool byteArrayPool) throws IOException {
assertEquals(-1, parser.getOrientation(byteBuffer, byteArrayPool));
}
});
}
@Test
public void testReturnsUnknownForEmptyData() throws IOException {
runTest(new byte[0], new ParserTestCase() {
@Override
public void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException {
assertEquals(ImageType.UNKNOWN, parser.getType(is));
}
@Override
public void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer,
ArrayPool byteArrayPool) throws IOException {
assertEquals(ImageType.UNKNOWN, parser.getType(byteBuffer));
}
});
}
// Test for #387.
@Test
public void testHandlesPartialReads() throws IOException {
InputStream is = TestResourceUtil.openResource(getClass(), "issue387_rotated_jpeg.jpg");
DefaultImageHeaderParser parser = new DefaultImageHeaderParser();
assertThat(parser.getOrientation(new PartialReadInputStream(is), byteArrayPool)).isEqualTo(6);
}
// Test for #387.
@Test
public void testHandlesPartialSkips() throws IOException {
InputStream is = TestResourceUtil.openResource(getClass(), "issue387_rotated_jpeg.jpg");
DefaultImageHeaderParser parser = new DefaultImageHeaderParser();
assertThat(parser.getOrientation(new PartialSkipInputStream(is), byteArrayPool)).isEqualTo(6);
}
@Test
public void testHandlesSometimesZeroSkips() throws IOException {
InputStream is = new ByteArrayInputStream(
new byte[] { (byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a });
DefaultImageHeaderParser parser =
new DefaultImageHeaderParser();
assertEquals(ImageType.PNG, parser.getType(new SometimesZeroSkipInputStream(is)));
}
@Test
public void getOrientation_withExifSegmentLessThanLength_returnsUnknown() throws IOException {
ByteBuffer jpegHeaderBytes = getExifMagicNumber();
byte[] data = new byte[] {
jpegHeaderBytes.get(0), jpegHeaderBytes.get(1),
(byte) DefaultImageHeaderParser.SEGMENT_START_ID,
(byte) DefaultImageHeaderParser.EXIF_SEGMENT_TYPE,
// SEGMENT_LENGTH
(byte) 0xFF, (byte) 0xFF,
};
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
DefaultImageHeaderParser parser = new DefaultImageHeaderParser();
assertEquals(ImageHeaderParser.UNKNOWN_ORIENTATION,
parser.getOrientation(byteBuffer, byteArrayPool));
}
@Test
public void getOrientation_withNonExifSegmentLessThanLength_returnsUnknown() throws IOException {
ByteBuffer jpegHeaderBytes = getExifMagicNumber();
byte[] data = new byte[] {
jpegHeaderBytes.get(0), jpegHeaderBytes.get(1),
(byte) DefaultImageHeaderParser.SEGMENT_START_ID,
// SEGMENT_TYPE (NOT EXIF_SEGMENT_TYPE)
(byte) 0xE5,
// SEGMENT_LENGTH
(byte) 0xFF, (byte) 0xFF,
};
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
DefaultImageHeaderParser parser = new DefaultImageHeaderParser();
assertEquals(ImageHeaderParser.UNKNOWN_ORIENTATION,
parser.getOrientation(byteBuffer, byteArrayPool));
}
@Test
public void getOrientation_withExifSegmentAndPreambleButLessThanLength_returnsUnknown()
throws IOException {
ByteBuffer jpegHeaderBytes = getExifMagicNumber();
ByteBuffer exifSegmentPreamble =
ByteBuffer.wrap(DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES);
ByteBuffer data = ByteBuffer.allocate(2 + 1 + 1 + 2 + exifSegmentPreamble.capacity());
data.put(jpegHeaderBytes)
.put((byte) DefaultImageHeaderParser.SEGMENT_START_ID)
.put((byte) DefaultImageHeaderParser.EXIF_SEGMENT_TYPE)
// SEGMENT_LENGTH, add two because length includes the segment length short, and one to go
// beyond the preamble bytes length for the test.
.putShort(
(short) (DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length + 2 + 1))
.put(DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES);
data.position(0);
DefaultImageHeaderParser parser = new DefaultImageHeaderParser();
assertEquals(ImageHeaderParser.UNKNOWN_ORIENTATION, parser.getOrientation(data, byteArrayPool));
}
@Test
public void getOrientation_withExifSegmentAndPreambleBetweenLengthAndExpected_returnsUnknown()
throws IOException {
ByteBuffer jpegHeaderBytes = getExifMagicNumber();
ByteBuffer exifSegmentPreamble =
ByteBuffer.wrap(DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES);
ByteBuffer data = ByteBuffer.allocate(2 + 1 + 1 + 2 + exifSegmentPreamble.capacity() + 2 + 1);
data.put(jpegHeaderBytes)
.put((byte) DefaultImageHeaderParser.SEGMENT_START_ID)
.put((byte) DefaultImageHeaderParser.EXIF_SEGMENT_TYPE)
// SEGMENT_LENGTH, add two because length includes the segment length short, and one to go
// beyond the preamble bytes length for the test.
.putShort(
(short) (DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length + 2 + 1))
.put(DefaultImageHeaderParser.JPEG_EXIF_SEGMENT_PREAMBLE_BYTES);
data.position(0);
DefaultImageHeaderParser parser = new DefaultImageHeaderParser();
assertEquals(ImageHeaderParser.UNKNOWN_ORIENTATION, parser.getOrientation(data, byteArrayPool));
}
private static ByteBuffer getExifMagicNumber() {
ByteBuffer jpegHeaderBytes = ByteBuffer.allocate(2);
jpegHeaderBytes.putShort((short) DefaultImageHeaderParser.EXIF_MAGIC_NUMBER);
jpegHeaderBytes.position(0);
return jpegHeaderBytes;
}
private interface ParserTestCase {
void run(DefaultImageHeaderParser parser, InputStream is, ArrayPool byteArrayPool)
throws IOException;
void run(DefaultImageHeaderParser parser, ByteBuffer byteBuffer, ArrayPool byteArrayPool)
throws IOException;
}
private static void runTest(byte[] data, ParserTestCase test) throws IOException {
InputStream is = new ByteArrayInputStream(data);
DefaultImageHeaderParser parser = new DefaultImageHeaderParser();
test.run(parser, is, new LruArrayPool());
ByteBuffer buffer = ByteBuffer.wrap(data);
parser = new DefaultImageHeaderParser();
test.run(parser, buffer, new LruArrayPool());
}
private static byte[] generatePngHeaderWithIhdr(int bitDepth) {
byte[] result = new byte[PNG_HEADER_WITH_IHDR_CHUNK.length];
System.arraycopy(PNG_HEADER_WITH_IHDR_CHUNK, 0, result, 0, PNG_HEADER_WITH_IHDR_CHUNK.length);
result[result.length - 1] = (byte) bitDepth;
return result;
}
private static class SometimesZeroSkipInputStream extends FilterInputStream {
boolean returnZeroFlag = true;
protected SometimesZeroSkipInputStream(InputStream in) {
super(in);
}
@Override
public long skip(long byteCount) throws IOException {
final long result;
if (returnZeroFlag) {
result = 0;
} else {
result = super.skip(byteCount);
}
returnZeroFlag = !returnZeroFlag;
return result;
}
}
private static class PartialSkipInputStream extends FilterInputStream {
protected PartialSkipInputStream(InputStream in) {
super(in);
}
@Override
public long skip(long byteCount) throws IOException {
long toActuallySkip = byteCount / 2;
if (byteCount == 1) {
toActuallySkip = 1;
}
return super.skip(toActuallySkip);
}
}
private static class PartialReadInputStream extends FilterInputStream {
protected PartialReadInputStream(InputStream in) {
super(in);
}
@Override
public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
int toActuallyRead = byteCount / 2;
if (byteCount == 1) {
toActuallyRead = 1;
}
return super.read(buffer, byteOffset, toActuallyRead);
}
}
}