Skip to content

Imporve custom compress speed via merging the multi Constraints and other optimizations as follows: #178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
24 changes: 15 additions & 9 deletions app/src/main/java/id/zelory/compressor/sample/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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!")
Expand Down
72 changes: 68 additions & 4 deletions compressor/src/main/java/id/zelory/compressor/Compressor.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Constraint>): 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<Constraint>, imageFile: File): MutableList<Constraint> {
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<Constraint>()
var sizeConstraint: SizeConstraint? = null
val visited = hashSetOf<Class<Constraint>>()

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
}
}
12 changes: 10 additions & 2 deletions compressor/src/main/java/id/zelory/compressor/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class QualityConstraint(private val quality: Int) : Constraint {
isResolved = true
return result
}

fun getQuality(): Int = quality
}

fun Compression.quality(quality: Int) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,14 +21,15 @@ 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 {
iteration++
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) {
Expand Down