Drop legacy decoder (#3786)
This commit is contained in:
@@ -291,7 +291,6 @@ dependencies {
|
|||||||
}
|
}
|
||||||
implementation(libs.image.decoder)
|
implementation(libs.image.decoder)
|
||||||
|
|
||||||
implementation(libs.image.decoder2)
|
|
||||||
implementation(libs.webgpuviewer)
|
implementation(libs.webgpuviewer)
|
||||||
implementation(libs.kim)
|
implementation(libs.kim)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package eu.kanade.domain.base
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -35,18 +34,6 @@ class BasePreferences(
|
|||||||
|
|
||||||
val highQualityRenderer: Preference<Boolean> = preferenceStore.getBoolean("pref_high_quality_renderer_key", false)
|
val highQualityRenderer: Preference<Boolean> = preferenceStore.getBoolean("pref_high_quality_renderer_key", false)
|
||||||
|
|
||||||
val displayProfile: Preference<String> = preferenceStore.getString("pref_display_profile_key", "")
|
|
||||||
|
|
||||||
val hardwareBitmapThreshold: Preference<Int> = preferenceStore.getInt(
|
|
||||||
"pref_hardware_bitmap_threshold",
|
|
||||||
GLUtil.SAFE_TEXTURE_LIMIT,
|
|
||||||
)
|
|
||||||
|
|
||||||
val alwaysDecodeLongStripWithSSIV: Preference<Boolean> = preferenceStore.getBoolean(
|
|
||||||
"pref_always_decode_long_strip_with_ssiv",
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
val installationId: Preference<String> = preferenceStore.getString(Preference.appStateKey("installation_id"), "")
|
val installationId: Preference<String> = preferenceStore.getString(Preference.appStateKey("installation_id"), "")
|
||||||
|
|
||||||
val donationCampaignShown: Preference<Boolean> = preferenceStore.getBoolean(
|
val donationCampaignShown: Preference<Boolean> = preferenceStore.getBoolean(
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import eu.kanade.presentation.components.AppBar
|
|||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.presentation.manga.EditCoverAction
|
import eu.kanade.presentation.manga.EditCoverAction
|
||||||
import eu.kanade.tachiyomi.data.coil.ImageDecoder2
|
import eu.kanade.tachiyomi.data.coil.ImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.coil.newDecoder
|
import eu.kanade.tachiyomi.data.coil.newDecoder
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -173,7 +173,7 @@ fun MangaCoverDialog(
|
|||||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||||
.newDecoder(true)
|
.newDecoder(true)
|
||||||
.target { result ->
|
.target { result ->
|
||||||
val res = (result as ImageDecoder2.DecodeResultImage).res
|
val res = (result as ImageDecoder.DecodeResultImage).res
|
||||||
val page = runBlocking(WebGpuRenderer.dispatcher) {
|
val page = runBlocking(WebGpuRenderer.dispatcher) {
|
||||||
ImagePage(res.image, res.width, res.height)
|
ImagePage(res.image, res.width, res.height)
|
||||||
}.apply {
|
}.apply {
|
||||||
|
|||||||
-41
@@ -47,7 +47,6 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
|
|||||||
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
|
||||||
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
|
||||||
import eu.kanade.tachiyomi.util.CrashLogUtil
|
import eu.kanade.tachiyomi.util.CrashLogUtil
|
||||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||||
import eu.kanade.tachiyomi.util.system.powerManager
|
import eu.kanade.tachiyomi.util.system.powerManager
|
||||||
@@ -334,15 +333,6 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
basePreferences: BasePreferences,
|
basePreferences: BasePreferences,
|
||||||
): Preference.PreferenceGroup {
|
): Preference.PreferenceGroup {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val chooseColorProfile = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.OpenDocument(),
|
|
||||||
) { uri ->
|
|
||||||
uri?.let {
|
|
||||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
|
||||||
basePreferences.displayProfile.set(uri.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_reader),
|
title = stringResource(MR.strings.pref_category_reader),
|
||||||
preferenceItems = listOf(
|
preferenceItems = listOf(
|
||||||
@@ -350,37 +340,6 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||||||
preference = basePreferences.highQualityRenderer,
|
preference = basePreferences.highQualityRenderer,
|
||||||
title = stringResource(MR.strings.pref_high_quality_renderer),
|
title = stringResource(MR.strings.pref_high_quality_renderer),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.ListPreference(
|
|
||||||
preference = basePreferences.hardwareBitmapThreshold,
|
|
||||||
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
|
|
||||||
.mapIndexed { index, option ->
|
|
||||||
val display = if (index == 0) {
|
|
||||||
stringResource(MR.strings.pref_hardware_bitmap_threshold_default, option)
|
|
||||||
} else {
|
|
||||||
option.toString()
|
|
||||||
}
|
|
||||||
option to display
|
|
||||||
}
|
|
||||||
.toMap(),
|
|
||||||
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
|
|
||||||
subtitleProvider = { value, options ->
|
|
||||||
stringResource(MR.strings.pref_hardware_bitmap_threshold_summary, options[value].orEmpty())
|
|
||||||
},
|
|
||||||
enabled = !ImageUtil.HARDWARE_BITMAP_UNSUPPORTED &&
|
|
||||||
GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
|
||||||
preference = basePreferences.alwaysDecodeLongStripWithSSIV,
|
|
||||||
title = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_2),
|
|
||||||
subtitle = stringResource(MR.strings.pref_always_decode_long_strip_with_ssiv_summary),
|
|
||||||
),
|
|
||||||
Preference.PreferenceItem.TextPreference(
|
|
||||||
title = stringResource(MR.strings.pref_display_profile),
|
|
||||||
subtitle = basePreferences.displayProfile.get(),
|
|
||||||
onClick = {
|
|
||||||
chooseColorProfile.launch(arrayOf("*/*"))
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ import eu.kanade.tachiyomi.core.security.PrivacyPreferences
|
|||||||
import eu.kanade.tachiyomi.crash.CrashActivity
|
import eu.kanade.tachiyomi.crash.CrashActivity
|
||||||
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
|
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
|
||||||
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
|
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.coil.ImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaKeyer
|
import eu.kanade.tachiyomi.data.coil.MangaKeyer
|
||||||
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.di.AppModule
|
import eu.kanade.tachiyomi.di.AppModule
|
||||||
import eu.kanade.tachiyomi.di.PreferenceModule
|
import eu.kanade.tachiyomi.di.PreferenceModule
|
||||||
@@ -41,7 +41,6 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
|||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||||
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||||
@@ -144,14 +143,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
.onEach(TelemetryConfig::setCrashlyticsEnabled)
|
.onEach(TelemetryConfig::setCrashlyticsEnabled)
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
basePreferences.hardwareBitmapThreshold.let { preference ->
|
|
||||||
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
|
|
||||||
}
|
|
||||||
|
|
||||||
basePreferences.hardwareBitmapThreshold.changes()
|
|
||||||
.onEach { ImageUtil.hardwareBitmapThreshold = it }
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode.get())
|
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode.get())
|
||||||
|
|
||||||
// Updates widget update
|
// Updates widget update
|
||||||
@@ -192,7 +183,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
// NetworkFetcher.Factory
|
// NetworkFetcher.Factory
|
||||||
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
|
add(OkHttpNetworkFetcherFactory(callFactoryLazy::value))
|
||||||
// Decoder.Factory
|
// Decoder.Factory
|
||||||
add(TachiyomiImageDecoder.Factory())
|
add(ImageDecoder.Factory())
|
||||||
// Fetcher.Factory
|
// Fetcher.Factory
|
||||||
add(BufferedSourceFetcher.Factory())
|
add(BufferedSourceFetcher.Factory())
|
||||||
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
|
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
import androidx.core.graphics.scale
|
||||||
|
import ca.mpreg.imagedecoder.ImageDecoder
|
||||||
|
import coil3.Canvas
|
||||||
|
import coil3.Image
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.asImage
|
||||||
|
import coil3.decode.DecodeResult
|
||||||
|
import coil3.decode.DecodeUtils
|
||||||
|
import coil3.decode.Decoder
|
||||||
|
import coil3.decode.ImageSource
|
||||||
|
import coil3.fetch.SourceFetchResult
|
||||||
|
import coil3.request.Options
|
||||||
|
import logcat.LogPriority
|
||||||
|
import okio.BufferedSource
|
||||||
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Decoder] that uses [ImageDecoder] (libvips-based) to decode image formats not supported
|
||||||
|
* by the Android system decoder (AVIF, JXL, HEIF, etc.).
|
||||||
|
*/
|
||||||
|
class ImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a raw [ImageDecoder.DecodeResult] as a Coil [Image] for callers that want
|
||||||
|
* direct access to the RGBA [java.nio.ByteBuffer] (e.g. the new-decoder path).
|
||||||
|
*/
|
||||||
|
class DecodeResultImage(val res: ImageDecoder.DecodeResult) : Image {
|
||||||
|
override val size: Long get() = res.image.capacity().toLong()
|
||||||
|
override val width: Int get() = res.width
|
||||||
|
override val height: Int get() = res.height
|
||||||
|
override val shareable: Boolean get() = true
|
||||||
|
override fun draw(canvas: Canvas) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun decode(): DecodeResult {
|
||||||
|
val decoder = resources.sourceOrNull()?.use {
|
||||||
|
try {
|
||||||
|
ImageDecoder.new(it.inputStream())
|
||||||
|
} catch (e: ImageDecoder.DecodeException) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "ImageDecoder.new failed: ${e.message}" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check(decoder != null && decoder.pages > 0) { "Failed to initialize decoder" }
|
||||||
|
|
||||||
|
val res = decoder.decode()
|
||||||
|
|
||||||
|
val srcWidth = res.width
|
||||||
|
val srcHeight = res.height
|
||||||
|
|
||||||
|
// newDecoder path: caller wants the raw DecodeResult (e.g. for custom rendering).
|
||||||
|
// Hand it back as-is; sampling is the caller's responsibility.
|
||||||
|
if (options.newDecoder) {
|
||||||
|
return DecodeResult(
|
||||||
|
image = DecodeResultImage(res),
|
||||||
|
isSampled = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal path: produce a Bitmap scaled to the requested output size.
|
||||||
|
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
||||||
|
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
|
||||||
|
val sampleSize = DecodeUtils.calculateInSampleSize(
|
||||||
|
srcWidth = srcWidth,
|
||||||
|
srcHeight = srcHeight,
|
||||||
|
dstWidth = dstWidth,
|
||||||
|
dstHeight = dstHeight,
|
||||||
|
scale = options.scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copy RGBA pixels from the native buffer into a full-resolution bitmap.
|
||||||
|
// We must do this while `res` (and its native memory) is still alive.
|
||||||
|
val fullBitmap = createBitmap(srcWidth, srcHeight)
|
||||||
|
res.image.rewind()
|
||||||
|
fullBitmap.copyPixelsFromBuffer(res.image)
|
||||||
|
|
||||||
|
// Downsample if needed. sampleSize is a power-of-two factor; the target
|
||||||
|
// dimensions are src / sampleSize, matching BitmapFactory inSampleSize behaviour.
|
||||||
|
val bitmap = if (sampleSize > 1) {
|
||||||
|
val scaledWidth = (srcWidth / sampleSize).coerceAtLeast(1)
|
||||||
|
val scaledHeight = (srcHeight / sampleSize).coerceAtLeast(1)
|
||||||
|
val scaled = fullBitmap.scale(scaledWidth, scaledHeight)
|
||||||
|
fullBitmap.recycle()
|
||||||
|
scaled
|
||||||
|
} else {
|
||||||
|
fullBitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecodeResult(
|
||||||
|
image = bitmap.asImage(),
|
||||||
|
isSampled = sampleSize > 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory : Decoder.Factory {
|
||||||
|
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
||||||
|
return if (options.newDecoder || options.customDecoder || isApplicable(result.source.source())) {
|
||||||
|
ImageDecoder(result.source, options)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isApplicable(source: BufferedSource): Boolean {
|
||||||
|
val type = source.peek().inputStream().use {
|
||||||
|
ImageUtil.findImageType(it)
|
||||||
|
}
|
||||||
|
return when (type) {
|
||||||
|
ImageUtil.ImageType.AVIF,
|
||||||
|
ImageUtil.ImageType.JXL,
|
||||||
|
ImageUtil.ImageType.HEIF,
|
||||||
|
ImageUtil.ImageType.JP2,
|
||||||
|
-> true
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?) = other is Factory
|
||||||
|
|
||||||
|
override fun hashCode() = javaClass.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.coil
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import coil3.Canvas
|
|
||||||
import coil3.Image
|
|
||||||
import coil3.ImageLoader
|
|
||||||
import coil3.asImage
|
|
||||||
import coil3.decode.DecodeResult
|
|
||||||
import coil3.decode.DecodeUtils
|
|
||||||
import coil3.decode.Decoder
|
|
||||||
import coil3.decode.ImageSource
|
|
||||||
import coil3.fetch.SourceFetchResult
|
|
||||||
import coil3.request.Options
|
|
||||||
import coil3.request.bitmapConfig
|
|
||||||
import okio.BufferedSource
|
|
||||||
import tachiyomi.core.common.util.system.ImageUtil
|
|
||||||
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: ImageSource, private val options: Options) : Decoder {
|
|
||||||
|
|
||||||
override suspend fun decode(): DecodeResult {
|
|
||||||
val decoder = resources.sourceOrNull()?.use {
|
|
||||||
ImageDecoder.newInstance(it.inputStream(), options.cropBorders, displayProfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
|
|
||||||
|
|
||||||
val srcWidth = decoder.width
|
|
||||||
val srcHeight = decoder.height
|
|
||||||
|
|
||||||
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
|
|
||||||
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
|
|
||||||
|
|
||||||
val sampleSize = DecodeUtils.calculateInSampleSize(
|
|
||||||
srcWidth = srcWidth,
|
|
||||||
srcHeight = srcHeight,
|
|
||||||
dstWidth = dstWidth,
|
|
||||||
dstHeight = dstHeight,
|
|
||||||
scale = options.scale,
|
|
||||||
)
|
|
||||||
|
|
||||||
var bitmap = decoder.decode(sampleSize = sampleSize)
|
|
||||||
decoder.recycle()
|
|
||||||
|
|
||||||
check(bitmap != null) { "Failed to decode image" }
|
|
||||||
|
|
||||||
if (options.bitmapConfig == Bitmap.Config.HARDWARE && ImageUtil.canUseHardwareBitmap(bitmap)) {
|
|
||||||
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
|
|
||||||
if (hwBitmap != null) {
|
|
||||||
bitmap.recycle()
|
|
||||||
bitmap = hwBitmap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return DecodeResult(
|
|
||||||
image = bitmap.asImage(),
|
|
||||||
isSampled = sampleSize > 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class Factory : Decoder.Factory {
|
|
||||||
|
|
||||||
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
|
||||||
return if (options.newDecoder) {
|
|
||||||
ImageDecoder2(result.source, options)
|
|
||||||
} else if (options.customDecoder || isApplicable(result.source.source())) {
|
|
||||||
TachiyomiImageDecoder(result.source, options)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isApplicable(source: BufferedSource): Boolean {
|
|
||||||
val type = source.peek().inputStream().use {
|
|
||||||
ImageUtil.findImageType(it)
|
|
||||||
}
|
|
||||||
return when (type) {
|
|
||||||
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL, ImageUtil.ImageType.HEIF -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?) = other is Factory
|
|
||||||
|
|
||||||
override fun hashCode() = javaClass.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
var displayProfile: ByteArray? = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImageDecoder2(private val resources: ImageSource, private val options: Options) : Decoder {
|
|
||||||
|
|
||||||
class DecodeResultImage(val res: ca.mpreg.imagedecoder.ImageDecoder.DecodeResult) : Image {
|
|
||||||
override val size: Long get() = res.image.capacity().toLong()
|
|
||||||
override val width: Int get() = res.width
|
|
||||||
override val height: Int get() = res.height
|
|
||||||
override val shareable: Boolean get() = true
|
|
||||||
override fun draw(canvas: Canvas) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun decode(): DecodeResult {
|
|
||||||
val source = resources.source()
|
|
||||||
|
|
||||||
val decoder = ca.mpreg.imagedecoder.ImageDecoder.new(source.inputStream())
|
|
||||||
|
|
||||||
val res = decoder.decode()
|
|
||||||
|
|
||||||
return DecodeResult(
|
|
||||||
image = DecodeResultImage(res),
|
|
||||||
isSampled = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -59,7 +59,6 @@ import eu.kanade.presentation.reader.appbars.ReaderAppBars
|
|||||||
import eu.kanade.presentation.reader.components.ChapterNavigatorType
|
import eu.kanade.presentation.reader.components.ChapterNavigatorType
|
||||||
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
|
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
|
||||||
@@ -868,10 +867,6 @@ class ReaderActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
||||||
preferences.displayProfile.changes()
|
|
||||||
.onEach { setDisplayProfile(it) }
|
|
||||||
.launchIn(lifecycleScope)
|
|
||||||
|
|
||||||
readerPreferences.keepScreenOn.changes()
|
readerPreferences.keepScreenOn.changes()
|
||||||
.onEach(::setKeepScreenOn)
|
.onEach(::setKeepScreenOn)
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
@@ -899,25 +894,6 @@ class ReaderActivity : BaseActivity() {
|
|||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the display profile to [path].
|
|
||||||
*/
|
|
||||||
private fun setDisplayProfile(path: String) {
|
|
||||||
val file = UniFile.fromUri(baseContext, path.toUri())
|
|
||||||
if (file != null && file.exists()) {
|
|
||||||
val inputStream = file.openInputStream()
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
inputStream.use { input ->
|
|
||||||
outputStream.use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val data = outputStream.toByteArray()
|
|
||||||
SubsamplingScaleImageView.setDisplayProfile(data)
|
|
||||||
TachiyomiImageDecoder.displayProfile = data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the keep screen on mode according to [enabled].
|
* Sets the keep screen on mode according to [enabled].
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -60,10 +60,6 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
private val isWebtoon: Boolean = false,
|
private val isWebtoon: Boolean = false,
|
||||||
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
|
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
|
||||||
|
|
||||||
private val alwaysDecodeLongStripWithSSIV by lazy {
|
|
||||||
Injekt.get<BasePreferences>().alwaysDecodeLongStripWithSSIV.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var pageView: View? = null
|
private var pageView: View? = null
|
||||||
|
|
||||||
private var config: Config? = null
|
private var config: Config? = null
|
||||||
@@ -240,7 +236,6 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
} else {
|
} else {
|
||||||
SubsamplingScaleImageView(context)
|
SubsamplingScaleImageView(context)
|
||||||
}.apply {
|
}.apply {
|
||||||
setMaxTileSize(ImageUtil.hardwareBitmapThreshold)
|
|
||||||
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
|
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
|
||||||
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
|
||||||
setMinimumTileDpi(180)
|
setMinimumTileDpi(180)
|
||||||
@@ -301,8 +296,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
isVisible = true
|
isVisible = true
|
||||||
}
|
}
|
||||||
is BufferedSource -> {
|
is BufferedSource -> {
|
||||||
if (!isWebtoon || alwaysDecodeLongStripWithSSIV) {
|
if (!isWebtoon) {
|
||||||
setHardwareConfig(ImageUtil.canUseHardwareBitmap(data))
|
|
||||||
setImage(ImageSource.inputStream(data.inputStream()))
|
setImage(ImageSource.inputStream(data.inputStream()))
|
||||||
isVisible = true
|
isVisible = true
|
||||||
return@apply
|
return@apply
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.graphics.Canvas
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.util.Log
|
|
||||||
import android.view.InputDevice
|
import android.view.InputDevice
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
@@ -52,6 +51,8 @@ import kotlinx.coroutines.asCoroutineDispatcher
|
|||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -170,14 +171,14 @@ open class WebGpuViewer(
|
|||||||
is TransitionPage -> createTransitionPage(page)
|
is TransitionPage -> createTransitionPage(page)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("WebGpuViewer", "Decode error: ${pageKey(page)}", e)
|
logcat(LogPriority.ERROR, e) { "Decode error: ${pageKey(page)}" }
|
||||||
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_: InterruptedException) {
|
} catch (_: InterruptedException) {
|
||||||
// Normal shutdown
|
// Normal shutdown
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("WebGpuViewer", "Decode worker died", e)
|
logcat(LogPriority.ERROR, e) { "Decode worker died" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -688,7 +689,7 @@ open class WebGpuViewer(
|
|||||||
when (state) {
|
when (state) {
|
||||||
Page.State.Queue, Page.State.LoadPage, Page.State.DownloadImage -> true
|
Page.State.Queue, Page.State.LoadPage, Page.State.DownloadImage -> true
|
||||||
is Page.State.Error -> {
|
is Page.State.Error -> {
|
||||||
Log.e("WebGpuViewer", "Page load error: ${state.error}")
|
logcat(LogPriority.ERROR) { "Page load error: ${state.error}" }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,7 +712,7 @@ open class WebGpuViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("WebGpuViewer", "startPageLoad error", e)
|
logcat(LogPriority.ERROR, e) { "startPageLoad error" }
|
||||||
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -762,11 +763,60 @@ open class WebGpuViewer(
|
|||||||
Image.Position.SINGLE
|
Image.Position.SINGLE
|
||||||
}
|
}
|
||||||
|
|
||||||
val dec = ImageDecoder.new(bytes?.inputStream() ?: input)
|
val dec = try {
|
||||||
|
ImageDecoder.new(bytes?.inputStream() ?: input)
|
||||||
|
} catch (e: ImageDecoder.DecodeException) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "ImageDecoder.new failed: ${e.message}" }
|
||||||
|
val errorMessage = e.message ?: "Failed to decode image"
|
||||||
|
val bitmap = createBitmap(pager.state.width.coerceAtLeast(1), pager.state.height.coerceAtLeast(1))
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
canvas.drawColor(readerBackgroundColor())
|
||||||
|
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = readerOnBackgroundColor()
|
||||||
|
textSize = 36f
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
val maxWidth = bitmap.width * 0.8f
|
||||||
|
val words = errorMessage.split(" ")
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
var currentLine = StringBuilder()
|
||||||
|
for (word in words) {
|
||||||
|
val testLine = if (currentLine.isEmpty()) word else "$currentLine $word"
|
||||||
|
if (paint.measureText(testLine) <= maxWidth) {
|
||||||
|
currentLine = StringBuilder(testLine)
|
||||||
|
} else {
|
||||||
|
if (currentLine.isNotEmpty()) lines.add(currentLine.toString())
|
||||||
|
currentLine = StringBuilder(word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentLine.isNotEmpty()) lines.add(currentLine.toString())
|
||||||
|
val lineHeight = 40f
|
||||||
|
var y = bitmap.height / 2f - lines.size * lineHeight / 2
|
||||||
|
for (line in lines) {
|
||||||
|
canvas.drawText(line, bitmap.width / 2f, y, paint)
|
||||||
|
y += lineHeight
|
||||||
|
}
|
||||||
|
val errorPage = ImagePage(bitmap, createMipMaps = false).also {
|
||||||
|
it.image?.position = Image.Position.SINGLE
|
||||||
|
}
|
||||||
|
synchronized(lock) {
|
||||||
|
if (pageInCache(page) && !page.imagePage.isDecoded && !page.imagePage.destroyed) {
|
||||||
|
val oldImagePage = page.imagePage
|
||||||
|
page.imagePage = errorPage
|
||||||
|
page.state = PageState.IDLE
|
||||||
|
if (oldImagePage !is ImagePage.Dummy) oldImagePage.cleanup()
|
||||||
|
pager.state.invalidate()
|
||||||
|
} else {
|
||||||
|
if (pageInCache(page)) page.state = PageState.IDLE
|
||||||
|
errorPage.cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
val pageCount = dec.pages
|
val pageCount = dec.pages
|
||||||
|
|
||||||
if (pageCount == 0) {
|
if (pageCount == 0) {
|
||||||
Log.e("WebGpuViewer", "decodeReaderPage: no frames decoded")
|
logcat(LogPriority.ERROR) { "decodeReaderPage: no frames decoded" }
|
||||||
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -838,7 +888,7 @@ open class WebGpuViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("WebGpuViewer", "decodeReaderPage error", e)
|
logcat(LogPriority.ERROR, e) { "decodeReaderPage error" }
|
||||||
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
||||||
} finally {
|
} finally {
|
||||||
imagePage?.cleanup()
|
imagePage?.cleanup()
|
||||||
@@ -921,7 +971,7 @@ open class WebGpuViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("WebGpuViewer", "createTransitionPage error", e)
|
logcat(LogPriority.ERROR, e) { "createTransitionPage error" }
|
||||||
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
synchronized(lock) { if (pageInCache(page)) page.state = PageState.IDLE }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ dependencies {
|
|||||||
api(libs.okio)
|
api(libs.okio)
|
||||||
|
|
||||||
implementation(libs.image.decoder)
|
implementation(libs.image.decoder)
|
||||||
implementation(libs.image.decoder2)
|
|
||||||
|
|
||||||
implementation(libs.unifile)
|
implementation(libs.unifile)
|
||||||
implementation(libs.archive)
|
implementation(libs.archive)
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.util.system
|
|
||||||
|
|
||||||
import javax.microedition.khronos.egl.EGL10
|
|
||||||
import javax.microedition.khronos.egl.EGLConfig
|
|
||||||
import javax.microedition.khronos.egl.EGLContext
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
object GLUtil {
|
|
||||||
val DEVICE_TEXTURE_LIMIT: Int by lazy {
|
|
||||||
// Get EGL Display
|
|
||||||
val egl = EGLContext.getEGL() as EGL10
|
|
||||||
val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY)
|
|
||||||
|
|
||||||
// Initialise
|
|
||||||
val version = IntArray(2)
|
|
||||||
egl.eglInitialize(display, version)
|
|
||||||
|
|
||||||
// Query total number of configurations
|
|
||||||
val totalConfigurations = IntArray(1)
|
|
||||||
egl.eglGetConfigs(display, null, 0, totalConfigurations)
|
|
||||||
|
|
||||||
// Query actual list configurations
|
|
||||||
val configurationsList = arrayOfNulls<EGLConfig>(totalConfigurations[0])
|
|
||||||
egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations)
|
|
||||||
|
|
||||||
val textureSize = IntArray(1)
|
|
||||||
var maximumTextureSize = 0
|
|
||||||
|
|
||||||
// Iterate through all the configurations to located the maximum texture size
|
|
||||||
for (i in 0..<totalConfigurations[0]) {
|
|
||||||
// Only need to check for width since opengl textures are always squared
|
|
||||||
egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize)
|
|
||||||
|
|
||||||
// Keep track of the maximum texture size
|
|
||||||
if (maximumTextureSize < textureSize[0]) maximumTextureSize = textureSize[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release
|
|
||||||
egl.eglTerminate(display)
|
|
||||||
|
|
||||||
// Return largest texture size found (after making it a multiplier of [Multiplier]), or default
|
|
||||||
max(maximumTextureSize, SAFE_TEXTURE_LIMIT)
|
|
||||||
}
|
|
||||||
|
|
||||||
const val SAFE_TEXTURE_LIMIT: Int = 2048
|
|
||||||
|
|
||||||
val CUSTOM_TEXTURE_LIMIT_OPTIONS: List<Int> by lazy {
|
|
||||||
val steps = DEVICE_TEXTURE_LIMIT / MULTIPLIER
|
|
||||||
buildList(steps) {
|
|
||||||
add(DEVICE_TEXTURE_LIMIT)
|
|
||||||
for (step in steps downTo 2) {
|
|
||||||
val value = step * MULTIPLIER
|
|
||||||
if (value >= DEVICE_TEXTURE_LIMIT) continue
|
|
||||||
add(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val MULTIPLIER: Int = 1024
|
|
||||||
@@ -21,13 +21,11 @@ import androidx.core.graphics.drawable.toDrawable
|
|||||||
import androidx.core.graphics.get
|
import androidx.core.graphics.get
|
||||||
import androidx.core.graphics.green
|
import androidx.core.graphics.green
|
||||||
import androidx.core.graphics.red
|
import androidx.core.graphics.red
|
||||||
|
import ca.mpreg.imagedecoder.ImageDecoder
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import tachiyomi.decoder.Format
|
|
||||||
import tachiyomi.decoder.ImageDecoder
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -49,17 +47,19 @@ object ImageUtil {
|
|||||||
|
|
||||||
fun findImageType(stream: InputStream): ImageType? {
|
fun findImageType(stream: InputStream): ImageType? {
|
||||||
return try {
|
return try {
|
||||||
when (getImageType(stream)?.format) {
|
val decoder = ImageDecoder.new(stream)
|
||||||
Format.Avif -> ImageType.AVIF
|
when (decoder.format) {
|
||||||
Format.Gif -> ImageType.GIF
|
"jpeg" -> ImageType.JPEG
|
||||||
Format.Heif -> ImageType.HEIF
|
"png" -> ImageType.PNG
|
||||||
Format.Jpeg -> ImageType.JPEG
|
"webp" -> ImageType.WEBP
|
||||||
Format.Jxl -> ImageType.JXL
|
"gif" -> ImageType.GIF
|
||||||
Format.Png -> ImageType.PNG
|
"heif" -> ImageType.HEIF
|
||||||
Format.Webp -> ImageType.WEBP
|
"jxl" -> ImageType.JXL
|
||||||
|
"jp2" -> ImageType.JP2
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR) { "findImageType: ${e.message}" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,38 +71,23 @@ object ImageUtil {
|
|||||||
|
|
||||||
fun isAnimatedAndSupported(source: BufferedSource): Boolean {
|
fun isAnimatedAndSupported(source: BufferedSource): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val type = getImageType(source.peek().inputStream()) ?: return false
|
val type = findImageType(source.peek().inputStream()) ?: return false
|
||||||
// https://coil-kt.github.io/coil/getting_started/#supported-image-formats
|
when (type) {
|
||||||
when (type.format) {
|
ImageType.GIF -> true
|
||||||
Format.Gif -> true
|
ImageType.WEBP, ImageType.HEIF -> {
|
||||||
// Animated WebP on Android 9+
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return false
|
||||||
Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
val decoder = ImageDecoder.new(source.peek().inputStream())
|
||||||
// Animated Heif on Android 11+
|
decoder.pages > 1
|
||||||
Format.Heif -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR) { "isAnimatedAndSupported: ${e.message}" }
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getImageType(stream: InputStream): tachiyomi.decoder.ImageType? {
|
|
||||||
val bytes = ByteArray(32)
|
|
||||||
|
|
||||||
val length = if (stream.markSupported()) {
|
|
||||||
stream.mark(bytes.size)
|
|
||||||
stream.read(bytes, 0, bytes.size).also { stream.reset() }
|
|
||||||
} else {
|
|
||||||
stream.read(bytes, 0, bytes.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (length == -1) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return ImageDecoder.findType(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class ImageType(val mime: String, val extension: String) {
|
enum class ImageType(val mime: String, val extension: String) {
|
||||||
AVIF("image/avif", "avif"),
|
AVIF("image/avif", "avif"),
|
||||||
GIF("image/gif", "gif"),
|
GIF("image/gif", "gif"),
|
||||||
@@ -111,6 +96,8 @@ object ImageUtil {
|
|||||||
JXL("image/jxl", "jxl"),
|
JXL("image/jxl", "jxl"),
|
||||||
PNG("image/png", "png"),
|
PNG("image/png", "png"),
|
||||||
WEBP("image/webp", "webp"),
|
WEBP("image/webp", "webp"),
|
||||||
|
JP2("image/jp2", "jp2"),
|
||||||
|
JPX("image/jpx", "jpx"),
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -312,30 +299,23 @@ object ImageUtil {
|
|||||||
val bottomOffset = topOffset + splitHeight
|
val bottomOffset = topOffset + splitHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canUseHardwareBitmap(bitmap: Bitmap): Boolean {
|
|
||||||
return canUseHardwareBitmap(bitmap.width, bitmap.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun canUseHardwareBitmap(imageSource: BufferedSource): Boolean {
|
|
||||||
return with(extractImageOptions(imageSource)) {
|
|
||||||
canUseHardwareBitmap(outWidth, outHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hardwareBitmapThreshold: Int = GLUtil.SAFE_TEXTURE_LIMIT
|
|
||||||
|
|
||||||
private fun canUseHardwareBitmap(width: Int, height: Int): Boolean {
|
|
||||||
if (HARDWARE_BITMAP_UNSUPPORTED) return false
|
|
||||||
return maxOf(width, height) <= hardwareBitmapThreshold
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Algorithm for determining what background to accompany a comic/manga page
|
* Algorithm for determining what background to accompany a comic/manga page
|
||||||
*/
|
*/
|
||||||
fun chooseBackground(context: Context, imageStream: InputStream): Drawable {
|
fun chooseBackground(context: Context, imageStream: InputStream): Drawable {
|
||||||
val decoder = ImageDecoder.newInstance(imageStream)
|
val decoder = try {
|
||||||
val image = decoder?.decode()
|
ImageDecoder.new(imageStream)
|
||||||
decoder?.recycle()
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR) { "chooseBackground: ${e.message}" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val result = decoder?.decode()
|
||||||
|
val image = result?.let {
|
||||||
|
createBitmap(it.width, it.height).also { bitmap ->
|
||||||
|
it.image.rewind()
|
||||||
|
bitmap.copyPixelsFromBuffer(it.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val whiteColor = Color.WHITE
|
val whiteColor = Color.WHITE
|
||||||
if (image == null) return whiteColor.toDrawable()
|
if (image == null) return whiteColor.toDrawable()
|
||||||
@@ -575,121 +555,6 @@ object ImageUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val optimalImageHeight = getDisplayMaxHeightInPx * 2
|
private val optimalImageHeight = getDisplayMaxHeightInPx * 2
|
||||||
|
|
||||||
/**
|
|
||||||
* Taken from Coil
|
|
||||||
* (https://github.com/coil-kt/coil/blob/1674d3516f061aeacbe749a435b1924f9648fd41/coil-core/src/androidMain/kotlin/coil3/util/hardwareBitmaps.kt)
|
|
||||||
* ---
|
|
||||||
* Maintains a list of devices with broken/incomplete/unstable hardware bitmap implementations.
|
|
||||||
*
|
|
||||||
* Model names are retrieved from
|
|
||||||
* [Google's official device list](https://support.google.com/googleplay/answer/1727131?hl=en).
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
val HARDWARE_BITMAP_UNSUPPORTED = when (Build.VERSION.SDK_INT) {
|
|
||||||
26 -> run {
|
|
||||||
val model = Build.MODEL ?: return@run false
|
|
||||||
|
|
||||||
// Samsung Galaxy (ALL)
|
|
||||||
if (model.removePrefix("SAMSUNG-").startsWith("SM-")) return@run true
|
|
||||||
|
|
||||||
val device = Build.DEVICE ?: return@run false
|
|
||||||
|
|
||||||
return@run device in arrayOf(
|
|
||||||
"nora", "nora_8917", "nora_8917_n", // Moto E5
|
|
||||||
"james", "rjames_f", "rjames_go", "pettyl", // Moto E5 Play
|
|
||||||
"hannah", "ahannah", "rhannah", // Moto E5 Plus
|
|
||||||
|
|
||||||
"ali", "ali_n", // Moto G6
|
|
||||||
"aljeter", "aljeter_n", "jeter", // Moto G6 Play
|
|
||||||
"evert", "evert_n", "evert_nt", // Moto G6 Plus
|
|
||||||
|
|
||||||
"G3112", "G3116", "G3121", "G3123", "G3125", // Xperia XA1
|
|
||||||
"G3412", "G3416", "G3421", "G3423", "G3426", // Xperia XA1 Plus
|
|
||||||
"G3212", "G3221", "G3223", "G3226", // Xperia XA1 Ultra
|
|
||||||
|
|
||||||
"BV6800Pro", // BlackView BV6800Pro
|
|
||||||
"CatS41", // Cat S41
|
|
||||||
"Hi9Pro", // CHUWI Hi9 Pro
|
|
||||||
"manning", // Lenovo K8 Note
|
|
||||||
"N5702L", // NUU Mobile G3
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
27 -> run {
|
|
||||||
val device = Build.DEVICE ?: return@run false
|
|
||||||
|
|
||||||
return@run device in arrayOf(
|
|
||||||
"mcv1s", // LG Tribute Empire
|
|
||||||
"mcv3", // LG K11
|
|
||||||
"mcv5a", // LG Q7
|
|
||||||
"mcv7a", // LG Stylo 4
|
|
||||||
|
|
||||||
"A30ATMO", // T-Mobile REVVL 2
|
|
||||||
"A70AXLTMO", // T-Mobile REVVL 2 PLUS
|
|
||||||
|
|
||||||
"A3A_8_4G_TMO", // Alcatel 9027W
|
|
||||||
"Edison_CKT", // Alcatel ONYX
|
|
||||||
"EDISON_TF", // Alcatel TCL XL2
|
|
||||||
"FERMI_TF", // Alcatel A501DL
|
|
||||||
"U50A_ATT", // Alcatel TETRA
|
|
||||||
"U50A_PLUS_ATT", // Alcatel 5059R
|
|
||||||
"U50A_PLUS_TF", // Alcatel TCL LX
|
|
||||||
"U50APLUSTMO", // Alcatel 5059Z
|
|
||||||
"U5A_PLUS_4G", // Alcatel 1X
|
|
||||||
|
|
||||||
"RCT6513W87DK5e", // RCA Galileo Pro
|
|
||||||
"RCT6873W42BMF9A", // RCA Voyager
|
|
||||||
"RCT6A03W13", // RCA 10 Viking
|
|
||||||
"RCT6B03W12", // RCA Atlas 10 Pro
|
|
||||||
"RCT6B03W13", // RCA Atlas 10 Pro+
|
|
||||||
"RCT6T06E13", // RCA Artemis 10
|
|
||||||
|
|
||||||
"A3_Pro", // Umidigi A3 Pro
|
|
||||||
"One", // Umidigi One
|
|
||||||
"One_Max", // Umidigi One Max
|
|
||||||
"One_Pro", // Umidigi One Pro
|
|
||||||
"Z2", // Umidigi Z2
|
|
||||||
"Z2_PRO", // Umidigi Z2 Pro
|
|
||||||
|
|
||||||
"Armor_3", // Ulefone Armor 3
|
|
||||||
"Armor_6", // Ulefone Armor 6
|
|
||||||
|
|
||||||
"Blackview", // Blackview BV6000
|
|
||||||
"BV9500", // Blackview BV9500
|
|
||||||
"BV9500Pro", // Blackview BV9500Pro
|
|
||||||
|
|
||||||
"A6L-C", // Nuu A6L-C
|
|
||||||
"N5002LA", // Nuu A7L
|
|
||||||
"N5501LA", // Nuu A5L
|
|
||||||
|
|
||||||
"Power_2_Pro", // Leagoo Power 2 Pro
|
|
||||||
"Power_5", // Leagoo Power 5
|
|
||||||
"Z9", // Leagoo Z9
|
|
||||||
|
|
||||||
"V0310WW", // Blu VIVO VI+
|
|
||||||
"V0330WW", // Blu VIVO XI
|
|
||||||
|
|
||||||
"A3", // BenQ A3
|
|
||||||
"ASUS_X018_4", // Asus ZenFone Max Plus M1 (ZB570TL)
|
|
||||||
"C210AE", // Wiko Life
|
|
||||||
"fireball", // DROID Incredible 4G LTE
|
|
||||||
"ILA_X1", // iLA X1
|
|
||||||
"Infinix-X605_sprout", // Infinix NOTE 5 Stylus
|
|
||||||
"j7maxlte", // Samsung Galaxy J7 Max
|
|
||||||
"KING_KONG_3", // Cubot King Kong 3
|
|
||||||
"M10500", // Packard Bell M10500
|
|
||||||
"S70", // Altice ALTICE S70
|
|
||||||
"S80Lite", // Doogee S80Lite
|
|
||||||
"SGINO6", // SGiNO 6
|
|
||||||
"st18c10bnn", // Barnes and Noble BNTV650
|
|
||||||
"TECNO-CA8", // Tecno CAMON X Pro,
|
|
||||||
"SHIFT6m", // SHIFT 6m
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val getDisplayMaxHeightInPx: Int
|
val getDisplayMaxHeightInPx: Int
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ firebase-bom = "34.17.0"
|
|||||||
firebase-crashlytics = "3.0.7"
|
firebase-crashlytics = "3.0.7"
|
||||||
flexibleAdapter = "c8013533"
|
flexibleAdapter = "c8013533"
|
||||||
google-services = "4.5.0"
|
google-services = "4.5.0"
|
||||||
image-decoder = "e03b81e18a"
|
image-decoder = "9"
|
||||||
imagedecoder2 = "9"
|
|
||||||
injekt = "91edab2317"
|
injekt = "91edab2317"
|
||||||
jsoup = "1.23.1"
|
jsoup = "1.23.1"
|
||||||
junit = "6.1.3"
|
junit = "6.1.3"
|
||||||
@@ -69,7 +68,7 @@ spotless = "8.9.0"
|
|||||||
sqldelight = "2.3.2"
|
sqldelight = "2.3.2"
|
||||||
sqldelight-androidx-driver = "0637a964a1"
|
sqldelight-androidx-driver = "0637a964a1"
|
||||||
stringSimilarity = "0.1.0"
|
stringSimilarity = "0.1.0"
|
||||||
subsamplingScaleImageView = "66e0db195d"
|
subsamplingScaleImageView = "94915e6f73"
|
||||||
swipe = "1.3.0"
|
swipe = "1.3.0"
|
||||||
tapmoc = "0.4.2"
|
tapmoc = "0.4.2"
|
||||||
unifile = "08f224c8f9"
|
unifile = "08f224c8f9"
|
||||||
@@ -135,8 +134,7 @@ firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
|||||||
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
|
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
|
||||||
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
|
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
|
||||||
flexibleAdapter = { module = "com.github.arkon.FlexibleAdapter:flexible-adapter", version.ref = "flexibleAdapter" }
|
flexibleAdapter = { module = "com.github.arkon.FlexibleAdapter:flexible-adapter", version.ref = "flexibleAdapter" }
|
||||||
image-decoder = { module = "com.github.mihonapp:image-decoder", version.ref = "image-decoder" }
|
image-decoder = { module = "ca.mpreg:imagedecoder", version.ref = "image-decoder" }
|
||||||
image-decoder2 = { module = "ca.mpreg:imagedecoder", version.ref = "imagedecoder2" }
|
|
||||||
injekt = { module = "com.github.mihonapp:injekt", version.ref = "injekt" }
|
injekt = { module = "com.github.mihonapp:injekt", version.ref = "injekt" }
|
||||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||||
@@ -184,7 +182,7 @@ sqldelight-async = { module = "app.cash.sqldelight:async-extensions", version.re
|
|||||||
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||||
sqldelight-sqliteDialect338 = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
|
sqldelight-sqliteDialect338 = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
|
||||||
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version.ref = "stringSimilarity" }
|
stringSimilarity = { module = "com.aallam.similarity:string-similarity-kotlin", version.ref = "stringSimilarity" }
|
||||||
subsamplingScaleImageView = { module = "com.github.tachiyomiorg:subsampling-scale-image-view", version.ref = "subsamplingScaleImageView" }
|
subsamplingScaleImageView = { module = "com.github.mihonapp:subsampling-scale-image-view", version.ref = "subsamplingScaleImageView" }
|
||||||
swipe = { module = "me.saket.swipe:swipe", version.ref = "swipe" }
|
swipe = { module = "me.saket.swipe:swipe", version.ref = "swipe" }
|
||||||
tapmoc-gradle = { module = "com.gradleup.tapmoc:tapmoc-gradle-plugin", version.ref = "tapmoc" }
|
tapmoc-gradle = { module = "com.gradleup.tapmoc:tapmoc-gradle-plugin", version.ref = "tapmoc" }
|
||||||
unifile = { module = "com.github.mihon:unifile", version.ref = "unifile" }
|
unifile = { module = "com.github.mihon:unifile", version.ref = "unifile" }
|
||||||
|
|||||||
@@ -408,11 +408,6 @@
|
|||||||
<string name="pref_show_reading_mode">Show reading mode</string>
|
<string name="pref_show_reading_mode">Show reading mode</string>
|
||||||
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
|
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
|
||||||
<string name="pref_high_quality_renderer">Use high quality renderer</string>
|
<string name="pref_high_quality_renderer">Use high quality renderer</string>
|
||||||
<string name="pref_hardware_bitmap_threshold">Custom hardware bitmap threshold</string>
|
|
||||||
<string name="pref_hardware_bitmap_threshold_default">Default (%d)</string>
|
|
||||||
<string name="pref_hardware_bitmap_threshold_summary">If reader loads a blank image incrementally reduce the threshold.\nSelected: %s</string>
|
|
||||||
<string name="pref_always_decode_long_strip_with_ssiv_2">Use legacy decoder for long strip reader</string>
|
|
||||||
<string name="pref_always_decode_long_strip_with_ssiv_summary">Affects performance. Only enable if reducing bitmap threshold doesn\'t fix blank image issues</string>
|
|
||||||
<string name="pref_display_profile">Custom display profile</string>
|
<string name="pref_display_profile">Custom display profile</string>
|
||||||
<string name="pref_crop_borders">Crop borders</string>
|
<string name="pref_crop_borders">Crop borders</string>
|
||||||
<string name="pref_custom_brightness">Custom brightness</string>
|
<string name="pref_custom_brightness">Custom brightness</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user