Skip to content

Commit 27167a2

Browse files
authored
Merge pull request #133 from callstack/fix/retyui/img-orientation
fix: Cropping an image while considering ExifInterface.TAG_ORIENTATION on Android
2 parents 3c9e235 + 030d873 commit 27167a2

File tree

1 file changed

+148
-15
lines changed

1 file changed

+148
-15
lines changed

android/src/main/java/com/reactnativecommunity/imageeditor/ImageEditorModuleImpl.kt

Lines changed: 148 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,28 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
193193
} else {
194194
@Suppress("DEPRECATION") BitmapRegionDecoder.newInstance(it, false)
195195
}
196+
197+
val imageHeight: Int = decoder!!.height
198+
val imageWidth: Int = decoder!!.width
199+
val orientation = getOrientation(reactContext, Uri.parse(uri))
200+
201+
val (left, top) =
202+
when (orientation) {
203+
90 -> y to imageHeight - width - x
204+
180 -> imageWidth - width - x to imageHeight - height - y
205+
270 -> imageWidth - height - y to x
206+
else -> x to y
207+
}
208+
209+
val (right, bottom) =
210+
when (orientation) {
211+
90,
212+
270 -> left + height to top + width
213+
else -> left + width to top + height
214+
}
215+
196216
return@use try {
197-
val rect = Rect(x, y, x + width, y + height)
217+
val rect = Rect(left, top, right, bottom)
198218
decoder!!.decodeRegion(rect, outOptions)
199219
} finally {
200220
decoder!!.recycle()
@@ -218,12 +238,12 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
218238
private fun cropAndResizeTask(
219239
outOptions: BitmapFactory.Options,
220240
uri: String,
221-
x: Int,
222-
y: Int,
223-
width: Int,
224-
height: Int,
225-
targetWidth: Int,
226-
targetHeight: Int,
241+
xPos: Int,
242+
yPos: Int,
243+
rectWidth: Int,
244+
rectHeight: Int,
245+
outputWidth: Int,
246+
outputHeight: Int,
227247
): Bitmap? {
228248
Assertions.assertNotNull(outOptions)
229249

@@ -233,6 +253,35 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
233253
// This uses scaling mode COVER
234254

235255
// Where would the crop rect end up within the scaled bitmap?
256+
257+
val bitmap =
258+
openBitmapInputStream(uri)?.use {
259+
// This can use significantly less memory than decoding the full-resolution bitmap
260+
BitmapFactory.decodeStream(it, null, outOptions)
261+
} ?: return null
262+
263+
val orientation = getOrientation(reactContext, Uri.parse(uri))
264+
val (x, y) =
265+
when (orientation) {
266+
90 -> yPos to bitmap.height - rectWidth - xPos
267+
270 -> bitmap.width - rectHeight - yPos to xPos
268+
180 -> bitmap.width - rectWidth - xPos to bitmap.height - rectHeight - yPos
269+
else -> xPos to yPos
270+
}
271+
272+
val (width, height) =
273+
when (orientation) {
274+
90,
275+
270 -> rectHeight to rectWidth
276+
else -> rectWidth to rectHeight
277+
}
278+
val (targetWidth, targetHeight) =
279+
when (orientation) {
280+
90,
281+
270 -> outputHeight to outputWidth
282+
else -> outputWidth to outputHeight
283+
}
284+
236285
val cropRectRatio = width / height.toFloat()
237286
val targetRatio = targetWidth / targetHeight.toFloat()
238287
val isCropRatioLargerThanTargetRatio = cropRectRatio > targetRatio
@@ -250,11 +299,6 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
250299
// Decode the bitmap. We have to open the stream again, like in the example linked above.
251300
// Is there a way to just continue reading from the stream?
252301
outOptions.inSampleSize = getDecodeSampleSize(width, height, targetWidth, targetHeight)
253-
val bitmap =
254-
openBitmapInputStream(uri)?.use {
255-
// This can use significantly less memory than decoding the full-resolution bitmap
256-
BitmapFactory.decodeStream(it, null, outOptions)
257-
} ?: return null
258302

259303
val cropX = (newX / outOptions.inSampleSize.toFloat()).roundToInt()
260304
val cropY = (newY / outOptions.inSampleSize.toFloat()).roundToInt()
@@ -296,30 +340,119 @@ class ImageEditorModuleImpl(private val reactContext: ReactApplicationContext) {
296340
@SuppressLint("InlinedApi")
297341
private val EXIF_ATTRIBUTES =
298342
arrayOf(
343+
ExifInterface.TAG_APERTURE_VALUE,
344+
ExifInterface.TAG_MAX_APERTURE_VALUE,
345+
ExifInterface.TAG_METERING_MODE,
346+
ExifInterface.TAG_ARTIST,
347+
ExifInterface.TAG_BITS_PER_SAMPLE,
348+
ExifInterface.TAG_COMPRESSION,
349+
ExifInterface.TAG_BODY_SERIAL_NUMBER,
350+
ExifInterface.TAG_BRIGHTNESS_VALUE,
351+
ExifInterface.TAG_CONTRAST,
352+
ExifInterface.TAG_CAMERA_OWNER_NAME,
353+
ExifInterface.TAG_COLOR_SPACE,
354+
ExifInterface.TAG_COPYRIGHT,
299355
ExifInterface.TAG_DATETIME,
300356
ExifInterface.TAG_DATETIME_DIGITIZED,
357+
ExifInterface.TAG_DATETIME_ORIGINAL,
358+
ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
359+
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
360+
ExifInterface.TAG_EXIF_VERSION,
361+
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
362+
ExifInterface.TAG_EXPOSURE_INDEX,
363+
ExifInterface.TAG_EXPOSURE_MODE,
301364
ExifInterface.TAG_EXPOSURE_TIME,
365+
ExifInterface.TAG_EXPOSURE_PROGRAM,
302366
ExifInterface.TAG_FLASH,
367+
ExifInterface.TAG_FLASH_ENERGY,
303368
ExifInterface.TAG_FOCAL_LENGTH,
369+
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
370+
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
371+
ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
372+
ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
373+
ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
374+
ExifInterface.TAG_PLANAR_CONFIGURATION,
375+
ExifInterface.TAG_F_NUMBER,
376+
ExifInterface.TAG_GAIN_CONTROL,
377+
ExifInterface.TAG_GAMMA,
304378
ExifInterface.TAG_GPS_ALTITUDE,
305379
ExifInterface.TAG_GPS_ALTITUDE_REF,
380+
ExifInterface.TAG_GPS_AREA_INFORMATION,
306381
ExifInterface.TAG_GPS_DATESTAMP,
382+
ExifInterface.TAG_GPS_DOP,
307383
ExifInterface.TAG_GPS_LATITUDE,
308384
ExifInterface.TAG_GPS_LATITUDE_REF,
309385
ExifInterface.TAG_GPS_LONGITUDE,
310386
ExifInterface.TAG_GPS_LONGITUDE_REF,
387+
ExifInterface.TAG_GPS_STATUS,
388+
ExifInterface.TAG_GPS_DEST_BEARING,
389+
ExifInterface.TAG_GPS_DEST_BEARING_REF,
390+
ExifInterface.TAG_GPS_DEST_DISTANCE,
391+
ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
392+
ExifInterface.TAG_GPS_DEST_LATITUDE,
393+
ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
394+
ExifInterface.TAG_GPS_DEST_LONGITUDE,
395+
ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
396+
ExifInterface.TAG_GPS_DIFFERENTIAL,
397+
ExifInterface.TAG_GPS_IMG_DIRECTION,
398+
ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
399+
ExifInterface.TAG_GPS_MAP_DATUM,
400+
ExifInterface.TAG_GPS_MEASURE_MODE,
311401
ExifInterface.TAG_GPS_PROCESSING_METHOD,
402+
ExifInterface.TAG_GPS_SATELLITES,
403+
ExifInterface.TAG_GPS_SPEED,
404+
ExifInterface.TAG_GPS_SPEED_REF,
405+
ExifInterface.TAG_GPS_STATUS,
312406
ExifInterface.TAG_GPS_TIMESTAMP,
313-
ExifInterface.TAG_IMAGE_LENGTH,
314-
ExifInterface.TAG_IMAGE_WIDTH,
407+
ExifInterface.TAG_GPS_TRACK,
408+
ExifInterface.TAG_GPS_TRACK_REF,
409+
ExifInterface.TAG_GPS_VERSION_ID,
410+
ExifInterface.TAG_IMAGE_DESCRIPTION,
411+
ExifInterface.TAG_IMAGE_UNIQUE_ID,
412+
ExifInterface.TAG_ISO_SPEED,
413+
ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
414+
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
415+
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
416+
ExifInterface.TAG_LENS_MAKE,
417+
ExifInterface.TAG_LENS_MODEL,
418+
ExifInterface.TAG_LENS_SERIAL_NUMBER,
419+
ExifInterface.TAG_LENS_SPECIFICATION,
420+
ExifInterface.TAG_LIGHT_SOURCE,
315421
ExifInterface.TAG_MAKE,
422+
ExifInterface.TAG_MAKER_NOTE,
316423
ExifInterface.TAG_MODEL,
317424
ExifInterface.TAG_ORIENTATION,
318-
ExifInterface.TAG_SUBSEC_TIME,
425+
ExifInterface.TAG_SATURATION,
426+
ExifInterface.TAG_SHARPNESS,
427+
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
428+
ExifInterface.TAG_SOFTWARE,
429+
ExifInterface.TAG_SUBJECT_DISTANCE,
430+
ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
431+
ExifInterface.TAG_SUBJECT_LOCATION,
432+
ExifInterface.TAG_USER_COMMENT,
319433
ExifInterface.TAG_WHITE_BALANCE
320434
)
321435

322436
// Utils
437+
private fun getOrientation(context: Context, uri: Uri): Int {
438+
val file = getFileFromUri(context, uri)
439+
if (file == null) {
440+
return 0
441+
}
442+
val exif = ExifInterface(file.absolutePath)
443+
return when (
444+
exif.getAttributeInt(
445+
ExifInterface.TAG_ORIENTATION,
446+
ExifInterface.ORIENTATION_NORMAL
447+
)
448+
) {
449+
ExifInterface.ORIENTATION_ROTATE_90 -> 90
450+
ExifInterface.ORIENTATION_ROTATE_180 -> 180
451+
ExifInterface.ORIENTATION_ROTATE_270 -> 270
452+
else -> 0
453+
}
454+
}
455+
323456
@Throws(IOException::class)
324457
private fun copyExif(context: Context, oldImage: Uri, newFile: File) {
325458
val oldFile = getFileFromUri(context, oldImage)

0 commit comments

Comments
 (0)