From 0e5554e65659ea9efba22f7ec1678db83718c0d0 Mon Sep 17 00:00:00 2001 From: yuruiyin Date: Sat, 6 Mar 2021 22:34:37 +0800 Subject: [PATCH] Imporve custom compress speed via merging the multi Constraints and other optimations as follows: 1. merge multi ResolutionConstraints and QualityConstraints and FormatConstraints (without DestinationConstraint and SizeConstraint) into only one DefaultConstraint 2. Move the SizeConstraint to the last post since it can reduce the number of compress times in this type of SizeConstraint. 3. In SizeConstraint, we can avoid performing the compression if the bitmap compress format is png. 4. If there is a DestinationConstraint, we can reduce the number of copy file times via using the dest file from DestinationConstraint in the method of copyToCache. --- app/build.gradle | 4 +- .../zelory/compressor/sample/MainActivity.kt | 20 +++--- .../java/id/zelory/compressor/Compressor.kt | 72 +++++++++++++++++-- .../main/java/id/zelory/compressor/Util.kt | 12 +++- .../constraint/DefaultConstraint.kt | 8 +++ .../constraint/DestinationConstraint.kt | 2 + .../compressor/constraint/FormatConstraint.kt | 2 + .../constraint/QualityConstraint.kt | 2 + .../constraint/ResolutionConstraint.kt | 4 ++ .../compressor/constraint/SizeConstraint.kt | 5 +- 10 files changed, 113 insertions(+), 18 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8e6973e..761086f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,8 +28,8 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0' implementation 'androidx.appcompat:appcompat:1.1.0' - //implementation project(':compressor') - implementation 'id.zelory:compressor:3.0.0' + implementation project(':compressor') +// implementation 'id.zelory:compressor:3.0.0' testImplementation 'junit:junit:4.12' } diff --git a/app/src/main/java/id/zelory/compressor/sample/MainActivity.kt b/app/src/main/java/id/zelory/compressor/sample/MainActivity.kt index e0141af..7e01f39 100644 --- a/app/src/main/java/id/zelory/compressor/sample/MainActivity.kt +++ b/app/src/main/java/id/zelory/compressor/sample/MainActivity.kt @@ -1,5 +1,6 @@ package id.zelory.compressor.sample +import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -11,12 +12,7 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import id.zelory.compressor.Compressor -import id.zelory.compressor.constraint.default -import id.zelory.compressor.constraint.destination -import id.zelory.compressor.constraint.format -import id.zelory.compressor.constraint.quality -import id.zelory.compressor.constraint.resolution -import id.zelory.compressor.constraint.size +import id.zelory.compressor.constraint.* import id.zelory.compressor.loadBitmap import kotlinx.android.synthetic.main.activity_main.actualImageView import kotlinx.android.synthetic.main.activity_main.actualSizeTextView @@ -77,6 +73,9 @@ class MainActivity : AppCompatActivity() { } ?: showError("Please choose an image!") } + private val separator = File.separator + private fun cachePath(context: Context) = "${context.cacheDir.path}${separator}compressor$separator" + private fun customCompressImage() { actualImage?.let { imageFile -> lifecycleScope.launch { @@ -90,11 +89,14 @@ class MainActivity : AppCompatActivity() { }*/ // Full custom + val destFile = File("${cachePath(this@MainActivity)}compressed_${imageFile.name}") compressedImage = Compressor.compress(this@MainActivity, imageFile) { - resolution(1280, 720) - quality(80) +// destination(destFile) + resolution(2000, 2000) format(Bitmap.CompressFormat.WEBP) - size(2_097_152) // 2 MB + quality(100) + size(2_000_000) // 5M +// size(2_097_152) // 2 MB } setCompressedImage() } diff --git a/compressor/src/main/java/id/zelory/compressor/Compressor.kt b/compressor/src/main/java/id/zelory/compressor/Compressor.kt index bfc5ffe..d09c336 100644 --- a/compressor/src/main/java/id/zelory/compressor/Compressor.kt +++ b/compressor/src/main/java/id/zelory/compressor/Compressor.kt @@ -1,8 +1,7 @@ package id.zelory.compressor import android.content.Context -import id.zelory.compressor.constraint.Compression -import id.zelory.compressor.constraint.default +import id.zelory.compressor.constraint.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File @@ -22,12 +21,77 @@ object Compressor { compressionPatch: Compression.() -> Unit = { default() } ) = withContext(coroutineContext) { val compression = Compression().apply(compressionPatch) - var result = copyToCache(context, imageFile) - compression.constraints.forEach { constraint -> + val mergedConstraints = mergeConstraints(compression.constraints, imageFile) + val destinationFile = getDestinationFile(mergedConstraints) + var result = copyToCache(context, imageFile, destinationFile) + mergedConstraints.forEach { constraint -> while (constraint.isSatisfied(result).not()) { result = constraint.satisfy(result) } } return@withContext result } + + private fun getDestinationFile(constraints: MutableList): File? { + constraints.forEach { + if (it is DestinationConstraint) { + constraints.remove(it) + return it.getDestination() + } + } + return null + } + + /** + * need merge constraints for improving compress speed + * 1)merge multi ResolutionConstraints and QualityConstraints and FormatConstraints + * (without DestinationConstraint and SizeConstraint) into only one DefaultConstraint. + * 2)SizeConstraint must be moved to the last pos since it can reduce + * the number of compress times in this type of SizeConstraint. + */ + private fun mergeConstraints(constraints: MutableList, imageFile: File): MutableList { + val size = getImageDimension(imageFile) + var (width: Int, height: Int) = size.run { get(0) to get(1) } + var quality = 100 + var format = imageFile.compressFormat() + val resConstraints = mutableListOf() + var sizeConstraint: SizeConstraint? = null + val visited = hashSetOf>() + + for (i in constraints.size - 1 downTo 0) { + when (val it = constraints[i]) { + is ResolutionConstraint -> { + width = it.getWidth() + height = it.getHeight() + } + is QualityConstraint -> { + quality = it.getQuality() + } + is FormatConstraint -> { + format = it.getFormat() + } + is DefaultConstraint -> { + width = it.getWidth() + height = it.getHeight() + quality = it.getQuality() + format = it.getFormat() + } + is SizeConstraint -> sizeConstraint = it + else -> run { + // DestinationConstraint or SizeConstraint + if (visited.contains(it.javaClass)) { + return@run + } + visited.add(it.javaClass) + resConstraints.add(it) + } + } + } + + resConstraints.add(DefaultConstraint(width, height, format, quality)) + sizeConstraint?.let { + resConstraints.add(it) + } + return resConstraints + } } \ No newline at end of file diff --git a/compressor/src/main/java/id/zelory/compressor/Util.kt b/compressor/src/main/java/id/zelory/compressor/Util.kt index bcb1cb3..e6c249b 100644 --- a/compressor/src/main/java/id/zelory/compressor/Util.kt +++ b/compressor/src/main/java/id/zelory/compressor/Util.kt @@ -46,6 +46,14 @@ fun decodeSampledBitmapFromFile(imageFile: File, reqWidth: Int, reqHeight: Int): } } +fun getImageDimension(imageFile: File): IntArray { + return BitmapFactory.Options().run { + inJustDecodeBounds = true + BitmapFactory.decodeFile(imageFile.absolutePath, this) + intArrayOf(outWidth, outHeight) + } +} + 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 } @@ -78,8 +86,8 @@ fun determineImageRotation(imageFile: File, bitmap: Bitmap): Bitmap { return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) } -internal fun copyToCache(context: Context, imageFile: File): File { - return imageFile.copyTo(File("${cachePath(context)}${imageFile.name}"), true) +internal fun copyToCache(context: Context, imageFile: File, targetFile: File? = null): File { + return imageFile.copyTo(targetFile ?: File("${cachePath(context)}${imageFile.name}"), true) } fun overWrite(imageFile: File, bitmap: Bitmap, format: Bitmap.CompressFormat = imageFile.compressFormat(), quality: Int = 100): File { diff --git a/compressor/src/main/java/id/zelory/compressor/constraint/DefaultConstraint.kt b/compressor/src/main/java/id/zelory/compressor/constraint/DefaultConstraint.kt index 7d4b112..4250765 100644 --- a/compressor/src/main/java/id/zelory/compressor/constraint/DefaultConstraint.kt +++ b/compressor/src/main/java/id/zelory/compressor/constraint/DefaultConstraint.kt @@ -33,6 +33,14 @@ class DefaultConstraint( isResolved = true return result } + + fun getWidth(): Int = width + + fun getHeight(): Int = height + + fun getFormat(): Bitmap.CompressFormat = format + + fun getQuality(): Int = quality } fun Compression.default( diff --git a/compressor/src/main/java/id/zelory/compressor/constraint/DestinationConstraint.kt b/compressor/src/main/java/id/zelory/compressor/constraint/DestinationConstraint.kt index 66134e8..cb0510c 100644 --- a/compressor/src/main/java/id/zelory/compressor/constraint/DestinationConstraint.kt +++ b/compressor/src/main/java/id/zelory/compressor/constraint/DestinationConstraint.kt @@ -16,6 +16,8 @@ class DestinationConstraint(private val destination: File) : Constraint { override fun satisfy(imageFile: File): File { return imageFile.copyTo(destination, true) } + + fun getDestination(): File = destination } fun Compression.destination(destination: File) { diff --git a/compressor/src/main/java/id/zelory/compressor/constraint/FormatConstraint.kt b/compressor/src/main/java/id/zelory/compressor/constraint/FormatConstraint.kt index 38111e9..5f9a318 100644 --- a/compressor/src/main/java/id/zelory/compressor/constraint/FormatConstraint.kt +++ b/compressor/src/main/java/id/zelory/compressor/constraint/FormatConstraint.kt @@ -21,6 +21,8 @@ class FormatConstraint(private val format: Bitmap.CompressFormat) : Constraint { override fun satisfy(imageFile: File): File { return overWrite(imageFile, loadBitmap(imageFile), format) } + + fun getFormat(): Bitmap.CompressFormat = format } fun Compression.format(format: Bitmap.CompressFormat) { diff --git a/compressor/src/main/java/id/zelory/compressor/constraint/QualityConstraint.kt b/compressor/src/main/java/id/zelory/compressor/constraint/QualityConstraint.kt index 828657e..f62d85a 100644 --- a/compressor/src/main/java/id/zelory/compressor/constraint/QualityConstraint.kt +++ b/compressor/src/main/java/id/zelory/compressor/constraint/QualityConstraint.kt @@ -22,6 +22,8 @@ class QualityConstraint(private val quality: Int) : Constraint { isResolved = true return result } + + fun getQuality(): Int = quality } fun Compression.quality(quality: Int) { diff --git a/compressor/src/main/java/id/zelory/compressor/constraint/ResolutionConstraint.kt b/compressor/src/main/java/id/zelory/compressor/constraint/ResolutionConstraint.kt index 9b1b75f..21f8729 100644 --- a/compressor/src/main/java/id/zelory/compressor/constraint/ResolutionConstraint.kt +++ b/compressor/src/main/java/id/zelory/compressor/constraint/ResolutionConstraint.kt @@ -30,6 +30,10 @@ class ResolutionConstraint(private val width: Int, private val height: Int) : Co } } } + + fun getWidth(): Int = width + + fun getHeight(): Int = height } fun Compression.resolution(width: Int, height: Int) { diff --git a/compressor/src/main/java/id/zelory/compressor/constraint/SizeConstraint.kt b/compressor/src/main/java/id/zelory/compressor/constraint/SizeConstraint.kt index 95d14ea..a5dad00 100644 --- a/compressor/src/main/java/id/zelory/compressor/constraint/SizeConstraint.kt +++ b/compressor/src/main/java/id/zelory/compressor/constraint/SizeConstraint.kt @@ -1,5 +1,7 @@ package id.zelory.compressor.constraint +import android.graphics.Bitmap +import id.zelory.compressor.compressFormat import id.zelory.compressor.loadBitmap import id.zelory.compressor.overWrite import java.io.File @@ -19,7 +21,7 @@ class SizeConstraint( private var iteration: Int = 0 override fun isSatisfied(imageFile: File): Boolean { - return imageFile.length() <= maxFileSize || iteration >= maxIteration + return imageFile.compressFormat() == Bitmap.CompressFormat.PNG || imageFile.length() <= maxFileSize || iteration >= maxIteration } override fun satisfy(imageFile: File): File { @@ -27,6 +29,7 @@ class SizeConstraint( val quality = (100 - iteration * stepSize).takeIf { it >= minQuality } ?: minQuality return overWrite(imageFile, loadBitmap(imageFile), quality = quality) } + } fun Compression.size(maxFileSize: Long, stepSize: Int = 10, maxIteration: Int = 10) {