diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Photo.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Photo.kt index 6ef88b2d11..d27b6b3bc7 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Photo.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraSession+Photo.kt @@ -6,6 +6,70 @@ import com.mrousavy.camera.core.types.Flash import com.mrousavy.camera.core.types.Orientation import com.mrousavy.camera.core.types.TakePhotoOptions import com.mrousavy.camera.core.utils.FileUtils +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.media.ExifInterface +import android.graphics.Bitmap +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +fun rotateImageAndSave(imagePath: String): Boolean { + // Decode the image into a Bitmap + val bitmap = BitmapFactory.decodeFile(imagePath) ?: return false + + try { + // Read the Exif data for orientation + val exif = ExifInterface(imagePath) + val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + + // Create a Matrix object to rotate the Bitmap + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f) + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1f, 1f) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1f, -1f) + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.postRotate(90f) + matrix.postScale(-1f, 1f) + } + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.postRotate(-90f) + matrix.postScale(-1f, 1f) + } + } + + // If there's no rotation or flipping needed, return the original bitmap + val correctedBitmap = if (!matrix.isIdentity) { + Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + } else { + bitmap + } + + // Save the rotated bitmap back to the file path + val file = File(imagePath) + val outputStream = FileOutputStream(file) + correctedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + outputStream.flush() + outputStream.close() + + // Update EXIF metadata to reset the orientation tag to "normal" + val newExif = ExifInterface(imagePath) + newExif.setAttribute(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL.toString()) + newExif.saveAttributes() + + // Recycle the bitmap to free up memory + correctedBitmap.recycle() + + return true + } catch (e: IOException) { + e.printStackTrace() + } + + return false +} suspend fun CameraSession.takePhoto(options: TakePhotoOptions): Photo { val camera = camera ?: throw CameraNotReadyError() @@ -33,6 +97,10 @@ suspend fun CameraSession.takePhoto(options: TakePhotoOptions): Photo { CameraQueues.cameraExecutor ) + if (options.normalizeOrientation) { + rotateImageAndSave(photoFile.uri.path) + } + // Parse resulting photo (EXIF data) val size = FileUtils.getImageSize(photoFile.uri.path) val rotation = photoOutput.targetRotation diff --git a/package/android/src/main/java/com/mrousavy/camera/core/types/TakePhotoOptions.kt b/package/android/src/main/java/com/mrousavy/camera/core/types/TakePhotoOptions.kt index 4c99c4b0e5..c3c9c98982 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/types/TakePhotoOptions.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/types/TakePhotoOptions.kt @@ -5,16 +5,15 @@ import com.facebook.react.bridge.ReadableMap import com.mrousavy.camera.core.utils.FileUtils import com.mrousavy.camera.core.utils.OutputFile -data class TakePhotoOptions(val file: OutputFile, val flash: Flash, val enableShutterSound: Boolean) { - +data class TakePhotoOptions(val file: OutputFile, val flash: Flash, val enableShutterSound: Boolean, val normalizeOrientation: Boolean) { companion object { fun fromJS(context: Context, map: ReadableMap): TakePhotoOptions { val flash = if (map.hasKey("flash")) Flash.fromUnionValue(map.getString("flash")) else Flash.OFF val enableShutterSound = if (map.hasKey("enableShutterSound")) map.getBoolean("enableShutterSound") else false val directory = if (map.hasKey("path")) FileUtils.getDirectory(map.getString("path")) else context.cacheDir - + val normalize = if (map.hasKey("normalizeOrientation")) map.getBoolean("normalizeOrientation") else false val outputFile = OutputFile(context, directory, ".jpg") - return TakePhotoOptions(outputFile, flash, enableShutterSound) + return TakePhotoOptions(outputFile, flash, enableShutterSound, normalize) } } } diff --git a/package/example/package.json b/package/example/package.json index c8d26a1701..f9dfdf5f45 100644 --- a/package/example/package.json +++ b/package/example/package.json @@ -20,6 +20,7 @@ "@shopify/react-native-skia": "^1.3.4", "react": "^18.3.1", "react-native": "^0.74.2", + "react-native-compressor": "^1.9.0", "react-native-gesture-handler": "^2.16.2", "react-native-mmkv": "^2.12.2", "react-native-pressable-opacity": "^1.0.10", diff --git a/package/example/src/CameraPage.tsx b/package/example/src/CameraPage.tsx index af65e71462..a58f70cb6e 100644 --- a/package/example/src/CameraPage.tsx +++ b/package/example/src/CameraPage.tsx @@ -69,11 +69,9 @@ export function CameraPage({ navigation }: Props): React.ReactElement { const screenAspectRatio = SCREEN_HEIGHT / SCREEN_WIDTH const format = useCameraFormat(device, [ - { fps: targetFps }, - { videoAspectRatio: screenAspectRatio }, - { videoResolution: 'max' }, - { photoAspectRatio: screenAspectRatio }, - { photoResolution: 'max' }, + + { photoResolution: { width: 1440, height: 1440 } }, + ]) const fps = Math.min(format?.maxFps ?? 1, targetFps) diff --git a/package/example/src/MediaPage.tsx b/package/example/src/MediaPage.tsx index 8eff60dd05..0852246f99 100644 --- a/package/example/src/MediaPage.tsx +++ b/package/example/src/MediaPage.tsx @@ -85,7 +85,7 @@ export function MediaPage({ navigation, route }: Props): React.ReactElement { return ( {type === 'photo' && ( - + )} {type === 'video' && (