Home | History | Annotate | Download | only in bitmap
      1 package com.bumptech.glide.load.resource.bitmap;
      2 
      3 import android.util.Log;
      4 
      5 import java.io.IOException;
      6 import java.io.InputStream;
      7 import java.nio.ByteBuffer;
      8 import java.nio.ByteOrder;
      9 
     10 import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.GIF;
     11 import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.JPEG;
     12 import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.PNG;
     13 import static com.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.PNG_A;
     14 import 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  */
     19 public 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