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