1package com.bumptech.glide.load.resource.bitmap;
2
3import android.util.Log;
4
5import java.io.IOException;
6import java.io.InputStream;
7import java.nio.ByteBuffer;
8import java.nio.ByteOrder;
9
10import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.GIF;
11import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.JPEG;
12import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.PNG;
13import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.PNG_A;
14import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.UNKNOWN;
15
16/**
17 * A class for parsing the exif orientation from an InputStream for an image. Handles jpegs and tiffs.
18 */
19public class ImageHeaderParser {
20    private static final String TAG = "ImageHeaderParser";
21
22    public static enum ImageType {
23        /** GIF type */
24        GIF(true),
25        /** JPG type */
26        JPEG(false),
27        /** PNG type with alpha */
28        PNG_A(true),
29        /** PNG type without alpha */
30        PNG(false),
31        /** Unrecognized type */
32        UNKNOWN(false);
33        private final boolean hasAlpha;
34
35        ImageType(boolean hasAlpha) {
36            this.hasAlpha = hasAlpha;
37        }
38
39        public boolean hasAlpha() {
40            return hasAlpha;
41        }
42    }
43
44    private static final int GIF_HEADER = 0x474946;
45    private static final int PNG_HEADER = 0x89504E47;
46    private static final int EXIF_MAGIC_NUMBER = 0xFFD8;
47    private static final int MOTOROLA_TIFF_MAGIC_NUMBER = 0x4D4D;  // "MM"
48    private static final int INTEL_TIFF_MAGIC_NUMBER = 0x4949;     // "II"
49    private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0";
50
51    private static final int SEGMENT_SOS = 0xDA;
52    private static final int MARKER_EOI = 0xD9;
53
54    private static final int SEGMENT_START_ID = 0xFF;
55    private static final int EXIF_SEGMENT_TYPE = 0xE1;
56
57    private static final int ORIENTATION_TAG_TYPE = 0x0112;
58
59    private static final int[] BYTES_PER_FORMAT = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 };
60
61    private final StreamReader streamReader;
62
63    public ImageHeaderParser(InputStream is) {
64        streamReader = new StreamReader(is);
65    }
66
67    // 0xD0A3C68 -> <htm
68    // 0xCAFEBABE -> <!DOCTYPE...
69    public boolean hasAlpha() throws IOException {
70        return getType().hasAlpha();
71    }
72
73    public ImageType getType() throws IOException {
74        int firstByte = streamReader.getUInt8();
75
76        if (firstByte == EXIF_MAGIC_NUMBER >> 8) { //JPEG
77            return JPEG;
78        }
79
80        final int firstTwoBytes = firstByte << 8 & 0xFF00 | streamReader.getUInt8() & 0xFF;
81        final int firstFourBytes = firstTwoBytes << 16 & 0xFFFF0000 | streamReader.getUInt16() & 0xFFFF;
82        if (firstFourBytes == PNG_HEADER) { //PNG
83            //see: http://stackoverflow.com/questions/2057923/how-to-check-a-png-for-grayscale-alpha-color-type
84            streamReader.skip(25 - 4);
85            int alpha = streamReader.getByte();
86            // A RGB indexed PNG can also have transparency. Better safe than sorry!
87            return alpha >= 3 ? PNG_A : PNG;
88        }
89
90        if (firstFourBytes >> 8 == GIF_HEADER) { //GIF from first 3 bytes
91            return GIF;
92        }
93
94        return UNKNOWN;
95    }
96
97    /**
98     * Parse the orientation from the image header. If it doesn't handle this image type (or this is not an image)
99     * it will return a default value rather than throwing an exception.
100     *
101     * @return The exif orientation if present or -1 if the header couldn't be parsed or doesn't contain an orientation
102     * @throws IOException
103     */
104    public int getOrientation() throws IOException {
105        final int magicNumber = streamReader.getUInt16();
106
107        if (!handles(magicNumber)) {
108            return -1;
109        } else {
110            byte[] exifData = getExifSegment();
111            if (exifData != null && exifData.length >= JPEG_EXIF_SEGMENT_PREAMBLE.length()
112                    && new String(exifData, 0, JPEG_EXIF_SEGMENT_PREAMBLE.length())
113                        .equalsIgnoreCase(JPEG_EXIF_SEGMENT_PREAMBLE)) {
114                return parseExifSegment(new RandomAccessReader(exifData));
115            } else {
116                return -1;
117            }
118        }
119    }
120
121    private byte[] getExifSegment() throws IOException {
122        short segmentId, segmentType;
123        int segmentLength;
124        while (true) {
125            segmentId = streamReader.getUInt8();
126
127            if (segmentId != SEGMENT_START_ID) {
128                if (Log.isLoggable(TAG, Log.DEBUG)) {
129                    Log.d(TAG, "Unknown segmentId=" + segmentId);
130                }
131                return null;
132            }
133
134            segmentType = streamReader.getUInt8();
135
136            if (segmentType == SEGMENT_SOS) {
137                return null;
138            } else if (segmentType == MARKER_EOI) {
139                if (Log.isLoggable(TAG, Log.DEBUG)) {
140                    Log.d(TAG, "Found MARKER_EOI in exif segment");
141                }
142                return null;
143            }
144
145            segmentLength = streamReader.getUInt16() - 2; //segment length includes bytes for segment length
146
147            if (segmentType != EXIF_SEGMENT_TYPE) {
148                if (segmentLength != streamReader.skip(segmentLength)) {
149                    if (Log.isLoggable(TAG, Log.DEBUG)) {
150                        Log.d(TAG, "Unable to skip enough data for type=" + segmentType);
151                    }
152                    return null;
153                }
154            } else {
155                byte[] segmentData = new byte[segmentLength];
156
157                if (segmentLength != streamReader.read(segmentData)) {
158                    if (Log.isLoggable(TAG, Log.DEBUG)) {
159                        Log.d(TAG, "Unable to read segment data for type=" + segmentType + " length=" + segmentLength);
160                    }
161                    return null;
162                } else {
163                    return segmentData;
164                }
165            }
166        }
167    }
168
169    private int parseExifSegment(RandomAccessReader segmentData) {
170
171        final int headerOffsetSize = JPEG_EXIF_SEGMENT_PREAMBLE.length();
172
173        short byteOrderIdentifier = segmentData.getInt16(headerOffsetSize);
174        final ByteOrder byteOrder;
175        if (byteOrderIdentifier == MOTOROLA_TIFF_MAGIC_NUMBER) { //
176            byteOrder = ByteOrder.BIG_ENDIAN;
177        } else if (byteOrderIdentifier == INTEL_TIFF_MAGIC_NUMBER) {
178            byteOrder = ByteOrder.LITTLE_ENDIAN;
179        } else {
180            if (Log.isLoggable(TAG, Log.DEBUG)) {
181                Log.d(TAG, "Unknown endianness = " + byteOrderIdentifier);
182            }
183            byteOrder = ByteOrder.BIG_ENDIAN;
184        }
185
186        segmentData.order(byteOrder);
187
188        int firstIfdOffset = segmentData.getInt32(headerOffsetSize + 4) + headerOffsetSize;
189        int tagCount = segmentData.getInt16(firstIfdOffset);
190
191        int tagOffset, tagType, formatCode, componentCount;
192        for (int i = 0; i < tagCount; i++) {
193            tagOffset = calcTagOffset(firstIfdOffset, i);
194
195            tagType = segmentData.getInt16(tagOffset);
196
197            if (tagType != ORIENTATION_TAG_TYPE) { //we only want orientation
198                continue;
199            }
200
201            formatCode = segmentData.getInt16(tagOffset + 2);
202
203            if (formatCode < 1 || formatCode > 12) { //12 is max format code
204                if (Log.isLoggable(TAG, Log.DEBUG)) {
205                    Log.d(TAG, "Got invalid format code = " + formatCode);
206                }
207                continue;
208            }
209
210            componentCount = segmentData.getInt32(tagOffset + 4);
211
212            if (componentCount < 0) {
213                if (Log.isLoggable(TAG, Log.DEBUG)) {
214                    Log.d(TAG, "Negative tiff component count");
215                }
216                continue;
217            }
218
219            if (Log.isLoggable(TAG, Log.DEBUG)) {
220                Log.d(TAG, "Got tagIndex=" + i + " tagType=" + tagType + " formatCode =" + formatCode
221                        + " componentCount=" + componentCount);
222            }
223
224            final int byteCount = componentCount + BYTES_PER_FORMAT[formatCode];
225
226            if (byteCount > 4) {
227                if (Log.isLoggable(TAG, Log.DEBUG)) {
228                    Log.d(TAG, "Got byte count > 4, not orientation, continuing, formatCode=" + formatCode);
229                }
230                continue;
231            }
232
233            final int tagValueOffset = tagOffset + 8;
234
235            if (tagValueOffset < 0 || tagValueOffset > segmentData.length()) {
236                if (Log.isLoggable(TAG, Log.DEBUG)) {
237                    Log.d(TAG, "Illegal tagValueOffset=" + tagValueOffset + " tagType=" + tagType);
238                }
239                continue;
240            }
241
242            if (byteCount < 0 || tagValueOffset + byteCount > segmentData.length()) {
243                if (Log.isLoggable(TAG, Log.DEBUG)) {
244                    Log.d(TAG, "Illegal number of bytes for TI tag data tagType=" + tagType);
245                }
246                continue;
247            }
248
249            //assume componentCount == 1 && fmtCode == 3
250            return segmentData.getInt16(tagValueOffset);
251        }
252
253        return -1;
254    }
255
256    private static int calcTagOffset(int ifdOffset, int tagIndex) {
257        return ifdOffset + 2 + (12 * tagIndex);
258    }
259
260    private boolean handles(int imageMagicNumber) {
261        return (imageMagicNumber & EXIF_MAGIC_NUMBER) == EXIF_MAGIC_NUMBER ||
262                imageMagicNumber == MOTOROLA_TIFF_MAGIC_NUMBER ||
263                imageMagicNumber == INTEL_TIFF_MAGIC_NUMBER;
264    }
265
266    private static class RandomAccessReader {
267        private final ByteBuffer data;
268
269        public RandomAccessReader(byte[] data) {
270            this.data = ByteBuffer.wrap(data);
271            this.data.order(ByteOrder.BIG_ENDIAN);
272        }
273
274        public void order(ByteOrder byteOrder) {
275            this.data.order(byteOrder);
276        }
277
278        public int length() {
279            return data.array().length;
280        }
281
282        public int getInt32(int offset) {
283            return data.getInt(offset);
284        }
285
286        public short getInt16(int offset) {
287            return data.getShort(offset);
288        }
289    }
290
291    private static class StreamReader {
292        private final InputStream is;
293        //motorola / big endian byte order
294
295        public StreamReader(InputStream is) {
296            this.is = is;
297        }
298
299        public int getUInt16() throws IOException {
300            return  (is.read() << 8 & 0xFF00) | (is.read() & 0xFF);
301        }
302
303        public short getUInt8() throws IOException {
304            return (short) (is.read() & 0xFF);
305        }
306
307        public long skip(long total) throws IOException {
308            return is.skip(total);
309        }
310
311        public int read(byte[] buffer) throws IOException {
312            return is.read(buffer);
313        }
314
315        public int getByte() throws IOException {
316            return is.read();
317        }
318    }
319}
320
321