Fix cropping and rotating photos with EXIF orientation set

This commit is contained in:
Ming Ming 2022-09-11 15:31:04 +08:00
parent 0862f9af6e
commit ddf059e609
10 changed files with 87 additions and 11 deletions

View file

@ -90,6 +90,8 @@ class _ImageEditorState extends State<ImageEditor> {
height: k.photoLargeSize, height: k.photoLargeSize,
a: true, a: true,
)); ));
// no need to set shouldfixOrientation because the previews are always in
// the correct orientation
_src = await ImageLoader.loadUri( _src = await ImageLoader.loadUri(
"file://${fileInfo!.file.path}", "file://${fileInfo!.file.path}",
_previewWidth, _previewWidth,

View file

@ -3,7 +3,9 @@ package com.nkming.nc_photos.plugin
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.net.Uri import android.net.Uri
import androidx.exifinterface.media.ExifInterface
import java.io.InputStream import java.io.InputStream
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
@ -91,6 +93,7 @@ interface BitmapUtil {
resizeMethod: BitmapResizeMethod, resizeMethod: BitmapResizeMethod,
isAllowSwapSide: Boolean = false, isAllowSwapSide: Boolean = false,
shouldUpscale: Boolean = true, shouldUpscale: Boolean = true,
shouldFixOrientation: Boolean = false,
): Bitmap { ): Bitmap {
val opt = loadImageBounds(context, uri) val opt = loadImageBounds(context, uri)
val shouldSwapSide = isAllowSwapSide && val shouldSwapSide = isAllowSwapSide &&
@ -116,9 +119,13 @@ interface BitmapUtil {
logD(TAG, "Bitmap subsampled: ${bitmap.width}x${bitmap.height}") logD(TAG, "Bitmap subsampled: ${bitmap.width}x${bitmap.height}")
} }
if (bitmap.width < dstW && bitmap.height < dstH && !shouldUpscale) { if (bitmap.width < dstW && bitmap.height < dstH && !shouldUpscale) {
return bitmap return if (shouldFixOrientation) {
fixOrientation(context, uri, bitmap)
} else {
bitmap
}
} }
return when (resizeMethod) { val result = when (resizeMethod) {
BitmapResizeMethod.FIT -> Bitmap.createScaledBitmap( BitmapResizeMethod.FIT -> Bitmap.createScaledBitmap(
bitmap, bitmap,
minOf(dstW, (dstH * bitmap.aspectRatio()).toInt()), minOf(dstW, (dstH * bitmap.aspectRatio()).toInt()),
@ -133,6 +140,62 @@ interface BitmapUtil {
true true
) )
} }
return if (shouldFixOrientation) {
fixOrientation(context, uri, result)
} else {
result
}
}
/**
* Rotate the image to its visible orientation
*/
private fun fixOrientation(
context: Context, uri: Uri, bitmap: Bitmap
): Bitmap {
return try {
openUriInputStream(context, uri)!!.use {
val iExif = ExifInterface(it)
val orientation =
iExif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
logI(
TAG,
"[fixOrientation] Input file orientation: $orientation"
)
val rotate = when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90,
ExifInterface.ORIENTATION_TRANSPOSE -> 90f
ExifInterface.ORIENTATION_ROTATE_180,
ExifInterface.ORIENTATION_FLIP_VERTICAL -> 180f
ExifInterface.ORIENTATION_ROTATE_270,
ExifInterface.ORIENTATION_TRANSVERSE -> 270f
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> 0f
else -> return bitmap
}
val mat = Matrix()
mat.postRotate(rotate)
if (orientation == ExifInterface.ORIENTATION_FLIP_HORIZONTAL ||
orientation == ExifInterface.ORIENTATION_TRANSVERSE ||
orientation == ExifInterface.ORIENTATION_FLIP_VERTICAL ||
orientation == ExifInterface.ORIENTATION_TRANSPOSE
) {
mat.postScale(-1f, 1f)
}
Bitmap.createBitmap(
bitmap, 0, 0, bitmap.width, bitmap.height, mat, true
)
}
} catch (e: Throwable) {
logE(
TAG,
"[fixOrientation] Failed fixing, assume normal orientation",
e
)
bitmap
}
} }
private fun openUriInputStream( private fun openUriInputStream(

View file

@ -24,6 +24,7 @@ class ImageLoaderChannelHandler(context: Context) :
call.argument("resizeMethod")!!, call.argument("resizeMethod")!!,
call.argument("isAllowSwapSide")!!, call.argument("isAllowSwapSide")!!,
call.argument("shouldUpscale")!!, call.argument("shouldUpscale")!!,
call.argument("shouldFixOrientation")!!,
result result
) )
} catch (e: Throwable) { } catch (e: Throwable) {
@ -44,17 +45,18 @@ class ImageLoaderChannelHandler(context: Context) :
* @param resizeMethod * @param resizeMethod
* @param isAllowSwapSide * @param isAllowSwapSide
* @param shouldUpscale * @param shouldUpscale
* @param shouldFixOrientation
* @param result * @param result
*/ */
private fun loadUri( private fun loadUri(
fileUri: String, maxWidth: Int, maxHeight: Int, resizeMethod: Int, fileUri: String, maxWidth: Int, maxHeight: Int, resizeMethod: Int,
isAllowSwapSide: Boolean, shouldUpscale: Boolean, isAllowSwapSide: Boolean, shouldUpscale: Boolean,
result: MethodChannel.Result shouldFixOrientation: Boolean, result: MethodChannel.Result
) { ) {
val image = BitmapUtil.loadImage( val image = BitmapUtil.loadImage(
context, Uri.parse(fileUri), maxWidth, maxHeight, context, Uri.parse(fileUri), maxWidth, maxHeight,
BitmapResizeMethod.values()[resizeMethod], isAllowSwapSide, BitmapResizeMethod.values()[resizeMethod], isAllowSwapSide,
shouldUpscale shouldUpscale, shouldFixOrientation
).use { ).use {
Rgba8Image(TfLiteHelper.bitmapToRgba8Array(it), it.width, it.height) Rgba8Image(TfLiteHelper.bitmapToRgba8Array(it), it.width, it.height)
} }

View file

@ -518,7 +518,9 @@ private open class ImageProcessorCommandTask(context: Context) :
ExifInterface.TAG_IMAGE_DESCRIPTION, ExifInterface.TAG_IMAGE_DESCRIPTION,
ExifInterface.TAG_MAKE, ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL, ExifInterface.TAG_MODEL,
ExifInterface.TAG_ORIENTATION, // while processing, we'll correct the orientation, if we copy the
// value to the resulting image, the orientation will be wrong
// ExifInterface.TAG_ORIENTATION,
ExifInterface.TAG_X_RESOLUTION, ExifInterface.TAG_X_RESOLUTION,
ExifInterface.TAG_Y_RESOLUTION, ExifInterface.TAG_Y_RESOLUTION,
ExifInterface.TAG_DATETIME, ExifInterface.TAG_DATETIME,

View file

@ -22,7 +22,8 @@ class ArbitraryStyleTransfer(
val height: Int val height: Int
val rgb8Image = BitmapUtil.loadImage( val rgb8Image = BitmapUtil.loadImage(
context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT, context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT,
isAllowSwapSide = true, shouldUpscale = false isAllowSwapSide = true, shouldUpscale = false,
shouldFixOrientation = true
).use { ).use {
width = it.width width = it.width
height = it.height height = it.height

View file

@ -23,7 +23,8 @@ class DeepLab3Portrait(
val height: Int val height: Int
val rgb8Image = BitmapUtil.loadImage( val rgb8Image = BitmapUtil.loadImage(
context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT, context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT,
isAllowSwapSide = true, shouldUpscale = false isAllowSwapSide = true, shouldUpscale = false,
shouldFixOrientation = true
).use { ).use {
width = it.width width = it.width
height = it.height height = it.height
@ -54,7 +55,8 @@ class DeepLab3ColorPop(
val height: Int val height: Int
val rgb8Image = BitmapUtil.loadImage( val rgb8Image = BitmapUtil.loadImage(
context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT, context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT,
isAllowSwapSide = true, shouldUpscale = false isAllowSwapSide = true, shouldUpscale = false,
shouldFixOrientation = true
).use { ).use {
width = it.width width = it.width
height = it.height height = it.height

View file

@ -15,7 +15,7 @@ class Esrgan(context: Context, maxWidth: Int, maxHeight: Int) {
val rgb8Image = BitmapUtil.loadImage( val rgb8Image = BitmapUtil.loadImage(
context, imageUri, maxWidth / 4, maxHeight / 4, context, imageUri, maxWidth / 4, maxHeight / 4,
BitmapResizeMethod.FIT, isAllowSwapSide = true, BitmapResizeMethod.FIT, isAllowSwapSide = true,
shouldUpscale = false shouldUpscale = false, shouldFixOrientation = true
).use { ).use {
width = it.width width = it.width
height = it.height height = it.height

View file

@ -18,7 +18,8 @@ class ImageFilterProcessor(
fun apply(imageUri: Uri): Bitmap { fun apply(imageUri: Uri): Bitmap {
var img = BitmapUtil.loadImage( var img = BitmapUtil.loadImage(
context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT, context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT,
isAllowSwapSide = true, shouldUpscale = false isAllowSwapSide = true, shouldUpscale = false,
shouldFixOrientation = true
).use { ).use {
Rgba8Image(TfLiteHelper.bitmapToRgba8Array(it), it.width, it.height) Rgba8Image(TfLiteHelper.bitmapToRgba8Array(it), it.width, it.height)
} }

View file

@ -14,7 +14,8 @@ class ZeroDce(context: Context, maxWidth: Int, maxHeight: Int, iteration: Int) {
val height: Int val height: Int
val rgb8Image = BitmapUtil.loadImage( val rgb8Image = BitmapUtil.loadImage(
context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT, context, imageUri, maxWidth, maxHeight, BitmapResizeMethod.FIT,
isAllowSwapSide = true, shouldUpscale = false isAllowSwapSide = true, shouldUpscale = false,
shouldFixOrientation = true
).use { ).use {
width = it.width width = it.width
height = it.height height = it.height

View file

@ -17,6 +17,7 @@ class ImageLoader {
ImageLoaderResizeMethod resizeMethod, { ImageLoaderResizeMethod resizeMethod, {
bool isAllowSwapSide = false, bool isAllowSwapSide = false,
bool shouldUpscale = false, bool shouldUpscale = false,
bool shouldFixOrientation = false,
}) async { }) async {
final result = final result =
await _methodChannel.invokeMethod<Map>("loadUri", <String, dynamic>{ await _methodChannel.invokeMethod<Map>("loadUri", <String, dynamic>{
@ -26,6 +27,7 @@ class ImageLoader {
"resizeMethod": resizeMethod.index, "resizeMethod": resizeMethod.index,
"isAllowSwapSide": isAllowSwapSide, "isAllowSwapSide": isAllowSwapSide,
"shouldUpscale": shouldUpscale, "shouldUpscale": shouldUpscale,
"shouldFixOrientation": shouldFixOrientation,
}); });
return Rgba8Image.fromJson(result!.cast<String, dynamic>()); return Rgba8Image.fromJson(result!.cast<String, dynamic>());
} }