From 1ef77225040842af778282168e6fb8b11c339eeb Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sun, 11 Jul 2021 02:44:34 +0700 Subject: [PATCH] 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 --- app/src/main/java/eu/kanade/tachiyomi/App.kt | 2 + .../data/coil/TachiyomiImageDecoder.kt | 53 +++++++++++++++++++ .../eu/kanade/tachiyomi/source/LocalSource.kt | 28 ++++++---- 3 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index feeda47862..fa71768543 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -23,6 +23,7 @@ import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher 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.preference.PreferencesHelper import eu.kanade.tachiyomi.network.NetworkHelper @@ -105,6 +106,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory { override fun newImageLoader(): ImageLoader { return ImageLoader.Builder(this).apply { componentRegistry { + add(TachiyomiImageDecoder(this@App.resources)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { add(ImageDecoderDecoder(this@App)) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt new file mode 100644 index 0000000000..7847e6f4f4 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index 77b093605e..c82b2868db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -29,7 +29,6 @@ class LocalSource(private val context: Context) : CatalogueSource { const val ID = 0L 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 LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS) @@ -40,18 +39,29 @@ class LocalSource(private val context: Context) : CatalogueSource { input.close() 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 - cover.parentFile?.mkdirs() - input.use { - cover.outputStream().use { - input.copyTo(it) + if (cover != null && cover.exists()) { + // It might not exist if using the external SD card + cover.parentFile?.mkdirs() + input.use { + cover.outputStream().use { + input.copyTo(it) + } } } 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 { val c = context.getString(R.string.app_name) + File.separator + "local" 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 for (dir in baseDirs) { - val cover = File("${dir.absolutePath}/$url", COVER_NAME) - if (cover.exists()) { + val cover = getCoverFile(File("${dir.absolutePath}/$url")) + if (cover != null && cover.exists()) { thumbnail_url = cover.absolutePath break }