Support for AVIF images
parent
5bccc595a8
commit
c15a0ece3e
@ -0,0 +1,67 @@
|
|||||||
|
package org.koitharu.kotatsu.core.image
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.decode.DecodeResult
|
||||||
|
import coil.decode.Decoder
|
||||||
|
import coil.decode.ImageSource
|
||||||
|
import coil.fetch.SourceResult
|
||||||
|
import coil.request.Options
|
||||||
|
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import org.aomedia.avif.android.AvifDecoder
|
||||||
|
import org.aomedia.avif.android.AvifDecoder.Info
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toByteBuffer
|
||||||
|
|
||||||
|
class AvifImageDecoder(source: ImageSource, options: Options, parallelismLock: Semaphore) :
|
||||||
|
BaseCoilDecoder(source, options, parallelismLock) {
|
||||||
|
|
||||||
|
override fun BitmapFactory.Options.decode(): DecodeResult {
|
||||||
|
val bytes = source.source().use {
|
||||||
|
it.inputStream().toByteBuffer()
|
||||||
|
}
|
||||||
|
val info = Info()
|
||||||
|
if (!AvifDecoder.getInfo(bytes, bytes.remaining(), info)) {
|
||||||
|
throw ImageDecodeException(
|
||||||
|
null,
|
||||||
|
"avif",
|
||||||
|
"Requested to decode byte buffer which cannot be handled by AvifDecoder",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
|
||||||
|
val bitmap = Bitmap.createBitmap(info.width, info.height, config)
|
||||||
|
if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) {
|
||||||
|
bitmap.recycle()
|
||||||
|
throw ImageDecodeException(null, "avif")
|
||||||
|
}
|
||||||
|
return DecodeResult(
|
||||||
|
drawable = bitmap.toDrawable(options.context.resources),
|
||||||
|
isSampled = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory : Decoder.Factory {
|
||||||
|
|
||||||
|
private val parallelismLock = Semaphore(DEFAULT_PARALLELISM)
|
||||||
|
|
||||||
|
override fun create(
|
||||||
|
result: SourceResult,
|
||||||
|
options: Options,
|
||||||
|
imageLoader: ImageLoader
|
||||||
|
): Decoder? = if (isApplicable(result)) {
|
||||||
|
AvifImageDecoder(result.source, options, parallelismLock)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?) = other is Factory
|
||||||
|
|
||||||
|
override fun hashCode() = javaClass.hashCode()
|
||||||
|
|
||||||
|
private fun isApplicable(result: SourceResult): Boolean {
|
||||||
|
return result.mimeType == "image/avif"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
package org.koitharu.kotatsu.core.image
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import coil.decode.DecodeResult
|
||||||
|
import coil.decode.Decoder
|
||||||
|
import coil.decode.ImageSource
|
||||||
|
import coil.request.Options
|
||||||
|
import coil.size.Dimension
|
||||||
|
import coil.size.Scale
|
||||||
|
import coil.size.Size
|
||||||
|
import coil.size.isOriginal
|
||||||
|
import coil.size.pxOrElse
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import org.jetbrains.annotations.Blocking
|
||||||
|
|
||||||
|
abstract class BaseCoilDecoder(
|
||||||
|
protected val source: ImageSource,
|
||||||
|
protected val options: Options,
|
||||||
|
private val parallelismLock: Semaphore,
|
||||||
|
) : Decoder {
|
||||||
|
|
||||||
|
final override suspend fun decode(): DecodeResult = parallelismLock.withPermit {
|
||||||
|
runInterruptible { BitmapFactory.Options().decode() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
protected abstract fun BitmapFactory.Options.decode(): DecodeResult
|
||||||
|
|
||||||
|
protected companion object {
|
||||||
|
|
||||||
|
const val DEFAULT_PARALLELISM = 4
|
||||||
|
|
||||||
|
inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
|
||||||
|
return if (isOriginal) original() else width.toPx(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
|
||||||
|
return if (isOriginal) original() else height.toPx(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Dimension.toPx(scale: Scale) = pxOrElse {
|
||||||
|
when (scale) {
|
||||||
|
Scale.FILL -> Int.MIN_VALUE
|
||||||
|
Scale.FIT -> Int.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package org.koitharu.kotatsu.core.image
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.os.Build
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import org.aomedia.avif.android.AvifDecoder
|
||||||
|
import org.aomedia.avif.android.AvifDecoder.Info
|
||||||
|
import org.jetbrains.annotations.Blocking
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toByteBuffer
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.file.Files
|
||||||
|
|
||||||
|
object BitmapDecoderCompat {
|
||||||
|
|
||||||
|
private const val FORMAT_AVIF = "avif"
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
fun decode(file: File): Bitmap = when (val format = getMimeType(file)?.subtype) {
|
||||||
|
FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) }
|
||||||
|
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
ImageDecoder.decodeBitmap(ImageDecoder.createSource(file))
|
||||||
|
} else {
|
||||||
|
checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath), format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
fun decode(stream: InputStream, type: MediaType?): Bitmap {
|
||||||
|
val format = type?.subtype
|
||||||
|
if (format == FORMAT_AVIF) {
|
||||||
|
return decodeAvif(stream.toByteBuffer())
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
return checkBitmapNotNull(BitmapFactory.decodeStream(stream), format)
|
||||||
|
}
|
||||||
|
val byteBuffer = stream.toByteBuffer()
|
||||||
|
return if (AvifDecoder.isAvifImage(byteBuffer)) {
|
||||||
|
decodeAvif(byteBuffer)
|
||||||
|
} else {
|
||||||
|
ImageDecoder.decodeBitmap(ImageDecoder.createSource(byteBuffer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMimeType(file: File): MediaType? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
Files.probeContentType(file.toPath())?.toMediaTypeOrNull()
|
||||||
|
} else {
|
||||||
|
MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)?.toMediaTypeOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap =
|
||||||
|
bitmap ?: throw ImageDecodeException(null, format)
|
||||||
|
|
||||||
|
private fun decodeAvif(bytes: ByteBuffer): Bitmap {
|
||||||
|
val info = Info()
|
||||||
|
if (!AvifDecoder.getInfo(bytes, bytes.remaining(), info)) {
|
||||||
|
throw ImageDecodeException(
|
||||||
|
null,
|
||||||
|
FORMAT_AVIF,
|
||||||
|
"Requested to decode byte buffer which cannot be handled by AvifDecoder",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
|
||||||
|
val bitmap = Bitmap.createBitmap(info.width, info.height, config)
|
||||||
|
if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) {
|
||||||
|
bitmap.recycle()
|
||||||
|
throw ImageDecodeException(null, FORMAT_AVIF)
|
||||||
|
}
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue