Skip to content

Instantly share code, notes, and snippets.

@igreenwood
Last active January 10, 2022 09:33
Show Gist options
  • Save igreenwood/9bd17f0b30396ea40bd5010c5b759c46 to your computer and use it in GitHub Desktop.
Save igreenwood/9bd17f0b30396ea40bd5010c5b759c46 to your computer and use it in GitHub Desktop.
How to create down-scaled image file before image upload
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