Last active
January 10, 2022 09:33
-
-
Save igreenwood/9bd17f0b30396ea40bd5010c5b759c46 to your computer and use it in GitHub Desktop.
How to create down-scaled image file before image upload
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.annotation.TargetApi | |
import android.content.ContentUris | |
import android.content.Context | |
import android.database.Cursor | |
import android.graphics.Bitmap | |
import android.graphics.Bitmap.createBitmap | |
import android.graphics.BitmapFactory | |
import android.graphics.Matrix | |
import android.net.Uri | |
import android.opengl.GLES10 | |
import android.os.Build | |
import android.os.Environment | |
import android.provider.DocumentsContract | |
import android.provider.MediaStore | |
import androidx.exifinterface.media.ExifInterface | |
import com.lang8.hinative.AppController | |
import timber.log.Timber | |
import java.io.BufferedInputStream | |
import java.io.File | |
import java.io.FileInputStream | |
import java.io.FileNotFoundException | |
import java.io.FileOutputStream | |
import java.io.IOException | |
import java.io.InputStream | |
import kotlin.math.max | |
object ImageUtil { | |
private const val UPLOAD_SIZE_LIMIT = 800 | |
fun createDownScaledImage(context: Context, inputUri: Uri, file: File) { | |
val maxSize = getMaxSize() | |
val sampled = getSampledBitmap(context, inputUri, maxSize) | |
val resized = getResizedBitmap(sampled, maxSize) | |
saveBitmapToFile(resized, file) | |
} | |
private fun saveBitmapToFile(bitmap: Bitmap, file: File) { | |
val fos = FileOutputStream(file) | |
fos.use { | |
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) | |
} | |
} | |
private fun getResizedBitmap(sampled: Bitmap, maxSize: Int): Bitmap { | |
val (width, height) = sampled.width to sampled.height | |
val longSide = max(width, height) | |
val targetScale = maxSize.toFloat() / longSide.toFloat() | |
val scaleMatrix = Matrix().apply { postScale(targetScale, targetScale) } | |
return createBitmap(sampled, 0, 0, width, height, scaleMatrix, true) | |
} | |
private fun getSampledBitmap(context: Context, uri: Uri, maxSize: Int): Bitmap { | |
val option = BitmapFactory.Options().apply { | |
inJustDecodeBounds = true | |
} | |
try { | |
context.contentResolver.openInputStream(uri).use { | |
BitmapFactory.decodeStream(BufferedInputStream(it), null, option) | |
val sampleSize = calculateInSampleSize(option, maxSize, maxSize) | |
option.apply { | |
inSampleSize = sampleSize | |
inJustDecodeBounds = false | |
} | |
} | |
context.contentResolver.openInputStream(uri).use { | |
val sampled = BitmapFactory.decodeStream(BufferedInputStream(it), null, option) ?: throw IOException("sample cannot be null!!") | |
val rotateDegrees = getExifRotation(context, uri) | |
return if(rotateDegrees == 0f) { | |
sampled | |
} else { | |
val matrix = Matrix().apply { postRotate(rotateDegrees) } | |
createBitmap(sampled, 0, 0, sampled.width, sampled.height, matrix, true).also { | |
sampled.recycle() | |
} | |
} | |
} | |
} catch (e: Throwable) { | |
Timber.e(e) | |
throw e | |
} | |
} | |
private fun getMaxSize(): Int { | |
var maxSize = UPLOAD_SIZE_LIMIT | |
val arr = IntArray(1) | |
GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, arr, 0) | |
if (arr[0] > 0) { | |
maxSize = Math.min(arr[0], UPLOAD_SIZE_LIMIT) | |
} | |
return maxSize | |
} | |
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { | |
// Raw height and width of image | |
val (height: Int, width: Int) = options.run { outHeight to outWidth } | |
var inSampleSize = 1 | |
if (height > reqHeight || width > reqWidth) { | |
val halfHeight: Int = height / 2 | |
val halfWidth: Int = width / 2 | |
// Calculate the largest inSampleSize value that is a power of 2 and keeps both | |
// height and width larger than the requested height and width. | |
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { | |
inSampleSize *= 2 | |
} | |
} | |
return inSampleSize | |
} | |
fun getExifRotation(context: Context, uri: Uri): Float { | |
val orientation = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){ | |
getOrientation(context, uri) | |
} else { | |
val file = getFileFromUri(context, uri) ?: return 0f | |
getOrientationLegacy(file.path) | |
} | |
return when (orientation) { | |
ExifInterface.ORIENTATION_ROTATE_90 -> 90f | |
ExifInterface.ORIENTATION_ROTATE_180 -> 180f | |
ExifInterface.ORIENTATION_ROTATE_270 -> 270f | |
else -> 0f | |
} | |
} | |
@TargetApi(Build.VERSION_CODES.N) | |
private fun getOrientation(context: Context, uri: Uri): Int { | |
var exif: ExifInterface? = null | |
try { | |
context.contentResolver.openInputStream(uri).use { | |
exif = ExifInterface(it) | |
} | |
} catch (e: IOException) { | |
Timber.e(e) | |
} | |
return getExifAttributeInt(exif) | |
} | |
private fun getOrientationLegacy(imagePath: String): Int { | |
var exif: ExifInterface? = null | |
try { | |
exif = ExifInterface(imagePath) | |
} catch (e: IOException) { | |
Timber.e(e) | |
} | |
return getExifAttributeInt(exif) | |
} | |
private fun getExifAttributeInt(exif: ExifInterface?): Int { | |
val orientation = exif?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) | |
return orientation ?: ExifInterface.ORIENTATION_UNDEFINED | |
} | |
/** | |
* Get image file from uri | |
* | |
* @param context The context | |
* @param uri The Uri of the image | |
* @return Image file | |
*/ | |
fun getFileFromUri(context: Context, | |
uri: Uri): File? { | |
var filePath: String? = null | |
// DocumentProvider | |
if (DocumentsContract.isDocumentUri(context, uri)) { | |
// ExternalStorageProvider | |
if (isExternalStorageDocument(uri)) { | |
val docId = DocumentsContract.getDocumentId(uri) | |
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() | |
val type = split[0] | |
if ("primary".equals(type, ignoreCase = true)) { | |
filePath = Environment.getExternalStorageDirectory().toString() + "/" + split[1] | |
} | |
} else if (isDownloadsDocument(uri)) { | |
val id = DocumentsContract.getDocumentId(uri) | |
// String "id" may not represent a valid Long type data, it may equals to | |
// something like "raw:/storage/emulated/0/Download/some_file" instead. | |
// Doing a check before passing the "id" to Long.valueOf(String) would be much safer. | |
filePath = if (RawDocumentsHelper.isRawDocId(id)) { | |
RawDocumentsHelper.getAbsoluteFilePath(id) | |
} else { | |
val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id)) | |
getDataColumn(context, contentUri, null, null) | |
} | |
} else if (isMediaDocument(uri)) { | |
val docId = DocumentsContract.getDocumentId(uri) | |
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() | |
val type = split[0] | |
var contentUri: Uri? = null | |
when (type) { | |
"image" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI | |
"video" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI | |
"audio" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI | |
} | |
val selection = "_id=?" | |
val selectionArgs = arrayOf(split[1]) | |
filePath = getDataColumn(context, contentUri, selection, selectionArgs) | |
} else if (isGoogleDriveDocument(uri)) { | |
return getGoogleDriveFile(context, uri) | |
}// MediaProvider | |
// DownloadsProvider | |
} else if ("content".equals(uri.scheme!!, ignoreCase = true)) { | |
if (isGooglePhotosUri(uri)) { | |
filePath = uri.lastPathSegment | |
} else { | |
filePath = getDataColumn(context, uri, null, null) | |
} | |
} else if ("file".equals(uri.scheme!!, ignoreCase = true)) { | |
filePath = uri.path | |
}// File | |
// MediaStore (and general) | |
return if (filePath != null) { | |
File(filePath) | |
} else null | |
} | |
// A copy of com.android.providers.downloads.RawDocumentsHelper since it is invisibility. | |
object RawDocumentsHelper { | |
val RAW_PREFIX = "raw:" | |
fun isRawDocId(docId: String?): Boolean { | |
return docId != null && docId.startsWith(RAW_PREFIX) | |
} | |
fun getDocIdForFile(file: File): String { | |
return RAW_PREFIX + file.absolutePath | |
} | |
fun getAbsoluteFilePath(rawDocumentId: String): String { | |
return rawDocumentId.substring(RAW_PREFIX.length) | |
} | |
} | |
/** | |
* Get the value of the data column for this Uri. This is useful for | |
* MediaStore Uris, and other file-based ContentProviders. | |
* | |
* @param context The context. | |
* @param uri The Uri to query. | |
* @param selection (Optional) Filter used in the query. | |
* @param selectionArgs (Optional) Selection arguments used in the query. | |
* @return The value of the _data column, which is typically a file path. | |
*/ | |
private fun getDataColumn(context: Context, uri: Uri?, selection: String?, | |
selectionArgs: Array<String>?): String? { | |
var cursor: Cursor? = null | |
val projection = arrayOf(MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME) | |
try { | |
cursor = context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) | |
if (cursor != null && cursor.moveToFirst()) { | |
val columnName = cursor.columnNames.toList().first() | |
val columnIndex = cursor.getColumnIndexOrThrow(columnName) | |
if (columnIndex != -1) { | |
return cursor.getString(columnIndex) | |
} | |
} | |
} finally { | |
cursor?.close() | |
} | |
return null | |
} | |
/** | |
* @param uri The Uri to check. | |
* @return Whether the Uri authority is ExternalStorageProvider. | |
*/ | |
private fun isExternalStorageDocument(uri: Uri): Boolean { | |
return "com.android.externalstorage.documents" == uri.authority | |
} | |
/** | |
* @param uri The Uri to check. | |
* @return Whether the Uri authority is DownloadsProvider. | |
*/ | |
private fun isDownloadsDocument(uri: Uri): Boolean { | |
return "com.android.providers.downloads.documents" == uri.authority | |
} | |
/** | |
* @param uri The Uri to check. | |
* @return Whether the Uri authority is MediaProvider. | |
*/ | |
private fun isMediaDocument(uri: Uri): Boolean { | |
return "com.android.providers.media.documents" == uri.authority | |
} | |
/** | |
* @param uri The Uri to check. | |
* @return Whether the Uri authority is Google Photos. | |
*/ | |
private fun isGooglePhotosUri(uri: Uri): Boolean { | |
return "com.google.android.apps.photos.content" == uri.authority | |
} | |
/** | |
* @param uri The Uri to check | |
* @return Whether the Uri authority is Google Drive. | |
*/ | |
private fun isGoogleDriveDocument(uri: Uri): Boolean { | |
return "com.google.android.apps.docs.storage" == uri.authority | |
} | |
/** | |
* @param context The context | |
* @param uri The Uri of Google Drive file | |
* @return Google Drive file | |
*/ | |
private fun getGoogleDriveFile(context: Context, uri: Uri?): File? { | |
if (uri == null) return null | |
val filePath = File(context.cacheDir, "tmp").absolutePath | |
val pfd = context.contentResolver.openFileDescriptor(uri, "r") ?: return null | |
val fd = pfd.fileDescriptor | |
FileInputStream(fd).use { ist -> | |
FileOutputStream(filePath).use { ost -> | |
val buffer = ByteArray(4096) | |
var size: Int = -1 | |
while (ist.read(buffer).let { size = it; it != -1 }) { | |
ost.write(buffer, 0, size) | |
} | |
} | |
return File(filePath) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment