ImageOrientation.kt
148 lignes · 5342 octets
package fr.ebii.card2vcf.scan import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import androidx.exifinterface.media.ExifInterface import java.io.ByteArrayInputStream import java.io.File import java.io.InputStream /** * Normalise l'orientation d'une photo avant scan/OCR. * * Cas couverts (difficultés utilisateur réelles) : * - EXIF Orientation 1/3/6/8 (0°, 180°, 90°, 270°) * - miroirs EXIF (FLIP_*) * - rotation CameraX [ImageInfo.rotationDegrees] si EXIF absent/normal * - pas de double rotation EXIF + CameraX * - JPEG décodé via [BitmapFactory.decodeByteArray] (ignore EXIF tout seul) */ object ImageOrientation { data class OrientedBitmap( val bitmap: Bitmap, /** Degrés appliqués (0/90/180/270), hors miroirs. */ val appliedRotationDegrees: Int, /** true si la correction vient de l'EXIF, false si fallback CameraX. */ val fromExif: Boolean, val exifOrientation: Int, ) fun fromJpegBytes( jpegBytes: ByteArray, cameraRotationDegrees: Int = 0, opts: BitmapFactory.Options? = null, ): OrientedBitmap? { val bitmap = BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size, opts) ?: return null val exif = runCatching { ExifInterface(ByteArrayInputStream(jpegBytes)) }.getOrNull() return orient(bitmap, exif, cameraRotationDegrees) } fun fromFile(file: File, cameraRotationDegrees: Int = 0): OrientedBitmap? { val bitmap = BitmapFactory.decodeFile(file.absolutePath) ?: return null val exif = runCatching { ExifInterface(file) }.getOrNull() return orient(bitmap, exif, cameraRotationDegrees) } fun fromStream(stream: InputStream, cameraRotationDegrees: Int = 0): OrientedBitmap? { val bytes = stream.readBytes() return fromJpegBytes(bytes, cameraRotationDegrees) } /** * Applique EXIF si présent et non-NORMAL, sinon la rotation caméra. * Les deux ne sont jamais cumulés. */ fun orient( bitmap: Bitmap, exif: ExifInterface?, cameraRotationDegrees: Int = 0, ): OrientedBitmap { val orientation = exif?.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED, ) ?: ExifInterface.ORIENTATION_UNDEFINED val exifUseful = orientation != ExifInterface.ORIENTATION_UNDEFINED && orientation != ExifInterface.ORIENTATION_NORMAL return if (exifUseful) { val transformed = applyExifTransform(bitmap, orientation) OrientedBitmap( bitmap = transformed, appliedRotationDegrees = exifRotationDegrees(orientation), fromExif = true, exifOrientation = orientation, ) } else { val deg = normalizeDegrees(cameraRotationDegrees) val transformed = if (deg == 0) bitmap else rotate(bitmap, deg) OrientedBitmap( bitmap = transformed, appliedRotationDegrees = deg, fromExif = false, exifOrientation = orientation, ) } } fun exifRotationDegrees(orientation: Int): Int = when (orientation) { ExifInterface.ORIENTATION_ROTATE_90, ExifInterface.ORIENTATION_TRANSPOSE, -> 90 ExifInterface.ORIENTATION_ROTATE_180, ExifInterface.ORIENTATION_FLIP_VERTICAL, -> 180 ExifInterface.ORIENTATION_ROTATE_270, ExifInterface.ORIENTATION_TRANSVERSE, -> 270 else -> 0 } fun normalizeDegrees(degrees: Int): Int { var d = degrees % 360 if (d < 0) d += 360 return when (d) { in 45 until 135 -> 90 in 135 until 225 -> 180 in 225 until 315 -> 270 else -> 0 } } internal fun applyExifTransform(src: Bitmap, orientation: Int): Bitmap { val matrix = Matrix() when (orientation) { ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f) ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f) ExifInterface.ORIENTATION_FLIP_VERTICAL -> { matrix.setRotate(180f) matrix.postScale(-1f, 1f) } ExifInterface.ORIENTATION_TRANSPOSE -> { matrix.setRotate(90f) matrix.postScale(-1f, 1f) } ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f) ExifInterface.ORIENTATION_TRANSVERSE -> { matrix.setRotate(270f) matrix.postScale(-1f, 1f) } ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(270f) else -> return src } return Bitmap.createBitmap(src, 0, 0, src.width, src.height, matrix, true).also { if (it !== src && !src.isRecycled) { // ne recycle pas src : l'appelant peut encore le tenir } } } private fun rotate(src: Bitmap, degrees: Int): Bitmap { val matrix = Matrix().apply { postRotate(degrees.toFloat()) } return Bitmap.createBitmap(src, 0, 0, src.width, src.height, matrix, true) } }
GitRust