Код: Выделить всё
class ImageCompressor(
private val context: Context,
private val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
private var calculatedInSampleSize: Int = 1
/**
* Compress image represented by [sourceUri] and save it to [targetFile].
*
* Decoding strategy:
* - **API 28+**: uses the Android 9.0+ `ImageDecoder` API for all formats
* including HEIC/HEIF, applying EXIF rotation and down-sampling in one step.
* - **API 26–27**: falls back to `BitmapFactory.decodeFileDescriptor`
*
* @param sourceUri [Uri] to source image.
* @param targetFile [File] to which compressed image will be saved.
* @param compressRatio The [CompressRatio].
* @return trueКод: Выделить всё
falsesuspend fun compress(
sourceUri: Uri,
targetFile: File,
compressRatio: CompressRatio
): Boolean = withContext(dispatcher) {
calculatedInSampleSize = 1
val contentResolver = context.contentResolver
val descriptor = contentResolver.openFileDescriptor(sourceUri, "r", null)
val statSize = descriptor?.statSize ?: 0L
if (statSize 0L) statSize else getFileSize(sourceUri)
if (queriedSize compressRatio.maxSizeBytes) {
Timber.d("File still too large, will try with higher compression in next iteration")
}
} while (compressedSize > compressRatio.maxSizeBytes)
val success = compressedSize != 0L && targetFile.exists() && targetFile.length() > 0
Timber.d("Compression ${if (success) "succeeded" else "failed"} after $iterationCount iterations. Final size: $compressedSize bytes")
success
} == true
}
private fun Uri.decodeSampledBitmap(
contentResolver: ContentResolver,
maxSize: Int,
calculateSampleSize: Boolean
): Bitmap? {
val bitmapUri = this
var sampledBitmap: Bitmap? = null
try {
BitmapFactory.Options().run {
val descriptor = contentResolver.openFileDescriptor(
bitmapUri,
"r",
null
)?.fileDescriptor
descriptor?.let {
if (calculateSampleSize) {
inJustDecodeBounds = true
BitmapFactory.decodeFileDescriptor(it, null, this)
calculateInSampleSize(this, maxSize)
}
inSampleSize = calculatedInSampleSize
inJustDecodeBounds = false
sampledBitmap = BitmapFactory.decodeFileDescriptor(it, null, this)
}
}
} catch (e: Exception) {
Timber.w(e, "Could not decode sampled bitmap.")
}
calculatedInSampleSize *= 2 // for next iteration if file still too big
return sampledBitmap?.rotate(contentResolver, this@decodeSampledBitmap)
}
/**
* Decodes this Uri into a Bitmap, down-sampling so both dimensions ≤ maxSize.
* On API 28+ it uses ImageDecoder (native HEIC/HEIF support + all other formats).
* On older APIs it falls back to BitmapFactory sampling.
*/
private fun Uri.decodeBitmapCompat(
resolver: ContentResolver,
maxSize: Int,
calculateSampleSize: Boolean
): Bitmap? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val src = ImageDecoder.createSource(resolver, this)
try {
ImageDecoder.decodeBitmap(src) { decoder, info, _ ->
if (!calculateSampleSize) {
val w = info.size.width
val h = info.size.height
val origMax = maxOf(w, h).toFloat()
if (origMax > maxSize) {
val ratio = origMax / maxSize
decoder.setTargetSize((w / ratio).toInt(), (h / ratio).toInt())
}
}
}
} catch (e: IOException) {
Timber.w(e, "ImageDecoder failed — falling back to BitmapFactory")
// fallback to old path
this.decodeSampledBitmap(resolver, maxSize, calculateSampleSize)
}
} else {
decodeSampledBitmap(resolver, maxSize, calculateSampleSize)
}
}
private fun calculateInSampleSize(options: BitmapFactory.Options, maxSize: Int) {
// Raw height and width of image
val (height: Int, width: Int) = options.run { outHeight to outWidth }
if (height > maxSize || width > maxSize) {
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 maxSize.
while (halfHeight / calculatedInSampleSize >= maxSize && halfWidth / calculatedInSampleSize >= maxSize) {
calculatedInSampleSize *= 2
}
}
}
private fun Bitmap.rotate(contentResolver: ContentResolver, contentUri: Uri): Bitmap {
var rotatedBitmap: Bitmap? = null
try {
val fd = contentResolver.openFileDescriptor(contentUri, "r", null)
fd?.fileDescriptor?.let {
val exif = ExifInterface(it)
val matrix: Matrix? = when (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)) {
ExifInterface.ORIENTATION_ROTATE_90 -> Matrix().apply { postRotate(90f) }
ExifInterface.ORIENTATION_ROTATE_180 -> Matrix().apply { postRotate(180f) }
ExifInterface.ORIENTATION_ROTATE_270 -> Matrix().apply { postRotate(270f) }
else -> null
}
if (matrix != null) {
rotatedBitmap = Bitmap.createBitmap(this, 0, 0, this.width, this.height, matrix, true)
}
}
} catch (e: Exception) {
Timber.w(e, "Could not rotate bitmap.")
}
return rotatedBitmap ?: this
}
/**
* Save bitmap and return its size or 0 is save was not successful.
*/
private fun Bitmap.save(destination: File, quality: Int): Long {
var fileOutputStream: FileOutputStream? = null
try {
fileOutputStream = FileOutputStream(destination.absolutePath)
compress(Bitmap.CompressFormat.JPEG, quality, fileOutputStream)
} catch (e: Exception) {
Timber.e(e, "Unable to decode stream.")
} finally {
fileOutputStream?.run {
try {
flush()
close()
} catch (e: Exception) {
// just ignore
}
}
}
return try {
destination.length()
} catch (e: Exception) {
0
}
}
private fun getFileSize(uri: Uri): Long =
context.contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)
?.use { cursor ->
val idx = cursor.getColumnIndex(OpenableColumns.SIZE)
if (cursor.moveToFirst() && idx != -1) cursor.getLong(idx) else -1L
} ?: -1L
}
< /code>
Журнал с затронутого устройства < /p>
[2025-08-18 11:49:44][DEBUG] Compressing file with original size=5619029 bytes, maxSizeBytes=3000000, maxWidthOrHeight=600, quality=90
[2025-08-18 11:49:45][DEBUG] Iteration 1: Decoded bitmap with inSampleSize=1, bitmap size=4160x3120
[2025-08-18 11:49:45][DEBUG] Iteration 1: Compressed file size=76888 bytes (1% of original), target max size=3000000
[2025-08-18 11:49:45][DEBUG] Compression succeeded after 1 iterations. Final size: 76888 bytes
Подробнее здесь: https://stackoverflow.com/questions/797 ... me-devices
Мобильная версия