Skip to content

Commit edf2554

Browse files
committed
Implements orientation field for Android
1 parent 4716a2d commit edf2554

File tree

6 files changed

+444
-40
lines changed

6 files changed

+444
-40
lines changed

android/src/main/java/com/margelo/nitro/multipleimagepicker/MultipleImagePickerImp.kt

Lines changed: 90 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ package com.margelo.nitro.multipleimagepicker
33
import android.app.Activity
44
import android.content.Context
55
import android.content.Intent
6-
import android.graphics.Bitmap
7-
import android.graphics.BitmapFactory
86
import android.graphics.Color
9-
import android.graphics.Matrix
107
import android.net.Uri
118
import android.util.Log
129
import androidx.exifinterface.media.ExifInterface
@@ -170,8 +167,8 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
170167
if (item != null) {
171168
val media = getResult(item)
172169
// Adjust orientation using ExifInterface for Android (only for images)
173-
val adjustedMedia = if (media.type == ResultType.IMAGE) {
174-
adjustOrientation(media, item.path)
170+
val adjustedMedia = if (media.type == ResultType.IMAGE && !item.realPath.isNullOrBlank()) {
171+
adjustOrientation(media, item.realPath)
175172
} else {
176173
media
177174
}
@@ -658,52 +655,111 @@ class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
658655
}
659656

660657
private fun adjustOrientation(pickerResult: PickerResult, filePath: String): PickerResult {
658+
// Validate file path
659+
if (filePath.isBlank()) {
660+
Log.w(TAG, "File path is blank, returning original result")
661+
return pickerResult
662+
}
663+
664+
// Clean the file path - remove file:// prefix and handle content URIs
665+
val cleanPath = when {
666+
filePath.startsWith("file://") -> filePath.removePrefix("file://")
667+
filePath.startsWith("content://") -> {
668+
Log.w(TAG, "Content URI provided instead of file path: $filePath, returning original result")
669+
return pickerResult
670+
}
671+
else -> filePath
672+
}
673+
674+
val file = File(cleanPath)
675+
Log.d(TAG, "Checking orientation for file: ${file.absolutePath}")
676+
677+
if (!file.exists()) {
678+
Log.w(TAG, "File does not exist: ${file.absolutePath} (original path: $filePath)")
679+
return pickerResult
680+
}
681+
682+
if (!file.canRead()) {
683+
Log.w(TAG, "Cannot read file: ${file.absolutePath}")
684+
return pickerResult
685+
}
686+
661687
try {
662-
val ei = ExifInterface(filePath)
688+
val ei = ExifInterface(file.absolutePath)
663689
val orientation = ei.getAttributeInt(
664690
ExifInterface.TAG_ORIENTATION,
665691
ExifInterface.ORIENTATION_UNDEFINED
666692
)
667693

668-
val rotatedBitmap: Bitmap? = when (orientation) {
669-
ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(filePath, 90f)
670-
ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(filePath, 180f)
671-
ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(filePath, 270f)
672-
else -> BitmapFactory.decodeFile(filePath)
673-
}
674-
675-
rotatedBitmap?.let { bitmap ->
676-
val width = bitmap.width.toDouble()
677-
val height = bitmap.height.toDouble()
678-
// Update width, height, and orientation in pickerResult
679-
return pickerResult.copy(
680-
width = width,
681-
height = height,
682-
orientation = orientation.toDouble()
683-
)
694+
Log.d(TAG, "EXIF orientation for $filePath: $orientation")
695+
696+
// Calculate adjusted dimensions based on orientation
697+
val (adjustedWidth, adjustedHeight) = when (orientation) {
698+
ExifInterface.ORIENTATION_ROTATE_90,
699+
ExifInterface.ORIENTATION_ROTATE_270,
700+
ExifInterface.ORIENTATION_TRANSPOSE,
701+
ExifInterface.ORIENTATION_TRANSVERSE -> {
702+
// Swap width and height for 90° and 270° rotations
703+
Log.d(TAG, "Swapping dimensions for orientation: $orientation")
704+
Pair(pickerResult.height, pickerResult.width)
705+
}
706+
else -> {
707+
// Keep original dimensions for 0°, 180°, and flips
708+
Log.d(TAG, "Keeping original dimensions for orientation: $orientation")
709+
Pair(pickerResult.width, pickerResult.height)
710+
}
684711
}
712+
713+
Log.d(TAG, "Adjusted dimensions: ${adjustedWidth}x${adjustedHeight} (orientation: $orientation)")
714+
715+
// Return result with adjusted dimensions and orientation angle
716+
return pickerResult.copy(
717+
width = adjustedWidth,
718+
height = adjustedHeight,
719+
orientation = exifOrientationToRotationDegrees(orientation)
720+
)
685721
} catch (e: IOException) {
686-
Log.e(TAG, "Failed to adjust orientation", e)
722+
Log.e(TAG, "IOException while adjusting orientation for $filePath", e)
723+
} catch (e: OutOfMemoryError) {
724+
Log.e(TAG, "OutOfMemoryError while processing image $filePath", e)
725+
} catch (e: Exception) {
726+
Log.e(TAG, "Unexpected error while adjusting orientation for $filePath", e)
687727
}
688-
// Return with orientation info even if bitmap processing fails
728+
729+
// Fallback: try to read orientation info even if bitmap processing fails
689730
return try {
690-
val ei = ExifInterface(filePath)
731+
val ei = ExifInterface(file.absolutePath)
691732
val orientation = ei.getAttributeInt(
692733
ExifInterface.TAG_ORIENTATION,
693734
ExifInterface.ORIENTATION_UNDEFINED
694735
)
695-
pickerResult.copy(orientation = orientation.toDouble())
696-
} catch (e: IOException) {
697-
Log.e(TAG, "Failed to read orientation", e)
698-
pickerResult
736+
val rotation = exifOrientationToRotationDegrees(orientation)
737+
Log.d(TAG, "Fallback: setting orientation to $rotation degrees (EXIF: $orientation) for $filePath")
738+
pickerResult.copy(orientation = rotation)
739+
} catch (e: Exception) {
740+
Log.e(TAG, "Failed to read orientation in fallback for $filePath", e)
741+
// Return original result with no rotation needed
742+
pickerResult.copy(orientation = 0.0)
699743
}
700744
}
701745

702-
private fun rotateImage(filePath: String, degree: Float): Bitmap? {
703-
val bitmap = BitmapFactory.decodeFile(filePath)
704-
val matrix = Matrix()
705-
matrix.postRotate(degree)
706-
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
746+
/**
747+
* Maps EXIF orientation values to rotation degrees for image display.
748+
* Returns the rotation angle needed to display the image correctly.
749+
*/
750+
private fun exifOrientationToRotationDegrees(exifOrientation: Int): Double {
751+
return when (exifOrientation) {
752+
ExifInterface.ORIENTATION_ROTATE_90 -> 90.0
753+
ExifInterface.ORIENTATION_ROTATE_180 -> 180.0
754+
ExifInterface.ORIENTATION_ROTATE_270 -> -90.0
755+
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> 0.0 // Flip, not rotate
756+
ExifInterface.ORIENTATION_FLIP_VERTICAL -> 0.0 // Flip, not rotate
757+
ExifInterface.ORIENTATION_TRANSPOSE -> 90.0 // Flip + rotate 90
758+
ExifInterface.ORIENTATION_TRANSVERSE -> -90.0 // Flip + rotate -90
759+
ExifInterface.ORIENTATION_NORMAL,
760+
ExifInterface.ORIENTATION_UNDEFINED -> 0.0
761+
else -> 0.0 // No rotation needed
762+
}
707763
}
708764

709765
override fun getAppContext(): Context {
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.margelo.nitro.multipleimagepicker
2+
3+
import androidx.exifinterface.media.ExifInterface
4+
import org.junit.Assert.assertEquals
5+
import org.junit.Assert.assertNotNull
6+
import org.junit.Assert.assertTrue
7+
import org.junit.Test
8+
import org.junit.runner.RunWith
9+
import org.robolectric.RobolectricTestRunner
10+
import org.robolectric.annotation.Config
11+
12+
@RunWith(RobolectricTestRunner::class)
13+
@Config(sdk = [28])
14+
class OrientationTest {
15+
16+
@Test
17+
fun testOrientationValues() {
18+
// Test all possible EXIF orientation values
19+
val orientationTests = mapOf(
20+
ExifInterface.ORIENTATION_NORMAL to "Normal",
21+
ExifInterface.ORIENTATION_FLIP_HORIZONTAL to "Flip Horizontal",
22+
ExifInterface.ORIENTATION_ROTATE_180 to "Rotate 180°",
23+
ExifInterface.ORIENTATION_FLIP_VERTICAL to "Flip Vertical",
24+
ExifInterface.ORIENTATION_TRANSPOSE to "Transpose",
25+
ExifInterface.ORIENTATION_ROTATE_90 to "Rotate 90° CW",
26+
ExifInterface.ORIENTATION_TRANSVERSE to "Transverse",
27+
ExifInterface.ORIENTATION_ROTATE_270 to "Rotate 270° CW",
28+
ExifInterface.ORIENTATION_UNDEFINED to "Undefined"
29+
)
30+
31+
orientationTests.forEach { (value, description) ->
32+
assertTrue("$description orientation value should be valid", value >= 0)
33+
assertTrue("$description orientation value should be within range", value <= 8)
34+
}
35+
}
36+
37+
@Test
38+
fun testOrientationMapping() {
39+
// Test that orientation values map correctly to transformations
40+
val orientationMappings = mapOf(
41+
ExifInterface.ORIENTATION_NORMAL to 0f,
42+
ExifInterface.ORIENTATION_ROTATE_90 to 90f,
43+
ExifInterface.ORIENTATION_ROTATE_180 to 180f,
44+
ExifInterface.ORIENTATION_ROTATE_270 to 270f
45+
)
46+
47+
orientationMappings.forEach { (orientation, expectedRotation) ->
48+
val actualRotation = when (orientation) {
49+
ExifInterface.ORIENTATION_ROTATE_90 -> 90f
50+
ExifInterface.ORIENTATION_ROTATE_180 -> 180f
51+
ExifInterface.ORIENTATION_ROTATE_270 -> 270f
52+
else -> 0f
53+
}
54+
assertEquals("Orientation $orientation should map to $expectedRotation degrees", expectedRotation, actualRotation)
55+
}
56+
}
57+
58+
@Test
59+
fun testPickerResultOrientationField() {
60+
// Test that PickerResult can handle orientation values
61+
val testResult = PickerResult(
62+
localIdentifier = "test-id",
63+
width = 1920.0,
64+
height = 1080.0,
65+
mime = "image/jpeg",
66+
size = 1024000.0,
67+
bucketId = null,
68+
realPath = null,
69+
parentFolderName = null,
70+
creationDate = null,
71+
crop = false,
72+
orientation = ExifInterface.ORIENTATION_ROTATE_90.toDouble(),
73+
path = "test/path",
74+
type = ResultType.IMAGE,
75+
duration = null,
76+
thumbnail = null,
77+
fileName = "test.jpg"
78+
)
79+
80+
assertNotNull("PickerResult should have orientation field", testResult.orientation)
81+
assertEquals("Orientation should be 90 degrees", ExifInterface.ORIENTATION_ROTATE_90.toDouble(), testResult.orientation!!)
82+
}
83+
84+
@Test
85+
fun testOrientationWithDifferentImageTypes() {
86+
// Test orientation handling for different image types
87+
val imageTypes = listOf(
88+
"image/jpeg",
89+
"image/png",
90+
"image/webp",
91+
"image/heic"
92+
)
93+
94+
imageTypes.forEach { mimeType ->
95+
val testResult = PickerResult(
96+
localIdentifier = "test-id",
97+
width = 1920.0,
98+
height = 1080.0,
99+
mime = mimeType,
100+
size = 1024000.0,
101+
bucketId = null,
102+
realPath = null,
103+
parentFolderName = null,
104+
creationDate = null,
105+
crop = false,
106+
orientation = ExifInterface.ORIENTATION_ROTATE_90.toDouble(),
107+
path = "test/path",
108+
type = ResultType.IMAGE,
109+
duration = null,
110+
thumbnail = null,
111+
fileName = "test.jpg"
112+
)
113+
114+
assertEquals("$mimeType should preserve orientation", ExifInterface.ORIENTATION_ROTATE_90.toDouble(), testResult.orientation!!)
115+
}
116+
}
117+
118+
@Test
119+
fun testOrientationForVideoFiles() {
120+
// Test that video files don't have orientation applied
121+
val videoResult = PickerResult(
122+
localIdentifier = "video-test-id",
123+
width = 1920.0,
124+
height = 1080.0,
125+
mime = "video/mp4",
126+
size = 10240000.0,
127+
bucketId = null,
128+
realPath = null,
129+
parentFolderName = null,
130+
creationDate = null,
131+
crop = false,
132+
orientation = null, // Videos typically don't have orientation
133+
path = "test/video/path",
134+
type = ResultType.VIDEO,
135+
duration = 120.0,
136+
thumbnail = null,
137+
fileName = "test.mp4"
138+
)
139+
140+
assertEquals("Video should have VIDEO type", ResultType.VIDEO, videoResult.type)
141+
// Note: Videos may or may not have orientation depending on implementation
142+
}
143+
}
144+

0 commit comments

Comments
 (0)