本文讲解在 Android 的日常开发中,针对图片的几个小 tips。
图片的 mimeType
在 Android 系统中,图片的 mimeType 系统默认是根据后缀名判断。比如 pic.jpg 的 mimeType 就是 "image/jpeg"。
这种逻辑在图片的后缀名正常时还 OK,但是如果图片的后缀名和图片的实际类型匹配不上,那大概率会导致业务异常。比如 zip 压缩文件的后缀名改为 "data.jpg",那么我们在筛选出手机上的图片时,这张 "data.jpg" 也会被识别出来,然后就会出现 ImageView 展示白屏、图片压缩失败、上传图片失败(压缩包过大,超出图片限制)等问题。
此时通过如 androidx 的 LoaderManager 那一套读取出来的系统的 mimeType 信息就不可靠了,我们需要自己确定文件实际的 mimeType,一般我们可以使用系统提供的 BitmapFactory,也可以读取文件头信息,拿到数据的 mimeType。前者适用范围更广,后者可定制性更强。实现时可按需选择。代码如下:
// 方法1:使用 BitmapFactory 确定 mimeType
val String.imageMimeType: String
get() {
return try {
val decodeOptions = BitmapFactory.Options()
decodeOptions.inJustDecodeBounds = true
BitmapFactory.decodeFile(data, decodeOptions)
decodeOptions.outMimeType
} catch (ignored: Throwable) {
Log.e("String.imageMimeType", ignored)
"image/*"
}
}
// 方法2:读取文件头确定 mimeType
val File.imageMimeType: String
get() {
return try {
FileInputStream(this).use { input ->
when (input.read()) {
0xFF -> "image/jpeg"
0x89 -> "image/png"
0x47 -> "image/gif"
else -> readOtherType()
}
}
} catch (ignored: Throwable) {
Log.e("File.imageMimeType", ignored)
"image/*"
}
}
// 读取 heic 图片(HEIF 图片)的文件头
private fun File.readOtherType(): String {
return reader().use {
// heic 格式,头4个字节不用管
it.skip(4)
// heic 格式:5 - 12 共 8 个字节是 ftypheic 8 个字符
val array = CharArray(8)
if (it.read(array) != -1 && String(array) == "ftypheic") {
"image/heic"
} else {
"image/*"
}
}
}
图片的尺寸信息
通常,我们可以使用 BitmapFactory 图片的尺寸:
val decodeOptions = BitmapFactory.Options()
decodeOptions.inJustDecodeBounds = true
BitmapFactory.decodeFile(path, decodeOptions)
val width = decodeOptions.outWidth
val height = decodeOptions.outHeight
但是上面的代码存在一个问题,即 width 和 height 可能取反了。即 width 是 height 的值,height 是 width 的值。出现问题的原因可能是图片带有 EXIF 信息,这个信息是关于图片拍摄时的信息,相机会把诸如图片的旋转角度、拍摄地点、曝光等参数写入到其中。而 Android 系统兼容了这个信息的读取。
当图片具有 EXIF 信息,并且旋转角度是 90 度或者 270 度时,系统会把图片的宽高信息取反,即宽变成高,高变成宽。如果我们忽略了 EXIF 的信息,那么可能导致业务出异常,比如图片的缩放比例出现问题。此时我们需要手动兼容存在 EXIF 信息的场景,即当存在 EXIF 信息并且旋转了时,得把宽高值再取反一次。负负得正,保证图片的宽高信息是正常的。
Android 系统读取 EXIF 信息主要是用到了 ExifInterface 这个类。这个类支持图片 EXIF 信息的读取和修改。
// 先定义 bean 类
class ImageInfo {
val width: Int
get() {
// 图片存在 Exif 信息,并且旋转了 90 度或者 270 度,则交换宽高值
return if (careOrientation && isRotated) {
options.outHeight
} else {
options.outWidth
}
}
val height: Int
get() {
// 图片存在 Exif 信息,并且旋转了 90 度或者 270 度,则交换宽高值
return if (careOrientation && isRotated) {
options.outWidth
} else {
options.outHeight
}
}
// 宽高不用从这里获取,可用于获取其他信息,比如 mimeType 等
var options: BitmapFactory.Options = BitmapFactory.Options()
// 是否关注图片旋转信息的标志位
var careOrientation: Boolean = true
// 图片的旋转角度
var orientation = ExifInterface.ORIENTATION_UNDEFINED
// 图片是否旋转了的标志
val isRotated: Boolean
get() {
return orientation in arrayOf(
ExifInterface.ORIENTATION_ROTATE_90,
ExifInterface.ORIENTATION_ROTATE_270,
)
}
}
// 获取图片的旋转信息
private fun getRotateInfo(info: ImageInfo, path: String) {
try {
val exif = ExifInterface(path)
info.orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
)
} catch (e: Exception) {
Log.e(TAG, e)
}
}
// 修改 Exif 的信息并保存
private fun setExifInfo(info: ImageInfo, path: String) {
try {
val exif = ExifInterface(path)
// 调用多个 set 方法
exif.setAttribute(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL.toString()
)
exif.resetOrientation()
// 最后调用 save 方法保存
exif.saveAttributes()
} catch (e: Exception) {
Log.e(TAG, e)
}
}
ExifInterface 还有大量其他的方法方便我们使用,具体的可见类的 API 文件。
图片的选项
在了解了图片的选项角度后,我们自然会想到如何根据旋转的图片获取正确的数据,其实我们只需要根据旋转角度再旋转下图片,就可以获取正确的图像数据了。
图片的旋转需要用到矩阵 Matrix,矩阵 Matrix 的详细讲解可以看这篇文章:https://blog.csdn.net/cquwentao/article/details/51445269
关于图片的翻转可以看这片文章:https://www.loongwind.com/archives/72.html
对于图像的处理,涉及到三种逻辑:
- 旋转:用 rotate 表示,旋转后的图片有旋转角度(orientation),旋转操作有顺时针(clockwise:CW)和逆时针(counterclockwise:CCW)的区别
- 翻转:用 flip 表示,有沿 x 轴和 y 轴翻转两种形式,沿 x 轴翻转是垂直翻转,沿 y 轴翻转是水平翻转。即沿 x 轴也沿 y 轴翻转相当于对图片进行了 180 度旋转。
- 转置:用 transpose 和 transverse 表示,transpose 相当于沿左上 - 右下对角线翻转。也可以表示为先水平翻转再顺时针旋转270度(等于逆时针旋转 90 度);transverse 表示图像沿右上 - 左下对角线翻转,也可以表示为先水平翻转再顺时针旋转90度(等于逆时针旋转 270 度)。
旋转方法如下:
/**
* 当角度为 [ExifInterface.ORIENTATION_UNDEFINED] 或者
* [ExifInterface.ORIENTATION_NORMAL] 时,表示不用旋转
*/
fun Bitmap.rotateBitmap(orientation: Int): Bitmap {
val matrix = Matrix()
when (orientation) {
// 图片已水平翻转
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> {
matrix.setScale(-1f, 1f)
}
// 图片已旋转 180 度
ExifInterface.ORIENTATION_ROTATE_180 -> {
// 顺时针旋转 180 度
matrix.setRotate(180f)
}
// 图片已垂直翻转
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
// 下面两个语句等价于 matrix.postScale(1f, -1f)
matrix.setRotate(180f)
matrix.postScale(-1f, 1f)
}
// 沿左上 - 右下对角线翻转
ExifInterface.ORIENTATION_TRANSPOSE -> {
// 先顺时针旋转 90 度,再水平翻转
matrix.setRotate(90f)
matrix.postScale(-1f, 1f)
}
// 图片已逆时针旋转 90 度
ExifInterface.ORIENTATION_ROTATE_90 -> {
// 顺时针旋转 90 度
matrix.setRotate(90f)
}
// 沿右上 - 左下对角线翻转
ExifInterface.ORIENTATION_TRANSVERSE -> {
// 先逆时针旋转 90 度,再水平翻转
matrix.setRotate(-90f)
matrix.postScale(-1f, 1f)
}
// 图片已逆时针旋转 270 度
ExifInterface.ORIENTATION_ROTATE_270 -> {
// 逆时针旋转 90 度
matrix.setRotate(-90f)
}
// 其他场景不做转换
else -> return this
}
try {
// 创建新的 bitmap
val bmRotated = Bitmap.createBitmap(
this,
0,
0,
width,
height,
matrix,
true
)
if (bmRotated != this)
recycle()
return bmRotated
} catch (e: Exception) {
e.printStackTrace()
return this
}
}
标签:知识点,ORIENTATION,val,旋转,ExifInterface,Android,翻转,图片
From: https://www.cnblogs.com/wellcherish/p/17188969.html