Support more image formats for covers (#5524)

* Add TachiyomiImageDecoder for Coil

Is currently used to decode AVIF and JPEG XL images.

* LocalSource: Check against file name for cover

This allows file with all supported formats to be set as cover

* TachiyomiImageDecoder: Handle HEIF on Android 7 and older
This commit is contained in:
Ivan Iskandar 2021-07-11 02:44:34 +07:00 committed by GitHub
parent 24bb2f02dc
commit 1ef7722504
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 74 additions and 9 deletions

View File

@ -23,6 +23,7 @@ import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder import coil.decode.ImageDecoderDecoder
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
@ -105,6 +106,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
override fun newImageLoader(): ImageLoader { override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply { return ImageLoader.Builder(this).apply {
componentRegistry { componentRegistry {
add(TachiyomiImageDecoder(this@App.resources))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder(this@App)) add(ImageDecoderDecoder(this@App))
} else { } else {

View File

@ -0,0 +1,53 @@
package eu.kanade.tachiyomi.data.coil
import android.content.res.Resources
import android.os.Build
import androidx.core.graphics.drawable.toDrawable
import coil.bitmap.BitmapPool
import coil.decode.DecodeResult
import coil.decode.Decoder
import coil.decode.Options
import coil.size.Size
import eu.kanade.tachiyomi.util.system.ImageUtil
import okio.BufferedSource
import tachiyomi.decoder.ImageDecoder
/**
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
*/
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
val type = source.peek().inputStream().use {
ImageUtil.findImageType(it)
}
return when (type) {
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
else -> false
}
}
override suspend fun decode(
pool: BitmapPool,
source: BufferedSource,
size: Size,
options: Options
): DecodeResult {
val decoder = source.use {
ImageDecoder.newInstance(it.inputStream())
}
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder." }
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
decoder.recycle()
check(bitmap != null) { "Failed to decode image." }
return DecodeResult(
drawable = bitmap.toDrawable(resources),
isSampled = false
)
}
}

View File

@ -29,7 +29,6 @@ class LocalSource(private val context: Context) : CatalogueSource {
const val ID = 0L const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/" const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
private const val COVER_NAME = "cover.jpg"
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub") private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS) private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
@ -40,18 +39,29 @@ class LocalSource(private val context: Context) : CatalogueSource {
input.close() input.close()
return null return null
} }
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME) val cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
// It might not exist if using the external SD card if (cover != null && cover.exists()) {
cover.parentFile?.mkdirs() // It might not exist if using the external SD card
input.use { cover.parentFile?.mkdirs()
cover.outputStream().use { input.use {
input.copyTo(it) cover.outputStream().use {
input.copyTo(it)
}
} }
} }
return cover return cover
} }
/**
* Returns valid cover file inside [parent] directory.
*/
private fun getCoverFile(parent: File): File? {
return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
}
}
private fun getBaseDirectories(context: Context): List<File> { private fun getBaseDirectories(context: Context): List<File> {
val c = context.getString(R.string.app_name) + File.separator + "local" val c = context.getString(R.string.app_name) + File.separator + "local"
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) } return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
@ -105,8 +115,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
// Try to find the cover // Try to find the cover
for (dir in baseDirs) { for (dir in baseDirs) {
val cover = File("${dir.absolutePath}/$url", COVER_NAME) val cover = getCoverFile(File("${dir.absolutePath}/$url"))
if (cover.exists()) { if (cover != null && cover.exists()) {
thumbnail_url = cover.absolutePath thumbnail_url = cover.absolutePath
break break
} }