diff --git a/app/build.gradle b/app/build.gradle index 07f03ed..1a4db13 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,7 +28,7 @@ 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 project(':compressor') implementation 'id.zelory:compressor:3.0.1' 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..1acba69 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 @@ -42,6 +38,7 @@ import kotlin.math.pow class MainActivity : AppCompatActivity() { companion object { private const val PICK_IMAGE_REQUEST = 1 + private const val TAG = "MainActivity" } private var actualImage: File? = null @@ -77,6 +74,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,12 +90,18 @@ class MainActivity : AppCompatActivity() { }*/ // Full custom + val destFile = File("${cachePath(this@MainActivity)}compressed_${imageFile.name}") + val start = System.currentTimeMillis() 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 } + val end = System.currentTimeMillis() + Log.d(TAG, "compress cost: ${end - start}ms") setCompressedImage() } } ?: showError("Please choose an image!") 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) {