一、 碰到的問題
寫這篇文章的動機源自于這波迭代中碰到的一個問題:
在IM拍照時,在三星s7 eadge上拍完照片后從sd上拿到的地址設置給Imageview后顯示時,圖片旋轉了90度。But我拍照的時候明明是豎著拍的,相冊預覽也是豎著的,為什么拿到圖片后就成了橫著的?
對比了另一臺手機錘子堅果U1,沒這個問題,因此懷疑是跟相機的機型相關。
想到的解決方案:
把讀取到的圖片作為一個bitmap放在一個畫布上,然后旋轉畫布來控制圖片展示的方向。
問題來了:
這樣確實解決了三星上的問題,但是原來沒問題的機型上——歪了。
至此,問題明確了:我如何拿到的圖片的實際方向?
二、Exif
借助強大的Google,搜到了一個叫做Exif
的東西,它是什么呢?
維基百科如是說:
EXIF:可交換圖像文件格式(英語:Exchangeable image file format,官方簡稱Exif), 是專門為數(shù)碼相機的照片設定的,可以記錄數(shù)碼照片的屬性信息和拍攝數(shù)據(jù)。
包括:分辨率,旋轉方向,感光度、白平衡、拍攝的光圈、焦距、分辨率、相機品牌、型號、GPS等信息。
Exif可以附加于JPEG、TIFF、RIFF等文件之中,為其增加有關數(shù)碼相機拍攝信息的內容和索引圖或圖像處理軟件的版本信息。
下圖是維基百科提供的一個exif圖片:
問題來了:
知道了Exif這個信息,對我有什么用?
可以看到在exif中有一個叫做圖像方向的東西,那是否可以借助這個屬性來解決我的問題呢?
三、ExifInterface 源碼解析
求助強大的Google 爸爸:
exif android
爸爸給我呈現(xiàn)了如下結果:
有了這兩個文檔我的問題迎刃而解。
看看ExifInterface是毛:
乍一看這是個接口挺迷的,點進文檔一看是個class。
ExifInterface是Android為我們提供的一個支持庫,隨著 25.1.0 支持庫的發(fā)布,支持庫大家庭迎來了一名新成員:ExifInterface 支持庫。由于 Android 7.1 引入了對框架 ExifInterface 的重大改進,最低可以支持到API 9+。
在build.gradle文件中引入下面的代碼,便可以使用ExifInterface了:
implementation 'com.android.support:exifinterface:27.1.1'
如何使用ExifInterface解決我的問題,定位到它的源碼,可以看到它為我們提供了3個構造方法:
/**
* 從給定的圖片路徑中讀取圖片的exif tag信息.
*/
public ExifInterface(String filename) throws IOException {
......
try {
......
loadAttributes(in);
} finally {
IoUtils.closeQuietly(in);
}
}
/**
* 從指定的圖像文件描述符中讀取Exif標簽. 屬性突變僅支持可寫和可搜索的文件描述符. 此構造函數(shù)不會倒回給定文件描述符的偏移量。開發(fā)人員在使用后應關閉文件描述符。
*/
public ExifInterface(FileDescriptor fileDescriptor) throws IOException {
......
try {
in = new FileInputStream(fileDescriptor);
loadAttributes(in);
} finally {
IoUtils.closeQuietly(in);
}
}
/**
* 從給定的輸入流中讀取圖片的exif 信息. 對文件輸入流的屬性圖片不支持. 開發(fā)者在使用完之后應該關閉輸入流.
*/
public ExifInterface(InputStream inputStream) throws IOException {
......
loadAttributes(inputStream);
}
可以看到的是在這三個構造方法里無一例外的都調用了loadAttributes(inputstream)方法。
接下來跟蹤到loadAttributes方法中:
/**
* This function decides which parser to read the image data according to the given input stream
* type and the content of the input stream. In each case, it reads the first three bytes to
* determine whether the image data format is JPEG or not.
*/
private void loadAttributes(@NonNull InputStream in) throws IOException {
try {
// Initialize mAttributes.
for (int i = 0; i < EXIF_TAGS.length; ++i) {
mAttributes[i] = new HashMap();
}
// Process RAW input stream
if (mAssetInputStream != null) {
long asset = mAssetInputStream.getNativeAsset();
if (handleRawResult(nativeGetRawAttributesFromAsset(asset))) {
return;
}
} else if (mSeekableFileDescriptor != null) {
if (handleRawResult(nativeGetRawAttributesFromFileDescriptor(
mSeekableFileDescriptor))) {
return;
}
} else {
in = new BufferedInputStream(in, JPEG_SIGNATURE_SIZE);
if (!isJpegInputStream((BufferedInputStream) in) && handleRawResult(
nativeGetRawAttributesFromInputStream(in))) {
return;
}
}
// Process JPEG input stream
getJpegAttributes(in);
mIsSupportedFile = true;
} catch (IOException e) {
// Ignore exceptions in order to keep the compatibility with the old versions of
// ExifInterface.
mIsSupportedFile = false;
Log.w(TAG, "Invalid image: ExifInterface got an unsupported image format file"
+ "(ExifInterface supports JPEG and some RAW image formats only) "
+ "or a corrupted JPEG file to ExifInterface.", e);
} finally {
addDefaultValuesForCompatibility();
if (DEBUG) {
printAttributes();
}
}
}
從注釋得到如下信息:
-
這個方法根據(jù)輸入的數(shù)據(jù)流類型和數(shù)據(jù)流內容來決定使用哪種類型的解析器來解析這個流數(shù)據(jù)。不論在哪一種類型的中,它都會讀取前3個字節(jié)的數(shù)據(jù)來決定這是否是JPEG格式的圖片。
換而言之——只有JPEG格式的圖片才會攜帶exif數(shù)據(jù),像PNG,WebP這類的圖片就不會有這些數(shù)據(jù)。
如果是JPEG類型的數(shù)據(jù)會將mIsSupportedFile 設置為true,并且調用getJpegAttributes(in)方法類獲取JPEG中屬性信息
在try-catch的finally方法中調用了addDefaultValuesForCompatibility()方法,這個方法會為每個JPEG格式的圖片添加默認的屬性。
瞜一眼addDefaultValuesForCompatibility的代碼:
private void addDefaultValuesForCompatibility() {
// The value of DATETIME tag has the same value of DATETIME_ORIGINAL tag.
String valueOfDateTimeOriginal = getAttribute(TAG_DATETIME_ORIGINAL);
if (valueOfDateTimeOriginal != null) {
mAttributes[IFD_TIFF_HINT].put(TAG_DATETIME,
ExifAttribute.createString(valueOfDateTimeOriginal));
}
// Add the default value.
if (getAttribute(TAG_IMAGE_WIDTH) == null) {
mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_WIDTH,
ExifAttribute.createULong(0, mExifByteOrder));
}
if (getAttribute(TAG_IMAGE_LENGTH) == null) {
mAttributes[IFD_TIFF_HINT].put(TAG_IMAGE_LENGTH,
ExifAttribute.createULong(0, mExifByteOrder));
}
if (getAttribute(TAG_ORIENTATION) == null) {
mAttributes[IFD_TIFF_HINT].put(TAG_ORIENTATION,
ExifAttribute.createULong(0, mExifByteOrder));
}
if (getAttribute(TAG_LIGHT_SOURCE) == null) {
mAttributes[IFD_EXIF_HINT].put(TAG_LIGHT_SOURCE,
ExifAttribute.createULong(0, mExifByteOrder));
}
}
這段代碼可以得到如下信息:
對于每一張JPEG圖片都會添加默認的屬性信息,包含:
- 圖片的寬、高:TAG_IMAGE_WIDTH、TAG_IMAGE_LENGTH
- 圖片的方向:TAG_ORIENTATION ,它的值大致有如下幾個:
- ORIENTATION_FLIP_HORIZONTAL
- ORIENTATION_FLIP_VERTICAL
- ORIENTATION_NORMAL
- ORIENTATION_ROTATE_180
- ORIENTATION_ROTATE_270
- ORIENTATION_ROTATE_90
- ORIENTATION_TRANSPOSE
- ORIENTATION_TRANSVERSE
- ORIENTATION_UNDEFINED
- 圖片光源:TAG_LIGHT_SOURCE(我猜的,不一定對)
四、解決我的問題
源碼讀到這里,已經了然了:我只要拿到當前圖片的orientation,如果有旋轉那么給它轉一下,就可以了。
接下來的問題:
如何拿到圖片的方向?
從文檔里看到,ExifInterface為我們提供了如下方法:
- getAttribute(String tag)
- getAttributeDouble(String tag, double defaultValue)
- getAttributeInt(String tag, int defaultValue)
下面給出這個問題的解決方案,步驟如下:
- 根據(jù)選中的圖片路徑獲取ExifInterface;
- 從 ExifInterface中獲取到當前圖片的旋轉方向;
- 把對應路徑的圖片Bitmap映射到一個畫布上
- 通過Matrix旋轉畫布,解決方向的問題。
Matrix mat = new Matrix();
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
ExifInterface ei = new ExifInterface(path);
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
mat.postRotate(90);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
mat.postRotate(180);
break;
}
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), mat, true);
至此我的問題就解決了。
五、看我家萌汪的Exif信息:
先上圖 :
你可能會很好奇,為毛這個圖片是轉了個呢?沒錯,這就是用三星手機拍的照片。
來讀取下它的信息:
ExifInterface exifInterface = new ExifInterface(path);
String orientation = exifInterface.getAttribute(ExifInterface.TAG_ORIENTATION);
String dateTime = exifInterface.getAttribute(ExifInterface.TAG_DATETIME);
String make = exifInterface.getAttribute(ExifInterface.TAG_MAKE);
String model = exifInterface.getAttribute(ExifInterface.TAG_MODEL);
String flash = exifInterface.getAttribute(ExifInterface.TAG_FLASH);
String imageLength = exifInterface.getAttribute(ExifInterface.TAG_IMAGE_LENGTH);
String imageWidth = exifInterface.getAttribute(ExifInterface.TAG_IMAGE_WIDTH);
String latitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
String longitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
String latitudeRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
String longitudeRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
String exposureTime = exifInterface.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
String aperture = exifInterface.getAttribute(ExifInterface.TAG_APERTURE);
String isoSpeedRatings = exifInterface.getAttribute(ExifInterface.TAG_ISO);
String dateTimeDigitized = exifInterface.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED);
String subSecTime = exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME);
String subSecTimeOrig = exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIG);
String subSecTimeDig = exifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_DIG);
String altitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_ALTITUDE);
String altitudeRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF);
String gpsTimeStamp = exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP);
String gpsDateStamp = exifInterface.getAttribute(ExifInterface.TAG_GPS_DATESTAMP);
String whiteBalance = exifInterface.getAttribute(ExifInterface.TAG_WHITE_BALANCE);
String focalLength = exifInterface.getAttribute(ExifInterface.TAG_FOCAL_LENGTH);
String processingMethod = exifInterface.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD);
Log.e("TAG", "## orientation=" + orientation);
Log.e("TAG", "## dateTime=" + dateTime);
Log.e("TAG", "## make=" + make);
Log.e("TAG", "## model=" + model);
Log.e("TAG", "## flash=" + flash);
Log.e("TAG", "## imageLength=" + imageLength);
Log.e("TAG", "## imageWidth=" + imageWidth);
Log.e("TAG", "## latitude=" + latitude);
Log.e("TAG", "## longitude=" + longitude);
Log.e("TAG", "## latitudeRef=" + latitudeRef);
Log.e("TAG", "## longitudeRef=" + longitudeRef);
Log.e("TAG", "## exposureTime=" + exposureTime);
Log.e("TAG", "## aperture=" + aperture);
Log.e("TAG", "## isoSpeedRatings=" + isoSpeedRatings);
Log.e("TAG", "## dateTimeDigitized=" + dateTimeDigitized);
Log.e("TAG", "## subSecTime=" + subSecTime);
Log.e("TAG", "## subSecTimeOrig=" + subSecTimeOrig);
Log.e("TAG", "## subSecTimeDig=" + subSecTimeDig);
Log.e("TAG", "## altitude=" + altitude);
Log.e("TAG", "## altitudeRef=" + altitudeRef);
Log.e("TAG", "## gpsTimeStamp=" + gpsTimeStamp);
Log.e("TAG", "## gpsDateStamp=" + gpsDateStamp);
Log.e("TAG", "## whiteBalance=" + whiteBalance);
Log.e("TAG", "## focalLength=" + focalLength);
Log.e("TAG", "## processingMethod=" + processingMethod);
得到的log如下:
05-07 18:40:40.813 27181-27181/zhanggeng.www.exifdemo E/TAG: ## orientation=6
## dateTime=2018:04:21 14:32:41
## make=samsung
## model=SM-G9350
## flash=0
## imageLength=3024
## imageWidth=4032
## latitude=34/1,0/1,536875/10000
## longitude=109/1,0/1,97687/10000
## latitudeRef=N
## longitudeRef=E
## exposureTime=0.002544529262086514
## aperture=1.7
## isoSpeedRatings=50
## dateTimeDigitized=2018:04:21 14:32:41
## subSecTime=null
## subSecTimeOrig=null
## subSecTimeDig=null
## altitude=816000/1000
05-07 18:40:40.814 27181-27181/zhanggeng.www.exifdemo E/TAG: ## altitudeRef=0
## gpsTimeStamp=06:32:06
## gpsDateStamp=2018:04:30
## whiteBalance=0
## focalLength=420/100
## processingMethod=null
以上是這張照片的所有Exif信息,至于具體值是什么意思,我也不懂,借助HandShaker,來看一眼:
上面拿到的Exif屬性信息,其實就是上圖查看的屬性信息。
竟然可以看到我當時拍照的地點,豈不是暴露了我的行蹤,不怕:
在Android相機的設置中關閉“位置信息” 就看不到拍照的地點了。
參考鏈接:
- HandShaker 是錘子科技出品的一款在Mac上使用的Android文件管理器,很好用——免費的;
- ExifInterface 官方文檔
- ExifInterface 支持庫簡介