musikr: document/cleanup covers

Probably the first module I'm comfortable fully documenting.
This commit is contained in:
Alexander Capehart 2025-03-17 12:28:14 -06:00
parent 7523298237
commit 3df6e2f0b1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
9 changed files with 379 additions and 100 deletions

View file

@ -28,6 +28,8 @@ import org.oxycblt.musikr.covers.Cover
import org.oxycblt.musikr.covers.Covers
import org.oxycblt.musikr.covers.FDCover
import org.oxycblt.musikr.covers.MutableCovers
import org.oxycblt.musikr.covers.chained.ChainedCovers
import org.oxycblt.musikr.covers.chained.MutableChainedCovers
import org.oxycblt.musikr.covers.embedded.CoverIdentifier
import org.oxycblt.musikr.covers.embedded.EmbeddedCovers
import org.oxycblt.musikr.covers.fs.FSCovers
@ -43,7 +45,7 @@ interface SettingCovers {
companion object {
suspend fun immutable(context: Context): Covers<FDCover> =
Covers.chain(StoredCovers(CoverStorage.at(context.coversDir())), FSCovers(context))
ChainedCovers(StoredCovers(CoverStorage.at(context.coversDir())), FSCovers(context))
}
}
@ -64,7 +66,7 @@ class SettingCoversImpl @Inject constructor(private val imageSettings: ImageSett
MutableStoredCovers(
EmbeddedCovers(CoverIdentifier.md5()), coverStorage, revisionedTranscoding)
val fsCovers = MutableFSCovers(context)
return MutableCovers.chain(storedCovers, fsCovers)
return MutableChainedCovers(storedCovers, fsCovers)
}
}

View file

@ -23,71 +23,95 @@ import java.io.InputStream
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
/**
* An immutable repository for cover information.
*
* While not directly required by the music loader, this can still be used to work with covers
* marshalled over some I/O boundary via their ID.
*/
interface Covers<T : Cover> {
/**
* Obtain a cover instance by it's ID.
*
* You cannot assume anything about the data source this will use.
*
* @param id The ID of the cover to obtain
* @return a [CoverResult] indicating whether the cover was found or not
*/
suspend fun obtain(id: String): CoverResult<T>
companion object {
fun <R : Cover, T : R> chain(vararg many: Covers<out T>): Covers<R> =
object : Covers<R> {
override suspend fun obtain(id: String): CoverResult<R> {
for (cover in many) {
val result = cover.obtain(id)
if (result is CoverResult.Hit) {
return CoverResult.Hit(result.cover)
}
}
return CoverResult.Miss()
}
}
}
}
/**
* An mutable repoistory for cover information.
*
* This is explicitly required by the music loader to figure out cover instances to use over some
* I/O boundary.
*/
interface MutableCovers<T : Cover> : Covers<T> {
/**
* Create a cover instance for the given [file] and [metadata].
*
* This could result in side-effect-laden storage, or be a simple translation into a lazily
* loaded [Cover] instance.
*
* @param file The [DeviceFile] to of the file to create a cover for.
* @param metadata The [Metadata] to use to create the cover.
* @return a [CoverResult] indicating whether the cover was created or not
*/
suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<T>
/**
* Cleanup the cover repository by removing any covers that are not in the [excluding]
* collection.
*
* This is useful with cached covers to prevent accumulation of useless data.
*
* @param excluding The collection of covers to exclude from cleanup.
*/
suspend fun cleanup(excluding: Collection<Cover>)
companion object {
fun <R : Cover, T : R> chain(vararg many: MutableCovers<out T>): MutableCovers<R> =
object : MutableCovers<R> {
override suspend fun obtain(id: String): CoverResult<R> {
for (cover in many) {
val result = cover.obtain(id)
if (result is CoverResult.Hit) {
return CoverResult.Hit(result.cover)
}
}
return CoverResult.Miss()
}
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<R> {
for (cover in many) {
val result = cover.create(file, metadata)
if (result is CoverResult.Hit) {
return CoverResult.Hit(result.cover)
}
}
return CoverResult.Miss()
}
override suspend fun cleanup(excluding: Collection<Cover>) {
for (cover in many) {
cover.cleanup(excluding)
}
}
}
}
}
/** A result of a cover lookup. */
sealed interface CoverResult<T : Cover> {
/**
* A cover was found for the given ID/file.
*
* @param cover The cover that was found.
*/
data class Hit<T : Cover>(val cover: T) : CoverResult<T>
/**
* A cover was not found for the given ID.
*
* For [Covers.obtain], this implies that the cover repository is outdated for the particular
* song's cover ID queries. Therefore, returning it in that context will trigger the song to be
* re-extracted.
*
* For [MutableCovers.create], this implies that the song being queries does not have a cover.
* In that case, the song will be represented as not having a cover at all.
*/
class Miss<T : Cover> : CoverResult<T>
}
/**
* Some song's cover art.
*
* A cover can be backed by any kind of data source and depends on the [Covers]/[MutableCovers] that
* yields it.
*/
interface Cover {
/**
* The ID of the cover. This is used to identify the cover in the [Covers] repository, and is
* useful if the cover data needs to be marshalled over an I/O boundary.
*/
val id: String
/**
* Open the cover for reading. This might require blocking operations.
*
* @return an [InputStream] for the cover, or null if an error occurred. Assume nothing about
* the internal implementation of the stream or the validity of the image format.
*/
suspend fun open(): InputStream?
override fun equals(other: Any?): Boolean
@ -95,20 +119,50 @@ interface Cover {
override fun hashCode(): Int
}
/**
* A cover that can be opened as a [ParcelFileDescriptor]. This more or less implies that the cover
* is explicitly stored on-device somewhere.
*/
interface FDCover : Cover {
/**
* Open the cover for reading as a [ParcelFileDescriptor]. Useful in some content provider
* contexts. This might require blocking operations.
*
* @return a [ParcelFileDescriptor] for the cover, or null if an error occurred preventing it
* from being opened. Assume nothing about the validity of the image format.
*/
suspend fun fd(): ParcelFileDescriptor?
}
/**
* A cover exclusively hosted in-memory. These tend to not be exposed in practice and are often
* cached into a [FDCover].
*/
interface MemoryCover : Cover {
/** Get the raw data this cover holds. Might be a valid image. */
fun data(): ByteArray
}
/**
* A large collection of [Cover]s, organized by frequency.
*
* This is useful if you want to compose several [Cover]s into a single image.
*/
class CoverCollection private constructor(val covers: List<Cover>) {
override fun hashCode() = covers.hashCode()
override fun equals(other: Any?) = other is CoverCollection && covers == other.covers
companion object {
/**
* Create a [CoverCollection] from a collection of [Cover]s.
*
* This will deduplicate and organize the covers by frequency. Since doing such is a
* time-consuming operation that should be done asynchronously to avoid lockups in UI
* contexts.
*
* @return a [CoverCollection] containing the most frequent covers in the given collection.
*/
fun from(covers: Collection<Cover>) =
CoverCollection(
covers

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2025 Auxio Project
* ChainedCovers.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.covers.chained
import org.oxycblt.musikr.covers.Cover
import org.oxycblt.musikr.covers.CoverResult
import org.oxycblt.musikr.covers.Covers
import org.oxycblt.musikr.covers.MutableCovers
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
/**
* A [Covers] implementation that chains multiple [Covers] together as fallbacks.
*
* This is useful for when you want to try multiple sources for a cover, such as first embedded and
* then filesystem-based covers.
*
* This implementation will return the first hit from the provided [Covers] instances.
*
* It's assumed that there is no ID overlap between [MutableCovers] outputs.
*
* @param many The [Covers] instances to chain together.
*/
class ChainedCovers<R : Cover, T : R>(vararg many: Covers<out T>) : Covers<R> {
private val _many = many
override suspend fun obtain(id: String): CoverResult<R> {
for (covers in _many) {
val result = covers.obtain(id)
if (result is CoverResult.Hit) {
return CoverResult.Hit(result.cover)
}
}
return CoverResult.Miss()
}
}
/**
* A [MutableCovers] implementation that chains multiple [MutableCovers] together as fallbacks.
*
* This is useful for when you want to try multiple sources for a cover, such as first embedded and
* then filesystem-based covers.
*
* This implementation will use the first hit from the provided [MutableCovers] instances, and
* propagate cleanup across all [MutableCovers] instances.
*
* It's assumed that there is no ID overlap between [MutableCovers] outputs.
*
* @param many The [MutableCovers] instances to chain together.
*/
class MutableChainedCovers<R : Cover, T : R>(vararg many: MutableCovers<out T>) : MutableCovers<R> {
private val inner = ChainedCovers<R, T>(*many)
private val _many = many
override suspend fun obtain(id: String): CoverResult<R> = inner.obtain(id)
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<R> {
for (cover in _many) {
val result = cover.create(file, metadata)
if (result is CoverResult.Hit) {
return CoverResult.Hit(result.cover)
}
}
return CoverResult.Miss()
}
override suspend fun cleanup(excluding: Collection<Cover>) {
for (cover in _many) {
cover.cleanup(excluding)
}
}
}

View file

@ -20,10 +20,25 @@ package org.oxycblt.musikr.covers.embedded
import java.security.MessageDigest
/** An interface to transform embedded cover data into cover IDs, used for [EmbeddedCovers]. */
interface CoverIdentifier {
/**
* Identify the cover data and return a unique identifier for it. This should use a strong
* hashing algorithm to ensure uniqueness and minimize memory use.
*
* @param data the cover data to identify
* @return a unique identifier for the cover data, such as a hash
*/
suspend fun identify(data: ByteArray): String
companion object {
/**
* Returns a default implementation of [CoverIdentifier] that uses the MD5 hashing
* algorithm. Reasonably efficient for most default use-cases, but not secure if any
* extensions could be brought down by ID collisions.
*
* @return a [CoverIdentifier] that uses the MD5 hashing algorithm
*/
fun md5(): CoverIdentifier = MD5CoverIdentifier()
}
}

View file

@ -26,6 +26,21 @@ import org.oxycblt.musikr.covers.MutableCovers
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
/**
* A [MutableCovers] implementation for embedded covers, which are stored in the metadata of a
* track.
*
* This should NOT be used standalone, due to two major issues:
* - You cannot [obtain] covers with this implementation, as the cover data must be obtained from a
* file's extracted metadata. This will make all caching more or less useless.
* - Covers generated by this implementation will take up large amounts of memory, more or less
* guaranteeing an OOM error if used with a large library.
*
* You are best to compose this with [org.oxycblt.musikr.covers.stored.StoredCovers] to get a full
* embedded cover repository.
*
* @param coverIdentifier The [CoverIdentifier] to use to create identifiers for the cover data.
*/
class EmbeddedCovers(private val coverIdentifier: CoverIdentifier) : MutableCovers<MemoryCover> {
override suspend fun obtain(id: String): CoverResult<MemoryCover> = CoverResult.Miss()

View file

@ -18,9 +18,11 @@
package org.oxycblt.musikr.covers.fs
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import androidx.core.net.toUri
import java.io.InputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -29,24 +31,26 @@ import org.oxycblt.musikr.covers.CoverResult
import org.oxycblt.musikr.covers.Covers
import org.oxycblt.musikr.covers.FDCover
import org.oxycblt.musikr.covers.MutableCovers
import org.oxycblt.musikr.fs.device.DeviceDirectory
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
private const val PREFIX = "mcf:"
open class FSCovers(private val context: Context) : Covers<FDCover> {
override suspend fun obtain(id: String): CoverResult<FDCover> {
// Parse the ID to get the directory URI
if (!id.startsWith("folder:")) {
if (!id.startsWith(PREFIX)) {
return CoverResult.Miss()
}
val directoryUri = id.substring("folder:".length)
val uri = Uri.parse(directoryUri)
val uri = id.substring(PREFIX.length).toUri()
// Check if the cover file still actually exists. Perhaps the file was deleted at some
// point or superceded by a new one.
val exists =
withContext(Dispatchers.IO) {
try {
context.contentResolver.openFileDescriptor(uri, "r")?.close()
true
context.contentResolver.openFileDescriptor(uri, "r")?.also { it.close() } !=
null
} catch (e: Exception) {
false
}
@ -62,8 +66,13 @@ open class FSCovers(private val context: Context) : Covers<FDCover> {
class MutableFSCovers(private val context: Context) : FSCovers(context), MutableCovers<FDCover> {
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<FDCover> {
// Since DeviceFiles is a streaming API, we have to wait for the current recursive
// query to finally finish to be able to have a complete list of siblings to search for.
val parent = file.parent.await()
val coverFile = findCoverInDirectory(parent) ?: return CoverResult.Miss()
val coverFile =
parent.children.firstNotNullOfOrNull { node ->
if (node is DeviceFile && isCoverArtFile(node)) node else null
} ?: return CoverResult.Miss()
return CoverResult.Hit(FolderCoverImpl(context, coverFile.uri))
}
@ -72,32 +81,12 @@ class MutableFSCovers(private val context: Context) : FSCovers(context), Mutable
// that should not be managed by the app
}
private fun findCoverInDirectory(directory: DeviceDirectory): DeviceFile? {
return directory.children.firstNotNullOfOrNull { node ->
if (node is DeviceFile && isCoverArtFile(node)) node else null
}
}
private fun isCoverArtFile(file: DeviceFile): Boolean {
val filename = requireNotNull(file.path.name).lowercase()
val mimeType = file.mimeType.lowercase()
if (!mimeType.startsWith("image/")) {
if (!file.mimeType.startsWith("image/", ignoreCase = true)) {
return false
}
val coverNames =
listOf(
"cover",
"folder",
"album",
"albumart",
"front",
"artwork",
"art",
"folder",
"coverart")
val filename = requireNotNull(file.path.name).lowercase()
val filenameWithoutExt = filename.substringBeforeLast(".")
val extension = filename.substringAfterLast(".", "")
@ -108,17 +97,35 @@ class MutableFSCovers(private val context: Context) : FSCovers(context), Mutable
extension.equals("png", ignoreCase = true))
}
}
private companion object {
private val coverNames =
listOf(
"cover",
"folder",
"album",
"albumart",
"front",
"artwork",
"art",
"folder",
"coverart")
}
}
private data class FolderCoverImpl(
private val context: Context,
private val uri: Uri,
) : FDCover {
override val id = "folder:$uri"
override val id = PREFIX + uri.toString()
override suspend fun fd(): ParcelFileDescriptor? =
withContext(Dispatchers.IO) { context.contentResolver.openFileDescriptor(uri, "r") }
// Implies that client will manage freeing the resources themselves.
@SuppressLint("Recycle")
override suspend fun open(): InputStream? =
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }
@SuppressLint("Recycle")
override suspend fun fd(): ParcelFileDescriptor? =
withContext(Dispatchers.IO) { context.contentResolver.openFileDescriptor(uri, "r") }
}

View file

@ -28,14 +28,46 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.oxycblt.musikr.covers.FDCover
/**
* A cover storage interface backing [StoredCovers].
*
* Covers written here should be reasonably persisted long-term, and can be queries roughly as a
* folder of cover files.
*/
interface CoverStorage {
/**
* Find a cover by a file-name.
*
* @return A [FDCover] if found, or null if not.
*/
suspend fun find(name: String): FDCover?
/**
* Write a cover to the storage, yielding a new Cover instance.
*
* [block] is a critical section that may require some time to execute, so the specific cover
* entry should be locked while it executes.
*
* @param name The name to write the cover to.
* @param block The critical section where the cover data is written to the output stream.
*/
suspend fun write(name: String, block: suspend (OutputStream) -> Unit): FDCover
/**
* List all cover files in the storage.
*
* @param exclude A set of file names to exclude from the result. This can make queries more
* efficient if used with native APIs.
* @return A list of file names in the storage, excluding the specified ones.
*/
suspend fun ls(exclude: Set<String>): List<String>
suspend fun rm(file: String)
/**
* Remove a cover file from the storage.
*
* @param name The name of the file to remove. Will do nothing if this file does not exist.
*/
suspend fun rm(name: String)
companion object {
suspend fun at(dir: File): CoverStorage {
@ -43,12 +75,12 @@ interface CoverStorage {
if (dir.exists()) check(dir.isDirectory) { "Not a directory" }
else check(dir.mkdirs()) { "Cannot create directory" }
}
return CoverStorageImpl(dir)
return FSCoverStorage(dir)
}
}
}
private class CoverStorageImpl(private val dir: File) : CoverStorage {
private class FSCoverStorage(private val dir: File) : CoverStorage {
private val fileMutexes = mutableMapOf<String, Mutex>()
private val mapMutex = Mutex()
@ -59,7 +91,7 @@ private class CoverStorageImpl(private val dir: File) : CoverStorage {
override suspend fun find(name: String): FDCover? =
withContext(Dispatchers.IO) {
try {
File(dir, name).takeIf { it.exists() }?.let { StoredCover(it) }
File(dir, name).takeIf { it.exists() }?.let { FSStoredCover(it) }
} catch (e: IOException) {
null
}
@ -76,29 +108,29 @@ private class CoverStorageImpl(private val dir: File) : CoverStorage {
try {
tempFile.outputStream().use { block(it) }
tempFile.renameTo(targetFile)
StoredCover(targetFile)
FSStoredCover(targetFile)
} catch (e: IOException) {
tempFile.delete()
throw e
}
}
} else {
StoredCover(targetFile)
FSStoredCover(targetFile)
}
}
}
override suspend fun ls(exclude: Set<String>): List<String> =
withContext(Dispatchers.IO) {
dir.listFiles()?.map { it.name }?.filter { exclude.contains(it) } ?: emptyList()
dir.listFiles { _, name -> !exclude.contains(name) }?.map { it.name } ?: emptyList()
}
override suspend fun rm(file: String) {
withContext(Dispatchers.IO) { File(dir, file).delete() }
override suspend fun rm(name: String) {
withContext(Dispatchers.IO) { File(dir, name).delete() }
}
}
private data class StoredCover(private val file: File) : FDCover {
private data class FSStoredCover(private val file: File) : FDCover {
override val id: String = file.name
override suspend fun fd() =
@ -112,7 +144,7 @@ private data class StoredCover(private val file: File) : FDCover {
override suspend fun open() = withContext(Dispatchers.IO) { file.inputStream() }
override fun equals(other: Any?) = other is StoredCover && file == other.file
override fun equals(other: Any?) = other is FSStoredCover && file == other.file
override fun hashCode() = file.hashCode()
}

View file

@ -27,13 +27,39 @@ import org.oxycblt.musikr.covers.MutableCovers
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
private const val PREFIX = "mcs:"
/**
* A [Covers] implementation for stored covers in the backing [CoverStorage]. Note that this
* instance is [Transcoding]-agnostic, it will yield a cover as long as it exists somewhere in the
* given storage.
*
* @param coverStorage The [CoverStorage] to use to obtain the cover data.
*/
class StoredCovers(private val coverStorage: CoverStorage) : Covers<FDCover> {
override suspend fun obtain(id: String): CoverResult<FDCover> {
val cover = coverStorage.find(id) ?: return CoverResult.Miss()
return CoverResult.Hit(cover)
if (!id.startsWith(PREFIX)) {
return CoverResult.Miss()
}
val file = id.substring(PREFIX.length)
val cover = coverStorage.find(file) ?: return CoverResult.Miss()
return CoverResult.Hit(StoredCover(cover))
}
}
/**
* A [MutableCovers] implementation for stored covers in the backing [CoverStorage]. This will open
* whatever cover data is yielded by [src], and then write it to the [coverStorage] using the
* whatever [transcoding] is provided.
*
* This allows large in-memory covers yielded by [MutableCovers] to be cached in storage rather than
* kept in memory. However, it can be used for any asynchronously fetched covers as well to save
* time, such as ones obtained by network.
*
* @param src The [MutableCovers] to use to obtain the cover data.
* @param coverStorage The [CoverStorage] to use to write the cover data to.
* @param transcoding The [Transcoding] to use to write the cover data to the [coverStorage].
*/
class MutableStoredCovers(
private val src: MutableCovers<MemoryCover>,
private val coverStorage: CoverStorage,
@ -44,24 +70,31 @@ class MutableStoredCovers(
override suspend fun obtain(id: String): CoverResult<FDCover> = base.obtain(id)
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<FDCover> {
val cover =
val memoryCover =
when (val cover = src.create(file, metadata)) {
is CoverResult.Hit -> cover.cover
is CoverResult.Miss -> return CoverResult.Miss()
}
val coverFile =
coverStorage.write(cover.id + transcoding.tag) {
transcoding.transcodeInto(cover.data(), it)
val innerCover =
coverStorage.write(memoryCover.id + transcoding.tag) {
transcoding.transcodeInto(memoryCover.data(), it)
}
return CoverResult.Hit(coverFile)
return CoverResult.Hit(StoredCover(innerCover))
}
override suspend fun cleanup(excluding: Collection<Cover>) {
src.cleanup(excluding)
val used = excluding.mapTo(mutableSetOf()) { it.id }
val used =
excluding.mapNotNullTo(mutableSetOf()) {
it.id.takeIf { id -> id.startsWith(PREFIX) }?.substring(PREFIX.length)
}
val unused = coverStorage.ls(exclude = used).filter { it !in used }
for (file in unused) {
coverStorage.rm(file)
}
}
}
private class StoredCover(private val inner: FDCover) : FDCover by inner {
override val id = PREFIX + inner.id
}

View file

@ -22,12 +22,36 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.io.OutputStream
/** An interface for transforming in-memory cover data into a different format for storage. */
interface Transcoding {
/**
* A tag to append to the file name to indicate the transcoding format used, such as a file
* extension or additional qualifier.
*
* This should allow the cover to be uniquely identified in storage, and shouldn't collide with
* other [Transcoding] implementations.
*/
val tag: String
/**
* Transcode the given cover data into a different format and write it to the output stream.
*
* You can assume that all code ran here is in a critical section, and that you are the only one
* with access to this [OutputStream] right now.
*
* @param data The cover data to transcode.
* @param output The [OutputStream] to write the transcoded data to.
*/
fun transcodeInto(data: ByteArray, output: OutputStream)
}
/**
* A [Transcoding] implementation that does not transcode the cover data at all, and simply writes
* it to the output stream as-is. This is useful for when the cover data is already in the desired
* format, or when the time/quality tradeoff of transcoding is not worth it. Note that this may mean
* that large or malformed data may be written to [CoverStorage] and yield bad results when loading
* the resulting covers.
*/
object NoTranscoding : Transcoding {
override val tag = ".img"
@ -36,6 +60,15 @@ object NoTranscoding : Transcoding {
}
}
/**
* A [Transcoding] implementation that compresses the cover data into a specific format, size, and
* quality. This is useful if you want to standardize the covers to a specific format and minimize
* the size of the cover data to save space.
*
* @param format The [Bitmap.CompressFormat] to use to compress the cover data.
* @param resolution The resolution to use for the cover data.
* @param quality The quality to use for the cover data, from 0 to 100.
*/
class Compress(
private val format: Bitmap.CompressFormat,
private val resolution: Int,