Skip to content

joelromanpr/tiny-compressor-ktx

Repository files navigation

Tiny Compressor KTX

Maven Central

A tiny, modern image compression library for Android. Kotlin-first, coroutine/Flow-friendly, and Compose-ready with a small but powerful API.

Features

  • Modern, concise API: suspend functions and an optional Flow for progress.
  • Small footprint: No heavyweight image pipelines; lightweight EXIF support.
  • Sensible defaults: Safe max dimensions, quality, and color space.
  • EXIF preservation: Copies common EXIF tags for JPEG outputs.
  • Broad format support: JPEG, PNG, WEBP (lossy and lossless when applicable).
  • Efficient decoding: Uses ImageDecoder on API 28+ for predictable memory use.
  • Compose-ready: Easy to wire progress into your UI.
  • Flexible I/O:
    • Inputs: File, Uri, ByteArray.
    • Outputs: File or ByteArray.

Installation

Make sure you have mavenCentral() in your root settings.gradle.kts repositories block:

dependencyResolutionManagement {
    repositories {
        // ...
        mavenCentral()
    }
}

Then, add the dependency to your module's build.gradle.kts:

dependencies {
    implementation("io.github.joelromanpr:tiny-compressor-ktx:1.0.0")
}

Usage & Recipes

1. Basic Compression (Fire-and-Forget)

For simple cases where you don't need progress updates, use the suspend functions.

Compress a File to another File:

suspend fun compressPhoto(context: Context, inputFile: File): File {
    return ImageCompressor.compress(
        context = context,
        source = Source.File(inputFile),
        options = Options(
            maxWidth = 1600,
            maxHeight = 1600,
            format = CompressFormat.JPEG,
            quality = 82
        )
    )
}

Compress a content Uri to a ByteArray:

suspend fun compressFromUriToBytes(context: Context, uri: Uri): ByteArray {
    return ImageCompressor.compressToByteArray(
        context = context,
        source = Source.Uri(uri),
        options = Options(
            maxWidth = 1280,
            maxHeight = 1280,
            format = CompressFormat.WEBP,
            quality = 85
        )
    )
}

2. Observing Progress with Flow

For UI integration, use compressAsFlow to receive Progress updates.

This example shows a ViewModel that launches compression and exposes the state to the UI.

ViewModel:

data class CompressionState(
    val inProgress: Boolean = false,
    val percent: Int = 0,
    val outputFile: File? = null,
    val error: Throwable? = null
)

class PhotoCompressViewModel(
    private val appContext: android.content.Context
) : ViewModel() {

    private val _state = MutableStateFlow(CompressionState())
    val state = _state.asStateFlow()

    private var job: Job? = null

    fun compress(uri: Uri) {
        job?.cancel()
        job = viewModelScope.launch {
            ImageCompressor.compressAsFlow(
                context = appContext,
                source = Source.Uri(uri)
            )
            .onStart { _state.value = CompressionState(inProgress = true) }
            .onCompletion { if (it != null) _state.value = _state.value.copy(inProgress = false, error = it) }
            .collect { p ->
                val isDone = p.step.isDone()
                _state.value = _state.value.copy(
                    inProgress = !isDone,
                    percent = p.percent,
                    outputFile = if (isDone) p.file else _state.value.outputFile
                )
            }
        }
    }
}

Jetpack Compose Screen:

@Composable
fun PhotoCompressScreen(vm: PhotoCompressViewModel) {
    val state by vm.state.collectAsStateWithLifecycle()
    val pickMedia = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.PickVisualMedia()
    ) { uri ->
        if (uri != null) vm.compress(uri)
    }

    Column {
        Button(
            onClick = {
                pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
            },
            enabled = !state.inProgress
        ) {
            Text("Pick & Compress Photo")
        }

        if (state.inProgress) {
            LinearProgressIndicator(progress = state.percent / 100f)
            Text("Compressing… ${state.percent}%")
        }

        state.outputFile?.let { file ->
            Text("Output: ${file.absolutePath} (${file.length() / 1024} KB)")
        }

        state.error?.let {
            Text("Error: ${it.message}", color = MaterialTheme.colorScheme.error)
        }
    }
}

3. Background Compression with WorkManager

For long-running or batch jobs, WorkManager is the best choice.

class CompressWorker(
    appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
    override suspend fun doWork(): Result {
        val uriStr = inputData.getString("sourceUri") ?: return Result.failure()
        val uri = Uri.parse(uriStr)

        return try {
            val outFile = ImageCompressor.compress(
                context = applicationContext,
                source = Source.Uri(uri)
            )
            Result.success(workDataOf("outPath" to outFile.absolutePath))
        } catch (t: Throwable) {
            Result.retry() // or Result.failure()
        }
    }
}

fun enqueueCompression(workManager: WorkManager, sourceUri: Uri) {
    val request = OneTimeWorkRequestBuilder<CompressWorker>()
        .setInputData(workDataOf("sourceUri" to sourceUri.toString()))
        .setConstraints(Constraints(requiresStorageNotLow = true))
        .build()
    workManager.enqueue(request)
}

API Reference

// Main entry points
public object ImageCompressor {
    public suspend fun compress(context: Context, source: Source, options: Options = Options()): File
    public suspend fun compressToByteArray(context: Context, source: Source, options: Options = Options()): ByteArray
    public fun compressAsFlow(context: Context, source: Source, options: Options = Options()): Flow<Progress>
}

// Configuration & State
public data class Options(
    public val maxWidth: Int = 1280,
    public val maxHeight: Int = 1280,
    public val format: CompressFormat = CompressFormat.JPEG,
    public val quality: Int = 80,
    public val maxBytes: Long? = null,
    public val keepExif: Boolean = true,
    public val colorSpace: ColorSpace = ColorSpace.SRGB,
    public val destination: Destination = Destination.Cache()
)

public data class Progress(
    public val step: Step,
    public val percent: Int,
    public val file: File? = null
)

public enum class Step {
    Loading, Decoding, Resizing, Encoding, Writing, Done;
    public fun isDone(): Boolean
}

// Inputs and Destination
public sealed class Source {
    public data class File(public val file: java.io.File) : Source()
    public data class Uri(public val uri: android.net.Uri) : Source()
    public data class Bytes(public val bytes: ByteArray) : Source()
}

public sealed class Destination {
    public data class File(public val file: java.io.File) : Destination()
    public data class Cache(public val subdir: String = "images") : Destination()
}

Best Practices

  • Permissions: This library only handles compression. Your app is responsible for requesting storage permissions or using Storage Access Framework (e.g., Photo Picker) to get a readable Uri.
  • Threading: All heavy work is dispatched to Dispatchers.IO internally. You can safely call these functions from the main thread.
  • Lifecycle: Keep compression work out of Composable functions. Trigger compression from a ViewModel or a LaunchedEffect tied to a specific state.
  • Size Constraints: To meet a specific file size (e.g., for uploads), set maxBytes. The library will first reduce quality (for lossy formats) and then downscale the image iteratively to meet the constraint.
  • Transparency: Prefer PNG or WEBP for images with an alpha channel. JPEG does not support transparency.

License

Licensed under the MIT License. See the root LICENSE file for details.

About

A tiny, modern image compression library for Android. Kotlin-first, coroutine/Flow-friendly, and Compose-ready with a small but powerful API. Sensible defaults, EXIF preservation, and efficient decoding via ImageDecoder on modern devices.

Resources

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

Sponsor this project

Contributors

Languages