image: further improve cover caching
- Don't rewrite files if they already exist - Use webp compression - Downsize cover images to save memory
This commit is contained in:
parent
c74c62d9b3
commit
e061f7cb26
3 changed files with 84 additions and 27 deletions
|
@ -33,9 +33,5 @@ class CoverRetrieverImpl
|
|||
constructor(private val coverCache: CoverCache, private val coverExtractor: CoverExtractor) :
|
||||
CoverRetriever {
|
||||
override suspend fun retrieve(cover: Cover.Single) =
|
||||
coverCache.read(cover)
|
||||
?: coverExtractor.extract(cover)?.also {
|
||||
coverCache.write(cover, it)
|
||||
it.reset()
|
||||
}
|
||||
coverCache.read(cover) ?: coverExtractor.extract(cover)?.let { coverCache.write(cover, it) }
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ interface AppFiles {
|
|||
suspend fun read(file: String): InputStream?
|
||||
|
||||
suspend fun write(file: String, inputStream: InputStream): Boolean
|
||||
|
||||
suspend fun exists(file: String): Boolean
|
||||
}
|
||||
|
||||
class AppFilesImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
||||
|
@ -77,4 +79,7 @@ class AppFilesImpl @Inject constructor(@ApplicationContext private val context:
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun exists(file: String): Boolean =
|
||||
withContext(Dispatchers.IO) { File(context.filesDir, file).exists() }
|
||||
}
|
||||
|
|
|
@ -18,6 +18,11 @@
|
|||
|
||||
package org.oxycblt.auxio.image.stack.cache
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.min
|
||||
|
@ -28,7 +33,7 @@ import org.oxycblt.auxio.image.Cover
|
|||
interface CoverCache {
|
||||
suspend fun read(cover: Cover.Single): InputStream?
|
||||
|
||||
suspend fun write(cover: Cover.Single, inputStream: InputStream): Boolean
|
||||
suspend fun write(cover: Cover.Single, inputStream: InputStream): InputStream?
|
||||
}
|
||||
|
||||
class CoverCacheImpl
|
||||
|
@ -43,7 +48,8 @@ constructor(private val storedCoversDao: StoredCoversDao, private val appFiles:
|
|||
return appFiles.read(fileName(perceptualHash))
|
||||
}
|
||||
|
||||
override suspend fun write(cover: Cover.Single, inputStream: InputStream): Boolean =
|
||||
override suspend fun write(cover: Cover.Single, inputStream: InputStream): InputStream? {
|
||||
val id =
|
||||
withContext(Dispatchers.IO) {
|
||||
val available = inputStream.available()
|
||||
val skip = min(available / 2L, available - COVER_KEY_SAMPLE.toLong())
|
||||
|
@ -51,24 +57,74 @@ constructor(private val storedCoversDao: StoredCoversDao, private val appFiles:
|
|||
val bytes = ByteArray(COVER_KEY_SAMPLE)
|
||||
inputStream.read(bytes)
|
||||
inputStream.reset()
|
||||
@OptIn(ExperimentalStdlibApi::class) bytes.toHexString()
|
||||
}
|
||||
val file = fileName(id)
|
||||
if (!appFiles.exists(file)) {
|
||||
val transcoded = transcodeImage(inputStream, FORMAT_WEBP)
|
||||
val writeSuccess = appFiles.write(fileName(id), transcoded)
|
||||
if (!writeSuccess) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class) val perceptualHash = bytes.toHexString()
|
||||
val writeSuccess = appFiles.write(fileName(perceptualHash), inputStream)
|
||||
|
||||
if (writeSuccess) {
|
||||
storedCoversDao.setCoverFile(
|
||||
StoredCover(
|
||||
uid = cover.uid,
|
||||
lastModified = cover.lastModified,
|
||||
perceptualHash = perceptualHash))
|
||||
StoredCover(uid = cover.uid, lastModified = cover.lastModified, perceptualHash = id))
|
||||
|
||||
return appFiles.read(file)
|
||||
}
|
||||
|
||||
writeSuccess
|
||||
private fun fileName(id: String) = "cover_$id"
|
||||
|
||||
private fun transcodeImage(
|
||||
inputStream: InputStream,
|
||||
targetFormat: Bitmap.CompressFormat
|
||||
): InputStream {
|
||||
val options =
|
||||
BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
BitmapFactory.decodeStream(inputStream, null, this)
|
||||
}
|
||||
|
||||
private fun fileName(perceptualHash: String) = "cover_$perceptualHash"
|
||||
options.inSampleSize = calculateInSampleSize(options, 750, 750)
|
||||
inputStream.reset()
|
||||
|
||||
val bitmap =
|
||||
BitmapFactory.decodeStream(
|
||||
inputStream, null, options.apply { inJustDecodeBounds = false })
|
||||
|
||||
return ByteArrayOutputStream().use { outputStream ->
|
||||
bitmap?.compress(targetFormat, 80, outputStream)
|
||||
ByteArrayInputStream(outputStream.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateInSampleSize(
|
||||
options: BitmapFactory.Options,
|
||||
reqWidth: Int,
|
||||
reqHeight: Int
|
||||
): Int {
|
||||
var inSampleSize = 1
|
||||
val (height, width) = options.outHeight to options.outWidth
|
||||
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
val halfHeight = height / 2
|
||||
val halfWidth = width / 2
|
||||
while ((halfHeight / inSampleSize) >= reqHeight &&
|
||||
(halfWidth / inSampleSize) >= reqWidth) {
|
||||
inSampleSize *= 2
|
||||
}
|
||||
}
|
||||
return inSampleSize
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val COVER_KEY_SAMPLE = 32
|
||||
val FORMAT_WEBP =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Bitmap.CompressFormat.WEBP_LOSSY
|
||||
} else {
|
||||
Bitmap.CompressFormat.WEBP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue