mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 18:19:16 +01:00
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:
parent
24bb2f02dc
commit
1ef7722504
@ -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 {
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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,8 +39,9 @@ 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}"))
|
||||||
|
|
||||||
|
if (cover != null && cover.exists()) {
|
||||||
// It might not exist if using the external SD card
|
// It might not exist if using the external SD card
|
||||||
cover.parentFile?.mkdirs()
|
cover.parentFile?.mkdirs()
|
||||||
input.use {
|
input.use {
|
||||||
@ -49,9 +49,19 @@ class LocalSource(private val context: Context) : CatalogueSource {
|
|||||||
input.copyTo(it)
|
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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user